diff --git a/Cargo.lock b/Cargo.lock index 6d876a0a1d..0d7576ebd7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -503,6 +503,15 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3f6d59c71e7dc3af60f0af9db32364d96a16e9310f3f5db2b55ed642162dd35" +[[package]] +name = "compact_str" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0e60dedcb8b23cedf6f23ee35ecf5c7889961e99f26f79ab196aaf4a8b48608" +dependencies = [ + "serde", +] + [[package]] name = "concurrent-queue" version = "1.2.2" @@ -1076,8 +1085,10 @@ name = "git-attributes" version = "0.1.0" dependencies = [ "bstr", + "compact_str", "git-features", "git-glob", + "git-path", "git-quote", "git-testtools", "quick-error", @@ -1122,15 +1133,16 @@ dependencies = [ "criterion", "dirs", "git-features", + "git-path", "git-sec", "memchr", "nom", "pwd", - "quick-error", "serde", "serde_derive", "serial_test", "tempfile", + "thiserror", "unicode-bom", ] @@ -1300,6 +1312,7 @@ dependencies = [ "git-hash", "git-object", "git-pack", + "git-path", "git-quote", "git-testtools", "parking_lot 0.12.0", @@ -1325,6 +1338,7 @@ dependencies = [ "git-hash", "git-object", "git-odb", + "git-path", "git-tempfile", "git-testtools", "git-traverse", @@ -1356,6 +1370,13 @@ dependencies = [ "serde", ] +[[package]] +name = "git-path" +version = "0.1.0" +dependencies = [ + "bstr", +] + [[package]] name = "git-pathspec" version = "0.0.0" @@ -1406,6 +1427,7 @@ dependencies = [ "git-lock", "git-object", "git-odb", + "git-path", "git-tempfile", "git-testtools", "git-validate", @@ -1425,6 +1447,7 @@ dependencies = [ "clru", "document-features", "git-actor", + "git-attributes", "git-config", "git-credentials", "git-diff", @@ -1437,6 +1460,7 @@ dependencies = [ "git-object", "git-odb", "git-pack", + "git-path", "git-protocol", "git-ref", "git-revision", @@ -1448,6 +1472,7 @@ dependencies = [ "git-url", "git-validate", "git-worktree", + "is_ci", "log", "signal-hook", "tempfile", @@ -1477,6 +1502,7 @@ dependencies = [ "libc", "serde", "tempfile", + "thiserror", "windows", ] @@ -1571,6 +1597,7 @@ version = "0.4.0" dependencies = [ "bstr", "git-features", + "git-path", "home", "quick-error", "serde", @@ -1592,11 +1619,14 @@ version = "0.1.0" dependencies = [ "bstr", "document-features", + "git-attributes", "git-features", + "git-glob", "git-hash", "git-index", "git-object", "git-odb", + "git-path", "git-testtools", "io-close", "serde", diff --git a/Cargo.toml b/Cargo.toml index 90d0bbd499..c10e07e54d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -161,6 +161,7 @@ members = [ "git-lock", "git-attributes", "git-pathspec", + "git-path", "git-repository", "gitoxide-core", "git-tui", diff --git a/README.md b/README.md index 55f2e4d034..e8c4d364de 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,8 @@ Please see _'Development Status'_ for a listing of all crates and their capabili * **mailmap** * [x] **verify** - check entries of a mailmap file for parse errors and display them * **repository** + * **exclude** + * [x] **query** - check if path specs are excluded via gits exclusion rules like `.gitignore`. * **verify** - validate a whole repository, for now only the object database. * **commit** * [x] **describe** - identify a commit by its closest tag in its past @@ -122,6 +124,7 @@ Crates that seem feature complete and need to see some more use before they can * [git-bitmap](https://github.com/Byron/gitoxide/blob/main/crate-status.md#git-bitmap) * [git-revision](https://github.com/Byron/gitoxide/blob/main/crate-status.md#git-revision) * [git-attributes](https://github.com/Byron/gitoxide/blob/main/crate-status.md#git-attributes) + * [git-path](https://github.com/Byron/gitoxide/blob/main/crate-status.md#git-path) * **idea** * [git-note](https://github.com/Byron/gitoxide/blob/main/crate-status.md#git-note) * [git-filter](https://github.com/Byron/gitoxide/blob/main/crate-status.md#git-filter) diff --git a/crate-status.md b/crate-status.md index 36a8d5c801..43e4374f73 100644 --- a/crate-status.md +++ b/crate-status.md @@ -221,6 +221,13 @@ Check out the [performance discussion][git-traverse-performance] as well. * [x] parsing * [x] lookup and mapping of author names +### git-path +* [x] transformations to and from bytes +* [x] conversions between different platforms +* **spec** + * [ ] parse + * [ ] check for match + ### git-pathspec * [ ] parse * [ ] check for match @@ -429,7 +436,8 @@ See its [README.md](https://github.com/Byron/gitoxide/blob/main/git-lock/README. * **refs** * [ ] run transaction hooks and handle special repository states like quarantine * [ ] support for different backends like `files` and `reftable` - * [ ] worktrees + * **worktrees** + * [ ] open a repository with worktrees * [ ] remotes with push and pull * [x] mailmap * [x] object replacements (`git replace`) diff --git a/etc/check-package-size.sh b/etc/check-package-size.sh index 1da6fbc4bf..3a2db8351f 100755 --- a/etc/check-package-size.sh +++ b/etc/check-package-size.sh @@ -19,6 +19,7 @@ echo "in root: gitoxide CLI" (enter cargo-smart-release && indent cargo diet -n --package-size-limit 90KB) (enter git-actor && indent cargo diet -n --package-size-limit 5KB) (enter git-pathspec && indent cargo diet -n --package-size-limit 5KB) +(enter git-path && indent cargo diet -n --package-size-limit 10KB) (enter git-attributes && indent cargo diet -n --package-size-limit 10KB) (enter git-index && indent cargo diet -n --package-size-limit 30KB) (enter git-worktree && indent cargo diet -n --package-size-limit 25KB) @@ -51,6 +52,6 @@ echo "in root: gitoxide CLI" (enter git-odb && indent cargo diet -n --package-size-limit 120KB) (enter git-protocol && indent cargo diet -n --package-size-limit 50KB) (enter git-packetline && indent cargo diet -n --package-size-limit 35KB) -(enter git-repository && indent cargo diet -n --package-size-limit 90KB) +(enter git-repository && indent cargo diet -n --package-size-limit 100KB) (enter git-transport && indent cargo diet -n --package-size-limit 50KB) (enter gitoxide-core && indent cargo diet -n --package-size-limit 70KB) diff --git a/git-attributes/Cargo.toml b/git-attributes/Cargo.toml index bf27419b3b..04fcf792bb 100644 --- a/git-attributes/Cargo.toml +++ b/git-attributes/Cargo.toml @@ -13,12 +13,13 @@ doctest = false [features] ## Data structures implement `serde::Serialize` and `serde::Deserialize`. -serde1 = ["serde", "bstr/serde1", "git-glob/serde1"] +serde1 = ["serde", "bstr/serde1", "git-glob/serde1", "compact_str/serde"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] git-features = { version = "^0.20.0", path = "../git-features" } +git-path = { version = "^0.1.0", path = "../git-path" } git-quote = { version = "^0.2.0", path = "../git-quote" } git-glob = { version = "^0.2.0", path = "../git-glob" } @@ -26,6 +27,7 @@ bstr = { version = "0.2.13", default-features = false, features = ["std"]} unicode-bom = "1.1.4" quick-error = "2.0.0" serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"]} +compact_str = "0.3.2" [dev-dependencies] git-testtools = { path = "../tests/tools"} diff --git a/git-attributes/src/lib.rs b/git-attributes/src/lib.rs index 1f2a941c85..26276a82c5 100644 --- a/git-attributes/src/lib.rs +++ b/git-attributes/src/lib.rs @@ -2,10 +2,17 @@ #![deny(rust_2018_idioms)] use bstr::{BStr, BString}; +use compact_str::CompactStr; +use std::path::PathBuf; +pub use git_glob as glob; + +/// The state an attribute can be in, referencing the value. +/// +/// Note that this doesn't contain the name. #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] #[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))] -pub enum State<'a> { +pub enum StateRef<'a> { /// The attribute is listed, or has the special value 'true' Set, /// The attribute has the special value 'false', or was prefixed with a `-` sign. @@ -18,11 +25,38 @@ pub enum State<'a> { Unspecified, } -/// A grouping of lists of patterns while possibly keeping associated to their base path. +/// The state an attribute can be in, owning the value. /// -/// Patterns with base path are queryable relative to that base, otherwise they are relative to the repository root. +/// Note that this doesn't contain the name. +#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] +#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))] +pub enum State { + /// The attribute is listed, or has the special value 'true' + Set, + /// The attribute has the special value 'false', or was prefixed with a `-` sign. + Unset, + /// The attribute is set to the given value, which followed the `=` sign. + /// Note that values can be empty. + Value(compact_str::CompactStr), + /// The attribute isn't mentioned with a given path or is explicitly set to `Unspecified` using the `!` sign. + Unspecified, +} + +/// Name an attribute and describe it's assigned state. #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] -pub struct MatchGroup { +#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))] +pub struct Assignment { + /// The name of the attribute. + pub name: CompactStr, + /// The state of the attribute. + pub state: State, +} + +/// A grouping of lists of patterns while possibly keeping associated to their base path. +/// +/// Pattern lists with base path are queryable relative to that base, otherwise they are relative to the repository root. +#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Default)] +pub struct MatchGroup { /// A list of pattern lists, each representing a patterns from a file or specified by hand, in the order they were /// specified in. /// @@ -30,75 +64,36 @@ pub struct MatchGroup { pub patterns: Vec>, } -/// A list of patterns with an optional names, for matching against it. -#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] -pub struct PatternList { - /// Patterns and their associated data in the order they were loaded in or specified. +/// A list of patterns which optionally know where they were loaded from and what their base is. +/// +/// Knowing their base which is relative to a source directory, it will ignore all path to match against +/// that don't also start with said base. +#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Default)] +pub struct PatternList { + /// Patterns and their associated data in the order they were loaded in or specified, + /// the line number in its source file or its sequence number (_`(pattern, value, line_number)`_). /// /// During matching, this order is reversed. - pub patterns: Vec<(git_glob::Pattern, T::Value)>, + pub patterns: Vec>, - /// The path at which the patterns are located in a format suitable for matches, or `None` if the patterns - /// are relative to the worktree root. - base: Option, -} - -mod match_group { - use crate::{MatchGroup, PatternList}; - use std::ffi::OsString; - use std::path::PathBuf; - - /// A marker trait to identify the type of a description. - pub trait Tag: Clone + PartialEq + Eq + std::fmt::Debug + std::hash::Hash + Ord + PartialOrd { - /// The value associated with a pattern. - type Value: PartialEq + Eq + std::fmt::Debug + std::hash::Hash + Ord + PartialOrd + Clone; - } - - /// Identify ignore patterns. - #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] - pub struct Ignore; - impl Tag for Ignore { - type Value = (); - } + /// The path from which the patterns were read, or `None` if the patterns + /// don't originate in a file on disk. + pub source: Option, - /// Identify patterns with attributes. - #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] - pub struct Attributes; - impl Tag for Attributes { - /// TODO: identify the actual value, should be name/State pairs, but there is the question of storage. - type Value = (); - } - - impl MatchGroup { - /// See [PatternList::::from_overrides()] for details. - pub fn from_overrides(patterns: impl IntoIterator>) -> Self { - MatchGroup { - patterns: vec![PatternList::::from_overrides(patterns)], - } - } - } + /// The parent directory of source, or `None` if the patterns are _global_ to match against the repository root. + /// It's processed to contain slashes only and to end with a trailing slash, and is relative to the repository root. + pub base: Option, +} - impl PatternList { - /// Parse a list of patterns, using slashes as path separators - pub fn from_overrides(patterns: impl IntoIterator>) -> Self { - PatternList { - patterns: patterns - .into_iter() - .map(Into::into) - .filter_map(|pattern| { - let pattern = git_features::path::into_bytes(PathBuf::from(pattern)).ok()?; - git_glob::parse(pattern.as_ref()).map(|p| (p, ())) - }) - .collect(), - base: None, - } - } - } +#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] +pub struct PatternMapping { + pub pattern: git_glob::Pattern, + pub value: T, + pub sequence_number: usize, } -pub use match_group::{Attributes, Ignore, Tag}; -pub type Files = MatchGroup; -pub type IgnoreFiles = MatchGroup; +mod match_group; +pub use match_group::{Attributes, Ignore, Match, Pattern}; pub mod parse; diff --git a/git-attributes/src/match_group.rs b/git-attributes/src/match_group.rs new file mode 100644 index 0000000000..8fd5b04599 --- /dev/null +++ b/git-attributes/src/match_group.rs @@ -0,0 +1,354 @@ +use crate::{Assignment, MatchGroup, PatternList, PatternMapping, State, StateRef}; +use bstr::{BStr, BString, ByteSlice, ByteVec}; +use std::ffi::OsString; +use std::io::Read; +use std::path::{Path, PathBuf}; + +impl<'a> From> for State { + fn from(s: StateRef<'a>) -> Self { + match s { + StateRef::Value(v) => State::Value(v.to_str().expect("no illformed unicode").into()), + StateRef::Set => State::Set, + StateRef::Unset => State::Unset, + StateRef::Unspecified => State::Unspecified, + } + } +} + +fn attrs_to_assignments<'a>( + attrs: impl Iterator), crate::parse::Error>>, +) -> Result, crate::parse::Error> { + attrs + .map(|res| { + res.map(|(name, state)| Assignment { + name: name.to_str().expect("no illformed unicode").into(), + state: state.into(), + }) + }) + .collect() +} + +/// A marker trait to identify the type of a description. +pub trait Pattern: Clone + PartialEq + Eq + std::fmt::Debug + std::hash::Hash + Ord + PartialOrd + Default { + /// The value associated with a pattern. + type Value: PartialEq + Eq + std::fmt::Debug + std::hash::Hash + Ord + PartialOrd + Clone; + + /// Parse all patterns in `bytes` line by line, ignoring lines with errors, and collect them. + fn bytes_to_patterns(bytes: &[u8]) -> Vec>; + + fn use_pattern(pattern: &git_glob::Pattern) -> bool; +} + +/// Identify ignore patterns. +#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Default)] +pub struct Ignore; + +impl Pattern for Ignore { + type Value = (); + + fn bytes_to_patterns(bytes: &[u8]) -> Vec> { + crate::parse::ignore(bytes) + .map(|(pattern, line_number)| PatternMapping { + pattern, + value: (), + sequence_number: line_number, + }) + .collect() + } + + fn use_pattern(_pattern: &git_glob::Pattern) -> bool { + true + } +} + +/// A value of an attribute pattern, which is either a macro definition or +#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] +pub enum Value { + MacroAttributes(Vec), + Attributes(Vec), +} + +/// Identify patterns with attributes. +#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Default)] +pub struct Attributes; + +impl Pattern for Attributes { + type Value = Value; + + fn bytes_to_patterns(bytes: &[u8]) -> Vec> { + crate::parse(bytes) + .filter_map(Result::ok) + .filter_map(|(pattern_kind, attrs, line_number)| { + let (pattern, value) = match pattern_kind { + crate::parse::Kind::Macro(macro_name) => ( + git_glob::Pattern { + text: macro_name, + mode: git_glob::pattern::Mode::all(), + first_wildcard_pos: None, + }, + Value::MacroAttributes(attrs_to_assignments(attrs).ok()?), + ), + crate::parse::Kind::Pattern(p) => ( + (!p.is_negative()).then(|| p)?, + Value::Attributes(attrs_to_assignments(attrs).ok()?), + ), + }; + PatternMapping { + pattern, + value, + sequence_number: line_number, + } + .into() + }) + .collect() + } + + fn use_pattern(pattern: &git_glob::Pattern) -> bool { + pattern.mode != git_glob::pattern::Mode::all() + } +} + +/// Describes a matching value within a [`MatchGroup`]. +#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] +pub struct Match<'a, T> { + pub pattern: &'a git_glob::Pattern, + /// The value associated with the pattern. + pub value: &'a T, + /// The path to the source from which the pattern was loaded, or `None` if it was specified by other means. + pub source: Option<&'a Path>, + /// The line at which the pattern was found in its `source` file, or the occurrence in which it was provided. + pub sequence_number: usize, +} + +impl MatchGroup +where + T: Pattern, +{ + /// Match `relative_path`, a path relative to the repository containing all patterns, and return the first match if available. + // TODO: better docs + pub fn pattern_matching_relative_path<'a>( + &self, + relative_path: impl Into<&'a BStr>, + is_dir: Option, + case: git_glob::pattern::Case, + ) -> Option> { + let relative_path = relative_path.into(); + let basename_pos = relative_path.rfind(b"/").map(|p| p + 1); + self.patterns + .iter() + .rev() + .find_map(|pl| pl.pattern_matching_relative_path(relative_path, basename_pos, is_dir, case)) + } +} + +impl MatchGroup { + /// Given `git_dir`, a `.git` repository, load ignore patterns from `info/exclude` and from `excludes_file` if it + /// is provided. + /// Note that it's not considered an error if the provided `excludes_file` does not exist. + pub fn from_git_dir( + git_dir: impl AsRef, + excludes_file: Option, + buf: &mut Vec, + ) -> std::io::Result { + let mut group = Self::default(); + + let follow_symlinks = true; + // order matters! More important ones first. + group.patterns.extend( + excludes_file + .map(|file| PatternList::::from_file(file, None, follow_symlinks, buf)) + .transpose()? + .flatten(), + ); + group.patterns.extend(PatternList::::from_file( + git_dir.as_ref().join("info").join("exclude"), + None, + follow_symlinks, + buf, + )?); + Ok(group) + } + + /// See [PatternList::::from_overrides()] for details. + pub fn from_overrides(patterns: impl IntoIterator>) -> Self { + MatchGroup { + patterns: vec![PatternList::::from_overrides(patterns)], + } + } + + /// Add the given file at `source` if it exists, otherwise do nothing. If a `root` is provided, it's not considered a global file anymore. + /// Returns true if the file was added, or false if it didn't exist. + pub fn add_patterns_file( + &mut self, + source: impl Into, + follow_symlinks: bool, + root: Option<&Path>, + buf: &mut Vec, + ) -> std::io::Result { + let previous_len = self.patterns.len(); + self.patterns.extend(PatternList::::from_file( + source.into(), + root, + follow_symlinks, + buf, + )?); + Ok(self.patterns.len() != previous_len) + } + + pub fn add_patterns_buffer(&mut self, bytes: &[u8], source: impl Into, root: Option<&Path>) { + self.patterns + .push(PatternList::::from_bytes(bytes, source.into(), root)); + } +} + +fn read_in_full_ignore_missing(path: &Path, follow_symlinks: bool, buf: &mut Vec) -> std::io::Result { + buf.clear(); + let file = if follow_symlinks { + std::fs::File::open(path) + } else { + git_features::fs::open_options_no_follow().read(true).open(path) + }; + Ok(match file { + Ok(mut file) => { + file.read_to_end(buf)?; + true + } + Err(err) if err.kind() == std::io::ErrorKind::NotFound => false, + Err(err) => return Err(err), + }) +} + +impl PatternList +where + T: Pattern, +{ + /// `source` is the location of the `bytes` which represent a list of patterns line by line. + pub fn from_bytes(bytes: &[u8], source: impl Into, root: Option<&Path>) -> Self { + let source = source.into(); + let patterns = T::bytes_to_patterns(bytes); + + let base = root + .and_then(|root| source.parent().expect("file").strip_prefix(root).ok()) + .and_then(|base| { + (!base.as_os_str().is_empty()).then(|| { + let mut base: BString = + git_path::to_unix_separators_on_windows(git_path::into_bstr(base)).into_owned(); + + base.push_byte(b'/'); + base + }) + }); + PatternList { + patterns, + source: Some(source), + base, + } + } + pub fn from_file( + source: impl Into, + root: Option<&Path>, + follow_symlinks: bool, + buf: &mut Vec, + ) -> std::io::Result> { + let source = source.into(); + Ok(read_in_full_ignore_missing(&source, follow_symlinks, buf)?.then(|| Self::from_bytes(buf, source, root))) + } +} + +impl PatternList +where + T: Pattern, +{ + pub fn pattern_matching_relative_path( + &self, + relative_path: &BStr, + basename_pos: Option, + is_dir: Option, + case: git_glob::pattern::Case, + ) -> Option> { + let (relative_path, basename_start_pos) = + self.strip_base_handle_recompute_basename_pos(relative_path, basename_pos)?; + self.patterns + .iter() + .rev() + .filter(|pm| T::use_pattern(&pm.pattern)) + .find_map( + |PatternMapping { + pattern, + value, + sequence_number, + }| { + pattern + .matches_repo_relative_path(relative_path, basename_start_pos, is_dir, case) + .then(|| Match { + pattern, + value, + source: self.source.as_deref(), + sequence_number: *sequence_number, + }) + }, + ) + } + + pub fn pattern_idx_matching_relative_path( + &self, + relative_path: &BStr, + basename_pos: Option, + is_dir: Option, + case: git_glob::pattern::Case, + ) -> Option { + let (relative_path, basename_start_pos) = + self.strip_base_handle_recompute_basename_pos(relative_path, basename_pos)?; + self.patterns + .iter() + .enumerate() + .rev() + .filter(|(_, pm)| T::use_pattern(&pm.pattern)) + .find_map(|(idx, pm)| { + pm.pattern + .matches_repo_relative_path(relative_path, basename_start_pos, is_dir, case) + .then(|| idx) + }) + } + + fn strip_base_handle_recompute_basename_pos<'a>( + &self, + relative_path: &'a BStr, + basename_pos: Option, + ) -> Option<(&'a BStr, Option)> { + match self.base.as_deref() { + Some(base) => ( + relative_path.strip_prefix(base.as_slice())?.as_bstr(), + basename_pos.and_then(|pos| { + let pos = pos - base.len(); + (pos != 0).then(|| pos) + }), + ), + None => (relative_path, basename_pos), + } + .into() + } +} + +impl PatternList { + /// Parse a list of patterns, using slashes as path separators + pub fn from_overrides(patterns: impl IntoIterator>) -> Self { + PatternList { + patterns: patterns + .into_iter() + .map(Into::into) + .enumerate() + .filter_map(|(seq_id, pattern)| { + let pattern = git_path::try_into_bstr(PathBuf::from(pattern)).ok()?; + git_glob::parse(pattern.as_ref()).map(|p| PatternMapping { + pattern: p, + value: (), + sequence_number: seq_id, + }) + }) + .collect(), + source: None, + base: None, + } + } +} diff --git a/git-attributes/src/parse/attribute.rs b/git-attributes/src/parse/attribute.rs index 8d044e933a..064e78a4a1 100644 --- a/git-attributes/src/parse/attribute.rs +++ b/git-attributes/src/parse/attribute.rs @@ -56,20 +56,20 @@ impl<'a> Iter<'a> { } } - fn parse_attr(&self, attr: &'a [u8]) -> Result<(&'a BStr, crate::State<'a>), Error> { + fn parse_attr(&self, attr: &'a [u8]) -> Result<(&'a BStr, crate::StateRef<'a>), Error> { let mut tokens = attr.splitn(2, |b| *b == b'='); let attr = tokens.next().expect("attr itself").as_bstr(); let possibly_value = tokens.next(); let (attr, state) = if attr.first() == Some(&b'-') { - (&attr[1..], crate::State::Unset) + (&attr[1..], crate::StateRef::Unset) } else if attr.first() == Some(&b'!') { - (&attr[1..], crate::State::Unspecified) + (&attr[1..], crate::StateRef::Unspecified) } else { ( attr, possibly_value - .map(|v| crate::State::Value(v.as_bstr())) - .unwrap_or(crate::State::Set), + .map(|v| crate::StateRef::Value(v.as_bstr())) + .unwrap_or(crate::StateRef::Set), ) }; Ok((check_attr(attr, self.line_no)?, state)) @@ -95,7 +95,7 @@ fn check_attr(attr: &BStr, line_number: usize) -> Result<&BStr, Error> { } impl<'a> Iterator for Iter<'a> { - type Item = Result<(&'a BStr, crate::State<'a>), Error>; + type Item = Result<(&'a BStr, crate::StateRef<'a>), Error>; fn next(&mut self) -> Option { let attr = self.attrs.next().filter(|a| !a.is_empty())?; diff --git a/git-attributes/tests/attributes.rs b/git-attributes/tests/attributes.rs index c25a606d73..36d782c5c9 100644 --- a/git-attributes/tests/attributes.rs +++ b/git-attributes/tests/attributes.rs @@ -1,2 +1,3 @@ +pub use git_testtools::Result; mod match_group; mod parse; diff --git a/git-attributes/tests/fixtures/make_global_and_external_and_dir_ignores.sh b/git-attributes/tests/fixtures/make_global_and_external_and_dir_ignores.sh new file mode 100644 index 0000000000..195d47f488 --- /dev/null +++ b/git-attributes/tests/fixtures/make_global_and_external_and_dir_ignores.sh @@ -0,0 +1,77 @@ +#!/bin/bash +set -eu -o pipefail + +cat <user.exclude +# a custom exclude configured per user +user-file-anywhere +/user-file-from-top + +user-dir-anywhere/ +/user-dir-from-top + +user-subdir/file +**/user-subdir-anywhere/file +EOF + +mkdir repo; +(cd repo + git init -q + git config core.excludesFile ../user.exclude + + cat <.git/info/exclude +# a sample .git/info/exclude +file-anywhere +/file-from-top + +dir-anywhere/ +/dir-from-top + +subdir/file +**/subdir-anywhere/file +EOF + + cat <.gitignore +# a sample .gitignore +top-level-local-file-anywhere +EOF + + mkdir dir-with-ignore + cat <dir-with-ignore/.gitignore +# a sample .gitignore +sub-level-local-file-anywhere +EOF + + git add .gitignore dir-with-ignore + git commit --allow-empty -m "init" + + mkdir user-dir-anywhere user-dir-from-top dir-anywhere dir-from-top + mkdir -p dir/user-dir-anywhere dir/dir-anywhere + + git check-ignore -vn --stdin 2>&1 <git-check-ignore.baseline || : +user-file-anywhere +dir/user-file-anywhere +user-file-from-top +no-match/user-file-from-top +user-dir-anywhere +user-dir-from-top +no-match/user-dir-from-top +user-subdir/file +subdir/user-subdir-anywhere/file +file-anywhere +dir/file-anywhere +file-from-top +no-match/file-from-top +dir-anywhere +dir/dir-anywhere +dir-from-top +no-match/dir-from-top +subdir/file +subdir/subdir-anywhere/file +top-level-local-file-anywhere +dir/top-level-local-file-anywhere +no-match/sub-level-local-file-anywhere +dir-with-ignore/sub-level-local-file-anywhere +dir-with-ignore/sub-dir/sub-level-local-file-anywhere +EOF + +) diff --git a/git-attributes/tests/match_group/mod.rs b/git-attributes/tests/match_group/mod.rs index 32d84a473d..a8b661eb1a 100644 --- a/git-attributes/tests/match_group/mod.rs +++ b/git-attributes/tests/match_group/mod.rs @@ -1,14 +1,117 @@ mod ignore { - use git_attributes::Ignore; + use bstr::{BStr, ByteSlice}; + use git_attributes::{Ignore, Match, MatchGroup}; + use git_glob::pattern::Case; + use std::io::Read; + + struct Expectations<'a> { + lines: bstr::Lines<'a>, + } + + impl<'a> Iterator for Expectations<'a> { + type Item = (&'a BStr, Option<(&'a BStr, usize, &'a BStr)>); + + fn next(&mut self) -> Option { + let line = self.lines.next()?; + let (left, value) = line.split_at(line.find_byte(b'\t').unwrap()); + let value = value[1..].as_bstr(); + + let source_and_line = if left == b"::" { + None + } else { + let mut tokens = left.split(|b| *b == b':'); + let source = tokens.next().unwrap().as_bstr(); + let line_number: usize = tokens.next().unwrap().to_str_lossy().parse().ok().unwrap(); + let pattern = tokens.next().unwrap().as_bstr(); + Some((source, line_number, pattern)) + }; + Some((value, source_and_line)) + } + } + + #[test] + fn from_git_dir() -> crate::Result { + let dir = git_testtools::scripted_fixture_repo_read_only("make_global_and_external_and_dir_ignores.sh")?; + let repo_dir = dir.join("repo"); + let git_dir = repo_dir.join(".git"); + let baseline = std::fs::read(git_dir.parent().unwrap().join("git-check-ignore.baseline"))?; + let mut buf = Vec::new(); + let mut group = MatchGroup::from_git_dir(git_dir, Some(dir.join("user.exclude")), &mut buf)?; + + assert!( + !group.add_patterns_file("not-a-file", false, None, &mut buf)?, + "missing files are no problem and cause a negative response" + ); + assert!( + group.add_patterns_file(repo_dir.join(".gitignore"), true, repo_dir.as_path().into(), &mut buf)?, + "existing files return true" + ); + + buf.clear(); + let ignore_file = repo_dir.join("dir-with-ignore").join(".gitignore"); + std::fs::File::open(&ignore_file)?.read_to_end(&mut buf)?; + group.add_patterns_buffer(&buf, ignore_file, repo_dir.as_path().into()); + + for (path, source_and_line) in (Expectations { + lines: baseline.lines(), + }) { + let actual = group.pattern_matching_relative_path( + path, + repo_dir + .join(path.to_str_lossy().as_ref()) + .metadata() + .ok() + .map(|m| m.is_dir()), + Case::Sensitive, + ); + match (actual, source_and_line) { + ( + Some(Match { + sequence_number, + pattern: _, + source, + value: _, + }), + Some((expected_source, line, _expected_pattern)), + ) => { + assert_eq!(sequence_number, line, "our counting should match the one used in git"); + assert_eq!( + source.map(|p| p.canonicalize().unwrap()), + Some(repo_dir.join(expected_source.to_str_lossy().as_ref()).canonicalize()?) + ); + } + (None, None) => {} + (actual, expected) => panic!("actual {:?} should match {:?} with path '{}'", actual, expected, path), + } + } + Ok(()) + } #[test] - fn init_from_overrides() { + fn from_overrides() { let input = ["simple", "pattern/"]; - let patterns = git_attributes::MatchGroup::::from_overrides(input).patterns; - assert_eq!(patterns.len(), 1); + let group = git_attributes::MatchGroup::::from_overrides(input); + assert_eq!( + group.pattern_matching_relative_path("Simple", None, git_glob::pattern::Case::Fold), + Some(pattern_to_match(&git_glob::parse("simple").unwrap(), 0)) + ); + assert_eq!( + group.pattern_matching_relative_path("pattern", Some(true), git_glob::pattern::Case::Sensitive), + Some(pattern_to_match(&git_glob::parse("pattern/").unwrap(), 1)) + ); + assert_eq!(group.patterns.len(), 1); assert_eq!( git_attributes::PatternList::::from_overrides(input), - patterns.into_iter().next().unwrap() + group.patterns.into_iter().next().unwrap() ); } + + fn pattern_to_match(pattern: &git_glob::Pattern, sequence_number: usize) -> Match<'_, ()> { + Match { + pattern, + value: &(), + source: None, + sequence_number, + } + } } diff --git a/git-attributes/tests/parse/attribute.rs b/git-attributes/tests/parse/attribute.rs index a5889dfe13..c4306c70cd 100644 --- a/git-attributes/tests/parse/attribute.rs +++ b/git-attributes/tests/parse/attribute.rs @@ -1,5 +1,5 @@ use bstr::{BStr, ByteSlice}; -use git_attributes::{parse, State}; +use git_attributes::{parse, StateRef}; use git_glob::pattern::Mode; use git_testtools::fixture_bytes; @@ -30,7 +30,7 @@ fn line_numbers_are_counted_correctly() { (pattern(r"!foo.html", Mode::NO_SUB_DIR, None), vec![set("x")], 8), (pattern(r"#a/path", Mode::empty(), None), vec![unset("a")], 10), ( - pattern(r"/*", Mode::ABSOLUTE | Mode::NO_SUB_DIR | Mode::ENDS_WITH, Some(1)), + pattern(r"*", Mode::ABSOLUTE | Mode::NO_SUB_DIR | Mode::ENDS_WITH, Some(0)), vec![unspecified("b")], 11 ), @@ -249,22 +249,22 @@ fn trailing_whitespace_in_attributes_is_ignored() { ); } -type ExpandedAttribute<'a> = (parse::Kind, Vec<(&'a BStr, git_attributes::State<'a>)>, usize); +type ExpandedAttribute<'a> = (parse::Kind, Vec<(&'a BStr, git_attributes::StateRef<'a>)>, usize); -fn set(attr: &str) -> (&BStr, State) { - (attr.as_bytes().as_bstr(), State::Set) +fn set(attr: &str) -> (&BStr, StateRef) { + (attr.as_bytes().as_bstr(), StateRef::Set) } -fn unset(attr: &str) -> (&BStr, State) { - (attr.as_bytes().as_bstr(), State::Unset) +fn unset(attr: &str) -> (&BStr, StateRef) { + (attr.as_bytes().as_bstr(), StateRef::Unset) } -fn unspecified(attr: &str) -> (&BStr, State) { - (attr.as_bytes().as_bstr(), State::Unspecified) +fn unspecified(attr: &str) -> (&BStr, StateRef) { + (attr.as_bytes().as_bstr(), StateRef::Unspecified) } -fn value<'a, 'b>(attr: &'a str, value: &'b str) -> (&'a BStr, State<'b>) { - (attr.as_bytes().as_bstr(), State::Value(value.as_bytes().as_bstr())) +fn value<'a, 'b>(attr: &'a str, value: &'b str) -> (&'a BStr, StateRef<'b>) { + (attr.as_bytes().as_bstr(), StateRef::Value(value.as_bytes().as_bstr())) } fn pattern(name: &str, flags: git_glob::pattern::Mode, first_wildcard_pos: Option) -> parse::Kind { diff --git a/git-attributes/tests/parse/ignore.rs b/git-attributes/tests/parse/ignore.rs index 2594e5b5e9..f3c94e059a 100644 --- a/git-attributes/tests/parse/ignore.rs +++ b/git-attributes/tests/parse/ignore.rs @@ -20,10 +20,10 @@ fn line_numbers_are_counted_correctly() { ("*.[oa]".into(), Mode::NO_SUB_DIR, 2), ("*.html".into(), Mode::NO_SUB_DIR | Mode::ENDS_WITH, 5), ("foo.html".into(), Mode::NO_SUB_DIR | Mode::NEGATIVE, 8), - ("/*".into(), Mode::NO_SUB_DIR | Mode::ENDS_WITH | Mode::ABSOLUTE, 11), - ("/foo".into(), Mode::NEGATIVE | Mode::NO_SUB_DIR | Mode::ABSOLUTE, 12), - ("/foo/*".into(), Mode::ABSOLUTE, 13), - ("/foo/bar".into(), Mode::ABSOLUTE | Mode::NEGATIVE, 14) + ("*".into(), Mode::NO_SUB_DIR | Mode::ENDS_WITH | Mode::ABSOLUTE, 11), + ("foo".into(), Mode::NEGATIVE | Mode::NO_SUB_DIR | Mode::ABSOLUTE, 12), + ("foo/*".into(), Mode::ABSOLUTE, 13), + ("foo/bar".into(), Mode::ABSOLUTE | Mode::NEGATIVE, 14) ] ); } diff --git a/git-config/Cargo.toml b/git-config/Cargo.toml index 18a5d9dd07..f4c67d8359 100644 --- a/git-config/Cargo.toml +++ b/git-config/Cargo.toml @@ -15,6 +15,7 @@ include = ["src/**/*", "LICENSE-*", "README.md", "CHANGELOG.md"] [dependencies] git-features = { version = "^0.20.0", path = "../git-features"} +git-path = { version = "^0.1.0", path = "../git-path" } git-sec = { version = "^0.1.0", path = "../git-sec" } dirs = "4" @@ -22,7 +23,7 @@ nom = { version = "7", default_features = false, features = [ "std" ] } memchr = "2" serde_crate = { version = "1", package = "serde", optional = true } pwd = "1.3.1" -quick-error = "2.0.0" +thiserror = "1.0.26" unicode-bom = "1.1.4" bstr = { version = "0.2.13", default-features = false, features = ["std"] } diff --git a/git-config/src/file/error.rs b/git-config/src/file/error.rs deleted file mode 100644 index 9113473e94..0000000000 --- a/git-config/src/file/error.rs +++ /dev/null @@ -1,35 +0,0 @@ -use std::{error::Error, fmt::Display}; - -use crate::parser::SectionHeaderName; -// TODO Consider replacing with quick_error -/// All possible error types that may occur from interacting with -/// [`GitConfig`](super::GitConfig). -#[allow(clippy::module_name_repetitions)] -#[derive(PartialEq, Eq, Hash, Clone, PartialOrd, Ord, Debug)] -pub enum GitConfigError<'a> { - /// The requested section does not exist. - SectionDoesNotExist(SectionHeaderName<'a>), - /// The requested subsection does not exist. - SubSectionDoesNotExist(Option<&'a str>), - /// The key does not exist in the requested section. - KeyDoesNotExist, - /// The conversion into the provided type for methods such as - /// [`GitConfig::value`](super::GitConfig::value) failed. - FailedConversion, -} - -impl Display for GitConfigError<'_> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::SectionDoesNotExist(s) => write!(f, "Section '{}' does not exist.", s), - Self::SubSectionDoesNotExist(s) => match s { - Some(s) => write!(f, "Subsection '{}' does not exist.", s), - None => write!(f, "Top level section does not exist."), - }, - Self::KeyDoesNotExist => write!(f, "The name for a value provided does not exist."), - Self::FailedConversion => write!(f, "Failed to convert to specified type."), - } - } -} - -impl Error for GitConfigError<'_> {} diff --git a/git-config/src/file/git_config.rs b/git-config/src/file/git_config.rs index 2d2662736e..ba119e8bd4 100644 --- a/git-config/src/file/git_config.rs +++ b/git-config/src/file/git_config.rs @@ -1,3 +1,4 @@ +use bstr::BStr; use std::{ borrow::Cow, collections::{HashMap, VecDeque}, @@ -8,17 +9,16 @@ use std::{ use crate::{ file::{ - error::GitConfigError, section::{MutableSection, SectionBody}, value::{EntryData, MutableMultiValue, MutableValue}, Index, Size, }, - parser, + lookup, parser, parser::{ parse_from_bytes, parse_from_path, parse_from_str, Error, Event, Key, ParsedSectionHeader, Parser, SectionHeaderName, }, - values, + value, values, }; /// The section ID is a monotonically increasing ID used to refer to sections. @@ -61,7 +61,7 @@ pub(super) enum LookupTreeNode<'a> { /// /// `git` is flexible enough to allow users to set a key multiple times in /// any number of identically named sections. When this is the case, the key -/// is known as a "multivar". In this case, `get_raw_value` follows the +/// is known as a "multivar". In this case, `raw_value` follows the /// "last one wins" approach that `git-config` internally uses for multivar /// resolution. /// @@ -78,7 +78,7 @@ pub(super) enum LookupTreeNode<'a> { /// e = f g h /// ``` /// -/// Calling methods that fetch or set only one value (such as [`get_raw_value`]) +/// Calling methods that fetch or set only one value (such as [`raw_value`]) /// key `a` with the above config will fetch `d` or replace `d`, since the last /// valid config key/value pair is `a = d`: /// @@ -87,14 +87,14 @@ pub(super) enum LookupTreeNode<'a> { /// # use std::borrow::Cow; /// # use std::convert::TryFrom; /// # let git_config = GitConfig::try_from("[core]a=b\n[core]\na=c\na=d").unwrap(); -/// assert_eq!(git_config.get_raw_value("core", None, "a"), Ok(Cow::Borrowed("d".as_bytes()))); +/// assert_eq!(git_config.raw_value("core", None, "a").unwrap(), Cow::Borrowed("d".as_bytes())); /// ``` /// /// Consider the `multi` variants of the methods instead, if you want to work /// with all values instead. /// /// [`ResolvedGitConfig`]: crate::file::ResolvedGitConfig -/// [`get_raw_value`]: Self::get_raw_value +/// [`raw_value`]: Self::raw_value #[derive(PartialEq, Eq, Clone, Debug, Default)] pub struct GitConfig<'event> { /// The list of events that occur before an actual section. Since a @@ -121,32 +121,20 @@ pub struct GitConfig<'event> { pub mod from_paths { use std::borrow::Cow; - use quick_error::quick_error; - use crate::{parser, values::path::interpolate}; - quick_error! { - #[derive(Debug)] - /// The error returned by [`GitConfig::from_paths()`][super::GitConfig::from_paths()] and [`GitConfig::from_env_paths()`][super::GitConfig::from_env_paths()]. - #[allow(missing_docs)] - pub enum Error { - ParserOrIoError(err: parser::ParserOrIoError<'static>) { - display("Could not read config") - source(err) - from() - } - Interpolate(err: interpolate::Error) { - display("Could not interpolate path") - source(err) - from() - } - IncludeDepthExceeded { max_depth: u8 } { - display("The maximum allowed length {} of the file include chain built by following nested includes is exceeded", max_depth) - } - MissingConfigPath { - display("Include paths from environment variables must not be relative.") - } - } + /// The error returned by [`GitConfig::from_paths()`][super::GitConfig::from_paths()] and [`GitConfig::from_env_paths()`][super::GitConfig::from_env_paths()]. + #[derive(Debug, thiserror::Error)] + #[allow(missing_docs)] + pub enum Error { + #[error(transparent)] + ParserOrIoError(#[from] parser::ParserOrIoError<'static>), + #[error(transparent)] + Interpolate(#[from] interpolate::Error), + #[error("The maximum allowed length {} of the file include chain built by following nested includes is exceeded", .max_depth)] + IncludeDepthExceeded { max_depth: u8 }, + #[error("Include paths from environment variables must not be relative")] + MissingConfigPath, } /// Options when loading git config using [`GitConfig::from_paths()`][super::GitConfig::from_paths()]. @@ -175,45 +163,30 @@ pub mod from_paths { } pub mod from_env { - use quick_error::quick_error; - use super::from_paths; use crate::values::path::interpolate; - quick_error! { - #[derive(Debug)] - /// Represents the errors that may occur when calling [`GitConfig::from_env`][crate::file::GitConfig::from_env()]. - #[allow(missing_docs)] - pub enum Error { - ParseError (err: String) { - display("GIT_CONFIG_COUNT was not a positive integer: {}", err) - } - InvalidKeyId (key_id: usize) { - display("GIT_CONFIG_KEY_{} was not set.", key_id) - } - InvalidKeyValue (key_id: usize, key_val: String) { - display("GIT_CONFIG_KEY_{} was set to an invalid value: {}", key_id, key_val) - } - InvalidValueId (value_id: usize) { - display("GIT_CONFIG_VALUE_{} was not set.", value_id) - } - PathInterpolationError (err: interpolate::Error) { - display("Could not interpolate path while loading a config file.") - source(err) - from() - } - FromPathsError (err: from_paths::Error) { - display("Could not load config from a file") - source(err) - from() - } - } + /// Represents the errors that may occur when calling [`GitConfig::from_env`][crate::file::GitConfig::from_env()]. + #[derive(Debug, thiserror::Error)] + #[allow(missing_docs)] + pub enum Error { + #[error("GIT_CONFIG_COUNT was not a positive integer: {}", .input)] + ParseError { input: String }, + #[error("GIT_CONFIG_KEY_{} was not set", .key_id)] + InvalidKeyId { key_id: usize }, + #[error("GIT_CONFIG_KEY_{} was set to an invalid value: {}", .key_id, .key_val)] + InvalidKeyValue { key_id: usize, key_val: String }, + #[error("GIT_CONFIG_VALUE_{} was not set", .value_id)] + InvalidValueId { value_id: usize }, + #[error(transparent)] + PathInterpolationError(#[from] interpolate::Error), + #[error(transparent)] + FromPathsError(#[from] from_paths::Error), } } impl<'event> GitConfig<'event> { /// Constructs an empty `git-config` file. - #[inline] #[must_use] pub fn new() -> Self { Self::default() @@ -225,7 +198,6 @@ impl<'event> GitConfig<'event> { /// /// Returns an error if there was an IO error or if the file wasn't a valid /// git-config file. - #[inline] pub fn open>(path: P) -> Result> { parse_from_path(path).map(Self::from) } @@ -239,7 +211,6 @@ impl<'event> GitConfig<'event> { /// git-config file. /// /// [`git-config`'s documentation]: https://git-scm.com/docs/git-config#Documentation/git-config.txt-FILES - #[inline] pub fn from_paths(paths: Vec, options: &from_paths::Options) -> Result { let mut target = Self::new(); for path in paths { @@ -375,14 +346,16 @@ impl<'event> GitConfig<'event> { pub fn from_env(options: &from_paths::Options) -> Result, from_env::Error> { use std::env; let count: usize = match env::var("GIT_CONFIG_COUNT") { - Ok(v) => v.parse().map_err(|_| from_env::Error::ParseError(v))?, + Ok(v) => v.parse().map_err(|_| from_env::Error::ParseError { input: v })?, Err(_) => return Ok(None), }; let mut config = Self::new(); for i in 0..count { - let key = env::var(format!("GIT_CONFIG_KEY_{}", i)).map_err(|_| from_env::Error::InvalidKeyId(i))?; - let value = env::var(format!("GIT_CONFIG_VALUE_{}", i)).map_err(|_| from_env::Error::InvalidValueId(i))?; + let key = + env::var(format!("GIT_CONFIG_KEY_{}", i)).map_err(|_| from_env::Error::InvalidKeyId { key_id: i })?; + let value = env::var(format!("GIT_CONFIG_VALUE_{}", i)) + .map_err(|_| from_env::Error::InvalidValueId { value_id: i })?; if let Some((section_name, maybe_subsection)) = key.split_once('.') { let (subsection, key) = if let Some((subsection, key)) = maybe_subsection.rsplit_once('.') { (Some(subsection), key) @@ -406,7 +379,10 @@ impl<'event> GitConfig<'event> { Cow::Owned(value.into_bytes()), ); } else { - return Err(from_env::Error::InvalidKeyValue(i, key.to_string())); + return Err(from_env::Error::InvalidKeyValue { + key_id: i, + key_val: key.to_string(), + }); } } @@ -432,7 +408,7 @@ impl<'event> GitConfig<'event> { /// # Examples /// /// ``` - /// # use git_config::file::{GitConfig, GitConfigError}; + /// # use git_config::file::{GitConfig}; /// # use git_config::values::{Integer, Boolean}; /// # use std::borrow::Cow; /// # use std::convert::TryFrom; @@ -457,15 +433,69 @@ impl<'event> GitConfig<'event> { /// /// [`values`]: crate::values /// [`TryFrom`]: std::convert::TryFrom - #[inline] - pub fn value<'lookup, T: TryFrom>>( + pub fn value>>( &'event self, - section_name: &'lookup str, - subsection_name: Option<&'lookup str>, - key: &'lookup str, - ) -> Result> { - T::try_from(self.get_raw_value(section_name, subsection_name, key)?) - .map_err(|_| GitConfigError::FailedConversion) + section_name: &str, + subsection_name: Option<&str>, + key: &str, + ) -> Result> { + T::try_from(self.raw_value(section_name, subsection_name, key)?).map_err(lookup::Error::FailedConversion) + } + + /// Like [`value()`][GitConfig::value()], but returning an `Option` if the value wasn't found. + pub fn try_value>>( + &'event self, + section_name: &str, + subsection_name: Option<&str>, + key: &str, + ) -> Option> { + self.raw_value(section_name, subsection_name, key).ok().map(T::try_from) + } + + /// Like [`value()`][GitConfig::value()], but returning an `Option` if the string wasn't found. + /// + /// As strings perform no conversions, this will never fail. + pub fn string( + &'event self, + section_name: &str, + subsection_name: Option<&str>, + key: &str, + ) -> Option> { + self.raw_value(section_name, subsection_name, key) + .ok() + .map(|v| values::String::from(v).value) + } + + /// Like [`value()`][GitConfig::value()], but returning an `Option` if the path wasn't found. + /// + /// Note that this path is not vetted and should only point to resources which can't be used + /// to pose a security risk. + /// + /// As paths perform no conversions, this will never fail. + // TODO: add `secure_path()` or similar to make use of our knowledge of the trust associated with each configuration + // file, maybe even remove the insecure version to force every caller to ask themselves if the resource can + // be used securely or not. + pub fn path( + &'event self, + section_name: &str, + subsection_name: Option<&str>, + key: &str, + ) -> Option> { + self.raw_value(section_name, subsection_name, key) + .ok() + .map(values::Path::from) + } + + /// Like [`value()`][GitConfig::value()], but returning an `Option` if the boolean wasn't found. + pub fn boolean( + &'event self, + section_name: &str, + subsection_name: Option<&str>, + key: &str, + ) -> Option> { + self.raw_value(section_name, subsection_name, key) + .ok() + .map(|v| values::Boolean::try_from(v).map(|b| b.to_bool())) } /// Returns all interpreted values given a section, an optional subsection @@ -481,7 +511,7 @@ impl<'event> GitConfig<'event> { /// # Examples /// /// ``` - /// # use git_config::file::{GitConfig, GitConfigError}; + /// # use git_config::file::{GitConfig}; /// # use git_config::values::{Integer, Bytes, Boolean, TrueVariant}; /// # use std::borrow::Cow; /// # use std::convert::TryFrom; @@ -507,7 +537,7 @@ impl<'event> GitConfig<'event> { /// // ... or explicitly declare the type to avoid the turbofish /// let c_value: Vec = git_config.multi_value("core", None, "c")?; /// assert_eq!(c_value, vec![Bytes { value: Cow::Borrowed(b"g") }]); - /// # Ok::<(), GitConfigError>(()) + /// # Ok::<(), Box>(()) /// ``` /// /// # Errors @@ -518,18 +548,17 @@ impl<'event> GitConfig<'event> { /// /// [`values`]: crate::values /// [`TryFrom`]: std::convert::TryFrom - #[inline] pub fn multi_value<'lookup, T: TryFrom>>( &'event self, section_name: &'lookup str, subsection_name: Option<&'lookup str>, key: &'lookup str, - ) -> Result, GitConfigError<'lookup>> { - self.get_raw_multi_value(section_name, subsection_name, key)? + ) -> Result, lookup::Error> { + self.raw_multi_value(section_name, subsection_name, key)? .into_iter() .map(T::try_from) .collect::, _>>() - .map_err(|_| GitConfigError::FailedConversion) + .map_err(lookup::Error::FailedConversion) } /// Returns an immutable section reference. @@ -542,15 +571,10 @@ impl<'event> GitConfig<'event> { &mut self, section_name: &'lookup str, subsection_name: Option<&'lookup str>, - ) -> Result<&SectionBody<'event>, GitConfigError<'lookup>> { - let section_ids = self.get_section_ids_by_name_and_subname(section_name, subsection_name)?; - let id = section_ids - .last() - .expect("Section lookup vec was empty, internal invariant violated"); - Ok(self - .sections - .get(id) - .expect("Section did not have id from lookup, internal invariant violated")) + ) -> Result<&SectionBody<'event>, lookup::existing::Error> { + let section_ids = self.section_ids_by_name_and_subname(section_name, subsection_name)?; + let id = section_ids.last().expect("BUG: Section lookup vec was empty"); + Ok(self.sections.get(id).expect("BUG: Section did not have id from lookup")) } /// Returns an mutable section reference. @@ -563,14 +587,14 @@ impl<'event> GitConfig<'event> { &mut self, section_name: &'lookup str, subsection_name: Option<&'lookup str>, - ) -> Result, GitConfigError<'lookup>> { - let section_ids = self.get_section_ids_by_name_and_subname(section_name, subsection_name)?; - let id = section_ids - .last() - .expect("Section lookup vec was empty, internal invariant violated"); - Ok(MutableSection::new(self.sections.get_mut(id).expect( - "Section did not have id from lookup, internal invariant violated", - ))) + ) -> Result, lookup::existing::Error> { + let section_ids = self.section_ids_by_name_and_subname(section_name, subsection_name)?; + let id = section_ids.last().expect("BUG: Section lookup vec was empty"); + Ok(MutableSection::new( + self.sections + .get_mut(id) + .expect("BUG: Section did not have id from lookup"), + )) } /// Gets all sections that match the provided name, ignoring any subsections. @@ -591,7 +615,7 @@ impl<'event> GitConfig<'event> { /// Calling this method will yield all sections: /// /// ``` - /// # use git_config::file::{GitConfig, GitConfigError}; + /// # use git_config::file::{GitConfig}; /// # use git_config::values::{Integer, Boolean, TrueVariant}; /// # use std::borrow::Cow; /// # use std::convert::TryFrom; @@ -608,7 +632,7 @@ impl<'event> GitConfig<'event> { /// ``` #[must_use] pub fn sections_by_name<'lookup>(&self, section_name: &'lookup str) -> Vec<&SectionBody<'event>> { - self.get_section_ids_by_name(section_name) + self.section_ids_by_name(section_name) .unwrap_or_default() .into_iter() .map(|id| { @@ -635,7 +659,7 @@ impl<'event> GitConfig<'event> { /// Calling this method will yield all section bodies and their header: /// /// ```rust - /// use git_config::file::{GitConfig, GitConfigError}; + /// use git_config::file::{GitConfig}; /// use git_config::parser::Key; /// use std::borrow::Cow; /// use std::convert::TryFrom; @@ -669,7 +693,7 @@ impl<'event> GitConfig<'event> { &self, section_name: &'lookup str, ) -> Vec<(&ParsedSectionHeader<'event>, &SectionBody<'event>)> { - self.get_section_ids_by_name(section_name) + self.section_ids_by_name(section_name) .unwrap_or_default() .into_iter() .map(|id| { @@ -694,7 +718,7 @@ impl<'event> GitConfig<'event> { /// Creating a new empty section: /// /// ``` - /// # use git_config::file::{GitConfig, GitConfigError}; + /// # use git_config::file::{GitConfig}; /// # use std::convert::TryFrom; /// let mut git_config = GitConfig::new(); /// let _section = git_config.new_section("hello", Some("world".into())); @@ -704,7 +728,7 @@ impl<'event> GitConfig<'event> { /// Creating a new empty section and adding values to it: /// /// ``` - /// # use git_config::file::{GitConfig, GitConfigError}; + /// # use git_config::file::{GitConfig}; /// # use std::convert::TryFrom; /// let mut git_config = GitConfig::new(); /// let mut section = git_config.new_section("hello", Some("world".into())); @@ -732,7 +756,7 @@ impl<'event> GitConfig<'event> { /// Creating and removing a section: /// /// ``` - /// # use git_config::file::{GitConfig, GitConfigError}; + /// # use git_config::file::{GitConfig}; /// # use std::convert::TryFrom; /// let mut git_config = GitConfig::try_from( /// r#"[hello "world"] @@ -746,7 +770,7 @@ impl<'event> GitConfig<'event> { /// Precedence example for removing sections with the same name: /// /// ``` - /// # use git_config::file::{GitConfig, GitConfigError}; + /// # use git_config::file::{GitConfig}; /// # use std::convert::TryFrom; /// let mut git_config = GitConfig::try_from( /// r#"[hello "world"] @@ -764,7 +788,7 @@ impl<'event> GitConfig<'event> { subsection_name: impl Into>, ) -> Option { let id = self - .get_section_ids_by_name_and_subname(section_name, subsection_name.into()) + .section_ids_by_name_and_subname(section_name, subsection_name.into()) .ok()? .pop()?; self.section_order.remove( @@ -817,8 +841,8 @@ impl<'event> GitConfig<'event> { subsection_name: impl Into>, new_section_name: impl Into>, new_subsection_name: impl Into>>, - ) -> Result<(), GitConfigError<'lookup>> { - let id = self.get_section_ids_by_name_and_subname(section_name, subsection_name.into())?; + ) -> Result<(), lookup::existing::Error> { + let id = self.section_ids_by_name_and_subname(section_name, subsection_name.into())?; let id = id .last() .expect("list of sections were empty, which violates invariant"); @@ -855,25 +879,25 @@ impl<'event> GitConfig<'event> { /// Returns an uninterpreted value given a section, an optional subsection /// and key. /// - /// Consider [`Self::get_raw_multi_value`] if you want to get all values of + /// Consider [`Self::raw_multi_value`] if you want to get all values of /// a multivar instead. /// /// # Errors /// /// This function will return an error if the key is not in the requested /// section and subsection, or if the section and subsection do not exist. - pub fn get_raw_value<'lookup>( + pub fn raw_value<'lookup>( &self, section_name: &'lookup str, subsection_name: Option<&'lookup str>, key: &'lookup str, - ) -> Result, GitConfigError<'lookup>> { + ) -> Result, lookup::existing::Error> { // Note: cannot wrap around the raw_multi_value method because we need // to guarantee that the highest section id is used (so that we follow // the "last one wins" resolution strategy by `git-config`). let key = Key(key.into()); for section_id in self - .get_section_ids_by_name_and_subname(section_name, subsection_name)? + .section_ids_by_name_and_subname(section_name, subsection_name)? .iter() .rev() { @@ -887,26 +911,26 @@ impl<'event> GitConfig<'event> { } } - Err(GitConfigError::KeyDoesNotExist) + Err(lookup::existing::Error::KeyMissing) } /// Returns a mutable reference to an uninterpreted value given a section, /// an optional subsection and key. /// - /// Consider [`Self::get_raw_multi_value_mut`] if you want to get mutable + /// Consider [`Self::raw_multi_value_mut`] if you want to get mutable /// references to all values of a multivar instead. /// /// # Errors /// /// This function will return an error if the key is not in the requested /// section and subsection, or if the section and subsection do not exist. - pub fn get_raw_value_mut<'lookup>( + pub fn raw_value_mut<'lookup>( &mut self, section_name: &'lookup str, subsection_name: Option<&'lookup str>, key: &'lookup str, - ) -> Result, GitConfigError<'lookup>> { - let section_ids = self.get_section_ids_by_name_and_subname(section_name, subsection_name)?; + ) -> Result, lookup::existing::Error> { + let section_ids = self.section_ids_by_name_and_subname(section_name, subsection_name)?; let key = Key(key.into()); for section_id in section_ids.iter().rev() { @@ -955,7 +979,7 @@ impl<'event> GitConfig<'event> { )); } - Err(GitConfigError::KeyDoesNotExist) + Err(lookup::existing::Error::KeyMissing) } /// Returns all uninterpreted values given a section, an optional subsection @@ -981,16 +1005,16 @@ impl<'event> GitConfig<'event> { /// # use std::convert::TryFrom; /// # let git_config = GitConfig::try_from("[core]a=b\n[core]\na=c\na=d").unwrap(); /// assert_eq!( - /// git_config.get_raw_multi_value("core", None, "a"), - /// Ok(vec![ + /// git_config.raw_multi_value("core", None, "a").unwrap(), + /// vec![ /// Cow::<[u8]>::Borrowed(b"b"), /// Cow::<[u8]>::Borrowed(b"c"), /// Cow::<[u8]>::Borrowed(b"d"), - /// ]), + /// ], /// ); /// ``` /// - /// Consider [`Self::get_raw_value`] if you want to get the resolved single + /// Consider [`Self::raw_value`] if you want to get the resolved single /// value for a given key, if your key does not support multi-valued values. /// /// # Errors @@ -998,14 +1022,14 @@ impl<'event> GitConfig<'event> { /// This function will return an error if the key is not in any requested /// section and subsection, or if no instance of the section and subsections /// exist. - pub fn get_raw_multi_value<'lookup>( + pub fn raw_multi_value( &self, - section_name: &'lookup str, - subsection_name: Option<&'lookup str>, - key: &'lookup str, - ) -> Result>, GitConfigError<'lookup>> { + section_name: &str, + subsection_name: Option<&str>, + key: &str, + ) -> Result>, lookup::existing::Error> { let mut values = vec![]; - for section_id in self.get_section_ids_by_name_and_subname(section_name, subsection_name)? { + for section_id in self.section_ids_by_name_and_subname(section_name, subsection_name)? { values.extend( self.sections .get(§ion_id) @@ -1017,7 +1041,7 @@ impl<'event> GitConfig<'event> { } if values.is_empty() { - Err(GitConfigError::KeyDoesNotExist) + Err(lookup::existing::Error::KeyMissing) } else { Ok(values) } @@ -1041,12 +1065,12 @@ impl<'event> GitConfig<'event> { /// Attempting to get all values of `a` yields the following: /// /// ``` - /// # use git_config::file::{GitConfig, GitConfigError}; + /// # use git_config::file::{GitConfig}; /// # use std::borrow::Cow; /// # use std::convert::TryFrom; /// # let mut git_config = GitConfig::try_from("[core]a=b\n[core]\na=c\na=d").unwrap(); /// assert_eq!( - /// git_config.get_raw_multi_value("core", None, "a")?, + /// git_config.raw_multi_value("core", None, "a")?, /// vec![ /// Cow::Borrowed(b"b"), /// Cow::Borrowed(b"c"), @@ -1054,20 +1078,20 @@ impl<'event> GitConfig<'event> { /// ] /// ); /// - /// git_config.get_raw_multi_value_mut("core", None, "a")?.set_str_all("g"); + /// git_config.raw_multi_value_mut("core", None, "a")?.set_str_all("g"); /// /// assert_eq!( - /// git_config.get_raw_multi_value("core", None, "a")?, + /// git_config.raw_multi_value("core", None, "a")?, /// vec![ /// Cow::Borrowed(b"g"), /// Cow::Borrowed(b"g"), /// Cow::Borrowed(b"g") /// ], /// ); - /// # Ok::<(), GitConfigError>(()) + /// # Ok::<(), git_config::lookup::existing::Error>(()) /// ``` /// - /// Consider [`Self::get_raw_value`] if you want to get the resolved single + /// Consider [`Self::raw_value`] if you want to get the resolved single /// value for a given key, if your key does not support multi-valued values. /// /// Note that this operation is relatively expensive, requiring a full @@ -1078,13 +1102,13 @@ impl<'event> GitConfig<'event> { /// This function will return an error if the key is not in any requested /// section and subsection, or if no instance of the section and subsections /// exist. - pub fn get_raw_multi_value_mut<'lookup>( + pub fn raw_multi_value_mut<'lookup>( &mut self, section_name: &'lookup str, subsection_name: Option<&'lookup str>, key: &'lookup str, - ) -> Result, GitConfigError<'lookup>> { - let section_ids = self.get_section_ids_by_name_and_subname(section_name, subsection_name)?; + ) -> Result, lookup::existing::Error> { + let section_ids = self.section_ids_by_name_and_subname(section_name, subsection_name)?; let key = Key(key.into()); let mut offsets = HashMap::new(); @@ -1125,7 +1149,7 @@ impl<'event> GitConfig<'event> { entries.sort(); if entries.is_empty() { - Err(GitConfigError::KeyDoesNotExist) + Err(lookup::existing::Error::KeyMissing) } else { Ok(MutableMultiValue::new(&mut self.sections, key, entries, offsets)) } @@ -1148,13 +1172,13 @@ impl<'event> GitConfig<'event> { /// Setting a new value to the key `core.a` will yield the following: /// /// ``` - /// # use git_config::file::{GitConfig, GitConfigError}; + /// # use git_config::file::{GitConfig}; /// # use std::borrow::Cow; /// # use std::convert::TryFrom; /// # let mut git_config = GitConfig::try_from("[core]a=b\n[core]\na=c\na=d").unwrap(); /// git_config.set_raw_value("core", None, "a", vec![b'e'])?; - /// assert_eq!(git_config.get_raw_value("core", None, "a")?, Cow::Borrowed(b"e")); - /// # Ok::<(), GitConfigError>(()) + /// assert_eq!(git_config.raw_value("core", None, "a")?, Cow::Borrowed(b"e")); + /// # Ok::<(), Box>(()) /// ``` /// /// # Errors @@ -1166,8 +1190,8 @@ impl<'event> GitConfig<'event> { subsection_name: Option<&'lookup str>, key: &'lookup str, new_value: Vec, - ) -> Result<(), GitConfigError<'lookup>> { - self.get_raw_value_mut(section_name, subsection_name, key) + ) -> Result<(), lookup::existing::Error> { + self.raw_value_mut(section_name, subsection_name, key) .map(|mut entry| entry.set_bytes(new_value)) } @@ -1180,7 +1204,7 @@ impl<'event> GitConfig<'event> { /// /// **Note**: Mutation order is _not_ guaranteed and is non-deterministic. /// If you need finer control over which values of the multivar are set, - /// consider using [`get_raw_multi_value_mut`], which will let you iterate + /// consider using [`raw_multi_value_mut`], which will let you iterate /// and check over the values instead. This is best used as a convenience /// function for setting multivars whose values should be treated as an /// unordered set. @@ -1200,7 +1224,7 @@ impl<'event> GitConfig<'event> { /// Setting an equal number of values: /// /// ``` - /// # use git_config::file::{GitConfig, GitConfigError}; + /// # use git_config::file::{GitConfig}; /// # use std::borrow::Cow; /// # use std::convert::TryFrom; /// # let mut git_config = GitConfig::try_from("[core]a=b\n[core]\na=c\na=d").unwrap(); @@ -1210,17 +1234,17 @@ impl<'event> GitConfig<'event> { /// Cow::Borrowed(b"z"), /// ]; /// git_config.set_raw_multi_value("core", None, "a", new_values.into_iter())?; - /// let fetched_config = git_config.get_raw_multi_value("core", None, "a")?; + /// let fetched_config = git_config.raw_multi_value("core", None, "a")?; /// assert!(fetched_config.contains(&Cow::Borrowed(b"x"))); /// assert!(fetched_config.contains(&Cow::Borrowed(b"y"))); /// assert!(fetched_config.contains(&Cow::Borrowed(b"z"))); - /// # Ok::<(), GitConfigError>(()) + /// # Ok::<(), git_config::lookup::existing::Error>(()) /// ``` /// /// Setting less than the number of present values sets the first ones found: /// /// ``` - /// # use git_config::file::{GitConfig, GitConfigError}; + /// # use git_config::file::{GitConfig}; /// # use std::borrow::Cow; /// # use std::convert::TryFrom; /// # let mut git_config = GitConfig::try_from("[core]a=b\n[core]\na=c\na=d").unwrap(); @@ -1229,16 +1253,16 @@ impl<'event> GitConfig<'event> { /// Cow::Borrowed(b"y"), /// ]; /// git_config.set_raw_multi_value("core", None, "a", new_values.into_iter())?; - /// let fetched_config = git_config.get_raw_multi_value("core", None, "a")?; + /// let fetched_config = git_config.raw_multi_value("core", None, "a")?; /// assert!(fetched_config.contains(&Cow::Borrowed(b"x"))); /// assert!(fetched_config.contains(&Cow::Borrowed(b"y"))); - /// # Ok::<(), GitConfigError>(()) + /// # Ok::<(), git_config::lookup::existing::Error>(()) /// ``` /// /// Setting more than the number of present values discards the rest: /// /// ``` - /// # use git_config::file::{GitConfig, GitConfigError}; + /// # use git_config::file::{GitConfig}; /// # use std::borrow::Cow; /// # use std::convert::TryFrom; /// # let mut git_config = GitConfig::try_from("[core]a=b\n[core]\na=c\na=d").unwrap(); @@ -1249,23 +1273,23 @@ impl<'event> GitConfig<'event> { /// Cow::Borrowed(b"discarded"), /// ]; /// git_config.set_raw_multi_value("core", None, "a", new_values.into_iter())?; - /// assert!(!git_config.get_raw_multi_value("core", None, "a")?.contains(&Cow::Borrowed(b"discarded"))); - /// # Ok::<(), GitConfigError>(()) + /// assert!(!git_config.raw_multi_value("core", None, "a")?.contains(&Cow::Borrowed(b"discarded"))); + /// # Ok::<(), git_config::lookup::existing::Error>(()) /// ``` /// /// # Errors /// /// This errors if any lookup input (section, subsection, and key value) fails. /// - /// [`get_raw_multi_value_mut`]: Self::get_raw_multi_value_mut + /// [`raw_multi_value_mut`]: Self::raw_multi_value_mut pub fn set_raw_multi_value<'lookup>( &mut self, section_name: &'lookup str, subsection_name: Option<&'lookup str>, key: &'lookup str, new_values: impl Iterator>, - ) -> Result<(), GitConfigError<'lookup>> { - self.get_raw_multi_value_mut(section_name, subsection_name, key) + ) -> Result<(), lookup::existing::Error> { + self.raw_multi_value_mut(section_name, subsection_name, key) .map(|mut v| v.set_values(new_values)) } } @@ -1321,16 +1345,16 @@ impl<'event> GitConfig<'event> { } /// Returns the mapping between section and subsection name to section ids. - fn get_section_ids_by_name_and_subname<'lookup>( + fn section_ids_by_name_and_subname<'lookup>( &self, section_name: impl Into>, subsection_name: Option<&'lookup str>, - ) -> Result, GitConfigError<'lookup>> { + ) -> Result, lookup::existing::Error> { let section_name = section_name.into(); let section_ids = self .section_lookup_tree .get(§ion_name) - .ok_or(GitConfigError::SectionDoesNotExist(section_name))?; + .ok_or(lookup::existing::Error::SectionMissing)?; let mut maybe_ids = None; // Don't simplify if and matches here -- the for loop currently needs // `n + 1` checks, while the if and matches will result in the for loop @@ -1352,13 +1376,13 @@ impl<'event> GitConfig<'event> { } maybe_ids .map(Vec::to_owned) - .ok_or(GitConfigError::SubSectionDoesNotExist(subsection_name)) + .ok_or(lookup::existing::Error::SubSectionMissing) } - fn get_section_ids_by_name<'lookup>( + fn section_ids_by_name<'lookup>( &self, section_name: impl Into>, - ) -> Result, GitConfigError<'lookup>> { + ) -> Result, lookup::existing::Error> { let section_name = section_name.into(); self.section_lookup_tree .get(§ion_name) @@ -1371,7 +1395,7 @@ impl<'event> GitConfig<'event> { }) .collect() }) - .ok_or(GitConfigError::SectionDoesNotExist(section_name)) + .ok_or(lookup::existing::Error::SectionMissing) } } @@ -1382,7 +1406,6 @@ impl<'a> TryFrom<&'a str> for GitConfig<'a> { /// [`GitConfig`]. See [`parse_from_str`] for more information. /// /// [`parse_from_str`]: crate::parser::parse_from_str - #[inline] fn try_from(s: &'a str) -> Result, Self::Error> { parse_from_str(s).map(Self::from) } @@ -1395,7 +1418,6 @@ impl<'a> TryFrom<&'a [u8]> for GitConfig<'a> { //// a [`GitConfig`]. See [`parse_from_bytes`] for more information. /// /// [`parse_from_bytes`]: crate::parser::parse_from_bytes - #[inline] fn try_from(value: &'a [u8]) -> Result, Self::Error> { parse_from_bytes(value).map(GitConfig::from) } @@ -1408,7 +1430,6 @@ impl<'a> TryFrom<&'a Vec> for GitConfig<'a> { //// a [`GitConfig`]. See [`parse_from_bytes`] for more information. /// /// [`parse_from_bytes`]: crate::parser::parse_from_bytes - #[inline] fn try_from(value: &'a Vec) -> Result, Self::Error> { parse_from_bytes(value).map(GitConfig::from) } @@ -1458,7 +1479,6 @@ impl<'a> From> for GitConfig<'a> { } impl From> for Vec { - #[inline] fn from(c: GitConfig) -> Self { c.into() } diff --git a/git-config/src/file/mod.rs b/git-config/src/file/mod.rs index 27c4bd4d97..c133b572ad 100644 --- a/git-config/src/file/mod.rs +++ b/git-config/src/file/mod.rs @@ -1,6 +1,5 @@ //! This module provides a high level wrapper around a single `git-config` file. -mod error; mod git_config; mod resolved; mod section; @@ -8,7 +7,6 @@ mod value; use std::ops::{Add, AddAssign}; -pub use error::*; pub use resolved::*; pub use section::*; pub use value::*; diff --git a/git-config/src/file/resolved.rs b/git-config/src/file/resolved.rs index e97c1f5cf3..42334dac0c 100644 --- a/git-config/src/file/resolved.rs +++ b/git-config/src/file/resolved.rs @@ -31,7 +31,6 @@ impl ResolvedGitConfig<'static> { /// /// This returns an error if an IO error occurs, or if the file is not a /// valid `git-config` file. - #[inline] pub fn open>(path: P) -> Result> { GitConfig::open(path.as_ref()).map(Self::from) } @@ -107,14 +106,12 @@ fn resolve_sections<'key, 'data>( impl TryFrom<&Path> for ResolvedGitConfig<'static> { type Error = parser::ParserOrIoError<'static>; - #[inline] fn try_from(path: &Path) -> Result { Self::open(path) } } impl<'data> From> for ResolvedGitConfig<'data> { - #[inline] fn from(config: GitConfig<'data>) -> Self { Self::from_config(config) } diff --git a/git-config/src/file/section.rs b/git-config/src/file/section.rs index ada8b479e4..a8aadcc5f9 100644 --- a/git-config/src/file/section.rs +++ b/git-config/src/file/section.rs @@ -7,7 +7,8 @@ use std::{ }; use crate::{ - file::{error::GitConfigError, Index}, + file::Index, + lookup, parser::{Event, Key}, values::{normalize_cow, normalize_vec}, }; @@ -72,7 +73,7 @@ impl<'borrow, 'event> MutableSection<'borrow, 'event> { /// Returns the previous value if it replaced a value, or None if it adds /// the value. pub fn set(&mut self, key: Key<'event>, value: Cow<'event, [u8]>) -> Option> { - let range = self.get_value_range_by_key(&key); + let range = self.value_range_by_key(&key); if range.is_empty() { self.push(key, value); return None; @@ -85,7 +86,7 @@ impl<'borrow, 'event> MutableSection<'borrow, 'event> { /// Removes the latest value by key and returns it, if it exists. pub fn remove(&mut self, key: &Key<'event>) -> Option> { - let range = self.get_value_range_by_key(key); + let range = self.value_range_by_key(key); if range.is_empty() { return None; } @@ -113,14 +114,12 @@ impl<'borrow, 'event> MutableSection<'borrow, 'event> { /// Adds a new line event. Note that you don't need to call this unless /// you've disabled implicit newlines. - #[inline] pub fn push_newline(&mut self) { self.section.0.push(Event::Newline("\n".into())); } /// Enables or disables automatically adding newline events after adding /// a value. This is enabled by default. - #[inline] pub fn implicit_newline(&mut self, on: bool) { self.implicit_newline = on; } @@ -128,14 +127,12 @@ impl<'borrow, 'event> MutableSection<'borrow, 'event> { /// Sets the number of spaces before the start of a key value. By default, /// this is set to two. Set to 0 to disable adding whitespace before a key /// value. - #[inline] pub fn set_whitespace(&mut self, num: usize) { self.whitespace = num; } /// Returns the number of whitespace this section will insert before the /// beginning of a key. - #[inline] #[must_use] pub const fn whitespace(&self) -> usize { self.whitespace @@ -144,7 +141,6 @@ impl<'borrow, 'event> MutableSection<'borrow, 'event> { // Internal methods that may require exact indices for faster operations. impl<'borrow, 'event> MutableSection<'borrow, 'event> { - #[inline] pub(super) fn new(section: &'borrow mut SectionBody<'event>) -> Self { Self { section, @@ -158,7 +154,7 @@ impl<'borrow, 'event> MutableSection<'borrow, 'event> { key: &Key<'key>, start: Index, end: Index, - ) -> Result, GitConfigError<'key>> { + ) -> Result, lookup::existing::Error> { let mut found_key = false; let mut latest_value = None; let mut partial_value = None; @@ -188,10 +184,9 @@ impl<'borrow, 'event> MutableSection<'borrow, 'event> { latest_value .map(normalize_cow) .or_else(|| partial_value.map(normalize_vec)) - .ok_or(GitConfigError::KeyDoesNotExist) + .ok_or(lookup::existing::Error::KeyMissing) } - #[inline] pub(super) fn delete(&mut self, start: Index, end: Index) { self.section.0.drain(start.0..=end.0); } @@ -206,7 +201,6 @@ impl<'borrow, 'event> MutableSection<'borrow, 'event> { impl<'event> Deref for MutableSection<'_, 'event> { type Target = SectionBody<'event>; - #[inline] fn deref(&self) -> &Self::Target { self.section } @@ -227,7 +221,6 @@ impl<'event> SectionBody<'event> { } /// Constructs a new empty section body. - #[inline] pub(super) fn new() -> Self { Self::default() } @@ -240,7 +233,7 @@ impl<'event> SectionBody<'event> { #[allow(clippy::missing_panics_doc)] #[must_use] pub fn value(&self, key: &Key) -> Option> { - let range = self.get_value_range_by_key(key); + let range = self.value_range_by_key(key); if range.is_empty() { return None; } @@ -276,10 +269,9 @@ impl<'event> SectionBody<'event> { /// # Errors /// /// Returns an error if the key was not found, or if the conversion failed. - #[inline] - pub fn value_as>>(&self, key: &Key) -> Result> { - T::try_from(self.value(key).ok_or(GitConfigError::KeyDoesNotExist)?) - .map_err(|_| GitConfigError::FailedConversion) + pub fn value_as>>(&self, key: &Key) -> Result> { + T::try_from(self.value(key).ok_or(lookup::existing::Error::KeyMissing)?) + .map_err(lookup::Error::FailedConversion) } /// Retrieves all values that have the provided key name. This may return @@ -325,17 +317,15 @@ impl<'event> SectionBody<'event> { /// # Errors /// /// Returns an error if the conversion failed. - #[inline] - pub fn values_as>>(&self, key: &Key) -> Result, GitConfigError<'event>> { + pub fn values_as>>(&self, key: &Key) -> Result, lookup::Error> { self.values(key) .into_iter() .map(T::try_from) .collect::, _>>() - .map_err(|_| GitConfigError::FailedConversion) + .map_err(lookup::Error::FailedConversion) } /// Returns an iterator visiting all keys in order. - #[inline] pub fn keys(&self) -> impl Iterator> { self.0 .iter() @@ -353,14 +343,12 @@ impl<'event> SectionBody<'event> { } /// Returns the number of entries in the section. - #[inline] #[must_use] pub fn len(&self) -> usize { self.0.iter().filter(|e| matches!(e, Event::Key(_))).count() } /// Returns if the section is empty. - #[inline] #[must_use] pub fn is_empty(&self) -> bool { self.0.is_empty() @@ -368,7 +356,7 @@ impl<'event> SectionBody<'event> { /// Returns the the range containing the value events for the section. /// If the value is not found, then this returns an empty range. - fn get_value_range_by_key(&self, key: &Key<'event>) -> Range { + fn value_range_by_key(&self, key: &Key<'event>) -> Range { let mut values_start = 0; // value end needs to be offset by one so that the last value's index // is included in the range @@ -406,7 +394,6 @@ impl<'event> IntoIterator for SectionBody<'event> { type IntoIter = SectionBodyIter<'event>; - #[inline] fn into_iter(self) -> Self::IntoIter { SectionBodyIter(self.0.into()) } @@ -448,7 +435,6 @@ impl<'event> Iterator for SectionBodyIter<'event> { impl FusedIterator for SectionBodyIter<'_> {} impl<'event> From>> for SectionBody<'event> { - #[inline] fn from(e: Vec>) -> Self { Self(e) } diff --git a/git-config/src/file/value.rs b/git-config/src/file/value.rs index 1935ff253f..63b3e07b3e 100644 --- a/git-config/src/file/value.rs +++ b/git-config/src/file/value.rs @@ -6,11 +6,11 @@ use std::{ use crate::{ file::{ - error::GitConfigError, git_config::SectionId, section::{MutableSection, SectionBody}, Index, Size, }, + lookup, parser::{Event, Key}, values::{normalize_bytes, normalize_vec}, }; @@ -54,15 +54,13 @@ impl<'borrow, 'lookup, 'event> MutableValue<'borrow, 'lookup, 'event> { /// # Errors /// /// Returns an error if the lookup failed. - #[inline] - pub fn get(&self) -> Result, GitConfigError> { + pub fn get(&self) -> Result, lookup::existing::Error> { self.section.get(&self.key, self.index, self.index + self.size) } /// Update the value to the provided one. This modifies the value such that /// the Value event(s) are replaced with a single new event containing the /// new value. - #[inline] pub fn set_string(&mut self, input: String) { self.set_bytes(input.into_bytes()); } @@ -149,7 +147,7 @@ impl<'borrow, 'lookup, 'event> MutableMultiValue<'borrow, 'lookup, 'event> { /// # Errors /// /// Returns an error if the lookup failed. - pub fn get(&self) -> Result>, GitConfigError> { + pub fn get(&self) -> Result>, lookup::existing::Error> { let mut found_key = false; let mut values = vec![]; let mut partial_value = None; @@ -160,7 +158,7 @@ impl<'borrow, 'lookup, 'event> MutableMultiValue<'borrow, 'lookup, 'event> { offset_index, } in &self.indices_and_sizes { - let (offset, size) = MutableMultiValue::get_index_and_size(&self.offsets, *section_id, *offset_index); + let (offset, size) = MutableMultiValue::index_and_size(&self.offsets, *section_id, *offset_index); for event in &self .section .get(section_id) @@ -190,14 +188,13 @@ impl<'borrow, 'lookup, 'event> MutableMultiValue<'borrow, 'lookup, 'event> { } if values.is_empty() { - return Err(GitConfigError::KeyDoesNotExist); + return Err(lookup::existing::Error::KeyMissing); } Ok(values) } /// Returns the size of values the multivar has. - #[inline] #[must_use] pub fn len(&self) -> usize { self.indices_and_sizes.len() @@ -205,7 +202,6 @@ impl<'borrow, 'lookup, 'event> MutableMultiValue<'borrow, 'lookup, 'event> { /// Returns if the multivar has any values. This might occur if the value /// was deleted but not set with a new value. - #[inline] #[must_use] pub fn is_empty(&self) -> bool { self.indices_and_sizes.is_empty() @@ -216,7 +212,6 @@ impl<'borrow, 'lookup, 'event> MutableMultiValue<'borrow, 'lookup, 'event> { /// # Safety /// /// This will panic if the index is out of range. - #[inline] pub fn set_string(&mut self, index: usize, input: String) { self.set_bytes(index, input.into_bytes()); } @@ -226,7 +221,6 @@ impl<'borrow, 'lookup, 'event> MutableMultiValue<'borrow, 'lookup, 'event> { /// # Safety /// /// This will panic if the index is out of range. - #[inline] pub fn set_bytes(&mut self, index: usize, input: Vec) { self.set_value(index, Cow::Owned(input)); } @@ -260,7 +254,6 @@ impl<'borrow, 'lookup, 'event> MutableMultiValue<'borrow, 'lookup, 'event> { /// remaining values are ignored. /// /// [`zip`]: std::iter::Iterator::zip - #[inline] pub fn set_values<'a: 'event>(&mut self, input: impl Iterator>) { for ( EntryData { @@ -285,14 +278,12 @@ impl<'borrow, 'lookup, 'event> MutableMultiValue<'borrow, 'lookup, 'event> { /// Sets all values in this multivar to the provided one by copying the /// input for all values. - #[inline] pub fn set_str_all(&mut self, input: &str) { self.set_owned_values_all(input.as_bytes()); } /// Sets all values in this multivar to the provided one by copying the /// input bytes for all values. - #[inline] pub fn set_owned_values_all(&mut self, input: &[u8]) { for EntryData { section_id, @@ -319,7 +310,6 @@ impl<'borrow, 'lookup, 'event> MutableMultiValue<'borrow, 'lookup, 'event> { /// need for a more ergonomic interface. /// /// [`GitConfig`]: super::GitConfig - #[inline] pub fn set_values_all<'a: 'event>(&mut self, input: &'a [u8]) { for EntryData { section_id, @@ -347,7 +337,7 @@ impl<'borrow, 'lookup, 'event> MutableMultiValue<'borrow, 'lookup, 'event> { offset_index: usize, input: Cow<'a, [u8]>, ) { - let (offset, size) = MutableMultiValue::get_index_and_size(offsets, section_id, offset_index); + let (offset, size) = MutableMultiValue::index_and_size(offsets, section_id, offset_index); section.as_mut().drain(offset..offset + size); MutableMultiValue::set_offset(offsets, section_id, offset_index, 3); @@ -369,7 +359,7 @@ impl<'borrow, 'lookup, 'event> MutableMultiValue<'borrow, 'lookup, 'event> { section_id, offset_index, } = &self.indices_and_sizes[index]; - let (offset, size) = MutableMultiValue::get_index_and_size(&self.offsets, *section_id, *offset_index); + let (offset, size) = MutableMultiValue::index_and_size(&self.offsets, *section_id, *offset_index); if size > 0 { self.section .get_mut(section_id) @@ -390,7 +380,7 @@ impl<'borrow, 'lookup, 'event> MutableMultiValue<'borrow, 'lookup, 'event> { offset_index, } in &self.indices_and_sizes { - let (offset, size) = MutableMultiValue::get_index_and_size(&self.offsets, *section_id, *offset_index); + let (offset, size) = MutableMultiValue::index_and_size(&self.offsets, *section_id, *offset_index); if size > 0 { self.section .get_mut(section_id) @@ -405,8 +395,7 @@ impl<'borrow, 'lookup, 'event> MutableMultiValue<'borrow, 'lookup, 'event> { // SectionId is the same size as a reference, which means it's just as // efficient passing in a value instead of a reference. - #[inline] - fn get_index_and_size( + fn index_and_size( offsets: &'lookup HashMap>, section_id: SectionId, offset_index: usize, @@ -424,7 +413,6 @@ impl<'borrow, 'lookup, 'event> MutableMultiValue<'borrow, 'lookup, 'event> { // // SectionId is the same size as a reference, which means it's just as // efficient passing in a value instead of a reference. - #[inline] fn set_offset( offsets: &mut HashMap>, section_id: SectionId, diff --git a/git-config/src/fs.rs b/git-config/src/fs.rs index fcd0904f1a..62db5f5cbf 100644 --- a/git-config/src/fs.rs +++ b/git-config/src/fs.rs @@ -7,7 +7,8 @@ use std::{ path::{Path, PathBuf}, }; -use crate::file::{from_paths, GitConfig, GitConfigError}; +use crate::file::{from_paths, GitConfig}; +use crate::lookup; #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] pub enum ConfigSource { @@ -40,7 +41,6 @@ pub struct ConfigBuilder { impl ConfigBuilder { /// Constructs a new builder that finds the default location - #[inline] #[must_use] pub fn new() -> Self { Self { @@ -146,7 +146,6 @@ pub struct Config<'config> { } impl<'config> Config<'config> { - #[inline] #[must_use] pub fn value>>( &'config self, @@ -177,13 +176,12 @@ impl<'config> Config<'config> { None } - #[inline] pub fn try_value<'lookup, T: TryFrom>>( &'config self, section_name: &'lookup str, subsection_name: Option<&'lookup str>, key: &'lookup str, - ) -> Result, GitConfigError<'lookup>> { + ) -> Result, lookup::Error> { self.try_value_with_source(section_name, subsection_name, key) .map(|res| res.map(|(value, _)| value)) } @@ -197,7 +195,7 @@ impl<'config> Config<'config> { section_name: &'lookup str, subsection_name: Option<&'lookup str>, key: &'lookup str, - ) -> Result, GitConfigError<'lookup>> { + ) -> Result, lookup::Error> { let mapping = self.mapping(); for (conf, source) in mapping.iter() { @@ -227,7 +225,7 @@ impl<'config> Config<'config> { /// Retrieves the underlying [`GitConfig`] object, if one was found during /// initialization. #[must_use] - pub fn get_config(&self, source: ConfigSource) -> Option<&GitConfig<'config>> { + pub fn config(&self, source: ConfigSource) -> Option<&GitConfig<'config>> { match source { ConfigSource::System => self.system_conf.as_ref(), ConfigSource::Global => self.global_conf.as_ref(), @@ -241,7 +239,7 @@ impl<'config> Config<'config> { /// Retrieves the underlying [`GitConfig`] object as a mutable reference, /// if one was found during initialization. #[must_use] - pub fn get_config_mut(&mut self, source: ConfigSource) -> Option<&mut GitConfig<'config>> { + pub fn config_mut(&mut self, source: ConfigSource) -> Option<&mut GitConfig<'config>> { match source { ConfigSource::System => self.system_conf.as_mut(), ConfigSource::Global => self.global_conf.as_mut(), diff --git a/git-config/src/lib.rs b/git-config/src/lib.rs index 238b250344..b7e89f1259 100644 --- a/git-config/src/lib.rs +++ b/git-config/src/lib.rs @@ -54,10 +54,67 @@ #[cfg(feature = "serde")] extern crate serde_crate as serde; +pub mod lookup { + + /// The error when looking up a value. + #[derive(Debug, thiserror::Error)] + pub enum Error { + #[error(transparent)] + ValueMissing(#[from] crate::lookup::existing::Error), + #[error(transparent)] + FailedConversion(E), + } + + pub mod existing { + /// The error when looking up a value that doesn't exist. + #[derive(Debug, thiserror::Error)] + pub enum Error { + #[error("The requested section does not exist")] + SectionMissing, + #[error("The requested subsection does not exist")] + SubSectionMissing, + #[error("The key does not exist in the requested section")] + KeyMissing, + } + } +} + pub mod file; pub mod fs; pub mod parser; pub mod values; +/// The future home of the `values` module (TODO). +pub mod value { + pub mod parse { + use bstr::BString; + + /// The error returned when creating `Integer` from byte string. + #[derive(Debug, thiserror::Error, Eq, PartialEq)] + #[allow(missing_docs)] + #[error("Could not decode '{}': {}", .input, .message)] + pub struct Error { + pub message: &'static str, + pub input: BString, + #[source] + pub utf8_err: Option, + } + + impl Error { + pub(crate) fn new(message: &'static str, input: impl Into) -> Self { + Error { + message, + input: input.into(), + utf8_err: None, + } + } + + pub(crate) fn with_err(mut self, err: std::str::Utf8Error) -> Self { + self.utf8_err = Some(err); + self + } + } + } +} mod permissions { use crate::Permissions; diff --git a/git-config/src/parser.rs b/git-config/src/parser.rs index 1c6800d588..3d59ff26e2 100644 --- a/git-config/src/parser.rs +++ b/git-config/src/parser.rs @@ -75,7 +75,6 @@ impl Event<'_> { /// Generates a byte representation of the value. This should be used when /// non-UTF-8 sequences are present or a UTF-8 representation can't be /// guaranteed. - #[inline] #[must_use] pub fn to_vec(&self) -> Vec { self.into() @@ -95,7 +94,6 @@ impl Event<'_> { /// not. /// /// [`clone`]: Self::clone - #[inline] #[must_use] pub fn to_owned(&self) -> Event<'static> { match self { @@ -116,7 +114,6 @@ impl Display for Event<'_> { /// Note that this is a best-effort attempt at printing an `Event`. If /// there are non UTF-8 values in your config, this will _NOT_ render /// as read. - #[inline] fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Value(e) | Self::ValueNotDone(e) | Self::ValueDone(e) => match std::str::from_utf8(e) { @@ -133,14 +130,12 @@ impl Display for Event<'_> { } impl From> for Vec { - #[inline] fn from(event: Event) -> Self { event.into() } } impl From<&Event<'_>> for Vec { - #[inline] fn from(event: &Event) -> Self { match event { Event::Value(e) | Event::ValueNotDone(e) | Event::ValueDone(e) => e.to_vec(), @@ -177,7 +172,6 @@ impl ParsedSection<'_> { /// not. /// /// [`clone`]: Self::clone - #[inline] #[must_use] pub fn to_owned(&self) -> ParsedSection<'static> { ParsedSection { @@ -218,7 +212,6 @@ macro_rules! generate_case_insensitive { /// while `clone` does not. /// /// [`clone`]: Self::clone - #[inline] #[must_use] pub fn to_owned(&self) -> $name<'static> { $name(Cow::Owned(self.0.clone().into_owned())) @@ -226,21 +219,18 @@ macro_rules! generate_case_insensitive { } impl PartialEq for $name<'_> { - #[inline] fn eq(&self, other: &Self) -> bool { self.0.eq_ignore_ascii_case(&other.0) } } impl Display for $name<'_> { - #[inline] fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.0.fmt(f) } } impl PartialOrd for $name<'_> { - #[inline] fn partial_cmp(&self, other: &Self) -> Option { self.0 .to_ascii_lowercase() @@ -249,21 +239,18 @@ macro_rules! generate_case_insensitive { } impl std::hash::Hash for $name<'_> { - #[inline] fn hash(&self, state: &mut H) { self.0.to_ascii_lowercase().hash(state) } } impl<'a> From<&'a str> for $name<'a> { - #[inline] fn from(s: &'a str) -> Self { Self(Cow::Borrowed(s)) } } impl<'a> From> for $name<'a> { - #[inline] fn from(s: Cow<'a, str>) -> Self { Self(s) } @@ -272,7 +259,6 @@ macro_rules! generate_case_insensitive { impl<'a> std::ops::Deref for $name<'a> { type Target = $cow_inner_type; - #[inline] fn deref(&self) -> &Self::Target { &self.0 } @@ -316,7 +302,6 @@ impl ParsedSectionHeader<'_> { /// non-UTF-8 sequences are present or a UTF-8 representation can't be /// guaranteed. #[must_use] - #[inline] pub fn to_vec(&self) -> Vec { self.into() } @@ -335,7 +320,6 @@ impl ParsedSectionHeader<'_> { /// not. /// /// [`clone`]: Self::clone - #[inline] #[must_use] pub fn to_owned(&self) -> ParsedSectionHeader<'static> { ParsedSectionHeader { @@ -366,21 +350,18 @@ impl Display for ParsedSectionHeader<'_> { } impl From> for Vec { - #[inline] fn from(header: ParsedSectionHeader) -> Self { header.into() } } impl From<&ParsedSectionHeader<'_>> for Vec { - #[inline] fn from(header: &ParsedSectionHeader) -> Self { header.to_string().into_bytes() } } impl<'a> From> for Event<'a> { - #[inline] fn from(header: ParsedSectionHeader) -> Event { Event::SectionHeader(header) } @@ -410,7 +391,6 @@ impl ParsedComment<'_> { /// not. /// /// [`clone`]: Self::clone - #[inline] #[must_use] pub fn to_owned(&self) -> ParsedComment<'static> { ParsedComment { @@ -435,14 +415,12 @@ impl Display for ParsedComment<'_> { } impl From> for Vec { - #[inline] fn from(c: ParsedComment) -> Self { c.into() } } impl From<&ParsedComment<'_>> for Vec { - #[inline] fn from(c: &ParsedComment) -> Self { let mut values = vec![c.comment_tag as u8]; values.extend(c.comment.iter()); @@ -463,14 +441,12 @@ pub struct Error<'a> { impl Error<'_> { /// The one-indexed line number where the error occurred. This is determined /// by the number of newlines that were successfully parsed. - #[inline] #[must_use] pub const fn line_number(&self) -> usize { self.line_number + 1 } /// The remaining data that was left unparsed. - #[inline] #[must_use] pub fn remaining_data(&self) -> &[u8] { &self.parsed_until @@ -490,7 +466,6 @@ impl Error<'_> { /// not. /// /// [`clone`]: std::clone::Clone::clone - #[inline] #[must_use] pub fn to_owned(&self) -> Error<'static> { Error { @@ -567,7 +542,6 @@ impl ParserOrIoError<'_> { } impl Display for ParserOrIoError<'_> { - #[inline] fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { ParserOrIoError::Parser(e) => e.fmt(f), @@ -577,7 +551,6 @@ impl Display for ParserOrIoError<'_> { } impl From for ParserOrIoError<'_> { - #[inline] fn from(e: std::io::Error) -> Self { Self::Io(e) } @@ -594,7 +567,6 @@ enum ParserNode { } impl Display for ParserNode { - #[inline] fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::SectionHeader => write!(f, "section header"), @@ -822,7 +794,6 @@ impl<'a> Parser<'a> { /// a section) from the parser. Consider [`Parser::take_frontmatter`] if /// you need an owned copy only once. If that function was called, then this /// will always return an empty slice. - #[inline] #[must_use] pub fn frontmatter(&self) -> &[Event<'a>] { &self.frontmatter @@ -832,7 +803,6 @@ impl<'a> Parser<'a> { /// a section) from the parser. Subsequent calls will return an empty vec. /// Consider [`Parser::frontmatter`] if you only need a reference to the /// frontmatter - #[inline] pub fn take_frontmatter(&mut self) -> Vec> { std::mem::take(&mut self.frontmatter) } @@ -840,7 +810,6 @@ impl<'a> Parser<'a> { /// Returns the parsed sections from the parser. Consider /// [`Parser::take_sections`] if you need an owned copy only once. If that /// function was called, then this will always return an empty slice. - #[inline] #[must_use] pub fn sections(&self) -> &[ParsedSection<'a>] { &self.sections @@ -849,7 +818,6 @@ impl<'a> Parser<'a> { /// Takes the parsed sections from the parser. Subsequent calls will return /// an empty vec. Consider [`Parser::sections`] if you only need a reference /// to the comments. - #[inline] pub fn take_sections(&mut self) -> Vec> { let mut to_return = vec![]; std::mem::swap(&mut self.sections, &mut to_return); @@ -857,7 +825,6 @@ impl<'a> Parser<'a> { } /// Consumes the parser to produce a Vec of Events. - #[inline] #[must_use] pub fn into_vec(self) -> Vec> { self.into_iter().collect() @@ -878,7 +845,6 @@ impl<'a> Parser<'a> { impl<'a> TryFrom<&'a str> for Parser<'a> { type Error = Error<'a>; - #[inline] fn try_from(value: &'a str) -> Result { parse_from_str(value) } @@ -887,7 +853,6 @@ impl<'a> TryFrom<&'a str> for Parser<'a> { impl<'a> TryFrom<&'a [u8]> for Parser<'a> { type Error = Error<'a>; - #[inline] fn try_from(value: &'a [u8]) -> Result { parse_from_bytes(value) } @@ -925,7 +890,6 @@ pub fn parse_from_path>(path: P) -> Result, Parse /// Returns an error if the string provided is not a valid `git-config`. /// This generally is due to either invalid names or if there's extraneous /// data succeeding valid `git-config` data. -#[inline] pub fn parse_from_str(input: &str) -> Result { parse_from_bytes(input.as_bytes()) } diff --git a/git-config/src/values.rs b/git-config/src/values.rs index 80620d2378..b6752b2f24 100644 --- a/git-config/src/values.rs +++ b/git-config/src/values.rs @@ -2,8 +2,8 @@ use std::{borrow::Cow, convert::TryFrom, fmt::Display, str::FromStr}; -use bstr::BStr; -use quick_error::quick_error; +use crate::value; +use bstr::{BStr, BString}; #[cfg(feature = "serde")] use serde::{Serialize, Serializer}; @@ -120,21 +120,18 @@ pub fn normalize_cow(input: Cow<'_, [u8]>) -> Cow<'_, [u8]> { } /// `&[u8]` variant of [`normalize_cow`]. -#[inline] #[must_use] pub fn normalize_bytes(input: &[u8]) -> Cow<'_, [u8]> { normalize_cow(Cow::Borrowed(input)) } /// `Vec[u8]` variant of [`normalize_cow`]. -#[inline] #[must_use] pub fn normalize_vec(input: Vec) -> Cow<'static, [u8]> { normalize_cow(Cow::Owned(input)) } /// [`str`] variant of [`normalize_cow`]. -#[inline] #[must_use] pub fn normalize_str(input: &str) -> Cow<'_, [u8]> { normalize_bytes(input.as_bytes()) @@ -149,7 +146,6 @@ pub struct Bytes<'a> { } impl<'a> From<&'a [u8]> for Bytes<'a> { - #[inline] fn from(s: &'a [u8]) -> Self { Self { value: Cow::Borrowed(s), @@ -164,7 +160,6 @@ impl From> for Bytes<'_> { } impl<'a> From> for Bytes<'a> { - #[inline] fn from(c: Cow<'a, [u8]>) -> Self { match c { Cow::Borrowed(c) => Self::from(c), @@ -181,7 +176,6 @@ pub struct String<'a> { } impl<'a> From> for String<'a> { - #[inline] fn from(c: Cow<'a, [u8]>) -> Self { String { value: match c { @@ -198,38 +192,28 @@ pub mod path { #[cfg(not(any(target_os = "android", target_os = "windows")))] use pwd::Passwd; - use quick_error::ResultExt; use crate::values::Path; pub mod interpolate { - use quick_error::quick_error; - - quick_error! { - #[derive(Debug)] - /// The error returned by [`Path::interpolate()`]. - #[allow(missing_docs)] - pub enum Error { - Missing { what: &'static str } { - display("{} is missing", what) - } - Utf8Conversion(what: &'static str, err: git_features::path::Utf8Error) { - display("Ill-formed UTF-8 in {}", what) - context(what: &'static str, err: git_features::path::Utf8Error) -> (what, err) - source(err) - } - UsernameConversion(err: std::str::Utf8Error) { - display("Ill-formed UTF-8 in username") - source(err) - from() - } - PwdFileQuery { - display("User home info missing") - } - UserInterpolationUnsupported { - display("User interpolation is not available on this platform") - } - } + /// The error returned by [`Path::interpolate()`][crate::values::Path::interpolate()]. + #[derive(Debug, thiserror::Error)] + #[allow(missing_docs)] + pub enum Error { + #[error("{} is missing", .what)] + Missing { what: &'static str }, + #[error("Ill-formed UTF-8 in {}", .what)] + Utf8Conversion { + what: &'static str, + #[source] + err: git_path::Utf8Error, + }, + #[error("Ill-formed UTF-8 in username")] + UsernameConversion(#[from] std::str::Utf8Error), + #[error("User home info missing")] + PwdFileQuery, + #[error("User interpolation is not available on this platform")] + UserInterpolationUnsupported, } } @@ -261,17 +245,25 @@ pub mod path { })?; let (_prefix, path_without_trailing_slash) = self.split_at(PREFIX.len()); let path_without_trailing_slash = - git_features::path::from_byte_vec(path_without_trailing_slash).context("path past %(prefix)")?; + git_path::try_from_bstring(path_without_trailing_slash).map_err(|err| { + interpolate::Error::Utf8Conversion { + what: "path past %(prefix)", + err, + } + })?; Ok(git_install_dir.join(path_without_trailing_slash).into()) } else if self.starts_with(USER_HOME) { let home_path = dirs::home_dir().ok_or(interpolate::Error::Missing { what: "home dir" })?; let (_prefix, val) = self.split_at(USER_HOME.len()); - let val = git_features::path::from_bytes(val).context("path past ~/")?; + let val = git_path::try_from_byte_slice(val).map_err(|err| interpolate::Error::Utf8Conversion { + what: "path past ~/", + err, + })?; Ok(home_path.join(val).into()) } else if self.starts_with(b"~") && self.contains(&b'/') { self.interpolate_user() } else { - Ok(git_features::path::from_bytes(self.value).context("unexpanded path")?) + Ok(git_path::from_bstr(self.value)) } } @@ -293,8 +285,13 @@ pub mod path { .map_err(|_| interpolate::Error::PwdFileQuery)? .ok_or(interpolate::Error::Missing { what: "pwd user info" })? .dir; - let path_past_user_prefix = git_features::path::from_byte_slice(&path_with_leading_slash["/".len()..]) - .context("path past ~user/")?; + let path_past_user_prefix = + git_path::try_from_byte_slice(&path_with_leading_slash["/".len()..]).map_err(|err| { + interpolate::Error::Utf8Conversion { + what: "path past ~user/", + err, + } + })?; Ok(std::path::PathBuf::from(home).join(path_past_user_prefix).into()) } } @@ -306,11 +303,11 @@ pub mod path { #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] pub struct Path<'a> { /// The path string, un-interpolated - pub value: Cow<'a, [u8]>, + pub value: Cow<'a, BStr>, } impl<'a> std::ops::Deref for Path<'a> { - type Target = [u8]; + type Target = BStr; fn deref(&self) -> &Self::Target { self.value.as_ref() @@ -323,10 +320,20 @@ impl<'a> AsRef<[u8]> for Path<'a> { } } +impl<'a> AsRef for Path<'a> { + fn as_ref(&self) -> &BStr { + self.value.as_ref() + } +} + impl<'a> From> for Path<'a> { - #[inline] fn from(value: Cow<'a, [u8]>) -> Self { - Path { value } + Path { + value: match value { + Cow::Borrowed(v) => Cow::Borrowed(v.into()), + Cow::Owned(v) => Cow::Owned(v.into()), + }, + } } } @@ -354,7 +361,6 @@ impl Boolean<'_> { /// Generates a byte representation of the value. This should be used when /// non-UTF-8 sequences are present or a UTF-8 representation can't be /// guaranteed. - #[inline] #[must_use] pub fn to_vec(&self) -> Vec { self.into() @@ -363,26 +369,21 @@ impl Boolean<'_> { /// Generates a byte representation of the value. This should be used when /// non-UTF-8 sequences are present or a UTF-8 representation can't be /// guaranteed. - #[inline] #[must_use] pub fn as_bytes(&self) -> &[u8] { self.into() } } -quick_error! { - #[derive(Debug, PartialEq)] - /// The error returned when creating `Boolean` from byte string. - #[allow(missing_docs)] - pub enum BooleanError { - InvalidFormat { - display("Invalid argument format") - } - } +fn bool_err(input: impl Into) -> value::parse::Error { + value::parse::Error::new( + "Booleans need to be 'no', 'off', 'false', 'zero' or 'yes', 'on', 'true', 'one'", + input, + ) } impl<'a> TryFrom<&'a [u8]> for Boolean<'a> { - type Error = BooleanError; + type Error = value::parse::Error; fn try_from(value: &'a [u8]) -> Result { if let Ok(v) = TrueVariant::try_from(value) { @@ -400,12 +401,12 @@ impl<'a> TryFrom<&'a [u8]> for Boolean<'a> { )); } - Err(BooleanError::InvalidFormat) + Err(bool_err(value)) } } impl TryFrom> for Boolean<'_> { - type Error = BooleanError; + type Error = value::parse::Error; fn try_from(value: Vec) -> Result { if value.eq_ignore_ascii_case(b"no") @@ -424,7 +425,7 @@ impl TryFrom> for Boolean<'_> { } impl<'a> TryFrom> for Boolean<'a> { - type Error = BooleanError; + type Error = value::parse::Error; fn try_from(c: Cow<'a, [u8]>) -> Result { match c { Cow::Borrowed(c) => Self::try_from(c), @@ -434,7 +435,6 @@ impl<'a> TryFrom> for Boolean<'a> { } impl Display for Boolean<'_> { - #[inline] fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Boolean::True(v) => v.fmt(f), @@ -444,7 +444,6 @@ impl Display for Boolean<'_> { } impl From> for bool { - #[inline] fn from(b: Boolean) -> Self { match b { Boolean::True(_) => true, @@ -454,7 +453,6 @@ impl From> for bool { } impl<'a, 'b: 'a> From<&'b Boolean<'a>> for &'a [u8] { - #[inline] fn from(b: &'b Boolean) -> Self { match b { Boolean::True(t) => t.into(), @@ -464,14 +462,12 @@ impl<'a, 'b: 'a> From<&'b Boolean<'a>> for &'a [u8] { } impl From> for Vec { - #[inline] fn from(b: Boolean) -> Self { b.into() } } impl From<&Boolean<'_>> for Vec { - #[inline] fn from(b: &Boolean) -> Self { b.to_string().into_bytes() } @@ -502,7 +498,7 @@ pub enum TrueVariant<'a> { } impl<'a> TryFrom<&'a [u8]> for TrueVariant<'a> { - type Error = BooleanError; + type Error = value::parse::Error; fn try_from(value: &'a [u8]) -> Result { if value.eq_ignore_ascii_case(b"yes") @@ -516,13 +512,13 @@ impl<'a> TryFrom<&'a [u8]> for TrueVariant<'a> { } else if value.is_empty() { Ok(Self::Implicit) } else { - Err(BooleanError::InvalidFormat) + Err(bool_err(value)) } } } impl TryFrom> for TrueVariant<'_> { - type Error = BooleanError; + type Error = value::parse::Error; fn try_from(value: Vec) -> Result { if value.eq_ignore_ascii_case(b"yes") @@ -536,7 +532,7 @@ impl TryFrom> for TrueVariant<'_> { } else if value.is_empty() { Ok(Self::Implicit) } else { - Err(BooleanError::InvalidFormat) + Err(bool_err(value)) } } } @@ -552,7 +548,6 @@ impl Display for TrueVariant<'_> { } impl<'a, 'b: 'a> From<&'b TrueVariant<'a>> for &'a [u8] { - #[inline] fn from(t: &'b TrueVariant<'a>) -> Self { match t { TrueVariant::Explicit(e) => e.as_bytes(), @@ -641,31 +636,18 @@ impl Serialize for Integer { } } -quick_error! { - #[derive(Debug)] - /// The error returned when creating `Integer` from byte string. - #[allow(missing_docs)] - pub enum IntegerError { - Utf8Conversion(err: std::str::Utf8Error) { - display("Ill-formed UTF-8") - source(err) - from() - } - InvalidFormat { - display("Invalid argument format") - } - InvalidSuffix { - display("Invalid suffix") - } - } +fn int_err(input: impl Into) -> value::parse::Error { + value::parse::Error::new( + "Intgers needs to be positive or negative numbers which may have a suffix like 1k, or 50G", + input, + ) } impl TryFrom<&[u8]> for Integer { - type Error = IntegerError; + type Error = value::parse::Error; - #[inline] fn try_from(s: &[u8]) -> Result { - let s = std::str::from_utf8(s)?; + let s = std::str::from_utf8(s).map_err(|err| int_err(s).with_err(err))?; if let Ok(value) = s.parse() { return Ok(Self { value, suffix: None }); } @@ -673,7 +655,7 @@ impl TryFrom<&[u8]> for Integer { // Assume we have a prefix at this point. if s.len() <= 1 { - return Err(IntegerError::InvalidFormat); + return Err(int_err(s)); } let (number, suffix) = s.split_at(s.len() - 1); @@ -683,24 +665,22 @@ impl TryFrom<&[u8]> for Integer { suffix: Some(suffix), }) } else { - Err(IntegerError::InvalidFormat) + Err(int_err(s)) } } } impl TryFrom> for Integer { - type Error = IntegerError; + type Error = value::parse::Error; - #[inline] fn try_from(value: Vec) -> Result { Self::try_from(value.as_ref()) } } impl TryFrom> for Integer { - type Error = IntegerError; + type Error = value::parse::Error; - #[inline] fn try_from(c: Cow<'_, [u8]>) -> Result { match c { Cow::Borrowed(c) => Self::try_from(c), @@ -710,14 +690,12 @@ impl TryFrom> for Integer { } impl From for Vec { - #[inline] fn from(i: Integer) -> Self { i.into() } } impl From<&Integer> for Vec { - #[inline] fn from(i: &Integer) -> Self { i.to_string().into_bytes() } @@ -736,7 +714,6 @@ pub enum IntegerSuffix { impl IntegerSuffix { /// Returns the number of bits that the suffix shifts left by. - #[inline] #[must_use] pub const fn bitwise_offset(self) -> usize { match self { @@ -748,7 +725,6 @@ impl IntegerSuffix { } impl Display for IntegerSuffix { - #[inline] fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Kibi => write!(f, "k"), @@ -773,32 +749,29 @@ impl Serialize for IntegerSuffix { } impl FromStr for IntegerSuffix { - type Err = IntegerError; + type Err = (); - #[inline] fn from_str(s: &str) -> Result { match s { "k" | "K" => Ok(Self::Kibi), "m" | "M" => Ok(Self::Mebi), "g" | "G" => Ok(Self::Gibi), - _ => Err(IntegerError::InvalidSuffix), + _ => Err(()), } } } impl TryFrom<&[u8]> for IntegerSuffix { - type Error = IntegerError; + type Error = (); - #[inline] fn try_from(s: &[u8]) -> Result { - Self::from_str(std::str::from_utf8(s)?) + Self::from_str(std::str::from_utf8(s).map_err(|_| ())?) } } impl TryFrom> for IntegerSuffix { - type Error = IntegerError; + type Error = (); - #[inline] fn try_from(value: Vec) -> Result { Self::try_from(value.as_ref()) } @@ -825,7 +798,6 @@ impl Color { /// Generates a byte representation of the value. This should be used when /// non-UTF-8 sequences are present or a UTF-8 representation can't be /// guaranteed. - #[inline] #[must_use] pub fn to_vec(&self) -> Vec { self.into() @@ -861,31 +833,18 @@ impl Serialize for Color { } } -quick_error! { - #[derive(Debug, PartialEq)] - /// - #[allow(missing_docs)] - pub enum ColorError { - Utf8Conversion(err: std::str::Utf8Error) { - display("Ill-formed UTF-8") - source(err) - from() - } - InvalidColorItem { - display("Invalid color item") - } - InvalidFormat { - display("Invalid argument format") - } - } +fn color_err(input: impl Into) -> value::parse::Error { + value::parse::Error::new( + "Colors are specific color values and their attributes, like 'brightred', or 'blue'", + input, + ) } impl TryFrom<&[u8]> for Color { - type Error = ColorError; + type Error = value::parse::Error; - #[inline] fn try_from(s: &[u8]) -> Result { - let s = std::str::from_utf8(s)?; + let s = std::str::from_utf8(s).map_err(|err| color_err(s).with_err(err))?; enum ColorItem { Value(ColorValue), Attr(ColorAttribute), @@ -913,12 +872,12 @@ impl TryFrom<&[u8]> for Color { } else if new_self.background.is_none() { new_self.background = Some(v); } else { - return Err(ColorError::InvalidColorItem); + return Err(color_err(s)); } } ColorItem::Attr(a) => new_self.attributes.push(a), }, - Err(_) => return Err(ColorError::InvalidColorItem), + Err(_) => return Err(color_err(s)), } } @@ -927,18 +886,16 @@ impl TryFrom<&[u8]> for Color { } impl TryFrom> for Color { - type Error = ColorError; + type Error = value::parse::Error; - #[inline] fn try_from(value: Vec) -> Result { Self::try_from(value.as_ref()) } } impl TryFrom> for Color { - type Error = ColorError; + type Error = value::parse::Error; - #[inline] fn try_from(c: Cow<'_, [u8]>) -> Result { match c { Cow::Borrowed(c) => Self::try_from(c), @@ -948,14 +905,12 @@ impl TryFrom> for Color { } impl From for Vec { - #[inline] fn from(c: Color) -> Self { c.into() } } impl From<&Color> for Vec { - #[inline] fn from(c: &Color) -> Self { c.to_string().into_bytes() } @@ -1026,7 +981,7 @@ impl Serialize for ColorValue { } impl FromStr for ColorValue { - type Err = ColorError; + type Err = value::parse::Error; fn from_str(s: &str) -> Result { let mut s = s; @@ -1039,7 +994,7 @@ impl FromStr for ColorValue { match s { "normal" if !bright => return Ok(Self::Normal), - "normal" if bright => return Err(ColorError::InvalidFormat), + "normal" if bright => return Err(color_err(s)), "black" if !bright => return Ok(Self::Black), "black" if bright => return Ok(Self::BrightBlack), "red" if !bright => return Ok(Self::Red), @@ -1077,16 +1032,15 @@ impl FromStr for ColorValue { } } - Err(ColorError::InvalidFormat) + Err(color_err(s)) } } impl TryFrom<&[u8]> for ColorValue { - type Error = ColorError; + type Error = value::parse::Error; - #[inline] fn try_from(s: &[u8]) -> Result { - Self::from_str(std::str::from_utf8(s)?) + Self::from_str(std::str::from_utf8(s).map_err(|err| color_err(s).with_err(err))?) } } @@ -1161,7 +1115,7 @@ impl Serialize for ColorAttribute { } impl FromStr for ColorAttribute { - type Err = ColorError; + type Err = value::parse::Error; fn from_str(s: &str) -> Result { let inverted = s.starts_with("no"); @@ -1190,16 +1144,15 @@ impl FromStr for ColorAttribute { "italic" if inverted => Ok(Self::NoItalic), "strike" if !inverted => Ok(Self::Strike), "strike" if inverted => Ok(Self::NoStrike), - _ => Err(ColorError::InvalidFormat), + _ => Err(color_err(parsed)), } } } impl TryFrom<&[u8]> for ColorAttribute { - type Error = ColorError; + type Error = value::parse::Error; - #[inline] fn try_from(s: &[u8]) -> Result { - Self::from_str(std::str::from_utf8(s)?) + Self::from_str(std::str::from_utf8(s).map_err(|err| color_err(s).with_err(err))?) } } diff --git a/git-config/tests/git_config/mod.rs b/git-config/tests/git_config/mod.rs index 904df0f629..4fe3ed6a85 100644 --- a/git-config/tests/git_config/mod.rs +++ b/git-config/tests/git_config/mod.rs @@ -19,7 +19,7 @@ mod mutable_value { fn value_is_correct() { let mut git_config = init_config(); - let value = git_config.get_raw_value_mut("core", None, "a").unwrap(); + let value = git_config.raw_value_mut("core", None, "a").unwrap(); assert_eq!(&*value.get().unwrap(), b"b100"); } @@ -27,7 +27,7 @@ mod mutable_value { fn set_string_cleanly_updates() { let mut git_config = init_config(); - let mut value = git_config.get_raw_value_mut("core", None, "a").unwrap(); + let mut value = git_config.raw_value_mut("core", None, "a").unwrap(); value.set_string("hello world".to_string()); assert_eq!( git_config.to_string(), @@ -38,7 +38,7 @@ mod mutable_value { e=f"#, ); - let mut value = git_config.get_raw_value_mut("core", None, "e").unwrap(); + let mut value = git_config.raw_value_mut("core", None, "e").unwrap(); value.set_string(String::new()); assert_eq!( git_config.to_string(), @@ -54,7 +54,7 @@ mod mutable_value { fn delete_value() { let mut git_config = init_config(); - let mut value = git_config.get_raw_value_mut("core", None, "a").unwrap(); + let mut value = git_config.raw_value_mut("core", None, "a").unwrap(); value.delete(); assert_eq!( git_config.to_string(), @@ -63,7 +63,7 @@ mod mutable_value { e=f", ); - let mut value = git_config.get_raw_value_mut("core", None, "c").unwrap(); + let mut value = git_config.raw_value_mut("core", None, "c").unwrap(); value.delete(); assert_eq!( git_config.to_string(), @@ -75,7 +75,7 @@ mod mutable_value { fn get_value_after_deleted() { let mut git_config = init_config(); - let mut value = git_config.get_raw_value_mut("core", None, "a").unwrap(); + let mut value = git_config.raw_value_mut("core", None, "a").unwrap(); value.delete(); assert!(value.get().is_err()); } @@ -84,7 +84,7 @@ mod mutable_value { fn set_string_after_deleted() { let mut git_config = init_config(); - let mut value = git_config.get_raw_value_mut("core", None, "a").unwrap(); + let mut value = git_config.raw_value_mut("core", None, "a").unwrap(); value.delete(); value.set_string("hello world".to_string()); assert_eq!( @@ -101,7 +101,7 @@ mod mutable_value { fn subsequent_delete_calls_are_noop() { let mut git_config = init_config(); - let mut value = git_config.get_raw_value_mut("core", None, "a").unwrap(); + let mut value = git_config.raw_value_mut("core", None, "a").unwrap(); for _ in 0..10 { value.delete(); } @@ -124,7 +124,7 @@ b e=f"#, ) .unwrap(); - let mut value = git_config.get_raw_value_mut("core", None, "a").unwrap(); + let mut value = git_config.raw_value_mut("core", None, "a").unwrap(); assert_eq!(&*value.get().unwrap(), b"b100b"); value.delete(); assert_eq!( @@ -157,7 +157,7 @@ mod mutable_multi_value { fn value_is_correct() { let mut git_config = init_config(); - let value = git_config.get_raw_multi_value_mut("core", None, "a").unwrap(); + let value = git_config.raw_multi_value_mut("core", None, "a").unwrap(); assert_eq!( &*value.get().unwrap(), vec![ @@ -171,17 +171,14 @@ mod mutable_multi_value { #[test] fn non_empty_sizes_are_correct() { let mut git_config = init_config(); - assert_eq!(git_config.get_raw_multi_value_mut("core", None, "a").unwrap().len(), 3); - assert!(!git_config - .get_raw_multi_value_mut("core", None, "a") - .unwrap() - .is_empty()); + assert_eq!(git_config.raw_multi_value_mut("core", None, "a").unwrap().len(), 3); + assert!(!git_config.raw_multi_value_mut("core", None, "a").unwrap().is_empty()); } #[test] fn set_value_at_start() { let mut git_config = init_config(); - let mut values = git_config.get_raw_multi_value_mut("core", None, "a").unwrap(); + let mut values = git_config.raw_multi_value_mut("core", None, "a").unwrap(); values.set_string(0, "Hello".to_string()); assert_eq!( git_config.to_string(), @@ -196,7 +193,7 @@ mod mutable_multi_value { #[test] fn set_value_at_end() { let mut git_config = init_config(); - let mut values = git_config.get_raw_multi_value_mut("core", None, "a").unwrap(); + let mut values = git_config.raw_multi_value_mut("core", None, "a").unwrap(); values.set_string(2, "Hello".to_string()); assert_eq!( git_config.to_string(), @@ -211,7 +208,7 @@ mod mutable_multi_value { #[test] fn set_values_all() { let mut git_config = init_config(); - let mut values = git_config.get_raw_multi_value_mut("core", None, "a").unwrap(); + let mut values = git_config.raw_multi_value_mut("core", None, "a").unwrap(); values.set_owned_values_all(b"Hello"); assert_eq!( git_config.to_string(), @@ -226,7 +223,7 @@ mod mutable_multi_value { #[test] fn delete() { let mut git_config = init_config(); - let mut values = git_config.get_raw_multi_value_mut("core", None, "a").unwrap(); + let mut values = git_config.raw_multi_value_mut("core", None, "a").unwrap(); values.delete(0); assert_eq!( git_config.to_string(), @@ -239,7 +236,7 @@ mod mutable_multi_value { #[test] fn delete_all() { let mut git_config = init_config(); - let mut values = git_config.get_raw_multi_value_mut("core", None, "a").unwrap(); + let mut values = git_config.raw_multi_value_mut("core", None, "a").unwrap(); values.delete_all(); assert!(values.get().is_err()); assert_eq!( @@ -261,7 +258,7 @@ b a"#, ) .unwrap(); - let mut values = git_config.get_raw_multi_value_mut("core", None, "a").unwrap(); + let mut values = git_config.raw_multi_value_mut("core", None, "a").unwrap(); assert_eq!( &*values.get().unwrap(), @@ -320,8 +317,8 @@ mod from_paths_tests { let config = GitConfig::from_paths(paths, &Default::default()).unwrap(); assert_eq!( - config.get_raw_value("core", None, "boolean"), - Ok(Cow::<[u8]>::Borrowed(b"true")) + config.raw_value("core", None, "boolean").unwrap(), + Cow::<[u8]>::Borrowed(b"true") ); assert_eq!(config.len(), 1); @@ -390,26 +387,26 @@ mod from_paths_tests { let config = GitConfig::from_paths(vec![c_path], &Default::default()).unwrap(); assert_eq!( - config.get_raw_value("core", None, "c"), - Ok(Cow::<[u8]>::Borrowed(b"12")) + config.raw_value("core", None, "c").unwrap(), + Cow::<[u8]>::Borrowed(b"12") ); assert_eq!( - config.get_raw_value("core", None, "d"), - Ok(Cow::<[u8]>::Borrowed(b"41")) + config.raw_value("core", None, "d").unwrap(), + Cow::<[u8]>::Borrowed(b"41") ); assert_eq!( - config.get_raw_value("http", None, "sslVerify"), - Ok(Cow::<[u8]>::Borrowed(b"false")) + config.raw_value("http", None, "sslVerify").unwrap(), + Cow::<[u8]>::Borrowed(b"false") ); assert_eq!( - config.get_raw_value("diff", None, "renames"), - Ok(Cow::<[u8]>::Borrowed(b"true")) + config.raw_value("diff", None, "renames").unwrap(), + Cow::<[u8]>::Borrowed(b"true") ); assert_eq!( - config.get_raw_value("core", None, "a"), - Ok(Cow::<[u8]>::Borrowed(b"false")) + config.raw_value("core", None, "a").unwrap(), + Cow::<[u8]>::Borrowed(b"false") ); } @@ -452,7 +449,7 @@ mod from_paths_tests { let options = from_paths::Options::default(); let config = GitConfig::from_paths(vec![dir.path().join("0")], &options).unwrap(); assert_eq!( - config.get_raw_multi_value("core", None, "i").unwrap(), + config.raw_multi_value("core", None, "i").unwrap(), vec![ Cow::Borrowed(b"0"), Cow::Borrowed(b"1"), @@ -470,12 +467,18 @@ mod from_paths_tests { ..Default::default() }; let config = GitConfig::from_paths(vec![dir.path().join("0")], &options).unwrap(); - assert_eq!(config.get_raw_value("core", None, "i"), Ok(Cow::<[u8]>::Borrowed(b"1"))); + assert_eq!( + config.raw_value("core", None, "i").unwrap(), + Cow::<[u8]>::Borrowed(b"1") + ); // with default max_allowed_depth of 10 and 4 levels of includes, last level is read let options = from_paths::Options::default(); let config = GitConfig::from_paths(vec![dir.path().join("0")], &options).unwrap(); - assert_eq!(config.get_raw_value("core", None, "i"), Ok(Cow::<[u8]>::Borrowed(b"4"))); + assert_eq!( + config.raw_value("core", None, "i").unwrap(), + Cow::<[u8]>::Borrowed(b"4") + ); // with max_allowed_depth of 5, the base and 4 levels of includes, last level is read let options = from_paths::Options { @@ -483,7 +486,10 @@ mod from_paths_tests { ..Default::default() }; let config = GitConfig::from_paths(vec![dir.path().join("0")], &options).unwrap(); - assert_eq!(config.get_raw_value("core", None, "i"), Ok(Cow::<[u8]>::Borrowed(b"4"))); + assert_eq!( + config.raw_value("core", None, "i").unwrap(), + Cow::<[u8]>::Borrowed(b"4") + ); // with max_allowed_depth of 2 and 4 levels of includes, max_allowed_depth is exceeded and error is returned let options = from_paths::Options { @@ -503,7 +509,10 @@ mod from_paths_tests { ..Default::default() }; let config = GitConfig::from_paths(vec![dir.path().join("0")], &options).unwrap(); - assert_eq!(config.get_raw_value("core", None, "i"), Ok(Cow::<[u8]>::Borrowed(b"2"))); + assert_eq!( + config.raw_value("core", None, "i").unwrap(), + Cow::<[u8]>::Borrowed(b"2") + ); // with max_allowed_depth of 0 and 4 levels of includes, max_allowed_depth is exceeded and error is returned let options = from_paths::Options { @@ -552,8 +561,8 @@ mod from_paths_tests { let config = GitConfig::from_paths(vec![a_path], &Default::default()).unwrap(); assert_eq!( - config.get_raw_value("core", None, "b"), - Ok(Cow::<[u8]>::Borrowed(b"false")) + config.raw_value("core", None, "b").unwrap(), + Cow::<[u8]>::Borrowed(b"false") ); } @@ -607,7 +616,7 @@ mod from_paths_tests { }; let config = GitConfig::from_paths(vec![a_path], &options).unwrap(); assert_eq!( - config.get_raw_multi_value("core", None, "b").unwrap(), + config.raw_multi_value("core", None, "b").unwrap(), vec![ Cow::Borrowed(b"0"), Cow::Borrowed(b"1"), @@ -662,16 +671,19 @@ mod from_paths_tests { let config = GitConfig::from_paths(vec![c_path], &Default::default()).unwrap(); - assert_eq!(config.get_raw_value("core", None, "c"), Ok(Cow::<[u8]>::Borrowed(b"1"))); + assert_eq!( + config.raw_value("core", None, "c").unwrap(), + Cow::<[u8]>::Borrowed(b"1") + ); assert_eq!( - config.get_raw_value("core", None, "b"), - Ok(Cow::<[u8]>::Borrowed(b"true")) + config.raw_value("core", None, "b").unwrap(), + Cow::<[u8]>::Borrowed(b"true") ); assert_eq!( - config.get_raw_value("core", None, "a"), - Ok(Cow::<[u8]>::Borrowed(b"false")) + config.raw_value("core", None, "a").unwrap(), + Cow::<[u8]>::Borrowed(b"false") ); } @@ -695,18 +707,18 @@ mod from_paths_tests { let config = GitConfig::from_paths(paths, &Default::default()).unwrap(); assert_eq!( - config.get_raw_value("core", None, "a"), - Ok(Cow::<[u8]>::Borrowed(b"false")) + config.raw_value("core", None, "a").unwrap(), + Cow::<[u8]>::Borrowed(b"false") ); assert_eq!( - config.get_raw_value("core", None, "b"), - Ok(Cow::<[u8]>::Borrowed(b"true")) + config.raw_value("core", None, "b").unwrap(), + Cow::<[u8]>::Borrowed(b"true") ); assert_eq!( - config.get_raw_value("core", None, "c"), - Ok(Cow::<[u8]>::Borrowed(b"true")) + config.raw_value("core", None, "c").unwrap(), + Cow::<[u8]>::Borrowed(b"true") ); assert_eq!(config.len(), 4); @@ -735,12 +747,12 @@ mod from_paths_tests { let config = GitConfig::from_paths(paths, &Default::default()).unwrap(); assert_eq!( - config.get_raw_multi_value("core", None, "key").unwrap(), + config.raw_multi_value("core", None, "key").unwrap(), vec![Cow::Borrowed(b"a"), Cow::Borrowed(b"b"), Cow::Borrowed(b"c")] ); assert_eq!( - config.get_raw_multi_value("include", None, "path").unwrap(), + config.raw_multi_value("include", None, "path").unwrap(), vec![Cow::Borrowed(b"d_path"), Cow::Borrowed(b"e_path")] ); @@ -802,7 +814,7 @@ mod from_env_tests { fn parse_error_with_invalid_count() { let _env = Env::new().set("GIT_CONFIG_COUNT", "invalid"); let err = GitConfig::from_env(&Options::default()).unwrap_err(); - assert!(matches!(err, from_env::Error::ParseError(_))); + assert!(matches!(err, from_env::Error::ParseError { .. })); } #[test] @@ -815,8 +827,8 @@ mod from_env_tests { let config = GitConfig::from_env(&Options::default()).unwrap().unwrap(); assert_eq!( - config.get_raw_value("core", None, "key"), - Ok(Cow::<[u8]>::Borrowed(b"value")) + config.raw_value("core", None, "key").unwrap(), + Cow::<[u8]>::Borrowed(b"value") ); assert_eq!(config.len(), 1); @@ -836,9 +848,18 @@ mod from_env_tests { let config = GitConfig::from_env(&Options::default()).unwrap().unwrap(); - assert_eq!(config.get_raw_value("core", None, "a"), Ok(Cow::<[u8]>::Borrowed(b"a"))); - assert_eq!(config.get_raw_value("core", None, "b"), Ok(Cow::<[u8]>::Borrowed(b"b"))); - assert_eq!(config.get_raw_value("core", None, "c"), Ok(Cow::<[u8]>::Borrowed(b"c"))); + assert_eq!( + config.raw_value("core", None, "a").unwrap(), + Cow::<[u8]>::Borrowed(b"a") + ); + assert_eq!( + config.raw_value("core", None, "b").unwrap(), + Cow::<[u8]>::Borrowed(b"b") + ); + assert_eq!( + config.raw_value("core", None, "c").unwrap(), + Cow::<[u8]>::Borrowed(b"c") + ); assert_eq!(config.len(), 3); } @@ -881,8 +902,8 @@ mod from_env_tests { let config = GitConfig::from_env(&Options::default()).unwrap().unwrap(); assert_eq!( - config.get_raw_value("core", None, "key"), - Ok(Cow::<[u8]>::Borrowed(b"changed")) + config.raw_value("core", None, "key").unwrap(), + Cow::<[u8]>::Borrowed(b"changed") ); assert_eq!(config.len(), 5); } @@ -892,64 +913,76 @@ mod from_env_tests { mod get_raw_value { use std::{borrow::Cow, convert::TryFrom}; - use git_config::{ - file::{GitConfig, GitConfigError}, - parser::SectionHeaderName, - }; + use git_config::{file::GitConfig, lookup}; #[test] fn single_section() { let config = GitConfig::try_from("[core]\na=b\nc=d").unwrap(); - assert_eq!(config.get_raw_value("core", None, "a"), Ok(Cow::<[u8]>::Borrowed(b"b"))); - assert_eq!(config.get_raw_value("core", None, "c"), Ok(Cow::<[u8]>::Borrowed(b"d"))); + assert_eq!( + config.raw_value("core", None, "a").unwrap(), + Cow::<[u8]>::Borrowed(b"b") + ); + assert_eq!( + config.raw_value("core", None, "c").unwrap(), + Cow::<[u8]>::Borrowed(b"d") + ); } #[test] fn last_one_wins_respected_in_section() { let config = GitConfig::try_from("[core]\na=b\na=d").unwrap(); - assert_eq!(config.get_raw_value("core", None, "a"), Ok(Cow::<[u8]>::Borrowed(b"d"))); + assert_eq!( + config.raw_value("core", None, "a").unwrap(), + Cow::<[u8]>::Borrowed(b"d") + ); } #[test] fn last_one_wins_respected_across_section() { let config = GitConfig::try_from("[core]\na=b\n[core]\na=d").unwrap(); - assert_eq!(config.get_raw_value("core", None, "a"), Ok(Cow::<[u8]>::Borrowed(b"d"))); + assert_eq!( + config.raw_value("core", None, "a").unwrap(), + Cow::<[u8]>::Borrowed(b"d") + ); } #[test] fn section_not_found() { let config = GitConfig::try_from("[core]\na=b\nc=d").unwrap(); - assert_eq!( - config.get_raw_value("foo", None, "a"), - Err(GitConfigError::SectionDoesNotExist(SectionHeaderName("foo".into()))) - ); + assert!(matches!( + config.raw_value("foo", None, "a"), + Err(lookup::existing::Error::SectionMissing) + )); } #[test] fn subsection_not_found() { let config = GitConfig::try_from("[core]\na=b\nc=d").unwrap(); - assert_eq!( - config.get_raw_value("core", Some("a"), "a"), - Err(GitConfigError::SubSectionDoesNotExist(Some("a"))) - ); + assert!(matches!( + config.raw_value("core", Some("a"), "a"), + Err(lookup::existing::Error::SubSectionMissing) + )); } #[test] fn key_not_found() { let config = GitConfig::try_from("[core]\na=b\nc=d").unwrap(); - assert_eq!( - config.get_raw_value("core", None, "aaaaaa"), - Err(GitConfigError::KeyDoesNotExist) - ); + assert!(matches!( + config.raw_value("core", None, "aaaaaa"), + Err(lookup::existing::Error::KeyMissing) + )); } #[test] fn subsection_must_be_respected() { let config = GitConfig::try_from("[core]a=b\n[core.a]a=c").unwrap(); - assert_eq!(config.get_raw_value("core", None, "a"), Ok(Cow::<[u8]>::Borrowed(b"b"))); assert_eq!( - config.get_raw_value("core", Some("a"), "a"), - Ok(Cow::<[u8]>::Borrowed(b"c")) + config.raw_value("core", None, "a").unwrap(), + Cow::<[u8]>::Borrowed(b"b") + ); + assert_eq!( + config.raw_value("core", Some("a"), "a").unwrap(), + Cow::<[u8]>::Borrowed(b"c") ); } } @@ -1008,17 +1041,15 @@ mod get_value { mod get_raw_multi_value { use std::{borrow::Cow, convert::TryFrom}; - use git_config::{ - file::{GitConfig, GitConfigError}, - parser::SectionHeaderName, - }; + use git_config::file::GitConfig; + use git_config::lookup; #[test] fn single_value_is_identical_to_single_value_query() { let config = GitConfig::try_from("[core]\na=b\nc=d").unwrap(); assert_eq!( - vec![config.get_raw_value("core", None, "a").unwrap()], - config.get_raw_multi_value("core", None, "a").unwrap() + vec![config.raw_value("core", None, "a").unwrap()], + config.raw_multi_value("core", None, "a").unwrap() ); } @@ -1026,7 +1057,7 @@ mod get_raw_multi_value { fn multi_value_in_section() { let config = GitConfig::try_from("[core]\na=b\na=c").unwrap(); assert_eq!( - config.get_raw_multi_value("core", None, "a").unwrap(), + config.raw_multi_value("core", None, "a").unwrap(), vec![Cow::Borrowed(b"b"), Cow::Borrowed(b"c")] ); } @@ -1035,7 +1066,7 @@ mod get_raw_multi_value { fn multi_value_across_sections() { let config = GitConfig::try_from("[core]\na=b\na=c\n[core]a=d").unwrap(); assert_eq!( - config.get_raw_multi_value("core", None, "a").unwrap(), + config.raw_multi_value("core", None, "a").unwrap(), vec![Cow::Borrowed(b"b"), Cow::Borrowed(b"c"), Cow::Borrowed(b"d")] ); } @@ -1043,39 +1074,39 @@ mod get_raw_multi_value { #[test] fn section_not_found() { let config = GitConfig::try_from("[core]\na=b\nc=d").unwrap(); - assert_eq!( - config.get_raw_multi_value("foo", None, "a"), - Err(GitConfigError::SectionDoesNotExist(SectionHeaderName("foo".into()))) - ); + assert!(matches!( + config.raw_multi_value("foo", None, "a"), + Err(lookup::existing::Error::SectionMissing) + )); } #[test] fn subsection_not_found() { let config = GitConfig::try_from("[core]\na=b\nc=d").unwrap(); - assert_eq!( - config.get_raw_multi_value("core", Some("a"), "a"), - Err(GitConfigError::SubSectionDoesNotExist(Some("a"))) - ); + assert!(matches!( + config.raw_multi_value("core", Some("a"), "a"), + Err(lookup::existing::Error::SubSectionMissing) + )); } #[test] fn key_not_found() { let config = GitConfig::try_from("[core]\na=b\nc=d").unwrap(); - assert_eq!( - config.get_raw_multi_value("core", None, "aaaaaa"), - Err(GitConfigError::KeyDoesNotExist) - ); + assert!(matches!( + config.raw_multi_value("core", None, "aaaaaa"), + Err(lookup::existing::Error::KeyMissing) + )); } #[test] fn subsection_must_be_respected() { let config = GitConfig::try_from("[core]a=b\n[core.a]a=c").unwrap(); assert_eq!( - config.get_raw_multi_value("core", None, "a").unwrap(), + config.raw_multi_value("core", None, "a").unwrap(), vec![Cow::Borrowed(b"b")] ); assert_eq!( - config.get_raw_multi_value("core", Some("a"), "a").unwrap(), + config.raw_multi_value("core", Some("a"), "a").unwrap(), vec![Cow::Borrowed(b"c")] ); } @@ -1084,7 +1115,7 @@ mod get_raw_multi_value { fn non_relevant_subsection_is_ignored() { let config = GitConfig::try_from("[core]\na=b\na=c\n[core]a=d\n[core]g=g").unwrap(); assert_eq!( - config.get_raw_multi_value("core", None, "a").unwrap(), + config.raw_multi_value("core", None, "a").unwrap(), vec![Cow::Borrowed(b"b"), Cow::Borrowed(b"c"), Cow::Borrowed(b"d")] ); } diff --git a/git-config/tests/value/mod.rs b/git-config/tests/value/mod.rs index c5b4076a48..82ca1597af 100644 --- a/git-config/tests/value/mod.rs +++ b/git-config/tests/value/mod.rs @@ -23,11 +23,20 @@ fn get_value_for_all_provided_values() -> crate::Result { file.value::("core", None, "bool-explicit")?, Boolean::False(Cow::Borrowed("false")) ); + assert!(!file.boolean("core", None, "bool-explicit").expect("exists")?); assert_eq!( file.value::("core", None, "bool-implicit")?, Boolean::True(TrueVariant::Implicit) ); + assert_eq!( + file.try_value::("core", None, "bool-implicit") + .expect("exists")?, + Boolean::True(TrueVariant::Implicit) + ); + + assert!(file.boolean("core", None, "bool-implicit").expect("present")?); + assert_eq!(file.try_value::("doesnt", None, "exist"), None); assert_eq!( file.value::("core", None, "integer-no-prefix")?, @@ -69,15 +78,22 @@ fn get_value_for_all_provided_values() -> crate::Result { } ); + assert_eq!( + file.string("core", None, "other").expect("present").as_ref(), + "hello world" + ); + let actual = file.value::("core", None, "location")?; assert_eq!( - &*actual, - "~/tmp".as_bytes(), + &*actual, "~/tmp", "no interpolation occurs when querying a path due to lack of context" ); let expected = PathBuf::from(format!("{}/tmp", dirs::home_dir().expect("empty home dir").display())); assert_eq!(actual.interpolate(None).unwrap(), expected); + let actual = file.path("core", None, "location").expect("present"); + assert_eq!(&*actual, "~/tmp",); + Ok(()) } @@ -113,10 +129,9 @@ fn get_value_looks_up_all_sections_before_failing() -> crate::Result { fn section_names_are_case_insensitive() -> crate::Result { let config = "[core] bool-implicit"; let file = GitConfig::try_from(config)?; - assert!(file.value::("core", None, "bool-implicit").is_ok()); assert_eq!( - file.value::("core", None, "bool-implicit"), - file.value::("CORE", None, "bool-implicit") + file.value::("core", None, "bool-implicit").unwrap(), + file.value::("CORE", None, "bool-implicit").unwrap() ); Ok(()) @@ -130,8 +145,8 @@ fn value_names_are_case_insensitive() -> crate::Result { let file = GitConfig::try_from(config)?; assert_eq!(file.multi_value::("core", None, "a")?.len(), 2); assert_eq!( - file.value::("core", None, "a"), - file.value::("core", None, "A") + file.value::("core", None, "a").unwrap(), + file.value::("core", None, "A").unwrap() ); Ok(()) diff --git a/git-features/Cargo.toml b/git-features/Cargo.toml index 518d914d80..0402a2eba4 100644 --- a/git-features/Cargo.toml +++ b/git-features/Cargo.toml @@ -81,11 +81,6 @@ name = "pipe" path = "tests/pipe.rs" required-features = ["io-pipe"] -[[test]] -name = "path" -path = "tests/path.rs" -required-features = ["bstr"] - [dependencies] #! ### Optional Dependencies @@ -122,11 +117,6 @@ quick-error = { version = "2.0.0", optional = true } ## make the `time` module available with access to the local time as configured by the system. time = { version = "0.3.2", optional = true, default-features = false, features = ["local-offset"] } - -# path -## make bstr utilities available in the `path` modules, which itself is gated by the `path` feature. -bstr = { version = "0.2.17", optional = true, default-features = false, features = ["std"] } - document-features = { version = "0.2.0", optional = true } [target.'cfg(unix)'.dependencies] diff --git a/git-features/src/lib.rs b/git-features/src/lib.rs index 83e9a9116b..f8af9589dd 100644 --- a/git-features/src/lib.rs +++ b/git-features/src/lib.rs @@ -24,7 +24,6 @@ pub mod interrupt; #[cfg(feature = "io-pipe")] pub mod io; pub mod parallel; -pub mod path; #[cfg(feature = "progress")] pub mod progress; pub mod threading; diff --git a/git-features/src/path.rs b/git-features/src/path.rs deleted file mode 100644 index 819cc89180..0000000000 --- a/git-features/src/path.rs +++ /dev/null @@ -1,234 +0,0 @@ -//! ### Research -//! -//! * **windows** -//! - [`dirent.c`](https://github.com/git/git/blob/main/compat/win32/dirent.c#L31:L31) contains all implementation (seemingly) of opening directories and reading their entries, along with all path conversions (UTF-16 for windows). This is done on the fly so git can work with [in UTF-8](https://github.com/git/git/blob/main/compat/win32/dirent.c#L12:L12). -//! - mingw [is used for the conversion](https://github.com/git/git/blob/main/compat/mingw.h#L579:L579) and it appears they handle surrogates during the conversion, maybe some sort of non-strict UTF-8 converter? Actually it uses [WideCharToMultiByte](https://docs.microsoft.com/en-us/windows/win32/api/stringapiset/nf-stringapiset-widechartomultibyte) -//! under the hood which by now does fail if the UTF-8 would be invalid unicode, i.e. unicode pairs. -//! - `OsString` on windows already stores strings as WTF-8, which supports [surrogate pairs](https://unicodebook.readthedocs.io/unicode_encodings.html), -//! something that UTF-8 isn't allowed do it for security reasons, after all it's UTF-16 specific and exists only to extend -//! the encodable code-points. -//! - informative reading on [WTF-8](https://simonsapin.github.io/wtf-8/#motivation) which is the encoding used by Rust -//! internally that deals with surrogates and non-wellformed surrogates (those that aren't in pairs). -//! * **unix** -//! - It uses [opendir](https://man7.org/linux/man-pages/man3/opendir.3.html) and [readdir](https://man7.org/linux/man-pages/man3/readdir.3.html) -//! respectively. There is no encoding specified, except that these paths are null-terminated. -//! -//! ### Learnings -//! -//! Surrogate pairs are a way to extend the encodable value range in UTF-16 encodings, used primarily on windows and in Javascript. -//! For a long time these codepoints used for surrogates, always to be used in pairs, were not assigned, until…they were for rare -//! emojies and the likes. The unicode standard does not require surrogates to happen in pairs, even though by now unpaired surrogates -//! in UTF-16 are considered ill-formed, which aren't supposed to be converted to UTF-8 for example. -//! -//! This is the reason we have to deal with `to_string_lossy()`, it's _just_ for that quirk. -//! -//! This also means the only platform ever eligible to see conversion errors is windows, and there it's only older pre-vista -//! windows versions which incorrectly allow ill-formed UTF-16 strings. Newer versions don't perform such conversions anymore, for -//! example when going from UTF-16 to UTF-8, they will trigger an error. -//! -//! ### Conclusions -//! -//! Since [WideCharToMultiByte](https://docs.microsoft.com/en-us/windows/win32/api/stringapiset/nf-stringapiset-widechartomultibyte) by now is -//! fixed (Vista onward) to produce valid UTF-8, lone surrogate codepoints will cause failure, which `git` -//! [doesn't care about](https://github.com/git/git/blob/main/compat/win32/dirent.c#L12:L12). -//! -//! We will, though, which means from now on we can just convert to UTF-8 on windows and bubble up errors where necessary, -//! preventing potential mismatched surrogate pairs to ever be saved on disk by gitoxide. -//! -//! Even though the error only exists on older windows versions, we will represent it in the type system through fallible function calls. -//! Callers may `.expect()` on the result to indicate they don't wish to handle this special and rare case. Note that servers should not -//! ever get into a code-path which does panic though. - -use std::{ - borrow::Cow, - ffi::OsStr, - path::{Path, PathBuf}, -}; - -#[derive(Debug)] -/// The error type returned by [`into_bytes()`] and others may suffer from failed conversions from or to bytes. -pub struct Utf8Error; - -impl std::fmt::Display for Utf8Error { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str("Could not convert to UTF8 or from UTF8 due to ill-formed input") - } -} - -impl std::error::Error for Utf8Error {} - -/// Like [`into_bytes()`], but takes `OsStr` as input for a lossless, but fallible, conversion. -pub fn os_str_into_bytes(path: &OsStr) -> Result<&[u8], Utf8Error> { - let path = into_bytes(Cow::Borrowed(path.as_ref()))?; - match path { - Cow::Borrowed(path) => Ok(path), - Cow::Owned(_) => unreachable!("borrowed cows stay borrowed"), - } -} - -/// Convert the given path either into its raw bytes on unix or its UTF8 encoded counterpart on windows. -/// -/// On windows, if the source Path contains ill-formed, lone surrogates, the UTF-8 conversion will fail -/// causing `Utf8Error` to be returned. -pub fn into_bytes<'a>(path: impl Into>) -> Result, Utf8Error> { - let path = path.into(); - let utf8_bytes = match path { - Cow::Owned(path) => Cow::Owned({ - #[cfg(unix)] - let p = { - use std::os::unix::ffi::OsStringExt; - path.into_os_string().into_vec() - }; - #[cfg(not(unix))] - let p: Vec<_> = path.into_os_string().into_string().map_err(|_| Utf8Error)?.into(); - p - }), - Cow::Borrowed(path) => Cow::Borrowed({ - #[cfg(unix)] - let p = { - use std::os::unix::ffi::OsStrExt; - path.as_os_str().as_bytes() - }; - #[cfg(not(unix))] - let p = path.to_str().ok_or(Utf8Error)?.as_bytes(); - p - }), - }; - Ok(utf8_bytes) -} - -/// Similar to [`into_bytes()`] but panics if malformed surrogates are encountered on windows. -pub fn into_bytes_or_panic_on_windows<'a>(path: impl Into>) -> Cow<'a, [u8]> { - into_bytes(path).expect("prefix path doesn't contain ill-formed UTF-8") -} - -/// Given `input` bytes, produce a `Path` from them ignoring encoding entirely if on unix. -/// -/// On windows, the input is required to be valid UTF-8, which is guaranteed if we wrote it before. There are some potential -/// git versions and windows installation which produce mal-formed UTF-16 if certain emojies are in the path. It's as rare as -/// it sounds, but possible. -pub fn from_byte_slice(input: &[u8]) -> Result<&Path, Utf8Error> { - #[cfg(unix)] - let p = { - use std::os::unix::ffi::OsStrExt; - OsStr::from_bytes(input).as_ref() - }; - #[cfg(not(unix))] - let p = Path::new(std::str::from_utf8(input).map_err(|_| Utf8Error)?); - Ok(p) -} - -/// Similar to [`from_byte_slice()`], but takes either borrowed or owned `input`. -pub fn from_bytes<'a>(input: impl Into>) -> Result, Utf8Error> { - let input = input.into(); - match input { - Cow::Borrowed(input) => from_byte_slice(input).map(Cow::Borrowed), - Cow::Owned(input) => from_byte_vec(input).map(Cow::Owned), - } -} - -/// Similar to [`from_byte_slice()`], but takes either borrowed or owned `input` as bstr. -#[cfg(feature = "bstr")] -pub fn from_bstr<'a>(input: impl Into>) -> Result, Utf8Error> { - let input = input.into(); - match input { - Cow::Borrowed(input) => from_byte_slice(input).map(Cow::Borrowed), - Cow::Owned(input) => from_byte_vec(input).map(Cow::Owned), - } -} - -/// Similar to [`from_byte_slice()`], but takes and produces owned data. -pub fn from_byte_vec(input: impl Into>) -> Result { - let input = input.into(); - #[cfg(unix)] - let p = { - use std::os::unix::ffi::OsStringExt; - std::ffi::OsString::from_vec(input).into() - }; - #[cfg(not(unix))] - let p = PathBuf::from(String::from_utf8(input).map_err(|_| Utf8Error)?); - Ok(p) -} - -/// Similar to [`from_byte_vec()`], but will panic if there is ill-formed UTF-8 in the `input`. -pub fn from_byte_vec_or_panic_on_windows(input: impl Into>) -> PathBuf { - from_byte_vec(input).expect("well-formed UTF-8 on windows") -} - -/// Similar to [`from_byte_slice()`], but will panic if there is ill-formed UTF-8 in the `input`. -pub fn from_byte_slice_or_panic_on_windows(input: &[u8]) -> &Path { - from_byte_slice(input).expect("well-formed UTF-8 on windows") -} - -/// Methods to handle paths as bytes and do conversions between them. -pub mod convert { - use std::borrow::Cow; - - fn replace<'a>(path: impl Into>, find: u8, replace: u8) -> Cow<'a, [u8]> { - let path = path.into(); - match path { - Cow::Owned(mut path) => { - for b in path.iter_mut().filter(|b| **b == find) { - *b = replace; - } - path.into() - } - Cow::Borrowed(path) => { - if !path.contains(&find) { - return path.into(); - } - let mut path = path.to_owned(); - for b in path.iter_mut().filter(|b| **b == find) { - *b = replace; - } - path.into() - } - } - } - - /// Replaces windows path separators with slashes. - pub fn to_native_separators<'a>(path: impl Into>) -> Cow<'a, [u8]> { - #[cfg(not(windows))] - let p = to_unix_separators(path); - #[cfg(windows)] - let p = to_windows_separators(path); - p - } - - /// Convert paths with slashes to backslashes on windows and do nothing on unix. - pub fn to_windows_separators_on_windows_or_panic(path: &std::path::Path) -> Cow<'_, std::path::Path> { - #[cfg(not(windows))] - { - path.into() - } - #[cfg(windows)] - { - crate::path::from_byte_slice_or_panic_on_windows( - crate::path::convert::to_windows_separators(crate::path::into_bytes_or_panic_on_windows(path)).as_ref(), - ) - .to_owned() - .into() - } - } - - /// Replaces windows path separators with slashes. - pub fn to_unix_separators<'a>(path: impl Into>) -> Cow<'a, [u8]> { - replace(path, b'\\', b'/') - } - - /// Find backslashes and replace them with slashes, which typically resembles a unix path. - /// - /// No other transformation is performed, the caller must check other invariants. - pub fn to_windows_separators<'a>(path: impl Into>) -> Cow<'a, [u8]> { - replace(path, b'/', b'\\') - } - - /// Obtain a `BStr` compatible `Cow` from one that is bytes. - #[cfg(feature = "bstr")] - pub fn into_bstr(path: Cow<'_, [u8]>) -> Cow<'_, bstr::BStr> { - match path { - Cow::Owned(p) => Cow::Owned(p.into()), - Cow::Borrowed(p) => Cow::Borrowed(p.into()), - } - } -} diff --git a/git-features/tests/path.rs b/git-features/tests/path.rs deleted file mode 100644 index 76d3f13f66..0000000000 --- a/git-features/tests/path.rs +++ /dev/null @@ -1,30 +0,0 @@ -mod bytes { - use bstr::ByteSlice; - use git_features::path; - - #[test] - fn assure_unix_separators() { - assert_eq!( - path::convert::to_unix_separators(b"no-backslash".as_ref()).as_bstr(), - "no-backslash" - ); - - assert_eq!( - path::convert::to_unix_separators(b"\\a\\b\\\\".as_ref()).as_bstr(), - "/a/b//" - ); - } - - #[test] - fn assure_windows_separators() { - assert_eq!( - path::convert::to_windows_separators(b"no-backslash".as_ref()).as_bstr(), - "no-backslash" - ); - - assert_eq!( - path::convert::to_windows_separators(b"/a/b//".as_ref()).as_bstr(), - "\\a\\b\\\\" - ); - } -} diff --git a/git-glob/src/lib.rs b/git-glob/src/lib.rs index 847600a71a..cfafeac7c3 100644 --- a/git-glob/src/lib.rs +++ b/git-glob/src/lib.rs @@ -4,9 +4,9 @@ use bstr::BString; -/// A glob pattern at a particular base path. +/// A glob pattern optimized for matching paths relative to a root directory. /// -/// This closely models how patterns appear in a directory hierarchy of include or attribute files. +/// For normal globbing, use [`wildmatch()`] instead. #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] #[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))] pub struct Pattern { @@ -28,6 +28,8 @@ pub use wildmatch::function::wildmatch; mod parse; /// Create a [`Pattern`] by parsing `text` or return `None` if `text` is empty. -pub fn parse(text: &[u8]) -> Option { - Pattern::from_bytes(text) +/// +/// Note that +pub fn parse(text: impl AsRef<[u8]>) -> Option { + Pattern::from_bytes(text.as_ref()) } diff --git a/git-glob/src/parse.rs b/git-glob/src/parse.rs index d39140438c..3693f88efc 100644 --- a/git-glob/src/parse.rs +++ b/git-glob/src/parse.rs @@ -26,6 +26,7 @@ pub fn pattern(mut pat: &[u8]) -> Option<(BString, pattern::Mode, Option) } if pat.first() == Some(&b'/') { mode |= Mode::ABSOLUTE; + pat = &pat[1..]; } let mut pat = truncate_non_escaped_trailing_spaces(pat); if pat.last() == Some(&b'/') { @@ -33,11 +34,10 @@ pub fn pattern(mut pat: &[u8]) -> Option<(BString, pattern::Mode, Option) pat.pop(); } - let relative_pattern = mode.contains(Mode::ABSOLUTE).then(|| &pat[1..]).unwrap_or(&pat); - if !relative_pattern.contains(&b'/') { + if !pat.contains(&b'/') { mode |= Mode::NO_SUB_DIR; } - if relative_pattern.first() == Some(&b'*') && first_wildcard_pos(&relative_pattern[1..]).is_none() { + if pat.first() == Some(&b'*') && first_wildcard_pos(&pat[1..]).is_none() { mode |= Mode::ENDS_WITH; } diff --git a/git-glob/src/pattern.rs b/git-glob/src/pattern.rs index 14fb4f6658..78c771e91c 100644 --- a/git-glob/src/pattern.rs +++ b/git-glob/src/pattern.rs @@ -1,5 +1,6 @@ use bitflags::bitflags; use bstr::{BStr, ByteSlice}; +use std::fmt; use crate::{pattern, wildmatch, Pattern}; @@ -36,6 +37,12 @@ pub enum Case { Fold, } +impl Default for Case { + fn default() -> Self { + Case::Sensitive + } +} + impl Pattern { /// Parse the given `text` as pattern, or return `None` if `text` was empty. pub fn from_bytes(text: &[u8]) -> Option { @@ -58,15 +65,14 @@ impl Pattern { /// `basename_start_pos` is the index at which the `path`'s basename starts. /// /// Lastly, `case` folding can be configured as well. - /// - /// Note that this method uses shortcuts to accelerate simple patterns. pub fn matches_repo_relative_path<'a>( &self, path: impl Into<&'a BStr>, basename_start_pos: Option, - is_dir: bool, + is_dir: Option, case: Case, ) -> bool { + let is_dir = is_dir.unwrap_or(false); if !is_dir && self.mode.contains(pattern::Mode::MUST_BE_DIR) { return false; } @@ -84,20 +90,11 @@ impl Pattern { ); debug_assert!(!path.starts_with(b"/"), "input path must be relative"); - let (text, first_wildcard_pos) = self - .mode - .contains(pattern::Mode::ABSOLUTE) - .then(|| (self.text[1..].as_bstr(), self.first_wildcard_pos.map(|p| p - 1))) - .unwrap_or((self.text.as_bstr(), self.first_wildcard_pos)); - if self.mode.contains(pattern::Mode::NO_SUB_DIR) { - let basename = if self.mode.contains(pattern::Mode::ABSOLUTE) { - path - } else { - &path[basename_start_pos.unwrap_or_default()..] - }; - self.matches_inner(text, first_wildcard_pos, basename, flags) + if self.mode.contains(pattern::Mode::NO_SUB_DIR) && !self.mode.contains(pattern::Mode::ABSOLUTE) { + let basename = &path[basename_start_pos.unwrap_or_default()..]; + self.matches(basename, flags) } else { - self.matches_inner(text, first_wildcard_pos, path, flags) + self.matches(path, flags) } } @@ -107,22 +104,12 @@ impl Pattern { /// strings with cases ignored as well. Note that the case folding performed here is ASCII only. /// /// Note that this method uses some shortcuts to accelerate simple patterns. - pub fn matches<'a>(&self, value: impl Into<&'a BStr>, mode: wildmatch::Mode) -> bool { - self.matches_inner(self.text.as_bstr(), self.first_wildcard_pos, value, mode) - } - - fn matches_inner<'a>( - &self, - text: &BStr, - first_wildcard_pos: Option, - value: impl Into<&'a BStr>, - mode: wildmatch::Mode, - ) -> bool { + fn matches<'a>(&self, value: impl Into<&'a BStr>, mode: wildmatch::Mode) -> bool { let value = value.into(); - match first_wildcard_pos { + match self.first_wildcard_pos { // "*literal" case, overrides starts-with Some(pos) if self.mode.contains(pattern::Mode::ENDS_WITH) && !value.contains(&b'/') => { - let text = &text[pos + 1..]; + let text = &self.text[pos + 1..]; if mode.contains(wildmatch::Mode::IGNORE_CASE) { value .len() @@ -137,22 +124,38 @@ impl Pattern { if mode.contains(wildmatch::Mode::IGNORE_CASE) { if !value .get(..pos) - .map_or(false, |value| value.eq_ignore_ascii_case(&text[..pos])) + .map_or(false, |value| value.eq_ignore_ascii_case(&self.text[..pos])) { return false; } - } else if !value.starts_with(&text[..pos]) { + } else if !value.starts_with(&self.text[..pos]) { return false; } - crate::wildmatch(text.as_bstr(), value, mode) + crate::wildmatch(self.text.as_bstr(), value, mode) } None => { if mode.contains(wildmatch::Mode::IGNORE_CASE) { - text.eq_ignore_ascii_case(value) + self.text.eq_ignore_ascii_case(value) } else { - text == value + self.text == value } } } } } + +impl fmt::Display for Pattern { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if self.mode.contains(Mode::NEGATIVE) { + "!".fmt(f)?; + } + if self.mode.contains(Mode::ABSOLUTE) { + "/".fmt(f)?; + } + self.text.fmt(f)?; + if self.mode.contains(Mode::MUST_BE_DIR) { + "/".fmt(f)?; + } + Ok(()) + } +} diff --git a/git-glob/tests/fixtures/generated-archives/make_baseline.tar.xz b/git-glob/tests/fixtures/generated-archives/make_baseline.tar.xz index b919a42dd6..5c62e7b853 100644 --- a/git-glob/tests/fixtures/generated-archives/make_baseline.tar.xz +++ b/git-glob/tests/fixtures/generated-archives/make_baseline.tar.xz @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cdade0fe7f3df3ac737130a101b077c82f9de1dcb3d59d5964e5ffd920405e8d -size 10384 +oid sha256:6bda59c1591dfd35f09cad5d09e2950b2a6ff885d9f7900535144a5275ab51c3 +size 10428 diff --git a/git-glob/tests/fixtures/make_baseline.sh b/git-glob/tests/fixtures/make_baseline.sh index e532574963..5787ff64ce 100644 --- a/git-glob/tests/fixtures/make_baseline.sh +++ b/git-glob/tests/fixtures/make_baseline.sh @@ -10,8 +10,10 @@ while read -r pattern value; do echo "$pattern" > .gitignore echo "$value" | git check-ignore -vn --stdin 2>&1 || : done <git-baseline.nmatch -*/\ XXX/\ -*/\\ XXX/\ +/*foo bam/barfoo/baz/bam +/*foo bar/bam/barfoo/baz/bam +foo foobaz +*/\' XXX/\' /*foo bar/foo /*foo bar/bazfoo foo*bar foo/baz/bar @@ -71,6 +73,7 @@ while read -r pattern value; do echo "$pattern" > .gitignore echo "$value" | git check-ignore -vn --stdin 2>&1 || : done <git-baseline.match +*/' XXX/' \a a \\\[a-z] \a \\\? \a diff --git a/git-glob/tests/glob.rs b/git-glob/tests/glob.rs index c7452ed5f3..3a90f1d510 100644 --- a/git-glob/tests/glob.rs +++ b/git-glob/tests/glob.rs @@ -1,3 +1,3 @@ -mod matching; mod parse; +mod pattern; mod wildmatch; diff --git a/git-glob/tests/matching/mod.rs b/git-glob/tests/matching/mod.rs index 8038488030..8b13789179 100644 --- a/git-glob/tests/matching/mod.rs +++ b/git-glob/tests/matching/mod.rs @@ -1,280 +1 @@ -use std::collections::BTreeSet; -use bstr::{BStr, ByteSlice}; -use git_glob::{pattern, pattern::Case}; - -#[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Copy, Clone)] -pub struct GitMatch<'a> { - pattern: &'a BStr, - value: &'a BStr, - /// True if git could match `value` with `pattern` - is_match: bool, -} - -pub struct Baseline<'a> { - inner: bstr::Lines<'a>, -} - -impl<'a> Iterator for Baseline<'a> { - type Item = GitMatch<'a>; - - fn next(&mut self) -> Option { - let mut tokens = self.inner.next()?.splitn(2, |b| *b == b' '); - let pattern = tokens.next().expect("pattern").as_bstr(); - let value = tokens.next().expect("value").as_bstr().trim_start().as_bstr(); - - let git_match = self.inner.next()?; - let is_match = !git_match.starts_with(b"::\t"); - Some(GitMatch { - pattern, - value, - is_match, - }) - } -} - -impl<'a> Baseline<'a> { - fn new(input: &'a [u8]) -> Self { - Baseline { - inner: input.as_bstr().lines(), - } - } -} - -#[test] -fn compare_baseline_with_ours() { - let dir = git_testtools::scripted_fixture_repo_read_only("make_baseline.sh").unwrap(); - let (mut total_matches, mut total_correct, mut panics) = (0, 0, 0); - let mut mismatches = Vec::new(); - for (input_file, expected_matches, case) in &[ - ("git-baseline.match", true, pattern::Case::Sensitive), - ("git-baseline.nmatch", false, pattern::Case::Sensitive), - ("git-baseline.match-icase", true, pattern::Case::Fold), - ] { - let input = std::fs::read(dir.join(*input_file)).unwrap(); - let mut seen = BTreeSet::default(); - - for m @ GitMatch { - pattern, - value, - is_match, - } in Baseline::new(&input) - { - total_matches += 1; - assert!(seen.insert(m), "duplicate match entry: {:?}", m); - assert_eq!( - is_match, *expected_matches, - "baseline for matches must be {} - check baseline and git version: {:?}", - expected_matches, m - ); - match std::panic::catch_unwind(|| { - let pattern = pat(pattern); - pattern.matches_repo_relative_path( - value, - basename_start_pos(value), - false, // TODO: does it make sense to pretend it is a dir and see what happens? - *case, - ) - }) { - Ok(actual_match) => { - if actual_match == is_match { - total_correct += 1; - } else { - mismatches.push((pattern.to_owned(), value.to_owned(), is_match, expected_matches)); - } - } - Err(_) => { - panics += 1; - continue; - } - }; - } - } - - dbg!(mismatches); - assert_eq!( - total_correct, - total_matches - panics, - "We perfectly agree with git here" - ); - assert_eq!(panics, 0); -} - -#[test] -fn non_dirs_for_must_be_dir_patterns_are_ignored() { - let pattern = pat("hello/"); - - assert!(pattern.mode.contains(pattern::Mode::MUST_BE_DIR)); - assert_eq!( - pattern.text, "hello", - "a dir pattern doesn't actually end with the trailing slash" - ); - let path = "hello"; - assert!( - !pattern.matches_repo_relative_path(path, None, false /* is-dir */, Case::Sensitive), - "non-dirs never match a dir pattern" - ); - assert!( - pattern.matches_repo_relative_path(path, None, true /* is-dir */, Case::Sensitive), - "dirs can match a dir pattern with the normal rules" - ); -} - -#[test] -fn matches_of_absolute_paths_work() { - let input = "/hello/git"; - let pat = pat(input); - assert!( - pat.matches(input, git_glob::wildmatch::Mode::empty()), - "patterns always match themselves" - ); - assert!( - pat.matches(input, git_glob::wildmatch::Mode::NO_MATCH_SLASH_LITERAL), - "patterns always match themselves, path mode doesn't change that" - ); -} - -#[test] -fn basename_matches_from_end() { - let pat = &pat("foo"); - assert!(match_file(pat, "FoO", Case::Fold)); - assert!(!match_file(pat, "FoOo", Case::Fold)); - assert!(!match_file(pat, "Foo", Case::Sensitive)); - assert!(match_file(pat, "foo", Case::Sensitive)); - assert!(!match_file(pat, "Foo", Case::Sensitive)); - assert!(!match_file(pat, "barfoo", Case::Sensitive)); -} - -#[test] -fn absolute_basename_matches_only_from_beginning() { - let pat = &pat("/foo"); - assert!(match_file(pat, "FoO", Case::Fold)); - assert!(!match_file(pat, "bar/Foo", Case::Fold)); - assert!(match_file(pat, "foo", Case::Sensitive)); - assert!(!match_file(pat, "Foo", Case::Sensitive)); - assert!(!match_file(pat, "bar/foo", Case::Sensitive)); -} - -#[test] -fn absolute_path_matches_only_from_beginning() { - let pat = &pat("/bar/foo"); - assert!(!match_file(pat, "FoO", Case::Fold)); - assert!(match_file(pat, "bar/Foo", Case::Fold)); - assert!(!match_file(pat, "foo", Case::Sensitive)); - assert!(match_file(pat, "bar/foo", Case::Sensitive)); - assert!(!match_file(pat, "bar/Foo", Case::Sensitive)); -} - -#[test] -fn absolute_path_with_recursive_glob_detects_mismatches_quickly() { - let pat = &pat("/bar/foo/**"); - assert!(!match_file(pat, "FoO", Case::Fold)); - assert!(!match_file(pat, "bar/Fooo", Case::Fold)); - assert!(!match_file(pat, "baz/bar/Foo", Case::Fold)); -} - -#[test] -fn absolute_path_with_recursive_glob_can_do_case_insensitive_prefix_search() { - let pat = &pat("/bar/foo/**"); - assert!(!match_file(pat, "bar/Foo/match", Case::Sensitive)); - assert!(match_file(pat, "bar/Foo/match", Case::Fold)); -} - -#[test] -fn relative_path_does_not_match_from_end() { - let pattern = &pat("bar/foo"); - assert!(!match_file(pattern, "FoO", Case::Fold)); - assert!(match_file(pattern, "bar/Foo", Case::Fold)); - assert!(!match_file(pattern, "baz/bar/Foo", Case::Fold)); - assert!(!match_file(pattern, "foo", Case::Sensitive)); - assert!(match_file(pattern, "bar/foo", Case::Sensitive)); - assert!(!match_file(pattern, "baz/bar/foo", Case::Sensitive)); - assert!(!match_file(pattern, "Baz/bar/Foo", Case::Sensitive)); -} - -#[test] -fn basename_glob_and_literal_is_ends_with() { - let pattern = &pat("*foo"); - assert!(match_file(pattern, "FoO", Case::Fold)); - assert!(match_file(pattern, "BarFoO", Case::Fold)); - assert!(!match_file(pattern, "BarFoOo", Case::Fold)); - assert!(!match_file(pattern, "Foo", Case::Sensitive)); - assert!(!match_file(pattern, "BarFoo", Case::Sensitive)); - assert!(match_file(pattern, "barfoo", Case::Sensitive)); - assert!(!match_file(pattern, "barfooo", Case::Sensitive)); - - assert!(match_file(pattern, "bar/foo", Case::Sensitive)); - assert!(match_file(pattern, "bar/bazfoo", Case::Sensitive)); -} - -#[test] -fn special_cases_from_corpus() { - let pattern = &pat("foo*bar"); - assert!( - !match_file(pattern, "foo/baz/bar", Case::Sensitive), - "asterisk does not match path separators" - ); - let pattern = &pat("*some/path/to/hello.txt"); - assert!( - !match_file(pattern, "a/bigger/some/path/to/hello.txt", Case::Sensitive), - "asterisk doesn't match path separators" - ); - - let pattern = &pat("/*foo.txt"); - assert!(match_file(pattern, "hello-foo.txt", Case::Sensitive)); - assert!( - !match_file(pattern, "hello/foo.txt", Case::Sensitive), - "absolute single asterisk doesn't match paths" - ); -} - -#[test] -fn absolute_basename_glob_and_literal_is_ends_with_in_basenames() { - let pattern = &pat("/*foo"); - - assert!(match_file(pattern, "FoO", Case::Fold)); - assert!(match_file(pattern, "BarFoO", Case::Fold)); - assert!(!match_file(pattern, "BarFoOo", Case::Fold)); - assert!(!match_file(pattern, "Foo", Case::Sensitive)); - assert!(!match_file(pattern, "BarFoo", Case::Sensitive)); - assert!(match_file(pattern, "barfoo", Case::Sensitive)); - assert!(!match_file(pattern, "barfooo", Case::Sensitive)); -} - -#[test] -fn absolute_basename_glob_and_literal_is_glob_in_paths() { - let pattern = &pat("/*foo"); - - assert!(!match_file(pattern, "bar/foo", Case::Sensitive), "* does not match /"); - assert!(!match_file(pattern, "bar/bazfoo", Case::Sensitive)); -} - -#[test] -fn negated_patterns_are_handled_by_caller() { - let pattern = &pat("!foo"); - assert!( - match_file(pattern, "foo", Case::Sensitive), - "negative patterns match like any other" - ); - assert!( - pattern.is_negative(), - "the caller checks for the negative flag and acts accordingly" - ); -} - -fn pat<'a>(pattern: impl Into<&'a BStr>) -> git_glob::Pattern { - git_glob::Pattern::from_bytes(pattern.into()).expect("parsing works") -} - -fn match_file<'a>(pattern: &git_glob::Pattern, path: impl Into<&'a BStr>, case: Case) -> bool { - match_path(pattern, path, false, case) -} - -fn match_path<'a>(pattern: &git_glob::Pattern, path: impl Into<&'a BStr>, is_dir: bool, case: Case) -> bool { - let path = path.into(); - pattern.matches_repo_relative_path(path, basename_start_pos(path), is_dir, case) -} - -fn basename_start_pos(value: &BStr) -> Option { - value.rfind_byte(b'/').map(|pos| pos + 1) -} diff --git a/git-glob/tests/parse/mod.rs b/git-glob/tests/parse/mod.rs index b95f854e1f..2d77d4f732 100644 --- a/git-glob/tests/parse/mod.rs +++ b/git-glob/tests/parse/mod.rs @@ -77,12 +77,12 @@ fn leading_exclamation_marks_can_be_escaped_with_backslash() { fn leading_slashes_mark_patterns_as_absolute() { assert_eq!( git_glob::parse(br"/absolute"), - pat("/absolute", Mode::NO_SUB_DIR | Mode::ABSOLUTE, None) + pat("absolute", Mode::NO_SUB_DIR | Mode::ABSOLUTE, None) ); assert_eq!( git_glob::parse(br"/absolute/path"), - pat("/absolute/path", Mode::ABSOLUTE, None) + pat("absolute/path", Mode::ABSOLUTE, None) ); } diff --git a/git-glob/tests/pattern/matching.rs b/git-glob/tests/pattern/matching.rs new file mode 100644 index 0000000000..4dddf804ec --- /dev/null +++ b/git-glob/tests/pattern/matching.rs @@ -0,0 +1,326 @@ +use std::collections::BTreeSet; + +use bstr::{BStr, ByteSlice}; +use git_glob::{pattern, pattern::Case}; + +#[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Copy, Clone)] +pub struct GitMatch<'a> { + pattern: &'a BStr, + value: &'a BStr, + /// True if git could match `value` with `pattern` + is_match: bool, +} + +pub struct Baseline<'a> { + inner: bstr::Lines<'a>, +} + +impl<'a> Iterator for Baseline<'a> { + type Item = GitMatch<'a>; + + fn next(&mut self) -> Option { + let mut tokens = self.inner.next()?.splitn(2, |b| *b == b' '); + let pattern = tokens.next().expect("pattern").as_bstr(); + let value = tokens.next().expect("value").as_bstr().trim_start().as_bstr(); + + let git_match = self.inner.next()?; + let is_match = !git_match.starts_with(b"::\t"); + Some(GitMatch { + pattern, + value, + is_match, + }) + } +} + +impl<'a> Baseline<'a> { + fn new(input: &'a [u8]) -> Self { + Baseline { + inner: input.as_bstr().lines(), + } + } +} + +#[test] +fn compare_baseline_with_ours() { + let dir = git_testtools::scripted_fixture_repo_read_only("make_baseline.sh").unwrap(); + let (mut total_matches, mut total_correct, mut panics) = (0, 0, 0); + let mut mismatches = Vec::new(); + for (input_file, expected_matches, case) in &[ + ("git-baseline.match", true, pattern::Case::Sensitive), + ("git-baseline.nmatch", false, pattern::Case::Sensitive), + ("git-baseline.match-icase", true, pattern::Case::Fold), + ] { + let input = std::fs::read(dir.join(*input_file)).unwrap(); + let mut seen = BTreeSet::default(); + + for m @ GitMatch { + pattern, + value, + is_match, + } in Baseline::new(&input) + { + total_matches += 1; + assert!(seen.insert(m), "duplicate match entry: {:?}", m); + assert_eq!( + is_match, *expected_matches, + "baseline for matches must be {} - check baseline and git version: {:?}", + expected_matches, m + ); + match std::panic::catch_unwind(|| { + let pattern = pat(pattern); + pattern.matches_repo_relative_path(value, basename_start_pos(value), None, *case) + }) { + Ok(actual_match) => { + if actual_match == is_match { + total_correct += 1; + } else { + mismatches.push((pattern.to_owned(), value.to_owned(), is_match, expected_matches)); + } + } + Err(_) => { + panics += 1; + continue; + } + }; + } + } + + dbg!(mismatches); + assert_eq!( + total_correct, + total_matches - panics, + "We perfectly agree with git here" + ); + assert_eq!(panics, 0); +} + +#[test] +fn non_dirs_for_must_be_dir_patterns_are_ignored() { + let pattern = pat("hello/"); + + assert!(pattern.mode.contains(pattern::Mode::MUST_BE_DIR)); + assert_eq!( + pattern.text, "hello", + "a dir pattern doesn't actually end with the trailing slash" + ); + let path = "hello"; + assert!( + !pattern.matches_repo_relative_path(path, None, false.into() /* is-dir */, Case::Sensitive), + "non-dirs never match a dir pattern" + ); + assert!( + pattern.matches_repo_relative_path(path, None, true.into() /* is-dir */, Case::Sensitive), + "dirs can match a dir pattern with the normal rules" + ); +} + +#[test] +fn matches_of_absolute_paths_work() { + let pattern = "/hello/git"; + assert!( + git_glob::wildmatch(pattern.into(), pattern.into(), git_glob::wildmatch::Mode::empty()), + "patterns always match themselves" + ); + assert!( + git_glob::wildmatch( + pattern.into(), + pattern.into(), + git_glob::wildmatch::Mode::NO_MATCH_SLASH_LITERAL + ), + "patterns always match themselves, path mode doesn't change that" + ); +} + +#[test] +fn basename_matches_from_end() { + let pat = &pat("foo"); + assert!(match_file(pat, "FoO", Case::Fold)); + assert!(!match_file(pat, "FoOo", Case::Fold)); + assert!(!match_file(pat, "Foo", Case::Sensitive)); + assert!(match_file(pat, "foo", Case::Sensitive)); + assert!(!match_file(pat, "Foo", Case::Sensitive)); + assert!(!match_file(pat, "barfoo", Case::Sensitive)); +} + +#[test] +fn absolute_basename_matches_only_from_beginning() { + let pat = &pat("/foo"); + assert!(match_file(pat, "FoO", Case::Fold)); + assert!(!match_file(pat, "bar/Foo", Case::Fold)); + assert!(match_file(pat, "foo", Case::Sensitive)); + assert!(!match_file(pat, "Foo", Case::Sensitive)); + assert!(!match_file(pat, "bar/foo", Case::Sensitive)); +} + +#[test] +fn absolute_path_matches_only_from_beginning() { + let pat = &pat("/bar/foo"); + assert!(!match_file(pat, "FoO", Case::Fold)); + assert!(match_file(pat, "bar/Foo", Case::Fold)); + assert!(!match_file(pat, "foo", Case::Sensitive)); + assert!(match_file(pat, "bar/foo", Case::Sensitive)); + assert!(!match_file(pat, "bar/Foo", Case::Sensitive)); +} + +#[test] +fn absolute_path_with_recursive_glob_detects_mismatches_quickly() { + let pat = &pat("/bar/foo/**"); + assert!(!match_file(pat, "FoO", Case::Fold)); + assert!(!match_file(pat, "bar/Fooo", Case::Fold)); + assert!(!match_file(pat, "baz/bar/Foo", Case::Fold)); +} + +#[test] +fn absolute_path_with_recursive_glob_can_do_case_insensitive_prefix_search() { + let pat = &pat("/bar/foo/**"); + assert!(!match_file(pat, "bar/Foo/match", Case::Sensitive)); + assert!(match_file(pat, "bar/Foo/match", Case::Fold)); +} + +#[test] +fn relative_path_does_not_match_from_end() { + for pattern in &["bar/foo", "/bar/foo"] { + let pattern = &pat(*pattern); + assert!(!match_file(pattern, "FoO", Case::Fold)); + assert!(match_file(pattern, "bar/Foo", Case::Fold)); + assert!(!match_file(pattern, "baz/bar/Foo", Case::Fold)); + assert!(!match_file(pattern, "foo", Case::Sensitive)); + assert!(match_file(pattern, "bar/foo", Case::Sensitive)); + assert!(!match_file(pattern, "baz/bar/foo", Case::Sensitive)); + assert!(!match_file(pattern, "Baz/bar/Foo", Case::Sensitive)); + } +} + +#[test] +fn basename_glob_and_literal_is_ends_with() { + let pattern = &pat("*foo"); + assert!(match_file(pattern, "FoO", Case::Fold)); + assert!(match_file(pattern, "BarFoO", Case::Fold)); + assert!(!match_file(pattern, "BarFoOo", Case::Fold)); + assert!(!match_file(pattern, "Foo", Case::Sensitive)); + assert!(!match_file(pattern, "BarFoo", Case::Sensitive)); + assert!(match_file(pattern, "barfoo", Case::Sensitive)); + assert!(!match_file(pattern, "barfooo", Case::Sensitive)); + + assert!(match_file(pattern, "bar/foo", Case::Sensitive)); + assert!(match_file(pattern, "bar/bazfoo", Case::Sensitive)); +} + +#[test] +fn special_cases_from_corpus() { + let pattern = &pat("foo*bar"); + assert!( + !match_file(pattern, "foo/baz/bar", Case::Sensitive), + "asterisk does not match path separators" + ); + let pattern = &pat("*some/path/to/hello.txt"); + assert!( + !match_file(pattern, "a/bigger/some/path/to/hello.txt", Case::Sensitive), + "asterisk doesn't match path separators" + ); + + let pattern = &pat("/*foo.txt"); + assert!(match_file(pattern, "hello-foo.txt", Case::Sensitive)); + assert!( + !match_file(pattern, "hello/foo.txt", Case::Sensitive), + "absolute single asterisk doesn't match paths" + ); +} + +#[test] +fn absolute_basename_glob_and_literal_is_ends_with_in_basenames() { + let pattern = &pat("/*foo"); + + assert!(match_file(pattern, "FoO", Case::Fold)); + assert!(match_file(pattern, "BarFoO", Case::Fold)); + assert!(!match_file(pattern, "BarFoOo", Case::Fold)); + assert!(!match_file(pattern, "Foo", Case::Sensitive)); + assert!(!match_file(pattern, "BarFoo", Case::Sensitive)); + assert!(match_file(pattern, "barfoo", Case::Sensitive)); + assert!(!match_file(pattern, "barfooo", Case::Sensitive)); +} + +#[test] +fn absolute_basename_glob_and_literal_is_glob_in_paths() { + let pattern = &pat("/*foo"); + + assert!(!match_file(pattern, "bar/foo", Case::Sensitive), "* does not match /"); + assert!(!match_file(pattern, "bar/bazfoo", Case::Sensitive)); +} + +#[test] +fn negated_patterns_are_handled_by_caller() { + let pattern = &pat("!foo"); + assert!( + match_file(pattern, "foo", Case::Sensitive), + "negative patterns match like any other" + ); + assert!( + pattern.is_negative(), + "the caller checks for the negative flag and acts accordingly" + ); +} +#[test] +fn names_do_not_automatically_match_entire_directories() { + // this feature is implemented with the directory stack. + let pattern = &pat("foo"); + assert!(!match_file(pattern, "foobar", Case::Sensitive)); + assert!(!match_file(pattern, "foo/bar", Case::Sensitive)); + assert!(!match_file(pattern, "foo/bar/baz", Case::Sensitive)); +} + +#[test] +fn directory_patterns_do_not_match_files_within_a_directory_as_well_like_slash_star_star() { + // this feature is implemented with the directory stack, which excludes entire directories + let pattern = &pat("dir/"); + assert!(!match_path(pattern, "dir/file", None, Case::Sensitive)); + assert!(!match_path(pattern, "base/dir/file", None, Case::Sensitive)); + assert!(!match_path(pattern, "base/ndir/file", None, Case::Sensitive)); + assert!(!match_path(pattern, "Dir/File", None, Case::Fold)); + assert!(!match_path(pattern, "Base/Dir/File", None, Case::Fold)); + assert!(!match_path(pattern, "dir2/file", None, Case::Sensitive)); + + let pattern = &pat("dir/sub-dir/"); + assert!(!match_path(pattern, "dir/sub-dir/file", None, Case::Sensitive)); + assert!(!match_path(pattern, "dir/Sub-dir/File", None, Case::Fold)); + assert!(!match_path(pattern, "dir/Sub-dir2/File", None, Case::Fold)); +} + +#[test] +fn single_paths_match_anywhere() { + let pattern = &pat("target"); + assert!(match_file(pattern, "dir/target", Case::Sensitive)); + assert!(!match_file(pattern, "dir/atarget", Case::Sensitive)); + assert!(!match_file(pattern, "dir/targeta", Case::Sensitive)); + assert!(match_path(pattern, "dir/target", Some(true), Case::Sensitive)); + + let pattern = &pat("target/"); + assert!(!match_file(pattern, "dir/target", Case::Sensitive)); + assert!( + !match_path(pattern, "dir/target", None, Case::Sensitive), + "it assumes unknown to not be a directory" + ); + assert!(match_path(pattern, "dir/target", Some(true), Case::Sensitive)); + assert!( + !match_path(pattern, "dir/target/", Some(true), Case::Sensitive), + "we need sanitized paths that don't have trailing slashes" + ); +} + +fn pat<'a>(pattern: impl Into<&'a BStr>) -> git_glob::Pattern { + git_glob::Pattern::from_bytes(pattern.into()).expect("parsing works") +} + +fn match_file<'a>(pattern: &git_glob::Pattern, path: impl Into<&'a BStr>, case: Case) -> bool { + match_path(pattern, path, false.into(), case) +} + +fn match_path<'a>(pattern: &git_glob::Pattern, path: impl Into<&'a BStr>, is_dir: Option, case: Case) -> bool { + let path = path.into(); + pattern.matches_repo_relative_path(path, basename_start_pos(path), is_dir, case) +} + +fn basename_start_pos(value: &BStr) -> Option { + value.rfind_byte(b'/').map(|pos| pos + 1) +} diff --git a/git-glob/tests/pattern/mod.rs b/git-glob/tests/pattern/mod.rs new file mode 100644 index 0000000000..1a66b98fa8 --- /dev/null +++ b/git-glob/tests/pattern/mod.rs @@ -0,0 +1,19 @@ +use git_glob::pattern::Mode; +use git_glob::Pattern; + +#[test] +fn display() { + fn pat(text: &str, mode: Mode) -> String { + Pattern { + text: text.into(), + mode, + first_wildcard_pos: None, + } + .to_string() + } + assert_eq!(pat("a", Mode::ABSOLUTE), "/a"); + assert_eq!(pat("a", Mode::MUST_BE_DIR), "a/"); + assert_eq!(pat("a", Mode::NEGATIVE), "!a"); + assert_eq!(pat("a", Mode::ABSOLUTE | Mode::NEGATIVE | Mode::MUST_BE_DIR), "!/a/"); +} +mod matching; diff --git a/git-glob/tests/wildmatch/mod.rs b/git-glob/tests/wildmatch/mod.rs index 9d6a034c5d..5b0962f753 100644 --- a/git-glob/tests/wildmatch/mod.rs +++ b/git-glob/tests/wildmatch/mod.rs @@ -1,3 +1,4 @@ +use bstr::ByteSlice; use std::{ fmt::{Debug, Display, Formatter}, panic::catch_unwind, @@ -222,6 +223,7 @@ fn corpus() { } } + dbg!(&failures); assert_eq!(failures.len(), 0); assert_eq!(at_least_one_panic, 0, "not a single panic in any invocation"); @@ -249,8 +251,10 @@ fn multi_match(pattern_text: &str, text: &str) -> (Pattern, MultiMatch) { let pattern = git_glob::Pattern::from_bytes(pattern_text.as_bytes()).expect("valid (enough) pattern"); let actual_path_match: MatchResult = catch_unwind(|| match_file_path(&pattern, text, Case::Sensitive)).into(); let actual_path_imatch: MatchResult = catch_unwind(|| match_file_path(&pattern, text, Case::Fold)).into(); - let actual_glob_match: MatchResult = catch_unwind(|| pattern.matches(text, wildmatch::Mode::empty())).into(); - let actual_glob_imatch: MatchResult = catch_unwind(|| pattern.matches(text, wildmatch::Mode::IGNORE_CASE)).into(); + let actual_glob_match: MatchResult = + catch_unwind(|| git_glob::wildmatch(pattern.text.as_bstr(), text.into(), wildmatch::Mode::empty())).into(); + let actual_glob_imatch: MatchResult = + catch_unwind(|| git_glob::wildmatch(pattern.text.as_bstr(), text.into(), wildmatch::Mode::IGNORE_CASE)).into(); let actual = MultiMatch { path_match: actual_path_match, path_imatch: actual_path_imatch, @@ -363,7 +367,7 @@ impl Display for MatchResult { } fn match_file_path(pattern: &git_glob::Pattern, path: &str, case: Case) -> bool { - pattern.matches_repo_relative_path(path, basename_of(path), false /* is_dir */, case) + pattern.matches_repo_relative_path(path, basename_of(path), false.into() /* is_dir */, case) } fn basename_of(path: &str) -> Option { path.rfind('/').map(|pos| pos + 1) diff --git a/git-index/src/access.rs b/git-index/src/access.rs index 92ae7309b2..c72fd75ad3 100644 --- a/git-index/src/access.rs +++ b/git-index/src/access.rs @@ -1,6 +1,6 @@ use bstr::{BStr, ByteSlice}; -use crate::{extension, Entry, State, Version}; +use crate::{extension, Entry, PathStorage, State, Version}; impl State { pub fn version(&self) -> Version { @@ -10,6 +10,35 @@ impl State { pub fn entries(&self) -> &[Entry] { &self.entries } + pub fn path_backing(&self) -> &PathStorage { + &self.path_backing + } + pub fn take_path_backing(&mut self) -> PathStorage { + assert_eq!( + self.entries.is_empty(), + self.path_backing.is_empty(), + "BUG: cannot take out backing multiple times" + ); + std::mem::take(&mut self.path_backing) + } + + pub fn return_path_backing(&mut self, backing: PathStorage) { + assert!( + self.path_backing.is_empty(), + "BUG: return path backing only after taking it, once" + ); + self.path_backing = backing; + } + + pub fn entries_with_paths_by_filter_map<'a, T>( + &'a self, + mut filter_map: impl FnMut(&'a BStr, &Entry) -> Option + 'a, + ) -> impl Iterator + 'a { + self.entries.iter().filter_map(move |e| { + let p = e.path(self); + filter_map(p, e).map(|t| (p, t)) + }) + } pub fn entries_mut(&mut self) -> &mut [Entry] { &mut self.entries } @@ -20,6 +49,15 @@ impl State { (e, path) }) } + pub fn entries_mut_with_paths_in<'state, 'backing>( + &'state mut self, + backing: &'backing PathStorage, + ) -> impl Iterator { + self.entries.iter_mut().map(move |e| { + let path = (&backing[e.path.clone()]).as_bstr(); + (e, path) + }) + } pub fn tree(&self) -> Option<&extension::Tree> { self.tree.as_ref() } diff --git a/git-index/src/decode/mod.rs b/git-index/src/decode/mod.rs index 36262430eb..82a6229681 100644 --- a/git-index/src/decode/mod.rs +++ b/git-index/src/decode/mod.rs @@ -41,12 +41,13 @@ use crate::util::read_u32; pub struct Options { pub object_hash: git_hash::Kind, /// If Some(_), we are allowed to use more than one thread. If Some(N), use no more than N threads. If Some(0)|None, use as many threads - /// as there are physical cores. + /// as there are logical cores. /// /// This applies to loading extensions in parallel to entries if the common EOIE extension is available. /// It also allows to use multiple threads for loading entries if the IEOT extension is present. pub thread_limit: Option, /// The minimum size in bytes to load extensions in their own thread, assuming there is enough `num_threads` available. + /// If set to 0, for example, extensions will always be read in their own thread if enough threads are available. pub min_extension_block_in_bytes_for_threading: usize, } diff --git a/git-index/src/entry.rs b/git-index/src/entry.rs index f165127fe3..6ca28e484f 100644 --- a/git-index/src/entry.rs +++ b/git-index/src/entry.rs @@ -143,6 +143,10 @@ mod access { (&state.path_backing[self.path.clone()]).as_bstr() } + pub fn path_in<'backing>(&self, backing: &'backing crate::PathStorage) -> &'backing BStr { + (backing[self.path.clone()]).as_bstr() + } + pub fn stage(&self) -> u32 { self.flags.stage() } diff --git a/git-index/src/lib.rs b/git-index/src/lib.rs index 8151618edc..364b6d0d5a 100644 --- a/git-index/src/lib.rs +++ b/git-index/src/lib.rs @@ -51,6 +51,9 @@ pub struct File { pub checksum: git_hash::ObjectId, } +/// The type to use and store paths to all entries. +pub type PathStorage = Vec; + /// An in-memory cache of a fully parsed git index file. /// /// As opposed to a snapshot, it's meant to be altered and eventually be written back to disk or converted into a tree. @@ -65,7 +68,7 @@ pub struct State { version: Version, entries: Vec, /// A memory area keeping all index paths, in full length, independently of the index version. - path_backing: Vec, + path_backing: PathStorage, /// True if one entry in the index has a special marker mode #[allow(dead_code)] is_sparse: bool, diff --git a/git-odb/Cargo.toml b/git-odb/Cargo.toml index 0ea6f4ce5d..5590570f29 100644 --- a/git-odb/Cargo.toml +++ b/git-odb/Cargo.toml @@ -30,7 +30,8 @@ required-features = [] all-features = true [dependencies] -git-features = { version = "^0.20.0", path = "../git-features", features = ["rustsha1", "walkdir", "zlib", "crc32", "bstr"] } +git-features = { version = "^0.20.0", path = "../git-features", features = ["rustsha1", "walkdir", "zlib", "crc32" ] } +git-path = { version = "^0.1.0", path = "../git-path" } git-hash = { version = "^0.9.3", path = "../git-hash" } git-quote = { version = "^0.2.0", path = "../git-quote" } git-object = { version = "^0.18.0", path = "../git-object" } diff --git a/git-odb/src/alternate/parse.rs b/git-odb/src/alternate/parse.rs index a244076fdc..5ca2f4353d 100644 --- a/git-odb/src/alternate/parse.rs +++ b/git-odb/src/alternate/parse.rs @@ -20,7 +20,7 @@ pub(crate) fn content(input: &[u8]) -> Result, Error> { continue; } out.push( - git_features::path::from_bstr(if line.starts_with(b"\"") { + git_path::try_from_bstr(if line.starts_with(b"\"") { git_quote::ansi_c::undo(line)?.0 } else { Cow::Borrowed(line) diff --git a/git-pack/Cargo.toml b/git-pack/Cargo.toml index bbd4f144ef..5eaf30b449 100644 --- a/git-pack/Cargo.toml +++ b/git-pack/Cargo.toml @@ -39,6 +39,7 @@ required-features = ["internal-testing-to-avoid-being-run-by-cargo-test-all"] [dependencies] git-features = { version = "^0.20.0", path = "../git-features", features = ["crc32", "rustsha1", "progress", "zlib"] } git-hash = { version = "^0.9.3", path = "../git-hash" } +git-path = { version = "^0.1.0", path = "../git-path" } git-chunk = { version = "^0.3.0", path = "../git-chunk" } git-object = { version = "^0.18.0", path = "../git-object" } git-traverse = { version = "^0.14.0", path = "../git-traverse" } diff --git a/git-pack/src/multi_index/chunk.rs b/git-pack/src/multi_index/chunk.rs index 8e266dc1a2..80982bfecf 100644 --- a/git-pack/src/multi_index/chunk.rs +++ b/git-pack/src/multi_index/chunk.rs @@ -34,7 +34,7 @@ pub mod index_names { let null_byte_pos = chunk.find_byte(b'\0').ok_or(decode::Error::MissingNullByte)?; let path = &chunk[..null_byte_pos]; - let path = git_features::path::from_byte_slice(path) + let path = git_path::try_from_byte_slice(path) .map_err(|_| decode::Error::PathEncoding { path: BString::from(path), })? diff --git a/git-path/CHANGELOG.md b/git-path/CHANGELOG.md new file mode 100644 index 0000000000..8f962e3406 --- /dev/null +++ b/git-path/CHANGELOG.md @@ -0,0 +1,42 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## 0.1.0 (2022-04-28) + +### Refactor (BREAKING) + + - various name changes for more convenient API + +### Commit Statistics + + + + - 8 commits contributed to the release over the course of 1 calendar day. + - 1 commit where understood as [conventional](https://www.conventionalcommits.org). + - 1 unique issue was worked on: [#301](https://github.com/Byron/gitoxide/issues/301) + +### Commit Details + + + +
view details + + * **[#301](https://github.com/Byron/gitoxide/issues/301)** + - frame for `gix repo exclude query` ([`a331314`](https://github.com/Byron/gitoxide/commit/a331314758629a93ba036245a5dd03cf4109dc52)) + - refactor ([`21d4076`](https://github.com/Byron/gitoxide/commit/21d407638285b728d0c64fabf2abe0e1948e9bec)) + - The first indication that directory-based excludes work ([`e868acc`](https://github.com/Byron/gitoxide/commit/e868acce2e7c3e2501497bf630e3a54f349ad38e)) + - various name changes for more convenient API ([`5480159`](https://github.com/Byron/gitoxide/commit/54801592488416ef2bb0f34c5061b62189c35c5e)) + - Use bstr intead of [u8] ([`9380e99`](https://github.com/Byron/gitoxide/commit/9380e9990065897e318b040f49b3c9a6de8bebb1)) + - Use `git-path` crate instead of `git_features::path` ([`47e607d`](https://github.com/Byron/gitoxide/commit/47e607dc256a43a3411406c645eb7ff04239dd3a)) + - Copy all existing functions from git-features::path to git-path:: ([`725e198`](https://github.com/Byron/gitoxide/commit/725e1985dc521d01ff9e1e89b6468ef62fc09656)) + - add empty git-path crate ([`8d13f81`](https://github.com/Byron/gitoxide/commit/8d13f81068b4663d322002a9617d39b307b63469)) +
+ +## 0.0.0 (2022-03-31) + +An empty crate without any content to reserve the name for the gitoxide project. + diff --git a/git-path/Cargo.toml b/git-path/Cargo.toml new file mode 100644 index 0000000000..2c9331f508 --- /dev/null +++ b/git-path/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "git-path" +version = "0.1.0" +repository = "https://github.com/Byron/gitoxide" +license = "MIT/Apache-2.0" +description = "A WIP crate of the gitoxide project dealing paths and their conversions" +authors = ["Sebastian Thiel "] +edition = "2018" + +[lib] +doctest = false + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +bstr = { version = "0.2.17", default-features = false, features = ["std"] } diff --git a/git-path/src/convert.rs b/git-path/src/convert.rs new file mode 100644 index 0000000000..5c2853aae4 --- /dev/null +++ b/git-path/src/convert.rs @@ -0,0 +1,197 @@ +use bstr::{BStr, BString}; +use std::{ + borrow::Cow, + ffi::OsStr, + path::{Path, PathBuf}, +}; + +#[derive(Debug)] +/// The error type returned by [`into_bstr()`] and others may suffer from failed conversions from or to bytes. +pub struct Utf8Error; + +impl std::fmt::Display for Utf8Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("Could not convert to UTF8 or from UTF8 due to ill-formed input") + } +} + +impl std::error::Error for Utf8Error {} + +/// Like [`into_bstr()`], but takes `OsStr` as input for a lossless, but fallible, conversion. +pub fn os_str_into_bstr(path: &OsStr) -> Result<&BStr, Utf8Error> { + let path = try_into_bstr(Cow::Borrowed(path.as_ref()))?; + match path { + Cow::Borrowed(path) => Ok(path), + Cow::Owned(_) => unreachable!("borrowed cows stay borrowed"), + } +} + +/// Convert the given path either into its raw bytes on unix or its UTF8 encoded counterpart on windows. +/// +/// On windows, if the source Path contains ill-formed, lone surrogates, the UTF-8 conversion will fail +/// causing `Utf8Error` to be returned. +pub fn try_into_bstr<'a>(path: impl Into>) -> Result, Utf8Error> { + let path = path.into(); + let path_str = match path { + Cow::Owned(path) => Cow::Owned({ + #[cfg(unix)] + let p: BString = { + use std::os::unix::ffi::OsStringExt; + path.into_os_string().into_vec().into() + }; + #[cfg(not(unix))] + let p: BString = path.into_os_string().into_string().map_err(|_| Utf8Error)?.into(); + p + }), + Cow::Borrowed(path) => Cow::Borrowed({ + #[cfg(unix)] + let p: &BStr = { + use std::os::unix::ffi::OsStrExt; + path.as_os_str().as_bytes().into() + }; + #[cfg(not(unix))] + let p: &BStr = path.to_str().ok_or(Utf8Error)?.as_bytes().into(); + p + }), + }; + Ok(path_str) +} + +/// Similar to [`try_into_bstr()`] but **panics** if malformed surrogates are encountered on windows. +pub fn into_bstr<'a>(path: impl Into>) -> Cow<'a, BStr> { + try_into_bstr(path).expect("prefix path doesn't contain ill-formed UTF-8") +} + +/// Given `input` bytes, produce a `Path` from them ignoring encoding entirely if on unix. +/// +/// On windows, the input is required to be valid UTF-8, which is guaranteed if we wrote it before. There are some potential +/// git versions and windows installation which produce mal-formed UTF-16 if certain emojies are in the path. It's as rare as +/// it sounds, but possible. +pub fn try_from_byte_slice(input: &[u8]) -> Result<&Path, Utf8Error> { + #[cfg(unix)] + let p = { + use std::os::unix::ffi::OsStrExt; + OsStr::from_bytes(input).as_ref() + }; + #[cfg(not(unix))] + let p = Path::new(std::str::from_utf8(input).map_err(|_| Utf8Error)?); + Ok(p) +} + +/// Similar to [`from_byte_slice()`], but takes either borrowed or owned `input`. +pub fn try_from_bstr<'a>(input: impl Into>) -> Result, Utf8Error> { + let input = input.into(); + match input { + Cow::Borrowed(input) => try_from_byte_slice(input).map(Cow::Borrowed), + Cow::Owned(input) => try_from_bstring(input).map(Cow::Owned), + } +} + +/// Similar to [`try_from_bstr()`], but **panics** if malformed surrogates are encountered on windows. +pub fn from_bstr<'a>(input: impl Into>) -> Cow<'a, Path> { + try_from_bstr(input).expect("prefix path doesn't contain ill-formed UTF-8") +} + +/// Similar to [`try_from_bstr()`], but takes and produces owned data. +pub fn try_from_bstring(input: impl Into) -> Result { + let input = input.into(); + #[cfg(unix)] + let p = { + use std::os::unix::ffi::OsStringExt; + std::ffi::OsString::from_vec(input.into()).into() + }; + #[cfg(not(unix))] + let p = { + use bstr::ByteVec; + PathBuf::from( + { + let v: Vec<_> = input.into(); + v + } + .into_string() + .map_err(|_| Utf8Error)?, + ) + }; + Ok(p) +} + +/// Similar to [`try_from_bstring()`], but will panic if there is ill-formed UTF-8 in the `input`. +pub fn from_bstring(input: impl Into) -> PathBuf { + try_from_bstring(input).expect("well-formed UTF-8 on windows") +} + +/// Similar to [`try_from_byte_slice()`], but will panic if there is ill-formed UTF-8 in the `input`. +pub fn from_byte_slice(input: &[u8]) -> &Path { + try_from_byte_slice(input).expect("well-formed UTF-8 on windows") +} + +fn replace<'a>(path: impl Into>, find: u8, replace: u8) -> Cow<'a, BStr> { + let path = path.into(); + match path { + Cow::Owned(mut path) => { + for b in path.iter_mut().filter(|b| **b == find) { + *b = replace; + } + path.into() + } + Cow::Borrowed(path) => { + if !path.contains(&find) { + return path.into(); + } + let mut path = path.to_owned(); + for b in path.iter_mut().filter(|b| **b == find) { + *b = replace; + } + path.into() + } + } +} + +/// Assures the given bytes use the native path separator. +pub fn to_native_separators<'a>(path: impl Into>) -> Cow<'a, BStr> { + #[cfg(not(windows))] + let p = to_unix_separators(path); + #[cfg(windows)] + let p = to_windows_separators(path); + p +} + +/// Convert paths with slashes to backslashes on windows and do nothing on unix, but **panics** if malformed surrogates are encountered on windows. +pub fn to_native_path_on_windows<'a>(path: impl Into>) -> Cow<'a, std::path::Path> { + #[cfg(not(windows))] + { + crate::from_bstr(path) + } + #[cfg(windows)] + { + crate::from_bstr(to_windows_separators(path)) + } +} + +/// Replaces windows path separators with slashes, but only do so on windows. +pub fn to_unix_separators_on_windows<'a>(path: impl Into>) -> Cow<'a, BStr> { + #[cfg(windows)] + { + replace(path, b'\\', b'/') + } + #[cfg(not(windows))] + { + path.into() + } +} + +/// Replaces windows path separators with slashes, unconditionally. +/// +/// **Note** Do not use these and prefer the conditional versions of this method. +// TODO: use https://lib.rs/crates/path-slash to handle escapes +pub fn to_unix_separators<'a>(path: impl Into>) -> Cow<'a, BStr> { + replace(path, b'\\', b'/') +} + +/// Find backslashes and replace them with slashes, which typically resembles a unix path, unconditionally. +/// +/// **Note** Do not use these and prefer the conditional versions of this method. +// TODO: use https://lib.rs/crates/path-slash to handle escapes +pub fn to_windows_separators<'a>(path: impl Into>) -> Cow<'a, BStr> { + replace(path, b'/', b'\\') +} diff --git a/git-path/src/lib.rs b/git-path/src/lib.rs new file mode 100644 index 0000000000..e7f301a66a --- /dev/null +++ b/git-path/src/lib.rs @@ -0,0 +1,52 @@ +#![forbid(unsafe_code, rust_2018_idioms, missing_docs)] +//! ### Research +//! +//! * **windows** +//! - [`dirent.c`](https://github.com/git/git/blob/main/compat/win32/dirent.c#L31:L31) contains all implementation (seemingly) of opening directories and reading their entries, along with all path conversions (UTF-16 for windows). This is done on the fly so git can work with [in UTF-8](https://github.com/git/git/blob/main/compat/win32/dirent.c#L12:L12). +//! - mingw [is used for the conversion](https://github.com/git/git/blob/main/compat/mingw.h#L579:L579) and it appears they handle surrogates during the conversion, maybe some sort of non-strict UTF-8 converter? Actually it uses [WideCharToMultiByte](https://docs.microsoft.com/en-us/windows/win32/api/stringapiset/nf-stringapiset-widechartomultibyte) +//! under the hood which by now does fail if the UTF-8 would be invalid unicode, i.e. unicode pairs. +//! - `OsString` on windows already stores strings as WTF-8, which supports [surrogate pairs](https://unicodebook.readthedocs.io/unicode_encodings.html), +//! something that UTF-8 isn't allowed do it for security reasons, after all it's UTF-16 specific and exists only to extend +//! the encodable code-points. +//! - informative reading on [WTF-8](https://simonsapin.github.io/wtf-8/#motivation) which is the encoding used by Rust +//! internally that deals with surrogates and non-wellformed surrogates (those that aren't in pairs). +//! * **unix** +//! - It uses [opendir](https://man7.org/linux/man-pages/man3/opendir.3.html) and [readdir](https://man7.org/linux/man-pages/man3/readdir.3.html) +//! respectively. There is no encoding specified, except that these paths are null-terminated. +//! +//! ### Learnings +//! +//! Surrogate pairs are a way to extend the encodable value range in UTF-16 encodings, used primarily on windows and in Javascript. +//! For a long time these codepoints used for surrogates, always to be used in pairs, were not assigned, until…they were for rare +//! emojies and the likes. The unicode standard does not require surrogates to happen in pairs, even though by now unpaired surrogates +//! in UTF-16 are considered ill-formed, which aren't supposed to be converted to UTF-8 for example. +//! +//! This is the reason we have to deal with `to_string_lossy()`, it's _just_ for that quirk. +//! +//! This also means the only platform ever eligible to see conversion errors is windows, and there it's only older pre-vista +//! windows versions which incorrectly allow ill-formed UTF-16 strings. Newer versions don't perform such conversions anymore, for +//! example when going from UTF-16 to UTF-8, they will trigger an error. +//! +//! ### Conclusions +//! +//! Since [WideCharToMultiByte](https://docs.microsoft.com/en-us/windows/win32/api/stringapiset/nf-stringapiset-widechartomultibyte) by now is +//! fixed (Vista onward) to produce valid UTF-8, lone surrogate codepoints will cause failure, which `git` +//! [doesn't care about](https://github.com/git/git/blob/main/compat/win32/dirent.c#L12:L12). +//! +//! We will, though, which means from now on we can just convert to UTF-8 on windows and bubble up errors where necessary, +//! preventing potential mismatched surrogate pairs to ever be saved on disk by gitoxide. +//! +//! Even though the error only exists on older windows versions, we will represent it in the type system through fallible function calls. +//! Callers may `.expect()` on the result to indicate they don't wish to handle this special and rare case. Note that servers should not +//! ever get into a code-path which does panic though. + +/// A dummy type to represent path specs and help finding all spots that take path specs once it is implemented. + +/// A preliminary version of a path-spec based on glances of the code. +#[derive(Clone, Debug)] +pub struct Spec(bstr::BString); + +mod convert; +mod spec; + +pub use convert::*; diff --git a/git-path/src/spec.rs b/git-path/src/spec.rs new file mode 100644 index 0000000000..4c41e40fb2 --- /dev/null +++ b/git-path/src/spec.rs @@ -0,0 +1,51 @@ +use crate::Spec; +use bstr::{BStr, ByteSlice, ByteVec}; +use std::ffi::OsStr; + +impl std::convert::TryFrom<&OsStr> for Spec { + type Error = crate::Utf8Error; + + fn try_from(value: &OsStr) -> Result { + crate::os_str_into_bstr(value).map(|value| { + assert_valid_hack(value); + Spec(value.into()) + }) + } +} + +fn assert_valid_hack(input: &BStr) { + assert!(!input.contains_str(b"/../")); + assert!(!input.contains_str(b"/./")); + assert!(!input.starts_with_str(b"../")); + assert!(!input.starts_with_str(b"./")); + assert!(!input.starts_with_str(b"/")); +} + +impl Spec { + /// Parse `input` into a `Spec` or `None` if it could not be parsed + // TODO: tests, actual implementation probably via `git-pathspec` to make use of the crate after all. + pub fn from_bytes(input: &BStr) -> Option { + assert_valid_hack(input); + Spec(input.into()).into() + } + /// Return all paths described by this path spec, using slashes on all platforms. + pub fn items(&self) -> impl Iterator { + std::iter::once(self.0.as_bstr()) + } + /// Adjust this path specification according to the given `prefix`, which may be empty to indicate we are the at work-tree root. + // TODO: this is a hack, needs test and time to do according to spec. This is just a minimum version to have -something-. + pub fn apply_prefix(&mut self, prefix: &std::path::Path) -> &Self { + // many more things we can't handle. `Path` never ends with trailing path separator. + let prefix = crate::into_bstr(prefix); + if !prefix.is_empty() { + let mut prefix = crate::to_unix_separators_on_windows(prefix); + { + let path = prefix.to_mut(); + path.push_byte(b'/'); + path.extend_from_slice(&self.0); + } + self.0 = prefix.into_owned(); + } + self + } +} diff --git a/git-path/tests/path.rs b/git-path/tests/path.rs new file mode 100644 index 0000000000..b1ad512bc6 --- /dev/null +++ b/git-path/tests/path.rs @@ -0,0 +1,21 @@ +mod convert { + use bstr::ByteSlice; + use git_path::{to_unix_separators, to_windows_separators}; + + #[test] + fn assure_unix_separators() { + assert_eq!(to_unix_separators(b"no-backslash".as_bstr()).as_bstr(), "no-backslash"); + + assert_eq!(to_unix_separators(b"\\a\\b\\\\".as_bstr()).as_bstr(), "/a/b//"); + } + + #[test] + fn assure_windows_separators() { + assert_eq!( + to_windows_separators(b"no-backslash".as_bstr()).as_bstr(), + "no-backslash" + ); + + assert_eq!(to_windows_separators(b"/a/b//".as_bstr()).as_bstr(), "\\a\\b\\\\"); + } +} diff --git a/git-ref/Cargo.toml b/git-ref/Cargo.toml index 5b73b955a3..44e1462b29 100644 --- a/git-ref/Cargo.toml +++ b/git-ref/Cargo.toml @@ -25,7 +25,8 @@ required-features = ["internal-testing-git-features-parallel"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -git-features = { version = "^0.20.0", path = "../git-features", features = ["walkdir", "bstr"]} +git-features = { version = "^0.20.0", path = "../git-features", features = ["walkdir"]} +git-path = { version = "^0.1.0", path = "../git-path" } git-hash = { version = "^0.9.3", path = "../git-hash" } git-object = { version = "^0.18.0", path = "../git-object" } git-validate = { version ="^0.5.3", path = "../git-validate" } diff --git a/git-ref/src/fullname.rs b/git-ref/src/fullname.rs index 7d89e9ef92..d07a441bba 100644 --- a/git-ref/src/fullname.rs +++ b/git-ref/src/fullname.rs @@ -75,7 +75,7 @@ impl FullName { /// Convert this name into the relative path, lossily, identifying the reference location relative to a repository pub fn to_path(&self) -> &Path { - git_features::path::from_byte_slice_or_panic_on_windows(&self.0) + git_path::from_byte_slice(&self.0) } /// Dissolve this instance and return the buffer. diff --git a/git-ref/src/name.rs b/git-ref/src/name.rs index 9ad9ddb86a..f273a14975 100644 --- a/git-ref/src/name.rs +++ b/git-ref/src/name.rs @@ -26,7 +26,7 @@ impl Category { impl<'a> FullNameRef<'a> { /// Convert this name into the relative path identifying the reference location. pub fn to_path(self) -> &'a Path { - git_features::path::from_byte_slice_or_panic_on_windows(self.0) + git_path::from_byte_slice(self.0) } /// Return ourselves as byte string which is a valid refname @@ -79,7 +79,7 @@ impl<'a> PartialNameRef<'a> { /// Convert this name into the relative path possibly identifying the reference location. /// Note that it may be only a partial path though. pub fn to_partial_path(&'a self) -> &'a Path { - git_features::path::from_byte_slice_or_panic_on_windows(self.0.as_ref()) + git_path::from_byte_slice(self.0.as_ref()) } /// Provide the name as binary string which is known to be a valid partial ref name. @@ -122,7 +122,7 @@ impl<'a> TryFrom<&'a OsStr> for PartialNameRef<'a> { type Error = Error; fn try_from(v: &'a OsStr) -> Result { - let v = git_features::path::os_str_into_bytes(v) + let v = git_path::os_str_into_bstr(v) .map_err(|_| Error::Tag(git_validate::tag::name::Error::InvalidByte("".into())))?; Ok(PartialNameRef( git_validate::reference::name_partial(v.as_bstr())?.into(), diff --git a/git-ref/src/namespace.rs b/git-ref/src/namespace.rs index d2bb21705b..47c628ed24 100644 --- a/git-ref/src/namespace.rs +++ b/git-ref/src/namespace.rs @@ -18,19 +18,12 @@ impl Namespace { } /// Return ourselves as a path for use within the filesystem. pub fn to_path(&self) -> &Path { - git_features::path::from_byte_slice_or_panic_on_windows(&self.0) + git_path::from_byte_slice(&self.0) } /// Append the given `prefix` to this namespace so it becomes usable for prefixed iteration. pub fn into_namespaced_prefix(mut self, prefix: impl AsRef) -> PathBuf { - self.0 - .push_str(git_features::path::into_bytes_or_panic_on_windows(prefix.as_ref())); - git_features::path::from_byte_vec_or_panic_on_windows( - git_features::path::convert::to_native_separators({ - let v: Vec<_> = self.0.into(); - v - }) - .into_owned(), - ) + self.0.push_str(git_path::into_bstr(prefix.as_ref()).as_ref()); + git_path::to_native_path_on_windows(self.0.clone()).into_owned() } } diff --git a/git-ref/src/store/file/find.rs b/git-ref/src/store/file/find.rs index deccd32eb6..78e7281f5e 100644 --- a/git-ref/src/store/file/find.rs +++ b/git-ref/src/store/file/find.rs @@ -124,7 +124,7 @@ impl file::Store { }; let relative_path = base.join(inbetween).join(relative_path); - let path_to_open = git_features::path::convert::to_windows_separators_on_windows_or_panic(&relative_path); + let path_to_open = git_path::to_native_path_on_windows(git_path::into_bstr(&relative_path)); let contents = match self .ref_contents(&path_to_open) .map_err(|err| Error::ReadFileContents { diff --git a/git-ref/src/store/file/loose/iter.rs b/git-ref/src/store/file/loose/iter.rs index 1545e7bc37..48c1240b44 100644 --- a/git-ref/src/store/file/loose/iter.rs +++ b/git-ref/src/store/file/loose/iter.rs @@ -49,7 +49,7 @@ impl Iterator for SortedLoosePaths { .as_deref() .and_then(|prefix| full_path.file_name().map(|name| (prefix, name))) { - match git_features::path::os_str_into_bytes(name) { + match git_path::os_str_into_bstr(name) { Ok(name) => { if !name.starts_with(prefix) { continue; @@ -61,17 +61,16 @@ impl Iterator for SortedLoosePaths { let full_name = full_path .strip_prefix(&self.base) .expect("prefix-stripping cannot fail as prefix is our root"); - let full_name = match git_features::path::into_bytes(full_name) { + let full_name = match git_path::try_into_bstr(full_name) { Ok(name) => { - #[cfg(windows)] - let name = git_features::path::convert::to_unix_separators(name); + let name = git_path::to_unix_separators_on_windows(name); name.into_owned() } Err(_) => continue, // TODO: silently skipping ill-formed UTF-8 on windows here, maybe there are better ways? }; if git_validate::reference::name_partial(full_name.as_bstr()).is_ok() { - let name = FullName(full_name.into()); + let name = FullName(full_name); return Some(Ok((full_path, name))); } else { continue; @@ -201,8 +200,8 @@ impl file::Store { base.file_name() .map(ToOwned::to_owned) .map(|p| { - git_features::path::into_bytes(PathBuf::from(p)) - .map(|p| BString::from(p.into_owned())) + git_path::try_into_bstr(PathBuf::from(p)) + .map(|p| p.into_owned()) .map_err(|_| { std::io::Error::new( std::io::ErrorKind::InvalidInput, diff --git a/git-ref/src/store/file/mod.rs b/git-ref/src/store/file/mod.rs index ec3bf22a7c..591e26a4a0 100644 --- a/git-ref/src/store/file/mod.rs +++ b/git-ref/src/store/file/mod.rs @@ -53,12 +53,8 @@ pub struct Transaction<'s> { } pub(in crate::store_impl::file) fn path_to_name<'a>(path: impl Into>) -> Cow<'a, BStr> { - let path = git_features::path::into_bytes_or_panic_on_windows(path.into()); - - #[cfg(windows)] - let path = git_features::path::convert::to_unix_separators(path); - - git_features::path::convert::into_bstr(path) + let path = git_path::into_bstr(path.into()); + git_path::to_unix_separators_on_windows(path) } /// diff --git a/git-repository/Cargo.toml b/git-repository/Cargo.toml index c3865265e9..ca3f7f323b 100644 --- a/git-repository/Cargo.toml +++ b/git-repository/Cargo.toml @@ -41,7 +41,7 @@ one-stop-shop = [ "local", "local-time-support" ] #! ### Other ## Data structures implement `serde::Serialize` and `serde::Deserialize`. -serde1 = ["git-pack/serde1", "git-object/serde1", "git-protocol/serde1", "git-transport/serde1", "git-ref/serde1", "git-odb/serde1", "git-index/serde1", "git-mailmap/serde1"] +serde1 = ["git-pack/serde1", "git-object/serde1", "git-protocol/serde1", "git-transport/serde1", "git-ref/serde1", "git-odb/serde1", "git-index/serde1", "git-mailmap/serde1", "git-attributes/serde1"] ## Activate other features that maximize performance, like usage of threads, `zlib-ng` and access to caching in object databases. ## **Note** that max-performance = ["git-features/parallel", "git-features/zlib-ng-compat", "git-pack/pack-cache-lru-static", "git-pack/pack-cache-lru-dynamic"] @@ -49,7 +49,7 @@ max-performance = ["git-features/parallel", "git-features/zlib-ng-compat", "git- local-time-support = ["git-actor/local-time-support"] ## Re-export stability tier 2 crates for convenience and make `Repository` struct fields with types from these crates publicly accessible. ## Doing so is less stable than the stability tier 1 that `git-repository` is a member of. -unstable = ["git-index", "git-worktree", "git-mailmap", "git-glob", "git-credentials"] +unstable = ["git-index", "git-worktree", "git-mailmap", "git-glob", "git-credentials", "git-path", "git-attributes"] ## Print debugging information about usage of object database caches, useful for tuning cache sizes. cache-efficiency-debug = ["git-features/cache-efficiency-debug"] @@ -60,7 +60,7 @@ git-ref = { version = "^0.12.1", path = "../git-ref" } git-tempfile = { version = "^2.0.0", path = "../git-tempfile" } git-lock = { version = "^2.0.0", path = "../git-lock" } git-validate = { version ="^0.5.3", path = "../git-validate" } -git-sec = { version = "^0.1.0", path = "../git-sec" } +git-sec = { version = "^0.1.0", path = "../git-sec", features = ["thiserror"] } git-config = { version = "^0.2.1", path = "../git-config" } git-odb = { version = "^0.28.0", path = "../git-odb" } @@ -70,6 +70,7 @@ git-actor = { version = "^0.9.0", path = "../git-actor" } git-pack = { version = "^0.18.0", path = "../git-pack", features = ["object-cache-dynamic"] } git-revision = { version = "^0.1.0", path = "../git-revision" } +git-path = { version = "^0.1.0", path = "../git-path", optional = true } git-url = { version = "^0.4.0", path = "../git-url", optional = true } git-traverse = { version = "^0.14.0", path = "../git-traverse" } git-protocol = { version = "^0.15.0", path = "../git-protocol", optional = true } @@ -79,6 +80,7 @@ git-mailmap = { version = "^0.1.0", path = "../git-mailmap", optional = true } git-features = { version = "^0.20.0", path = "../git-features", features = ["progress"] } # unstable only +git-attributes = { version = "^0.1.0", path = "../git-attributes", optional = true } git-glob = { version = "^0.2.0", path = "../git-glob", optional = true } git-credentials = { version = "^0.1.0", path = "../git-credentials", optional = true } git-index = { version = "^0.2.0", path = "../git-index", optional = true } @@ -98,6 +100,7 @@ unicode-normalization = { version = "0.1.19", default-features = false } [dev-dependencies] git-testtools = { path = "../tests/tools" } +is_ci = "1.1.1" anyhow = "1" tempfile = "3.2.0" diff --git a/git-repository/src/config.rs b/git-repository/src/config.rs index 19de3b8653..3c262e7e94 100644 --- a/git-repository/src/config.rs +++ b/git-repository/src/config.rs @@ -1,4 +1,5 @@ use crate::bstr::BString; +use crate::permission::EnvVarResourcePermission; #[derive(Debug, thiserror::Error)] pub enum Error { @@ -10,85 +11,112 @@ pub enum Error { 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::values::path::interpolate::Error), } /// Utility type to keep pre-obtained configuration values. #[derive(Debug, Clone)] pub(crate) struct Cache { // TODO: remove this once resolved is used without a feature dependency - #[cfg_attr(not(feature = "git-mailmap"), allow(dead_code))] + #[cfg_attr(not(any(feature = "git-mailmap", feature = "git-index")), allow(dead_code))] 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 + /// true if the repository is designated as 'bare', without work tree. pub is_bare: bool, - /// The type of hash to use + /// 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, + /// 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: EnvVarResourcePermission, + /// 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: EnvVarResourcePermission, // TODO: make core.precomposeUnicode available as well. } mod cache { - use std::{borrow::Cow, convert::TryFrom}; + use std::convert::TryFrom; + use std::path::PathBuf; use git_config::{ file::GitConfig, - values, values::{Boolean, Integer}, }; use super::{Cache, Error}; use crate::bstr::ByteSlice; + use crate::permission::EnvVarResourcePermission; impl Cache { - pub fn new(git_dir: &std::path::Path) -> Result { + pub fn new( + git_dir: &std::path::Path, + xdg_config_home_env: EnvVarResourcePermission, + home_env: EnvVarResourcePermission, + git_install_dir: Option<&std::path::Path>, + ) -> Result { let config = GitConfig::open(git_dir.join("config"))?; - let is_bare = config_bool(&config, "core.bare", false); - let use_multi_pack_index = config_bool(&config, "core.multiPackIndex", true); + + 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(git_install_dir).map(|p| p.into_owned())) + .transpose()?; let repo_format_version = config .value::("core", None, "repositoryFormatVersion") .map_or(0, |v| v.value); - let object_hash = if repo_format_version == 1 { - if let Ok(format) = config.value::>("extensions", None, "objectFormat") { - match format.as_ref() { - b"sha1" => git_hash::Kind::Sha1, - _ => { - return Err(Error::UnsupportedObjectFormat { + let object_hash = (repo_format_version != 1) + .then(|| Ok(git_hash::Kind::Sha1)) + .or_else(|| { + config + .raw_value("extensions", None, "objectFormat") + .ok() + .map(|format| match format.as_ref() { + b"sha1" => Ok(git_hash::Kind::Sha1), + _ => Err(Error::UnsupportedObjectFormat { name: format.to_vec().into(), - }) - } - } - } else { - git_hash::Kind::Sha1 - } - } else { - git_hash::Kind::Sha1 - }; + }), + }) + }) + .transpose()? + .unwrap_or(git_hash::Kind::Sha1); let mut hex_len = None; - if let Ok(hex_len_str) = config.value::>("core", None, "abbrev") { - if hex_len_str.value.trim().is_empty() { + 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.value.as_ref() != "auto" { - let value_bytes = hex_len_str.value.as_ref().as_ref(); + if hex_len_str.as_ref() != "auto" { + let value_bytes = hex_len_str.as_ref().as_ref(); if let Ok(Boolean::False(_)) = Boolean::try_from(value_bytes) { hex_len = object_hash.len_in_hex().into(); } else { let value = Integer::try_from(value_bytes) .map_err(|_| Error::CoreAbbrev { - value: hex_len_str.value.clone().into_owned(), + 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.value.clone().into_owned(), + 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.value.clone().into_owned(), + value: hex_len_str.clone().into_owned(), max: object_hash.len_in_hex() as u8, }); } @@ -102,15 +130,39 @@ mod cache { use_multi_pack_index, object_hash, 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() + } } - fn config_bool(config: &GitConfig<'_>, key: &str, default: bool) -> bool { + fn config_bool(config: &GitConfig<'_>, key: &str, default: bool) -> Result { let (section, key) = key.split_once('.').expect("valid section.key format"); config - .value::>(section, None, key) - .map_or(default, |b| b.to_bool()) + .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/lib.rs b/git-repository/src/lib.rs index 07da578a39..caba6dec2a 100644 --- a/git-repository/src/lib.rs +++ b/git-repository/src/lib.rs @@ -83,12 +83,14 @@ //! even if this crate doesn't, hence breaking downstream. //! //! `git_repository::` +//! * [`attrs`] //! * [`hash`] //! * [`url`] //! * [`actor`] //! * [`bstr`][bstr] //! * [`index`] //! * [`glob`] +//! * [`path`] //! * [`credentials`] //! * [`sec`] //! * [`worktree`] @@ -124,6 +126,8 @@ use std::path::PathBuf; // This also means that their major version changes affect our major version, but that's alright as we directly expose their // APIs/instances anyway. pub use git_actor as actor; +#[cfg(all(feature = "unstable", feature = "git-attributes"))] +pub use git_attributes as attrs; #[cfg(all(feature = "unstable", feature = "git-credentials"))] pub use git_credentials as credentials; #[cfg(all(feature = "unstable", feature = "git-diff"))] @@ -156,8 +160,6 @@ pub use git_url as url; #[doc(inline)] #[cfg(all(feature = "unstable", feature = "git-url"))] pub use git_url::Url; -#[cfg(all(feature = "unstable", feature = "git-worktree"))] -pub use git_worktree as worktree; pub use hash::{oid, ObjectId}; pub mod interrupt; @@ -192,7 +194,9 @@ pub enum Path { /// mod types; -pub use types::{Commit, DetachedObject, Head, Id, Object, Reference, Repository, Tag, ThreadSafeRepository, Tree}; +pub use types::{ + Commit, DetachedObject, Head, Id, Object, Reference, Repository, Tag, ThreadSafeRepository, Tree, Worktree, +}; pub mod commit; pub mod head; @@ -200,7 +204,6 @@ pub mod id; pub mod object; pub mod reference; mod repository; -pub use repository::{permissions, permissions::Permissions}; pub mod tag; /// The kind of `Repository` @@ -239,6 +242,16 @@ pub fn open(directory: impl Into) -> Result; +} +pub use repository::permissions::Permissions; + /// pub mod open; @@ -268,6 +281,9 @@ pub mod mailmap { } } +/// +pub mod worktree; + /// pub mod rev_parse { /// The error returned by [`crate::Repository::rev_parse()`]. diff --git a/git-repository/src/open.rs b/git-repository/src/open.rs index 0312fe3081..69f4276c6f 100644 --- a/git-repository/src/open.rs +++ b/git-repository/src/open.rs @@ -153,10 +153,15 @@ impl crate::ThreadSafeRepository { Options { object_store_slots, replacement_objects, - permissions, + permissions: + Permissions { + git_dir: git_dir_perm, + xdg_config_home, + home, + }, }: Options, ) -> Result { - if *permissions.git_dir != git_sec::ReadWrite::all() { + 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 }); } @@ -164,7 +169,12 @@ impl crate::ThreadSafeRepository { // This would be something read in later as have to first check for extensions. Also this means // that each worktree, even if accessible through this instance, has to come in its own Repository instance // as it may have its own configuration. That's fine actually. - let config = crate::config::Cache::new(&git_dir)?; + let config = crate::config::Cache::new( + &git_dir, + xdg_config_home, + home, + crate::path::install_dir().ok().as_deref(), + )?; match worktree_dir { None if !config.is_bare => { worktree_dir = Some(git_dir.parent().expect("parent is always available").to_owned()); diff --git a/git-repository/src/path/discover.rs b/git-repository/src/path/discover.rs index 1ebd0184ff..087c7273c9 100644 --- a/git-repository/src/path/discover.rs +++ b/git-repository/src/path/discover.rs @@ -8,6 +8,12 @@ pub enum Error { InaccessibleDirectory { path: PathBuf }, #[error("Could find a git repository in '{}' or in any of its parents", .path.display())] NoGitRepository { path: PathBuf }, + #[error("Could find a trusted git repository in '{}' or in any of its parents, candidate at '{}' discarded", .path.display(), .candidate.display())] + NoTrustedGitRepository { + path: PathBuf, + candidate: PathBuf, + required: git_sec::Trust, + }, #[error("Could not determine trust level for path '{}'.", .path.display())] CheckTrust { path: PathBuf, @@ -57,23 +63,20 @@ pub(crate) mod function { // us the parent directory. (`Path::parent` just strips off the last // path component, which means it will not do what you expect when // working with paths paths that contain '..'.) - let directory = maybe_canonicalize(directory.as_ref()).map_err(|_| Error::InaccessibleDirectory { - path: directory.as_ref().into(), - })?; - if !directory.is_dir() { - return Err(Error::InaccessibleDirectory { - path: directory.into_owned(), - }); + let directory = directory.as_ref(); + let dir = maybe_canonicalize(directory).map_err(|_| Error::InaccessibleDirectory { path: directory.into() })?; + let is_canonicalized = dir.as_ref() != directory; + if !dir.is_dir() { + return Err(Error::InaccessibleDirectory { path: dir.into_owned() }); } - let filter_by_trust = - |x: &std::path::Path, kind: crate::path::Kind| -> Result, Error> { - let trust = - git_sec::Trust::from_path_ownership(x).map_err(|err| Error::CheckTrust { path: x.into(), err })?; - Ok((trust >= required_trust).then(|| (crate::Path::from_dot_git_dir(x, kind), trust))) - }; + let filter_by_trust = |x: &std::path::Path| -> Result, Error> { + let trust = + git_sec::Trust::from_path_ownership(x).map_err(|err| Error::CheckTrust { path: x.into(), err })?; + Ok((trust >= required_trust).then(|| (trust))) + }; - let mut cursor = directory.clone(); + let mut cursor = dir.clone(); 'outer: loop { for append_dot_git in &[false, true] { if *append_dot_git { @@ -83,11 +86,36 @@ pub(crate) mod function { } } if let Ok(kind) = path::is::git(&cursor) { - match filter_by_trust(&cursor, kind)? { - Some(res) => break 'outer Ok(res), + match filter_by_trust(&cursor)? { + Some(trust) => { + // TODO: test this more + let path = if is_canonicalized { + match std::env::current_dir() { + Ok(cwd) => cwd + .strip_prefix(&cursor.parent().expect(".git appended")) + .ok() + .and_then(|p| { + let short_path_components = p.components().count(); + (short_path_components < cursor.components().count()).then(|| { + std::iter::repeat("..") + .take(short_path_components) + .chain(Some(".git")) + .collect() + }) + }) + .unwrap_or_else(|| cursor.into_owned()), + Err(_) => cursor.into_owned(), + } + } else { + cursor.into_owned() + }; + break 'outer Ok((crate::Path::from_dot_git_dir(path, kind), trust)); + } None => { - break 'outer Err(Error::NoGitRepository { - path: directory.into_owned(), + break 'outer Err(Error::NoTrustedGitRepository { + path: dir.into_owned(), + candidate: cursor.into_owned(), + required: required_trust, }) } } @@ -100,11 +128,7 @@ pub(crate) mod function { } match cursor.parent() { Some(parent) => cursor = parent.to_owned().into(), - None => { - break Err(Error::NoGitRepository { - path: directory.into_owned(), - }) - } + None => break Err(Error::NoGitRepository { path: dir.into_owned() }), } } } diff --git a/git-repository/src/path/is.rs b/git-repository/src/path/is.rs index e6570f4d6d..c39f6ef411 100644 --- a/git-repository/src/path/is.rs +++ b/git-repository/src/path/is.rs @@ -28,6 +28,7 @@ pub fn bare(git_dir_candidate: impl AsRef) -> bool { /// What constitutes a valid git repository, and what's yet to be implemented, returning the guessed repository kind /// purely based on the presence of files. Note that the git-config ultimately decides what's bare. /// +/// * [ ] git files /// * [x] a valid head /// * [ ] git common directory /// * [ ] respect GIT_COMMON_DIR diff --git a/git-repository/src/path/mod.rs b/git-repository/src/path/mod.rs index bc0ab7ce30..3668fa3dd0 100644 --- a/git-repository/src/path/mod.rs +++ b/git-repository/src/path/mod.rs @@ -2,6 +2,9 @@ use std::path::PathBuf; use crate::{Kind, Path}; +#[cfg(all(feature = "unstable", feature = "git-path"))] +pub use git_path::*; + /// pub mod create; /// @@ -24,7 +27,11 @@ impl Path { pub fn from_dot_git_dir(dir: impl Into, kind: Kind) -> Self { let dir = dir.into(); match kind { - Kind::WorkTree => Path::WorkTree(dir.parent().expect("this is a sub-directory").to_owned()), + Kind::WorkTree => Path::WorkTree(if dir == std::path::Path::new(".git") { + PathBuf::from(".") + } else { + dir.parent().expect("this is a sub-directory").to_owned() + }), Kind::Bare => Path::Repository(dir), } } @@ -44,3 +51,11 @@ impl Path { } } } + +pub(crate) fn install_dir() -> std::io::Result { + std::env::current_exe().and_then(|exe| { + exe.parent() + .map(ToOwned::to_owned) + .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::Other, "no parent for current executable")) + }) +} diff --git a/git-repository/src/repository/init.rs b/git-repository/src/repository/init.rs new file mode 100644 index 0000000000..f674492c03 --- /dev/null +++ b/git-repository/src/repository/init.rs @@ -0,0 +1,32 @@ +use std::cell::RefCell; + +impl crate::Repository { + pub(crate) fn from_refs_and_objects( + refs: crate::RefStore, + objects: crate::OdbHandle, + work_tree: Option, + config: crate::config::Cache, + ) -> Self { + crate::Repository { + bufs: RefCell::new(Vec::with_capacity(4)), + work_tree, + objects: { + #[cfg(feature = "max-performance")] + { + objects.with_pack_cache(|| Box::new(git_pack::cache::lru::StaticLinkedList::<64>::default())) + } + #[cfg(not(feature = "max-performance"))] + { + objects + } + }, + refs, + config, + } + } + + /// Convert this instance into a [`ThreadSafeRepository`][crate::ThreadSafeRepository] by dropping all thread-local data. + pub fn into_sync(self) -> crate::ThreadSafeRepository { + self.into() + } +} diff --git a/git-repository/src/repository/location.rs b/git-repository/src/repository/location.rs index f3998dbbf2..4eab54d8f3 100644 --- a/git-repository/src/repository/location.rs +++ b/git-repository/src/repository/location.rs @@ -12,10 +12,31 @@ impl crate::Repository { // TODO: tests, respect precomposeUnicode /// The directory of the binary path of the current process. pub fn install_dir(&self) -> std::io::Result { - std::env::current_exe().and_then(|exe| { - exe.parent() - .map(ToOwned::to_owned) - .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::Other, "no parent for current executable")) + crate::path::install_dir() + } + + /// Returns the relative path which is the components between the working tree and the current working dir (CWD). + /// Note that there may be `None` if there is no work tree, even though the `PathBuf` will be empty + /// if the CWD is at the root of the work tree. + // TODO: tests, details - there is a lot about environment variables to change things around. + pub fn prefix(&self) -> Option> { + self.work_tree.as_ref().map(|root| { + root.canonicalize().and_then(|root| { + std::env::current_dir().and_then(|cwd| { + cwd.strip_prefix(&root) + .map_err(|_| { + std::io::Error::new( + std::io::ErrorKind::Other, + format!( + "CWD '{}' isn't within the work tree '{}'", + cwd.display(), + root.display() + ), + ) + }) + .map(ToOwned::to_owned) + }) + }) }) } diff --git a/git-repository/src/repository/mod.rs b/git-repository/src/repository/mod.rs index 633036b360..4947ec0ab8 100644 --- a/git-repository/src/repository/mod.rs +++ b/git-repository/src/repository/mod.rs @@ -39,98 +39,12 @@ impl crate::Repository { } } -/// Various permissions for parts of git repositories. -pub mod permissions { - use git_sec::permission::Resource; - use git_sec::{Access, Trust}; - - /// Permissions associated with various resources of a git repository - pub struct Permissions { - /// Control how a git-dir can be used. - /// - /// Note that a repository won't be usable at all unless read and write permissions are given. - pub git_dir: Access, - } - - impl Permissions { - /// Return permissions similar to what git does when the repository isn't owned by the current user, - /// thus refusing all operations in it. - pub fn strict() -> Self { - Permissions { - git_dir: Access::resource(git_sec::ReadWrite::empty()), - } - } - - /// Return permissions that will not include configuration files not owned by the current user, - /// but trust system and global configuration files along with those which are owned by the current user. - /// - /// This allows to read and write repositories even if they aren't owned by the current user, but avoid using - /// anything else that could cause us to write into unknown locations or use programs beyond our `PATH`. - pub fn secure() -> Self { - Permissions { - git_dir: Access::resource(git_sec::ReadWrite::all()), - } - } - - /// Everything is allowed with this set of permissions, thus we read all configuration and do what git typically - /// does with owned repositories. - pub fn all() -> Self { - Permissions { - git_dir: Access::resource(git_sec::ReadWrite::all()), - } - } - } +mod worktree; - impl git_sec::trust::DefaultForLevel for Permissions { - fn default_for_level(level: Trust) -> Self { - match level { - Trust::Full => Permissions::all(), - Trust::Reduced => Permissions::secure(), - } - } - } - - impl Default for Permissions { - fn default() -> Self { - Permissions::secure() - } - } -} - -mod init { - use std::cell::RefCell; - - impl crate::Repository { - pub(crate) fn from_refs_and_objects( - refs: crate::RefStore, - objects: crate::OdbHandle, - work_tree: Option, - config: crate::config::Cache, - ) -> Self { - crate::Repository { - bufs: RefCell::new(Vec::with_capacity(4)), - work_tree, - objects: { - #[cfg(feature = "max-performance")] - { - objects.with_pack_cache(|| Box::new(git_pack::cache::lru::StaticLinkedList::<64>::default())) - } - #[cfg(not(feature = "max-performance"))] - { - objects - } - }, - refs, - config, - } - } +/// Various permissions for parts of git repositories. +pub(crate) mod permissions; - /// Convert this instance into a [`ThreadSafeRepository`][crate::ThreadSafeRepository] by dropping all thread-local data. - pub fn into_sync(self) -> crate::ThreadSafeRepository { - self.into() - } - } -} +mod init; mod location; diff --git a/git-repository/src/repository/permissions.rs b/git-repository/src/repository/permissions.rs new file mode 100644 index 0000000000..5f0713abbe --- /dev/null +++ b/git-repository/src/repository/permissions.rs @@ -0,0 +1,67 @@ +use crate::permission::EnvVarResourcePermission; +use git_sec::permission::Resource; +use git_sec::{Access, Trust}; + +/// Permissions associated with various resources of a git repository +pub struct Permissions { + /// Control how a git-dir can be used. + /// + /// Note that a repository won't be usable at all unless read and write permissions are given. + pub git_dir: Access, + /// Control whether resources pointed to by `XDG_CONFIG_HOME` can be used when looking up common configuration values. + /// + /// Note that [`git_sec::Permission::Forbid`] will cause the operation to abort if a resource is set via the XDG config environment. + pub xdg_config_home: EnvVarResourcePermission, + /// Control if resources pointed to by the + pub home: EnvVarResourcePermission, +} + +impl Permissions { + /// Return permissions similar to what git does when the repository isn't owned by the current user, + /// thus refusing all operations in it. + pub fn strict() -> Self { + Permissions { + git_dir: Access::resource(git_sec::ReadWrite::empty()), + xdg_config_home: Access::resource(git_sec::Permission::Allow), + home: Access::resource(git_sec::Permission::Allow), + } + } + + /// Return permissions that will not include configuration files not owned by the current user, + /// but trust system and global configuration files along with those which are owned by the current user. + /// + /// This allows to read and write repositories even if they aren't owned by the current user, but avoid using + /// anything else that could cause us to write into unknown locations or use programs beyond our `PATH`. + pub fn secure() -> Self { + Permissions { + git_dir: Access::resource(git_sec::ReadWrite::all()), + xdg_config_home: Access::resource(git_sec::Permission::Allow), + home: Access::resource(git_sec::Permission::Allow), + } + } + + /// Everything is allowed with this set of permissions, thus we read all configuration and do what git typically + /// does with owned repositories. + pub fn all() -> Self { + Permissions { + git_dir: Access::resource(git_sec::ReadWrite::all()), + xdg_config_home: Access::resource(git_sec::Permission::Allow), + home: Access::resource(git_sec::Permission::Allow), + } + } +} + +impl git_sec::trust::DefaultForLevel for Permissions { + fn default_for_level(level: Trust) -> Self { + match level { + Trust::Full => Permissions::all(), + Trust::Reduced => Permissions::secure(), + } + } +} + +impl Default for Permissions { + fn default() -> Self { + Permissions::secure() + } +} diff --git a/git-repository/src/repository/snapshots.rs b/git-repository/src/repository/snapshots.rs index 63c53fafa5..b6624b533b 100644 --- a/git-repository/src/repository/snapshots.rs +++ b/git-repository/src/repository/snapshots.rs @@ -48,7 +48,7 @@ impl crate::Repository { let mut blob_id = self .config .resolved - .get_raw_value("mailmap", None, "blob") + .raw_value("mailmap", None, "blob") .ok() .and_then(|spec| { // TODO: actually resolve this as spec (once we can do that) diff --git a/git-repository/src/repository/worktree.rs b/git-repository/src/repository/worktree.rs new file mode 100644 index 0000000000..5571aafa5c --- /dev/null +++ b/git-repository/src/repository/worktree.rs @@ -0,0 +1,21 @@ +use crate::{worktree, Worktree}; + +impl crate::Repository { + /// Return a platform for interacting with worktrees + pub fn worktree(&self) -> worktree::Platform<'_> { + worktree::Platform { parent: self } + } +} + +impl<'repo> worktree::Platform<'repo> { + /// Return the currently set worktree if there is one. + /// + /// Note that there would be `None` if this repository is `bare` and the parent [`Repository`][crate::Repository] was instantiated without + /// registered worktree in the current working dir. + pub fn current(&self) -> Option> { + self.parent.work_dir().map(|path| Worktree { + parent: self.parent, + path, + }) + } +} diff --git a/git-repository/src/types.rs b/git-repository/src/types.rs index ae35f24daf..6ffbc39bd2 100644 --- a/git-repository/src/types.rs +++ b/git-repository/src/types.rs @@ -4,13 +4,22 @@ use git_hash::ObjectId; use crate::head; +/// A worktree checkout containing the files of the repository in consumable form. +pub struct Worktree<'repo> { + #[cfg_attr(not(feature = "git-index"), allow(dead_code))] + pub(crate) parent: &'repo Repository, + /// The root path of the checkout. + #[allow(dead_code)] + pub(crate) path: &'repo std::path::Path, +} + /// The head reference, as created from looking at `.git/HEAD`, able to represent all of its possible states. /// /// Note that like [`Reference`], this type's data is snapshot of persisted state on disk. pub struct Head<'repo> { /// One of various possible states for the HEAD reference pub kind: head::Kind, - pub(crate) repo: &'repo crate::Repository, + pub(crate) repo: &'repo Repository, } /// An [ObjectId] with access to a repository. @@ -18,7 +27,7 @@ pub struct Head<'repo> { pub struct Id<'r> { /// The actual object id pub(crate) inner: ObjectId, - pub(crate) repo: &'r crate::Repository, + pub(crate) repo: &'r Repository, } /// A decoded object with a reference to its owning repository. @@ -29,7 +38,7 @@ pub struct Object<'repo> { pub kind: git_object::Kind, /// The fully decoded object data pub data: Vec, - pub(crate) repo: &'repo crate::Repository, + pub(crate) repo: &'repo Repository, } impl<'a> Drop for Object<'a> { @@ -44,7 +53,7 @@ pub struct Tree<'repo> { pub id: ObjectId, /// The fully decoded tree data pub data: Vec, - pub(crate) repo: &'repo crate::Repository, + pub(crate) repo: &'repo Repository, } impl<'a> Drop for Tree<'a> { @@ -59,7 +68,7 @@ pub struct Tag<'repo> { pub id: ObjectId, /// The fully decoded tag data pub data: Vec, - pub(crate) repo: &'repo crate::Repository, + pub(crate) repo: &'repo Repository, } impl<'a> Drop for Tag<'a> { @@ -74,7 +83,7 @@ pub struct Commit<'repo> { pub id: ObjectId, /// The fully decoded commit data pub data: Vec, - pub(crate) repo: &'repo crate::Repository, + pub(crate) repo: &'repo Repository, } impl<'a> Drop for Commit<'a> { @@ -102,7 +111,7 @@ pub struct DetachedObject { pub struct Reference<'r> { /// The actual reference data pub inner: git_ref::Reference, - pub(crate) repo: &'r crate::Repository, + pub(crate) repo: &'r Repository, } /// A thread-local handle to interact with a repository from a single thread. @@ -127,7 +136,7 @@ pub struct Repository { /// An instance with access to everything a git repository entails, best imagined as container implementing `Sync + Send` for _most_ /// for system resources required to interact with a `git` repository which are loaded in once the instance is created. /// -/// Use this type to reference it in a threaded context for creation the creation of a thread-local [`Repositories`][crate::Repository]. +/// Use this type to reference it in a threaded context for creation the creation of a thread-local [`Repositories`][Repository]. /// /// Note that this type purposefully isn't very useful until it is converted into a thread-local repository with `to_thread_local()`, /// it's merely meant to be able to exist in a `Sync` context. diff --git a/git-repository/src/worktree.rs b/git-repository/src/worktree.rs new file mode 100644 index 0000000000..421cea89e2 --- /dev/null +++ b/git-repository/src/worktree.rs @@ -0,0 +1,129 @@ +use crate::Repository; +#[cfg(all(feature = "unstable", feature = "git-worktree"))] +pub use git_worktree::*; + +/// +#[cfg(feature = "git-index")] +pub mod open_index { + use crate::bstr::BString; + + /// The error returned by [`Worktree::open_index()`][crate::Worktree::open_index()]. + #[derive(Debug, thiserror::Error)] + #[allow(missing_docs)] + pub enum Error { + #[error("Could not interpret value '{}' as 'index.threads'", .value)] + ConfigIndexThreads { + value: BString, + #[source] + err: git_config::value::parse::Error, + }, + #[error(transparent)] + IndexFile(#[from] git_index::file::init::Error), + } +} + +/// +#[cfg(feature = "git-index")] +pub mod excludes { + use std::path::PathBuf; + + /// The error returned by [`Worktree::excludes()`][crate::Worktree::excludes()]. + #[derive(Debug, thiserror::Error)] + #[allow(missing_docs)] + pub enum Error { + #[error("Could not read repository exclude.")] + Io(#[from] std::io::Error), + #[error(transparent)] + EnvironmentPermission(#[from] git_sec::permission::Error), + } +} + +/// A structure to make the API more stuctured. +pub struct Platform<'repo> { + pub(crate) parent: &'repo Repository, +} + +/// Access +impl<'repo> crate::Worktree<'repo> { + /// Returns the root of the worktree under which all checked out files are located. + pub fn root(&self) -> &std::path::Path { + self.path + } +} + +impl<'repo> crate::Worktree<'repo> { + /// Configure a file-system cache checking if files below the repository are excluded. + /// + /// This takes into consideration all the usual repository configuration. + // TODO: test + #[cfg(feature = "git-index")] + pub fn excludes<'a>( + &self, + index: &'a git_index::State, + overrides: Option>, + ) -> Result, excludes::Error> { + let repo = self.parent; + let case = repo + .config + .ignore_case + .then(|| git_glob::pattern::Case::Fold) + .unwrap_or_default(); + let mut buf = Vec::with_capacity(512); + let state = git_worktree::fs::cache::State::IgnoreStack(git_worktree::fs::cache::state::Ignore::new( + overrides.unwrap_or_default(), + git_attributes::MatchGroup::::from_git_dir( + repo.git_dir(), + match repo.config.excludes_file.as_ref() { + Some(user_path) => Some(user_path.to_owned()), + None => repo.config.xdg_config_path("ignore")?, + }, + &mut buf, + )?, + None, + case, + )); + let attribute_list = state.build_attribute_list(index, index.path_backing(), case); + Ok(git_worktree::fs::Cache::new( + self.path, + state, + case, + buf, + attribute_list, + )) + } + + // pub fn + /// Open a new copy of the index file and decode it entirely. + /// + /// It will use the `index.threads` configuration key to learn how many threads to use. + // TODO: test + #[cfg(feature = "git-index")] + pub fn open_index(&self) -> Result { + use std::convert::{TryFrom, TryInto}; + let repo = self.parent; + let thread_limit = repo + .config + .resolved + .boolean("index", None, "threads") + .map(|res| { + res.map(|value| if value { 0usize } else { 1 }).or_else(|err| { + git_config::values::Integer::try_from(err.input.as_ref()) + .map_err(|err| crate::worktree::open_index::Error::ConfigIndexThreads { + value: err.input.clone(), + err, + }) + .map(|value| value.to_decimal().and_then(|v| v.try_into().ok()).unwrap_or(1)) + }) + }) + .transpose()?; + git_index::File::at( + repo.git_dir().join("index"), + git_index::decode::Options { + object_hash: repo.object_hash(), + thread_limit, + min_extension_block_in_bytes_for_threading: 0, + }, + ) + .map_err(Into::into) + } +} diff --git a/git-repository/tests/discover/mod.rs b/git-repository/tests/discover/mod.rs index 69735a7d22..a03fdf3ad2 100644 --- a/git-repository/tests/discover/mod.rs +++ b/git-repository/tests/discover/mod.rs @@ -1,5 +1,5 @@ mod existing { - use std::path::{Component, PathBuf}; + use std::path::PathBuf; use git_repository::Kind; @@ -71,17 +71,20 @@ mod existing { // up far enough. (This tests that `discover::existing` canonicalizes paths before // exploring ancestors.) let working_dir = repo_path()?; - let dir = working_dir.join("some/very/deeply/nested/subdir/../../../../../../.."); + let dir = working_dir.join("some/very/deeply/nested/subdir/../../../../../.."); let (path, trust) = git_repository::path::discover(&dir)?; assert_eq!(path.kind(), Kind::WorkTree); - assert_eq!( - path.as_ref() - .components() - .filter(|c| matches!(c, Component::ParentDir | Component::CurDir)) - .count(), - 0, - "there are no relative path components anymore" - ); + if !(cfg!(windows) && is_ci::cached()) { + // On CI on windows we get a cursor like this with a question mark so our prefix check won't work. + // We recover, but that means this assertion will fail. + // &cursor = "\\\\?\\D:\\a\\gitoxide\\gitoxide\\.git" + // &cwd = "D:\\a\\gitoxide\\gitoxide\\git-repository" + assert_eq!( + path.as_ref(), + std::path::Path::new(".."), + "there is only the minimal amount of relative path components to see this worktree" + ); + } assert_ne!( path.as_ref().canonicalize()?, working_dir.canonicalize()?, diff --git a/git-sec/Cargo.toml b/git-sec/Cargo.toml index 7a40bcb7b8..0a5b1ad1f6 100644 --- a/git-sec/Cargo.toml +++ b/git-sec/Cargo.toml @@ -19,6 +19,7 @@ serde1 = [ "serde" ] [dependencies] serde = { version = "1.0.114", optional = true, default-features = false, features = ["std", "derive"] } bitflags = "1.3.2" +thiserror = { version = "1.0.26", optional = true } [target.'cfg(not(windows))'.dependencies] libc = "0.2.123" diff --git a/git-sec/src/lib.rs b/git-sec/src/lib.rs index 47d074bfd3..bea8a7d8e6 100644 --- a/git-sec/src/lib.rs +++ b/git-sec/src/lib.rs @@ -1,6 +1,7 @@ #![deny(unsafe_code, rust_2018_idioms, missing_docs)] //! A shared trust model for `gitoxide` crates. +use std::fmt::{Debug, Display, Formatter}; use std::marker::PhantomData; use std::ops::Deref; @@ -75,19 +76,22 @@ pub mod trust { /// pub mod permission { use crate::Access; + use std::fmt::{Debug, Display}; /// A marker trait to signal tags for permissions. - pub trait Tag {} + pub trait Tag: Debug + Clone {} /// A tag indicating that a permission is applying to the contents of a configuration file. + #[derive(Debug, Clone)] pub struct Config; impl Tag for Config {} /// A tag indicating that a permission is applying to the resource itself. + #[derive(Debug, Clone)] pub struct Resource; impl Tag for Resource {} - impl

Access { + impl Access { /// Create a permission for values contained in git configuration files. /// /// This applies permissions to values contained inside of these files. @@ -99,7 +103,7 @@ pub mod permission { } } - impl

Access { + impl Access { /// Create a permission a file or directory itself. /// /// This applies permissions to a configuration file itself and whether it can be used at all, or to a directory @@ -111,6 +115,20 @@ pub mod permission { } } } + + /// An error to use if an operation cannot proceed due to insufficient permissions. + /// + /// It's up to the implementation to decide which permission is required for an operation, and which one + /// causes errors. + #[cfg(feature = "thiserror")] + #[derive(Debug, thiserror::Error)] + #[error("Not allowed to handle resource {:?}: permission {}", .resource, .permission)] + pub struct Error { + /// The resource which cannot be used. + pub resource: R, + /// The permission causing it to be disallowed. + pub permission: P, + } } /// Allow, deny or forbid using a resource or performing an action. @@ -125,6 +143,36 @@ pub enum Permission { Allow, } +impl Permission { + /// Check this permissions and produce a reply to indicate if the `resource` can be used and in which way. + /// + /// Only if this permission is set to `Allow` will the resource be usable. + #[cfg(feature = "thiserror")] + pub fn check(&self, resource: R) -> Result, permission::Error> { + match self { + Permission::Allow => Ok(Some(resource)), + Permission::Deny => Ok(None), + Permission::Forbid => Err(permission::Error { + resource, + permission: *self, + }), + } + } +} + +impl Display for Permission { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + Display::fmt( + match self { + Permission::Allow => "allowed", + Permission::Deny => "denied", + Permission::Forbid => "forbidden", + }, + f, + ) + } +} + bitflags::bitflags! { /// Whether something can be read or written. #[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))] @@ -136,14 +184,27 @@ bitflags::bitflags! { } } +impl Display for ReadWrite { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + Debug::fmt(self, f) + } +} + /// A container to define tagged access permissions, rendering the permission read-only. -pub struct Access { +#[derive(Debug, Clone)] +pub struct Access { /// The access permission itself. permission: P, _data: PhantomData, } -impl Deref for Access { +impl Display for Access { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + Display::fmt(&self.permission, f) + } +} + +impl Deref for Access { type Target = P; fn deref(&self) -> &Self::Target { diff --git a/git-url/Cargo.toml b/git-url/Cargo.toml index c19bf17621..49b1caec3f 100644 --- a/git-url/Cargo.toml +++ b/git-url/Cargo.toml @@ -20,6 +20,7 @@ serde1 = ["serde", "bstr/serde1"] [dependencies] serde = { version = "1.0.114", optional = true, default-features = false, features = ["std", "derive"]} git-features = { version = "^0.20.0", path = "../git-features" } +git-path = { version = "^0.1.0", path = "../git-path" } quick-error = "2.0.0" url = "2.1.1" bstr = { version = "0.2.13", default-features = false, features = ["std"] } diff --git a/git-url/src/expand_path.rs b/git-url/src/expand_path.rs index 3546c52a27..59cab95b36 100644 --- a/git-url/src/expand_path.rs +++ b/git-url/src/expand_path.rs @@ -106,7 +106,7 @@ pub fn with( fn make_relative(path: &Path) -> PathBuf { path.components().skip(1).collect() } - let path = git_features::path::from_byte_slice(path).map_err(|_| Error::IllformedUtf8 { path: path.to_owned() })?; + let path = git_path::try_from_byte_slice(path).map_err(|_| Error::IllformedUtf8 { path: path.to_owned() })?; Ok(match user { Some(user) => home_for_user(user) .ok_or_else(|| Error::MissingHome(user.to_owned().into()))? diff --git a/git-worktree/Cargo.toml b/git-worktree/Cargo.toml index cf6b63dc52..2c4f2af0a5 100644 --- a/git-worktree/Cargo.toml +++ b/git-worktree/Cargo.toml @@ -33,6 +33,9 @@ internal-testing-to-avoid-being-run-by-cargo-test-all = [] git-index = { version = "^0.2.0", path = "../git-index" } git-hash = { version = "^0.9.3", path = "../git-hash" } git-object = { version = "^0.18.0", path = "../git-object" } +git-glob = { version = "^0.2.0", path = "../git-glob" } +git-path = { version = "^0.1.0", path = "../git-path" } +git-attributes = { version = "^0.1.0", path = "../git-attributes" } git-features = { version = "^0.20.0", path = "../git-features" } serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"]} diff --git a/git-worktree/src/fs/cache.rs b/git-worktree/src/fs/cache.rs deleted file mode 100644 index 7a30467543..0000000000 --- a/git-worktree/src/fs/cache.rs +++ /dev/null @@ -1,185 +0,0 @@ -use super::Cache; -use crate::fs::Stack; -use crate::{fs, os}; -use std::path::{Path, PathBuf}; - -#[derive(Clone)] -pub enum Mode { - /// Useful for checkout where directories need creation, but we need to access attributes as well. - CreateDirectoryAndProvideAttributes { - /// If there is a symlink or a file in our path, try to unlink it before creating the directory. - unlink_on_collision: bool, - - /// just for testing - #[cfg(debug_assertions)] - test_mkdir_calls: usize, - /// An additional per-user attributes file, similar to `$GIT_DIR/info/attributes` - attributes_file: Option, - }, - /// Used when adding files, requiring access to both attributes and ignore information. - ProvideAttributesAndIgnore { - /// An additional per-user excludes file, similar to `$GIT_DIR/info/exclude`. It's an error if it is set but can't be read/opened. - excludes_file: Option, - /// An additional per-user attributes file, similar to `$GIT_DIR/info/attributes` - attributes_file: Option, - }, -} - -impl Mode { - /// Configure a mode to be suitable for checking out files. - pub fn checkout(unlink_on_collision: bool, attributes_file: Option) -> Self { - Mode::CreateDirectoryAndProvideAttributes { - unlink_on_collision, - #[cfg(debug_assertions)] - test_mkdir_calls: 0, - attributes_file, - } - } - - /// Configure a mode for adding files. - pub fn add(excludes_file: Option, attributes_file: Option) -> Self { - Mode::ProvideAttributesAndIgnore { - excludes_file, - attributes_file, - } - } -} - -#[cfg(debug_assertions)] -impl Cache { - pub fn num_mkdir_calls(&self) -> usize { - match self.mode { - Mode::CreateDirectoryAndProvideAttributes { test_mkdir_calls, .. } => test_mkdir_calls, - _ => 0, - } - } - - pub fn reset_mkdir_calls(&mut self) { - if let Mode::CreateDirectoryAndProvideAttributes { test_mkdir_calls, .. } = &mut self.mode { - *test_mkdir_calls = 0; - } - } - - pub fn unlink_on_collision(&mut self, value: bool) { - if let Mode::CreateDirectoryAndProvideAttributes { - unlink_on_collision, .. - } = &mut self.mode - { - *unlink_on_collision = value; - } - } -} - -pub struct Platform<'a> { - parent: &'a Cache, -} - -impl<'a> Platform<'a> { - /// The full path to `relative` will be returned for use on the file system. - pub fn leading_dir(&self) -> &'a Path { - self.parent.stack.current() - } -} - -impl<'a> std::fmt::Debug for Platform<'a> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - std::fmt::Debug::fmt(&self.leading_dir(), f) - } -} - -impl Cache { - fn assure_init(&mut self) -> std::io::Result<()> { - Ok(()) - } -} - -impl Cache { - /// Create a new instance with `worktree_root` being the base for all future paths we handle, assuming it to be valid which includes - /// symbolic links to be included in it as well. - pub fn new(worktree_root: impl Into, mode: Mode) -> Self { - let root = worktree_root.into(); - Cache { - stack: fs::Stack::new(root), - mode, - } - } - - /// Append the `relative` path to the root directory the cache contains and efficiently create leading directories - /// unless `mode` indicates `relative` points to a directory itself in which case the entire resulting path is created as directory. - /// - /// Provide access to cached information for that `relative` entry via the platform returned. - pub fn at_entry( - &mut self, - relative: impl AsRef, - mode: git_index::entry::Mode, - ) -> std::io::Result> { - self.assure_init()?; - let op_mode = &mut self.mode; - self.stack.make_relative_path_current( - relative, - |components, stack: &fs::Stack| { - match op_mode { - Mode::CreateDirectoryAndProvideAttributes { - #[cfg(debug_assertions)] - test_mkdir_calls, - unlink_on_collision, - attributes_file: _, - } => { - #[cfg(debug_assertions)] - { - create_leading_directory(components, stack, mode, test_mkdir_calls, *unlink_on_collision)? - } - #[cfg(not(debug_assertions))] - { - create_leading_directory(components, stack, mode, *unlink_on_collision)? - } - } - Mode::ProvideAttributesAndIgnore { .. } => todo!(), - } - Ok(()) - }, - |_| {}, - )?; - Ok(Platform { parent: self }) - } -} - -fn create_leading_directory( - components: &mut std::iter::Peekable>, - stack: &Stack, - mode: git_index::entry::Mode, - #[cfg(debug_assertions)] mkdir_calls: &mut usize, - unlink_on_collision: bool, -) -> std::io::Result<()> { - let target_is_dir = mode == git_index::entry::Mode::COMMIT || mode == git_index::entry::Mode::DIR; - if !(components.peek().is_some() || target_is_dir) { - return Ok(()); - } - #[cfg(debug_assertions)] - { - *mkdir_calls += 1; - } - match std::fs::create_dir(stack.current()) { - Ok(()) => Ok(()), - Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => { - let meta = stack.current().symlink_metadata()?; - if meta.is_dir() { - Ok(()) - } else if unlink_on_collision { - if meta.file_type().is_symlink() { - os::remove_symlink(stack.current())?; - } else { - std::fs::remove_file(stack.current())?; - } - #[cfg(debug_assertions)] - { - *mkdir_calls += 1; - } - std::fs::create_dir(stack.current()) - } else { - Err(err) - } - } - Err(err) => Err(err), - } -} diff --git a/git-worktree/src/fs/cache/mod.rs b/git-worktree/src/fs/cache/mod.rs new file mode 100644 index 0000000000..bd098ad4d4 --- /dev/null +++ b/git-worktree/src/fs/cache/mod.rs @@ -0,0 +1,139 @@ +use super::Cache; +use crate::fs; +use crate::fs::PathOidMapping; +use bstr::{BStr, ByteSlice}; +use git_hash::oid; +use std::path::{Path, PathBuf}; + +#[derive(Clone)] +pub enum State { + /// Useful for checkout where directories need creation, but we need to access attributes as well. + CreateDirectoryAndAttributesStack { + /// If there is a symlink or a file in our path, try to unlink it before creating the directory. + unlink_on_collision: bool, + + /// just for testing + #[cfg(debug_assertions)] + test_mkdir_calls: usize, + /// State to handle attribute information + attributes: state::Attributes, + }, + /// Used when adding files, requiring access to both attributes and ignore information, for example during add operations. + AttributesAndIgnoreStack { + /// State to handle attribute information + attributes: state::Attributes, + /// State to handle exclusion information + ignore: state::Ignore, + }, + /// Used when providing worktree status information. + IgnoreStack(state::Ignore), +} + +#[cfg(debug_assertions)] +impl<'paths> Cache<'paths> { + pub fn set_case(&mut self, case: git_glob::pattern::Case) { + self.case = case; + } + pub fn num_mkdir_calls(&self) -> usize { + match self.state { + State::CreateDirectoryAndAttributesStack { test_mkdir_calls, .. } => test_mkdir_calls, + _ => 0, + } + } + + pub fn reset_mkdir_calls(&mut self) { + if let State::CreateDirectoryAndAttributesStack { test_mkdir_calls, .. } = &mut self.state { + *test_mkdir_calls = 0; + } + } + + pub fn unlink_on_collision(&mut self, value: bool) { + if let State::CreateDirectoryAndAttributesStack { + unlink_on_collision, .. + } = &mut self.state + { + *unlink_on_collision = value; + } + } +} + +#[must_use] +pub struct Platform<'a, 'paths> { + parent: &'a Cache<'paths>, + is_dir: Option, +} + +impl<'paths> Cache<'paths> { + /// Create a new instance with `worktree_root` being the base for all future paths we handle, assuming it to be valid which includes + /// symbolic links to be included in it as well. + /// The `case` configures attribute and exclusion query case sensitivity. + pub fn new( + worktree_root: impl Into, + state: State, + case: git_glob::pattern::Case, + buf: Vec, + attribute_files_in_index: Vec>, + ) -> Self { + let root = worktree_root.into(); + Cache { + stack: fs::Stack::new(root), + state, + case, + buf, + attribute_files_in_index, + } + } + + /// Append the `relative` path to the root directory the cache contains and efficiently create leading directories + /// unless `is_dir` is known (`Some(…)`) then `relative` points to a directory itself in which case the entire resulting + /// path is created as directory. If it's not known it is assumed to be a file. + /// + /// Provide access to cached information for that `relative` entry via the platform returned. + pub fn at_path( + &mut self, + relative: impl AsRef, + is_dir: Option, + find: Find, + ) -> std::io::Result> + where + Find: for<'a> FnMut(&oid, &'a mut Vec) -> Result, E>, + E: std::error::Error + Send + Sync + 'static, + { + let mut delegate = platform::StackDelegate { + state: &mut self.state, + buf: &mut self.buf, + is_dir: is_dir.unwrap_or(false), + attribute_files_in_index: &self.attribute_files_in_index, + find, + }; + self.stack.make_relative_path_current(relative, &mut delegate)?; + Ok(Platform { parent: self, is_dir }) + } + + /// **Panics** on illformed UTF8 in `relative` + // TODO: more docs + pub fn at_entry<'r, Find, E>( + &mut self, + relative: impl Into<&'r BStr>, + is_dir: Option, + find: Find, + ) -> std::io::Result> + where + Find: for<'a> FnMut(&oid, &'a mut Vec) -> Result, E>, + E: std::error::Error + Send + Sync + 'static, + { + let relative = relative.into(); + let relative_path = git_path::from_bstr(relative); + + self.at_path( + relative_path, + is_dir.or_else(|| relative.ends_with_str("/").then(|| true)), + // is_dir, + find, + ) + } +} + +mod platform; +/// +pub mod state; diff --git a/git-worktree/src/fs/cache/platform.rs b/git-worktree/src/fs/cache/platform.rs new file mode 100644 index 0000000000..4ee5cfa13b --- /dev/null +++ b/git-worktree/src/fs/cache/platform.rs @@ -0,0 +1,165 @@ +use crate::fs; +use crate::fs::cache::{Platform, State}; +use crate::fs::PathOidMapping; +use bstr::ByteSlice; +use git_hash::oid; +use std::path::Path; + +impl<'a, 'paths> Platform<'a, 'paths> { + /// The full path to `relative` will be returned for use on the file system. + pub fn path(&self) -> &'a Path { + self.parent.stack.current() + } + + /// See if the currently set entry is excluded as per exclude and git-ignore files. + /// + /// # Panics + /// + /// If the cache was configured without exclude patterns. + pub fn is_excluded(&self) -> bool { + self.matching_exclude_pattern() + .map_or(false, |m| m.pattern.is_negative()) + } + + /// Check all exclude patterns to see if the currently set path matches any of them. + /// + /// Note that this pattern might be negated, and means this path in included. + /// + /// # Panics + /// + /// If the cache was configured without exclude patterns. + pub fn matching_exclude_pattern(&self) -> Option> { + let ignore = self.parent.state.ignore_or_panic(); + let relative_path = + git_path::to_unix_separators_on_windows(git_path::into_bstr(self.parent.stack.current_relative.as_path())); + ignore.matching_exclude_pattern(relative_path.as_bstr(), self.is_dir, self.parent.case) + } +} + +impl<'a, 'paths> std::fmt::Debug for Platform<'a, 'paths> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + std::fmt::Debug::fmt(&self.path(), f) + } +} + +pub struct StackDelegate<'a, 'paths, Find> { + pub state: &'a mut State, + pub buf: &'a mut Vec, + pub is_dir: bool, + pub attribute_files_in_index: &'a Vec>, + pub find: Find, +} + +impl<'a, 'paths, Find, E> fs::stack::Delegate for StackDelegate<'a, 'paths, Find> +where + Find: for<'b> FnMut(&oid, &'b mut Vec) -> Result, E>, + E: std::error::Error + Send + Sync + 'static, +{ + fn push_directory(&mut self, stack: &fs::Stack) -> std::io::Result<()> { + match &mut self.state { + State::CreateDirectoryAndAttributesStack { attributes: _, .. } => { + // TODO: attributes + } + State::AttributesAndIgnoreStack { ignore, attributes: _ } => { + // TODO: attributes + ignore.push_directory( + &stack.root, + &stack.current, + self.buf, + self.attribute_files_in_index, + &mut self.find, + )? + } + State::IgnoreStack(ignore) => ignore.push_directory( + &stack.root, + &stack.current, + self.buf, + self.attribute_files_in_index, + &mut self.find, + )?, + } + Ok(()) + } + + fn push(&mut self, is_last_component: bool, stack: &fs::Stack) -> std::io::Result<()> { + match &mut self.state { + State::CreateDirectoryAndAttributesStack { + #[cfg(debug_assertions)] + test_mkdir_calls, + unlink_on_collision, + attributes: _, + } => { + #[cfg(debug_assertions)] + { + create_leading_directory( + is_last_component, + stack, + self.is_dir, + test_mkdir_calls, + *unlink_on_collision, + )? + } + #[cfg(not(debug_assertions))] + { + create_leading_directory(is_last_component, stack, self.is_dir, *unlink_on_collision)? + } + } + State::AttributesAndIgnoreStack { .. } | State::IgnoreStack(_) => {} + } + Ok(()) + } + + fn pop_directory(&mut self) { + match &mut self.state { + State::CreateDirectoryAndAttributesStack { attributes: _, .. } => { + // TODO: attributes + } + State::AttributesAndIgnoreStack { attributes: _, ignore } => { + // TODO: attributes + ignore.pop_directory(); + } + State::IgnoreStack(ignore) => { + ignore.pop_directory(); + } + } + } +} + +fn create_leading_directory( + is_last_component: bool, + stack: &fs::Stack, + is_dir: bool, + #[cfg(debug_assertions)] mkdir_calls: &mut usize, + unlink_on_collision: bool, +) -> std::io::Result<()> { + if is_last_component && !is_dir { + return Ok(()); + } + #[cfg(debug_assertions)] + { + *mkdir_calls += 1; + } + match std::fs::create_dir(stack.current()) { + Ok(()) => Ok(()), + Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => { + let meta = stack.current().symlink_metadata()?; + if meta.is_dir() { + Ok(()) + } else if unlink_on_collision { + if meta.file_type().is_symlink() { + crate::os::remove_symlink(stack.current())?; + } else { + std::fs::remove_file(stack.current())?; + } + #[cfg(debug_assertions)] + { + *mkdir_calls += 1; + } + std::fs::create_dir(stack.current()) + } else { + Err(err) + } + } + Err(err) => Err(err), + } +} diff --git a/git-worktree/src/fs/cache/state.rs b/git-worktree/src/fs/cache/state.rs new file mode 100644 index 0000000000..273c3dacb3 --- /dev/null +++ b/git-worktree/src/fs/cache/state.rs @@ -0,0 +1,287 @@ +use crate::fs::cache::State; +use crate::fs::PathOidMapping; +use bstr::{BStr, BString, ByteSlice}; +use git_glob::pattern::Case; +use git_hash::oid; +use std::path::Path; + +type AttributeMatchGroup = git_attributes::MatchGroup; +type IgnoreMatchGroup = git_attributes::MatchGroup; + +/// State related to attributes associated with files in the repository. +#[derive(Default, Clone)] +#[allow(unused)] +pub struct Attributes { + /// Attribute patterns that match the currently set directory (in the stack). + pub stack: AttributeMatchGroup, + /// Attribute patterns which aren't tied to the repository root, hence are global. They are consulted last. + pub globals: AttributeMatchGroup, +} + +/// State related to the exclusion of files. +#[derive(Default, Clone)] +#[allow(unused)] +pub struct Ignore { + /// Ignore patterns passed as overrides to everything else, typically passed on the command-line and the first patterns to + /// be consulted. + overrides: IgnoreMatchGroup, + /// Ignore patterns that match the currently set director (in the stack), which is pushed and popped as needed. + stack: IgnoreMatchGroup, + /// Ignore patterns which aren't tied to the repository root, hence are global. They are consulted last. + globals: IgnoreMatchGroup, + /// A matching stack of pattern indices which is empty if we have just been initialized to indicate that the + /// currently set directory had a pattern matched. Note that this one could be negated. + /// (index into match groups, index into list of pattern lists, index into pattern list) + matched_directory_patterns_stack: Vec>, + /// The name of the file to look for in directories. + exclude_file_name_for_directories: BString, + /// The case to use when matching directories as they are pushed onto the stack. We run them against the exclude engine + /// to know if an entire path can be ignored as a parent directory is ignored. + case: Case, +} + +impl Ignore { + /// The `exclude_file_name_for_directories` is an optional override for the filename to use when checking per-directory + /// ignore files within the repository, defaults to`.gitignore`. + // TODO: more docs + pub fn new( + overrides: IgnoreMatchGroup, + globals: IgnoreMatchGroup, + exclude_file_name_for_directories: Option<&BStr>, + case: Case, + ) -> Self { + Ignore { + case, + overrides, + globals, + stack: Default::default(), + matched_directory_patterns_stack: Vec::with_capacity(6), + exclude_file_name_for_directories: exclude_file_name_for_directories + .map(ToOwned::to_owned) + .unwrap_or_else(|| ".gitignore".into()), + } + } +} + +impl Ignore { + pub(crate) fn pop_directory(&mut self) { + self.matched_directory_patterns_stack.pop().expect("something to pop"); + self.stack.patterns.pop().expect("something to pop"); + } + /// The match groups from lowest priority to highest. + pub(crate) fn match_groups(&self) -> [&IgnoreMatchGroup; 3] { + [&self.globals, &self.stack, &self.overrides] + } + + pub(crate) fn matching_exclude_pattern( + &self, + relative_path: &BStr, + is_dir: Option, + case: Case, + ) -> Option> { + let groups = self.match_groups(); + if let Some((source, mapping)) = self + .matched_directory_patterns_stack + .iter() + .rev() + .filter_map(|v| *v) + .map(|(gidx, plidx, pidx)| { + let list = &groups[gidx].patterns[plidx]; + (list.source.as_deref(), &list.patterns[pidx]) + }) + .next() + { + if !mapping.pattern.is_negative() { + return git_attributes::Match { + pattern: &mapping.pattern, + value: &mapping.value, + sequence_number: mapping.sequence_number, + source, + } + .into(); + } + } + groups + .iter() + .rev() + .find_map(|group| group.pattern_matching_relative_path(relative_path.as_ref(), is_dir, case)) + } + + /// Like `matching_exclude_pattern()` but without checking if the current directory is excluded. + /// It returns a triple-index into our data structure from which a match can be reconstructed. + pub(crate) fn matching_exclude_pattern_no_dir( + &self, + relative_path: &BStr, + is_dir: Option, + case: Case, + ) -> Option<(usize, usize, usize)> { + let groups = self.match_groups(); + groups.iter().enumerate().rev().find_map(|(gidx, group)| { + let basename_pos = relative_path.rfind(b"/").map(|p| p + 1); + group + .patterns + .iter() + .enumerate() + .rev() + .find_map(|(plidx, pl)| { + pl.pattern_idx_matching_relative_path(relative_path, basename_pos, is_dir, case) + .map(|idx| (plidx, idx)) + }) + .map(|(plidx, pidx)| (gidx, plidx, pidx)) + }) + } + + pub(crate) fn push_directory( + &mut self, + root: &Path, + dir: &Path, + buf: &mut Vec, + attribute_files_in_index: &[PathOidMapping<'_>], + mut find: Find, + ) -> std::io::Result<()> + where + Find: for<'b> FnMut(&oid, &'b mut Vec) -> Result, E>, + E: std::error::Error + Send + Sync + 'static, + { + let rela_dir = dir.strip_prefix(root).expect("dir in root"); + self.matched_directory_patterns_stack + .push(self.matching_exclude_pattern_no_dir(git_path::into_bstr(rela_dir).as_ref(), Some(true), self.case)); + + let ignore_path_relative = rela_dir.join(".gitignore"); + let ignore_path_relative = git_path::to_unix_separators_on_windows(git_path::into_bstr(ignore_path_relative)); + let ignore_file_in_index = + attribute_files_in_index.binary_search_by(|t| t.0.cmp(ignore_path_relative.as_ref())); + let follow_symlinks = ignore_file_in_index.is_err(); + if !self + .stack + .add_patterns_file(dir.join(".gitignore"), follow_symlinks, Some(root), buf)? + { + match ignore_file_in_index { + Ok(idx) => { + let ignore_blob = find(&attribute_files_in_index[idx].1, buf) + .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))?; + let ignore_path = git_path::from_bstring(ignore_path_relative.into_owned()); + self.stack + .add_patterns_buffer(ignore_blob.data, ignore_path, Some(root)); + } + Err(_) => { + // Need one stack level per component so push and pop matches. + self.stack.patterns.push(Default::default()) + } + } + } + Ok(()) + } +} + +impl Attributes { + pub fn new(globals: AttributeMatchGroup) -> Self { + Attributes { + globals, + stack: Default::default(), + } + } +} + +impl From for Attributes { + fn from(group: AttributeMatchGroup) -> Self { + Attributes::new(group) + } +} + +impl State { + /// Configure a state to be suitable for checking out files. + pub fn for_checkout(unlink_on_collision: bool, attributes: Attributes) -> Self { + State::CreateDirectoryAndAttributesStack { + unlink_on_collision, + #[cfg(debug_assertions)] + test_mkdir_calls: 0, + attributes, + } + } + + /// Configure a state for adding files. + pub fn for_add(attributes: Attributes, ignore: Ignore) -> Self { + State::AttributesAndIgnoreStack { attributes, ignore } + } + + /// Configure a state for status retrieval. + pub fn for_status(ignore: Ignore) -> Self { + State::IgnoreStack(ignore) + } +} + +impl State { + /// Returns a vec of tuples of relative index paths along with the best usable OID for either ignore, attribute files or both. + /// + /// - ignores entries which aren't blobs + /// - ignores ignore entries which are not skip-worktree + /// - within merges, picks 'our' stage both for ignore and attribute files. + pub fn build_attribute_list<'paths>( + &self, + index: &git_index::State, + paths: &'paths git_index::PathStorage, + case: Case, + ) -> Vec> { + let a1_backing; + let a2_backing; + let names = match self { + State::IgnoreStack(v) => { + a1_backing = [(v.exclude_file_name_for_directories.as_bytes().as_bstr(), true)]; + a1_backing.as_ref() + } + State::AttributesAndIgnoreStack { ignore, .. } => { + a2_backing = [ + (ignore.exclude_file_name_for_directories.as_bytes().as_bstr(), true), + (".gitattributes".into(), false), + ]; + a2_backing.as_ref() + } + State::CreateDirectoryAndAttributesStack { .. } => { + a1_backing = [(".gitattributes".into(), true)]; + a1_backing.as_ref() + } + }; + + index + .entries() + .iter() + .filter_map(move |entry| { + let path = entry.path_in(paths); + + // Stage 0 means there is no merge going on, stage 2 means it's 'our' side of the merge, but then + // there won't be a stage 0. + if entry.mode == git_index::entry::Mode::FILE && (entry.stage() == 0 || entry.stage() == 2) { + let basename = path + .rfind_byte(b'/') + .map(|pos| path[pos + 1..].as_bstr()) + .unwrap_or(path); + let is_ignore = names.iter().find_map(|t| { + match case { + Case::Sensitive => basename == t.0, + Case::Fold => basename.eq_ignore_ascii_case(t.0), + } + .then(|| t.1) + })?; + // See https://github.com/git/git/blob/master/dir.c#L912:L912 + if is_ignore && !entry.flags.contains(git_index::entry::Flags::SKIP_WORKTREE) { + return None; + } + Some((path, entry.id)) + } else { + None + } + }) + .collect() + } + + pub(crate) fn ignore_or_panic(&self) -> &Ignore { + match self { + State::IgnoreStack(v) => v, + State::AttributesAndIgnoreStack { ignore, .. } => ignore, + State::CreateDirectoryAndAttributesStack { .. } => { + unreachable!("BUG: must not try to check excludes without it being setup") + } + } + } +} diff --git a/git-worktree/src/fs/mod.rs b/git-worktree/src/fs/mod.rs index b95afcc6d7..4092f56f71 100644 --- a/git-worktree/src/fs/mod.rs +++ b/git-worktree/src/fs/mod.rs @@ -1,3 +1,4 @@ +use bstr::BStr; use std::path::PathBuf; /// Common knowledge about the worktree that is needed across most interactions with the work tree @@ -21,6 +22,7 @@ pub struct Capabilities { pub symlink: bool, } +#[derive(Clone)] pub struct Stack { /// The prefix/root for all paths we handle. root: PathBuf, @@ -30,6 +32,8 @@ pub struct Stack { current_relative: PathBuf, /// The amount of path components of 'current' beyond the roots components. valid_components: usize, + /// If set, we assume the `current` element is a directory to affect calls to `(push|pop)_directory()`. + current_is_directory: bool, } /// A cache for efficiently executing operations on directories and files which are encountered in sorted order. @@ -52,13 +56,21 @@ pub struct Stack { /// As directories are created, the cache will be adjusted to reflect the latest seen directory. /// /// The caching is only useful if consecutive calls to create a directory are using a sorted list of entries. -#[allow(unused)] -pub struct Cache { +#[derive(Clone)] +pub struct Cache<'paths> { stack: Stack, /// tells us what to do as we change paths. - mode: cache::Mode, + state: cache::State, + /// A buffer used when reading attribute or ignore files or their respective objects from the object database. + buf: Vec, + /// If case folding should happen when looking up attributes or exclusions. + case: git_glob::pattern::Case, + /// A lookup table for object ids to read from in some situations when looking up attributes or exclusions. + attribute_files_in_index: Vec>, } +pub(crate) type PathOidMapping<'paths> = (&'paths BStr, git_hash::ObjectId); + /// pub mod cache; /// diff --git a/git-worktree/src/fs/stack.rs b/git-worktree/src/fs/stack.rs index 793e2fa86b..6ccf84769a 100644 --- a/git-worktree/src/fs/stack.rs +++ b/git-worktree/src/fs/stack.rs @@ -15,6 +15,12 @@ impl Stack { } } +pub trait Delegate { + fn push_directory(&mut self, stack: &Stack) -> std::io::Result<()>; + fn push(&mut self, is_last_component: bool, stack: &Stack) -> std::io::Result<()>; + fn pop_directory(&mut self); +} + impl Stack { /// Create a new instance with `root` being the base for all future paths we handle, assuming it to be valid which includes /// symbolic links to be included in it as well. @@ -25,6 +31,7 @@ impl Stack { current_relative: PathBuf::with_capacity(128), valid_components: 0, root, + current_is_directory: true, } } @@ -32,17 +39,23 @@ impl Stack { /// along with the stacks state for inspection to perform an operation that produces some data. /// /// The full path to `relative` will be returned along with the data returned by push_comp. + /// Note that this only works correctly for the delegate's `push_directory()` and `pop_directory()` methods if + /// `relative` paths are terminal, so point to their designated file or directory. pub fn make_relative_path_current( &mut self, relative: impl AsRef, - mut push_comp: impl FnMut(&mut std::iter::Peekable>, &Self) -> std::io::Result<()>, - mut pop_comp: impl FnMut(&Self), + delegate: &mut impl Delegate, ) -> std::io::Result<()> { let relative = relative.as_ref(); debug_assert!( relative.is_relative(), "only index paths are handled correctly here, must be relative" ); + debug_assert!(!relative.to_string_lossy().is_empty(), "empty paths are not allowed"); + + if self.valid_components == 0 { + delegate.push_directory(self)?; + } let mut components = relative.components().peekable(); let mut existing_components = self.current_relative.components(); @@ -59,21 +72,32 @@ impl Stack { for _ in 0..self.valid_components - matching_components { self.current.pop(); self.current_relative.pop(); - pop_comp(&*self); + if self.current_is_directory { + delegate.pop_directory(); + } + self.current_is_directory = true; } self.valid_components = matching_components; + if !self.current_is_directory && components.peek().is_some() { + delegate.push_directory(self)?; + } + while let Some(comp) = components.next() { + let is_last_component = components.peek().is_none(); + self.current_is_directory = !is_last_component; self.current.push(comp); self.current_relative.push(comp); self.valid_components += 1; - let res = push_comp(&mut components, &*self); + let res = delegate.push(is_last_component, self); + if self.current_is_directory { + delegate.push_directory(self)?; + } if let Err(err) = res { self.current.pop(); self.current_relative.pop(); self.valid_components -= 1; - pop_comp(&*self); return Err(err); } } diff --git a/git-worktree/src/index/checkout.rs b/git-worktree/src/index/checkout.rs index 3fa8e0ed1c..d30291e6dc 100644 --- a/git-worktree/src/index/checkout.rs +++ b/git-worktree/src/index/checkout.rs @@ -1,5 +1,5 @@ use bstr::BString; -use std::path::PathBuf; +use git_attributes::Attributes; #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] pub struct Collision { @@ -57,8 +57,8 @@ pub struct Options { /// /// Default true. pub check_stat: bool, - /// The location of the per-user attributes file. It must exist if it is set, causing failure otherwise. - pub attributes_file: Option, + /// A group of attribute patterns that are applied globally, i.e. aren't rooted within the repository itself. + pub attribute_globals: git_attributes::MatchGroup, } impl Default for Options { @@ -71,7 +71,7 @@ impl Default for Options { trust_ctime: true, check_stat: true, overwrite_existing: false, - attributes_file: None, + attribute_globals: Default::default(), } } } diff --git a/git-worktree/src/index/entry.rs b/git-worktree/src/index/entry.rs index e959369ab1..c5ba009392 100644 --- a/git-worktree/src/index/entry.rs +++ b/git-worktree/src/index/entry.rs @@ -7,9 +7,9 @@ use io_close::Close; use crate::{fs, index, os}; -pub struct Context<'a, Find> { +pub struct Context<'a, 'paths, Find> { pub find: &'a mut Find, - pub path_cache: &'a mut fs::Cache, + pub path_cache: &'a mut fs::Cache<'paths>, pub buf: &'a mut Vec, } @@ -17,7 +17,7 @@ pub struct Context<'a, Find> { pub fn checkout( entry: &mut Entry, entry_path: &BStr, - Context { find, path_cache, buf }: Context<'_, Find>, + Context { find, path_cache, buf }: Context<'_, '_, Find>, index::checkout::Options { fs: crate::fs::Capabilities { symlink, @@ -33,11 +33,11 @@ where Find: for<'a> FnMut(&oid, &'a mut Vec) -> Result, E>, E: std::error::Error + Send + Sync + 'static, { - let dest_relative = - git_features::path::from_byte_slice(entry_path).map_err(|_| index::checkout::Error::IllformedUtf8 { - path: entry_path.to_owned(), - })?; - let dest = path_cache.at_entry(dest_relative, entry.mode)?.leading_dir(); + let dest_relative = git_path::try_from_bstr(entry_path).map_err(|_| index::checkout::Error::IllformedUtf8 { + path: entry_path.to_owned(), + })?; + let is_dir = Some(entry.mode == git_index::entry::Mode::COMMIT || entry.mode == git_index::entry::Mode::DIR); + let dest = path_cache.at_path(dest_relative, is_dir, &mut *find)?.path(); let object_size = match entry.mode { git_index::entry::Mode::FILE | git_index::entry::Mode::FILE_EXECUTABLE => { @@ -81,7 +81,7 @@ where oid: entry.id, path: dest.to_path_buf(), })?; - let symlink_destination = git_features::path::from_byte_slice(obj.data) + let symlink_destination = git_path::try_from_byte_slice(obj.data) .map_err(|_| index::checkout::Error::IllformedUtf8 { path: obj.data.into() })?; if symlink { diff --git a/git-worktree/src/index/mod.rs b/git-worktree/src/index/mod.rs index 872e1792cf..6851517fe5 100644 --- a/git-worktree/src/index/mod.rs +++ b/git-worktree/src/index/mod.rs @@ -21,22 +21,36 @@ pub fn checkout( should_interrupt: &AtomicBool, options: checkout::Options, ) -> Result> +where + Find: for<'a> FnMut(&oid, &'a mut Vec) -> Result, E> + Send + Clone, + E: std::error::Error + Send + Sync + 'static, +{ + let paths = index.take_path_backing(); + let res = checkout_inner(index, &paths, dir, find, files, bytes, should_interrupt, options); + index.return_path_backing(paths); + res +} +#[allow(clippy::too_many_arguments)] +fn checkout_inner( + index: &mut git_index::State, + paths: &git_index::PathStorage, + dir: impl Into, + find: Find, + files: &mut impl Progress, + bytes: &mut impl Progress, + should_interrupt: &AtomicBool, + options: checkout::Options, +) -> Result> where Find: for<'a> FnMut(&oid, &'a mut Vec) -> Result, E> + Send + Clone, E: std::error::Error + Send + Sync + 'static, { let num_files = AtomicUsize::default(); let dir = dir.into(); - - let mut ctx = chunk::Context { - buf: Vec::new(), - path_cache: fs::Cache::new( - dir.clone(), - fs::cache::Mode::checkout(options.overwrite_existing, options.attributes_file.clone()), - ), - find: find.clone(), - options: options.clone(), - num_files: &num_files, + let case = if options.fs.ignore_case { + git_glob::pattern::Case::Fold + } else { + git_glob::pattern::Case::Sensitive }; let (chunk_size, thread_limit, num_threads) = git_features::parallel::optimize_chunk_size_and_thread_limit( 100, @@ -45,15 +59,26 @@ where None, ); - let entries_with_paths = interrupt::Iter::new(index.entries_mut_with_paths(), should_interrupt); + let state = fs::cache::State::for_checkout(options.overwrite_existing, options.attribute_globals.clone().into()); + let attribute_files = state.build_attribute_list(index, paths, case); + let mut ctx = chunk::Context { + buf: Vec::new(), + path_cache: fs::Cache::new(dir, state, case, Vec::with_capacity(512), attribute_files), + find, + options, + num_files: &num_files, + }; + let chunk::Outcome { mut collisions, mut errors, mut bytes_written, delayed, } = if num_threads == 1 { + let entries_with_paths = interrupt::Iter::new(index.entries_mut_with_paths_in(paths), should_interrupt); chunk::process(entries_with_paths, files, bytes, &mut ctx)? } else { + let entries_with_paths = interrupt::Iter::new(index.entries_mut_with_paths_in(paths), should_interrupt); in_parallel( git_features::iter::Chunks { inner: entries_with_paths, @@ -61,23 +86,8 @@ where }, thread_limit, { - let num_files = &num_files; - move |_| { - ( - progress::Discard, - progress::Discard, - chunk::Context { - find: find.clone(), - path_cache: fs::Cache::new( - dir.clone(), - fs::cache::Mode::checkout(options.overwrite_existing, options.attributes_file.clone()), - ), - buf: Vec::new(), - options: options.clone(), - num_files, - }, - ) - } + let ctx = ctx.clone(); + move |_| (progress::Discard, progress::Discard, ctx.clone()) }, |chunk, (files, bytes, ctx)| chunk::process(chunk.into_iter(), files, bytes, ctx), chunk::Reduce { @@ -103,7 +113,7 @@ where } Ok(checkout::Outcome { - files_updated: ctx.num_files.load(Ordering::Relaxed), + files_updated: num_files.load(Ordering::Relaxed), collisions, errors, bytes_written, @@ -186,9 +196,10 @@ mod chunk { pub bytes_written: u64, } - pub struct Context<'a, Find> { + #[derive(Clone)] + pub struct Context<'a, 'paths, Find: Clone> { pub find: Find, - pub path_cache: fs::Cache, + pub path_cache: fs::Cache<'paths>, pub buf: Vec, pub options: checkout::Options, /// We keep these shared so that there is the chance for printing numbers that aren't looking like @@ -200,10 +211,10 @@ mod chunk { entries_with_paths: impl Iterator, files: &mut impl Progress, bytes: &mut impl Progress, - ctx: &mut Context<'_, Find>, + ctx: &mut Context<'_, '_, Find>, ) -> Result, checkout::Error> where - Find: for<'a> FnMut(&oid, &'a mut Vec) -> Result, E>, + Find: for<'a> FnMut(&oid, &'a mut Vec) -> Result, E> + Clone, E: std::error::Error + Send + Sync + 'static, { let mut delayed = Vec::new(); @@ -255,10 +266,10 @@ mod chunk { buf, options, num_files, - }: &mut Context<'_, Find>, + }: &mut Context<'_, '_, Find>, ) -> Result> where - Find: for<'a> FnMut(&oid, &'a mut Vec) -> Result, E>, + Find: for<'a> FnMut(&oid, &'a mut Vec) -> Result, E> + Clone, E: std::error::Error + Send + Sync + 'static, { let res = entry::checkout( diff --git a/git-worktree/tests/fixtures/generated-archives/make_ignore_and_attributes_setup.tar.xz b/git-worktree/tests/fixtures/generated-archives/make_ignore_and_attributes_setup.tar.xz new file mode 100644 index 0000000000..2eb265bd06 --- /dev/null +++ b/git-worktree/tests/fixtures/generated-archives/make_ignore_and_attributes_setup.tar.xz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ae297f2c067e2cc7b0c8eabfff721ff775a7d8266ffef979bde2458de2ab03c9 +size 10944 diff --git a/git-worktree/tests/fixtures/make_ignore_and_attributes_setup.sh b/git-worktree/tests/fixtures/make_ignore_and_attributes_setup.sh new file mode 100644 index 0000000000..e176e6c814 --- /dev/null +++ b/git-worktree/tests/fixtures/make_ignore_and_attributes_setup.sh @@ -0,0 +1,112 @@ +#!/bin/bash +set -eu -o pipefail + +cat <user.exclude +# a custom exclude configured per user +user-file-anywhere +/user-file-from-top + +user-dir-anywhere/ +/user-dir-from-top + +user-subdir/file +**/user-subdir-anywhere/file +EOF + +mkdir repo; +(cd repo + git init -q + git config core.excludesFile ../user.exclude + + cat <.git/info/exclude +# a sample .git/info/exclude +file-anywhere +/file-from-top + +dir-anywhere/ +/dir-from-top + +subdir/file +**/subdir-anywhere/file +EOF + + cat <.gitignore +# a sample .gitignore +top-level-local-file-anywhere +EOF + + mkdir dir-with-ignore + cat <dir-with-ignore/.gitignore +# a sample .gitignore +sub-level-local-file-anywhere +sub-level-dir-anywhere/ +!/negated +/negated-dir/ +!/negated-dir/ +EOF + + git add .gitignore dir-with-ignore + git commit --allow-empty -m "init" + + # just add this git-ignore file, so it's a new file that doesn't exist on disk. + mkdir other-dir-with-ignore + skip_worktree_ignore=other-dir-with-ignore/.gitignore + cat <"$skip_worktree_ignore" +# a sample .gitignore +other-sub-level-local-file-anywhere +other-sub-level-dir-anywhere/ +EOF + git add $skip_worktree_ignore && git update-index --skip-worktree $skip_worktree_ignore && rm $skip_worktree_ignore + + mkdir user-dir-anywhere user-dir-from-top dir-anywhere dir-from-top + mkdir -p dir/user-dir-anywhere dir/dir-anywhere + + git check-ignore -vn --stdin 2>&1 <git-check-ignore.baseline || : +dir-with-ignore/sub-level-dir-anywhere/ +dir-with-ignore/foo/sub-level-dir-anywhere/ +dir-with-ignore/sub-level-dir-anywhere +user-file-anywhere +dir/user-file-anywhere +user-file-from-top +no-match/user-file-from-top +user-dir-anywhere +user-dir-from-top +no-match/user-dir-from-top +user-subdir/file +subdir/user-subdir-anywhere/file +user-dir-anywhere/hello +dir/user-dir-anywhere/hello +file-anywhere +dir/file-anywhere +file-from-top +no-match/file-from-top +dir-anywhere +dir/dir-anywhere +dir-from-top +no-match/dir-from-top +subdir/file +subdir/subdir-anywhere/file +top-level-local-file-anywhere +dir/top-level-local-file-anywhere +no-match/sub-level-local-file-anywhere +dir-with-ignore/sub-level-local-file-anywhere +dir-with-ignore/sub-dir/sub-level-local-file-anywhere +other-dir-with-ignore/other-sub-level-local-file-anywhere +other-dir-with-ignore/sub-level-local-file-anywhere +other-dir-with-ignore/sub-dir/other-sub-level-local-file-anywhere +other-dir-with-ignore/no-match/sub-level-local-file-anywhere +non-existing/dir-anywhere +dir-anywhere/hello +dir/dir-anywhere/hello +no-match/sub-level-dir-anywhere/hello +no-match/other-sub-level-dir-anywhere/hello +dir-with-ignore/sub-level-dir-anywhere/hello +dir-with-ignore/sub-level-dir-anywhere/ +other-dir-with-ignore/sub-level-dir-anywhere/hello +other-dir-with-ignore/other-sub-level-dir-anywhere/hello +other-dir-with-ignore/other-sub-level-dir-anywhere/ +dir-with-ignore/negated +dir-with-ignore/negated-dir/hello +EOF + +) diff --git a/git-worktree/tests/worktree-multi-threaded.rs b/git-worktree/tests/worktree-multi-threaded.rs index 5a53f9c26f..cdae7eb15e 100644 --- a/git-worktree/tests/worktree-multi-threaded.rs +++ b/git-worktree/tests/worktree-multi-threaded.rs @@ -1,2 +1,4 @@ +extern crate core; + mod worktree; use worktree::*; diff --git a/git-worktree/tests/worktree/fs/cache.rs b/git-worktree/tests/worktree/fs/cache.rs index 17bbcf4474..bb92087715 100644 --- a/git-worktree/tests/worktree/fs/cache.rs +++ b/git-worktree/tests/worktree/fs/cache.rs @@ -1,39 +1,49 @@ mod create_directory { use std::path::Path; - use git_index::entry::Mode; use git_worktree::fs; use tempfile::{tempdir, TempDir}; + fn panic_on_find<'buf>( + _oid: &git_hash::oid, + _buf: &'buf mut Vec, + ) -> std::io::Result> { + unreachable!("find should nto be called") + } + #[test] - fn root_is_assumed_to_exist_and_files_in_root_do_not_create_directory() { - let dir = tempdir().unwrap(); + fn root_is_assumed_to_exist_and_files_in_root_do_not_create_directory() -> crate::Result { + let dir = tempdir()?; let mut cache = fs::Cache::new( dir.path().join("non-existing-root"), - fs::cache::Mode::checkout(false, None), + fs::cache::State::for_checkout(false, Default::default()), + Default::default(), + Vec::new(), + Default::default(), ); assert_eq!(cache.num_mkdir_calls(), 0); - let path = cache.at_entry("hello", Mode::FILE).unwrap().leading_dir(); + let path = cache.at_path("hello", Some(false), panic_on_find)?.path(); assert!(!path.parent().unwrap().exists(), "prefix itself is never created"); assert_eq!(cache.num_mkdir_calls(), 0); + Ok(()) } #[test] fn directory_paths_are_created_in_full() { let (mut cache, _tmp) = new_cache(); - for (name, mode) in &[ - ("dir", Mode::DIR), - ("submodule", Mode::COMMIT), - ("file", Mode::FILE), - ("exe", Mode::FILE_EXECUTABLE), - ("link", Mode::SYMLINK), + for (name, is_dir) in &[ + ("dir", Some(true)), + ("submodule", Some(true)), + ("file", Some(false)), + ("exe", Some(false)), + ("link", None), ] { let path = cache - .at_entry(Path::new("dir").join(name), *mode) + .at_path(Path::new("dir").join(name), *is_dir, panic_on_find) .unwrap() - .leading_dir(); + .path(); assert!(path.parent().unwrap().is_dir(), "dir exists"); } @@ -41,29 +51,33 @@ mod create_directory { } #[test] - fn existing_directories_are_fine() { + fn existing_directories_are_fine() -> crate::Result { let (mut cache, tmp) = new_cache(); - std::fs::create_dir(tmp.path().join("dir")).unwrap(); + std::fs::create_dir(tmp.path().join("dir"))?; - let path = cache.at_entry("dir/file", Mode::FILE).unwrap().leading_dir(); + let path = cache.at_path("dir/file", Some(false), panic_on_find)?.path(); assert!(path.parent().unwrap().is_dir(), "directory is still present"); assert!(!path.exists(), "it won't create the file"); assert_eq!(cache.num_mkdir_calls(), 1); + Ok(()) } #[test] - fn symlinks_or_files_in_path_are_forbidden_or_unlinked_when_forced() { + fn symlinks_or_files_in_path_are_forbidden_or_unlinked_when_forced() -> crate::Result { let (mut cache, tmp) = new_cache(); let forbidden = tmp.path().join("forbidden"); - std::fs::create_dir(&forbidden).unwrap(); - symlink::symlink_dir(&forbidden, tmp.path().join("link-to-dir")).unwrap(); - std::fs::write(tmp.path().join("file-in-dir"), &[]).unwrap(); + std::fs::create_dir(&forbidden)?; + symlink::symlink_dir(&forbidden, tmp.path().join("link-to-dir"))?; + std::fs::write(tmp.path().join("file-in-dir"), &[])?; for dirname in &["file-in-dir", "link-to-dir"] { cache.unlink_on_collision(false); let relative_path = format!("{}/file", dirname); assert_eq!( - cache.at_entry(&relative_path, Mode::FILE).unwrap_err().kind(), + cache + .at_path(&relative_path, Some(false), panic_on_find) + .unwrap_err() + .kind(), std::io::ErrorKind::AlreadyExists ); } @@ -76,7 +90,7 @@ mod create_directory { for dirname in &["link-to-dir", "file-in-dir"] { cache.unlink_on_collision(true); let relative_path = format!("{}/file", dirname); - let path = cache.at_entry(&relative_path, Mode::FILE).unwrap().leading_dir(); + let path = cache.at_path(&relative_path, Some(false), panic_on_find)?.path(); assert!(path.parent().unwrap().is_dir(), "directory was forcefully created"); assert!(!path.exists()); } @@ -85,26 +99,131 @@ mod create_directory { 4, "like before, but it unlinks what's there and tries again" ); + Ok(()) } - fn new_cache() -> (fs::Cache, TempDir) { + fn new_cache() -> (fs::Cache<'static>, TempDir) { let dir = tempdir().unwrap(); - let cache = fs::Cache::new(dir.path(), fs::cache::Mode::checkout(false, None)); + let cache = fs::Cache::new( + dir.path(), + fs::cache::State::for_checkout(false, Default::default()), + Default::default(), + Vec::new(), + Default::default(), + ); (cache, dir) } } #[allow(unused)] -mod ignore_only { +mod ignore_and_attributes { + use bstr::{BStr, ByteSlice}; use std::path::Path; + use git_glob::pattern::Case; use git_index::entry::Mode; + use git_odb::pack::bundle::write::Options; + use git_odb::FindExt; + use git_testtools::hex_to_id; use git_worktree::fs; use tempfile::{tempdir, TempDir}; - fn new_cache() -> fs::Cache { - let dir = git_testtools::scripted_fixture_repo_read_only("make_ignore_setup.sh").unwrap(); - let cache = fs::Cache::new(dir, todo!()); // TODO: also test initialization - cache + struct IgnoreExpectations<'a> { + lines: bstr::Lines<'a>, + } + + impl<'a> Iterator for IgnoreExpectations<'a> { + type Item = (&'a BStr, Option<(&'a BStr, usize, &'a BStr)>); + + fn next(&mut self) -> Option { + let line = self.lines.next()?; + let (left, value) = line.split_at(line.find_byte(b'\t').unwrap()); + let value = value[1..].as_bstr(); + + let source_and_line = if left == b"::" { + None + } else { + let mut tokens = left.split(|b| *b == b':'); + let source = tokens.next().unwrap().as_bstr(); + let line_number: usize = tokens.next().unwrap().to_str_lossy().parse().ok().unwrap(); + let pattern = tokens.next().unwrap().as_bstr(); + Some((source, line_number, pattern)) + }; + Some((value, source_and_line)) + } + } + + #[test] + fn check_against_baseline() -> crate::Result { + let dir = git_testtools::scripted_fixture_repo_read_only("make_ignore_and_attributes_setup.sh")?; + let worktree_dir = dir.join("repo"); + let git_dir = worktree_dir.join(".git"); + let mut buf = Vec::new(); + let baseline = std::fs::read(git_dir.parent().unwrap().join("git-check-ignore.baseline"))?; + let user_exclude_path = dir.join("user.exclude"); + assert!(user_exclude_path.is_file()); + + let mut index = git_index::File::at(git_dir.join("index"), Default::default())?; + let odb = git_odb::at(git_dir.join("objects"))?; + let case = git_glob::pattern::Case::Sensitive; + let state = git_worktree::fs::cache::State::for_add( + Default::default(), // TODO: attribute tests + git_worktree::fs::cache::state::Ignore::new( + git_attributes::MatchGroup::from_overrides(vec!["!force-include"]), + git_attributes::MatchGroup::from_git_dir(&git_dir, Some(user_exclude_path), &mut buf)?, + None, + case, + ), + ); + let paths_storage = index.take_path_backing(); + let attribute_files_in_index = state.build_attribute_list(&index.state, &paths_storage, case); + assert_eq!( + attribute_files_in_index, + vec![( + "other-dir-with-ignore/.gitignore".as_bytes().as_bstr(), + hex_to_id("5c7e0ed672d3d31d83a3df61f13cc8f7b22d5bfd") + )] + ); + let mut cache = fs::Cache::new(&worktree_dir, state, case, buf, attribute_files_in_index); + + for (relative_entry, source_and_line) in (IgnoreExpectations { + lines: baseline.lines(), + }) { + let relative_path = git_path::from_byte_slice(relative_entry); + let is_dir = worktree_dir.join(&relative_path).metadata().ok().map(|m| m.is_dir()); + + let platform = cache.at_entry(relative_entry, is_dir, |oid, buf| odb.find_blob(oid, buf))?; + + let match_ = platform.matching_exclude_pattern(); + let is_excluded = platform.is_excluded(); + match (match_, source_and_line) { + (None, None) => { + assert!(!is_excluded); + } + (Some(m), Some((source_file, line, pattern))) => { + assert_eq!(m.pattern.to_string(), pattern); + assert_eq!(m.sequence_number, line); + // Paths read from the index are relative to the repo, and they don't exist locally due tot skip-worktree + if m.source.map_or(false, |p| p.exists()) { + assert_eq!( + m.source.map(|p| p.canonicalize().unwrap()), + Some(worktree_dir.join(source_file.to_str_lossy().as_ref()).canonicalize()?) + ); + } + } + (actual, expected) => { + panic!( + "actual {:?} didn't match {:?} at '{}'", + actual, expected, relative_entry + ); + } + } + } + + cache.set_case(Case::Fold); + let platform = cache.at_entry("User-file-ANYWHERE", Some(false), |oid, buf| odb.find_blob(oid, buf))?; + let m = platform.matching_exclude_pattern().expect("match"); + assert_eq!(m.pattern.text, "user-file-anywhere"); + Ok(()) } } diff --git a/git-worktree/tests/worktree/fs/mod.rs b/git-worktree/tests/worktree/fs/mod.rs index b300f17c2b..65064b1a50 100644 --- a/git-worktree/tests/worktree/fs/mod.rs +++ b/git-worktree/tests/worktree/fs/mod.rs @@ -19,3 +19,4 @@ fn from_probing_cwd() { } mod cache; +mod stack; diff --git a/git-worktree/tests/worktree/fs/stack/mod.rs b/git-worktree/tests/worktree/fs/stack/mod.rs new file mode 100644 index 0000000000..2be7f9b519 --- /dev/null +++ b/git-worktree/tests/worktree/fs/stack/mod.rs @@ -0,0 +1,144 @@ +use git_worktree::fs::Stack; +use std::path::{Path, PathBuf}; + +#[derive(Debug, Default, Eq, PartialEq)] +struct Record { + push_dir: usize, + dirs: Vec, + push: usize, +} + +impl git_worktree::fs::stack::Delegate for Record { + fn push_directory(&mut self, stack: &Stack) -> std::io::Result<()> { + self.push_dir += 1; + self.dirs.push(stack.current().into()); + Ok(()) + } + + fn push(&mut self, _is_last_component: bool, _stack: &Stack) -> std::io::Result<()> { + self.push += 1; + Ok(()) + } + + fn pop_directory(&mut self) { + self.dirs.pop(); + } +} + +#[test] +fn delegate_calls_are_consistent() -> crate::Result { + let root = PathBuf::from("."); + let mut s = Stack::new(&root); + + assert_eq!(s.current(), root); + assert_eq!(s.current_relative(), Path::new("")); + + let mut r = Record::default(); + s.make_relative_path_current("a/b", &mut r)?; + let mut dirs = vec![root.clone(), root.join("a")]; + assert_eq!( + r, + Record { + push_dir: 2, + dirs: dirs.clone(), + push: 2, + } + ); + + s.make_relative_path_current("a/b2", &mut r)?; + assert_eq!( + r, + Record { + push_dir: 2, + dirs: dirs.clone(), + push: 3, + } + ); + + s.make_relative_path_current("c/d/e", &mut r)?; + dirs.pop(); + dirs.extend([root.join("c"), root.join("c").join("d")]); + assert_eq!( + r, + Record { + push_dir: 4, + dirs: dirs.clone(), + push: 6, + } + ); + + dirs.push(root.join("c").join("d").join("x")); + s.make_relative_path_current("c/d/x/z", &mut r)?; + assert_eq!( + r, + Record { + push_dir: 5, + dirs: dirs.clone(), + push: 8, + } + ); + + dirs.drain(dirs.len() - 3..).count(); + s.make_relative_path_current("f", &mut r)?; + assert_eq!(s.current_relative(), Path::new("f")); + assert_eq!( + r, + Record { + push_dir: 5, + dirs: dirs.clone(), + push: 9, + } + ); + + dirs.push(root.join("x")); + s.make_relative_path_current("x/z", &mut r)?; + assert_eq!( + r, + Record { + push_dir: 6, + dirs: dirs.clone(), + push: 11, + } + ); + + dirs.push(root.join("x").join("z")); + s.make_relative_path_current("x/z/a", &mut r)?; + assert_eq!( + r, + Record { + push_dir: 7, + dirs: dirs.clone(), + push: 12, + } + ); + + dirs.push(root.join("x").join("z").join("a")); + dirs.push(root.join("x").join("z").join("a").join("b")); + s.make_relative_path_current("x/z/a/b/c", &mut r)?; + assert_eq!( + r, + Record { + push_dir: 9, + dirs: dirs.clone(), + push: 14, + } + ); + + dirs.drain(dirs.len() - 2..).count(); + s.make_relative_path_current("x/z", &mut r)?; + assert_eq!( + r, + Record { + push_dir: 9, + dirs: dirs.clone(), + push: 14, + } + ); + assert_eq!( + dirs.last(), + Some(&PathBuf::from("./x/z")), + "the stack is state so keeps thinking it's a directory which is consistent. Git does it differently though." + ); + + Ok(()) +} diff --git a/gitoxide-core/src/organize.rs b/gitoxide-core/src/organize.rs index 17f312212a..8f63f94246 100644 --- a/gitoxide-core/src/organize.rs +++ b/gitoxide-core/src/organize.rs @@ -257,7 +257,7 @@ where progress.fail(format!( "Error when handling directory {:?}: {}", path_to_move.display(), - err.to_string() + err )); num_errors += 1; } diff --git a/gitoxide-core/src/pack/receive.rs b/gitoxide-core/src/pack/receive.rs index 80cfb96ff9..88cf827d1b 100644 --- a/gitoxide-core/src/pack/receive.rs +++ b/gitoxide-core/src/pack/receive.rs @@ -4,6 +4,7 @@ use std::{ sync::{atomic::AtomicBool, Arc}, }; +pub use git_repository as git; use git_repository::{ hash::ObjectId, objs::bstr::{BString, ByteSlice}, @@ -304,7 +305,7 @@ fn print(out: &mut impl io::Write, res: pack::bundle::write::Outcome, refs: &[Re fn write_raw_refs(refs: &[Ref], directory: PathBuf) -> std::io::Result<()> { let assure_dir_exists = |path: &BString| { assert!(!path.starts_with_str("/"), "no ref start with a /, they are relative"); - let path = directory.join(git_features::path::from_byte_slice_or_panic_on_windows(path)); + let path = directory.join(git::path::from_byte_slice(path)); std::fs::create_dir_all(path.parent().expect("multi-component path")).map(|_| path) }; for r in refs { diff --git a/gitoxide-core/src/repository/commit.rs b/gitoxide-core/src/repository/commit.rs index 1e44912136..ec7c3c8aad 100644 --- a/gitoxide-core/src/repository/commit.rs +++ b/gitoxide-core/src/repository/commit.rs @@ -1,10 +1,8 @@ -use std::path::PathBuf; - use anyhow::{Context, Result}; use git_repository as git; pub fn describe( - repo: impl Into, + repo: git::Repository, rev_spec: Option<&str>, mut out: impl std::io::Write, mut err: impl std::io::Write, @@ -18,7 +16,6 @@ pub fn describe( long_format, }: describe::Options, ) -> Result<()> { - let repo = git::open(repo)?.apply_environment(); let commit = match rev_spec { Some(spec) => repo.rev_parse(spec)?.object()?.try_into_commit()?, None => repo.head_commit()?, diff --git a/gitoxide-core/src/repository/exclude.rs b/gitoxide-core/src/repository/exclude.rs new file mode 100644 index 0000000000..a3b5e4c2bc --- /dev/null +++ b/gitoxide-core/src/repository/exclude.rs @@ -0,0 +1,68 @@ +use anyhow::{bail, Context}; +use std::io; + +use crate::OutputFormat; +use git_repository as git; +use git_repository::prelude::FindExt; + +pub mod query { + use crate::OutputFormat; + use std::ffi::OsString; + + pub struct Options { + pub format: OutputFormat, + pub overrides: Vec, + pub show_ignore_patterns: bool, + } +} + +pub fn query( + repo: git::Repository, + pathspecs: impl Iterator, + mut out: impl io::Write, + query::Options { + overrides, + format, + show_ignore_patterns, + }: query::Options, +) -> anyhow::Result<()> { + if format != OutputFormat::Human { + bail!("JSON output isn't implemented yet"); + } + + let worktree = repo + .worktree() + .current() + .with_context(|| "Cannot check excludes without a current worktree")?; + let index = worktree.open_index()?; + let mut cache = worktree.excludes( + &index.state, + Some(git::attrs::MatchGroup::::from_overrides(overrides)), + )?; + + let prefix = repo.prefix().expect("worktree - we have an index by now")?; + + for mut spec in pathspecs { + for path in spec.apply_prefix(&prefix).items() { + // TODO: what about paths that end in /? Pathspec might handle it, it's definitely something git considers + // even if the directory doesn't exist. Seems to work as long as these are kept in the spec. + let is_dir = git::path::from_bstr(path).metadata().ok().map(|m| m.is_dir()); + let entry = cache.at_entry(path, is_dir, |oid, buf| repo.objects.find_blob(oid, buf))?; + let match_ = entry + .matching_exclude_pattern() + .and_then(|m| (show_ignore_patterns || !m.pattern.is_negative()).then(|| m)); + match match_ { + Some(m) => writeln!( + out, + "{}:{}:{}\t{}", + m.source.map(|p| p.to_string_lossy()).unwrap_or_default(), + m.sequence_number, + m.pattern, + path + )?, + None => writeln!(out, "::\t{}", path)?, + } + } + } + Ok(()) +} diff --git a/gitoxide-core/src/repository/mailmap.rs b/gitoxide-core/src/repository/mailmap.rs index b513de5d23..c00540842e 100644 --- a/gitoxide-core/src/repository/mailmap.rs +++ b/gitoxide-core/src/repository/mailmap.rs @@ -1,4 +1,4 @@ -use std::{io, path::PathBuf}; +use std::io; use git_repository as git; #[cfg(feature = "serde1")] @@ -29,7 +29,7 @@ impl<'a> From> for JsonEntry { } pub fn entries( - repository: PathBuf, + repo: git::Repository, format: OutputFormat, #[cfg_attr(not(feature = "serde1"), allow(unused_variables))] out: impl io::Write, mut err: impl io::Write, @@ -38,7 +38,6 @@ pub fn entries( writeln!(err, "Defaulting to JSON as human format isn't implemented").ok(); } - let repo = git::open(repository)?.apply_environment(); let mut mailmap = git::mailmap::Snapshot::default(); if let Err(e) = repo.load_mailmap_into(&mut mailmap) { writeln!(err, "Error while loading mailmap, the first error is: {}", e).ok(); diff --git a/gitoxide-core/src/repository/mod.rs b/gitoxide-core/src/repository/mod.rs index 92cd215128..a4acbb0b05 100644 --- a/gitoxide-core/src/repository/mod.rs +++ b/gitoxide-core/src/repository/mod.rs @@ -16,3 +16,5 @@ pub mod verify; pub mod odb; pub mod mailmap; + +pub mod exclude; diff --git a/gitoxide-core/src/repository/odb.rs b/gitoxide-core/src/repository/odb.rs index 21b329b428..835874432d 100644 --- a/gitoxide-core/src/repository/odb.rs +++ b/gitoxide-core/src/repository/odb.rs @@ -1,4 +1,4 @@ -use std::{io, path::PathBuf}; +use std::io; use anyhow::bail; use git_repository as git; @@ -22,7 +22,7 @@ mod info { #[cfg_attr(not(feature = "serde1"), allow(unused_variables))] pub fn info( - repository: PathBuf, + repo: git::Repository, format: OutputFormat, out: impl io::Write, mut err: impl io::Write, @@ -31,7 +31,6 @@ pub fn info( writeln!(err, "Only JSON is implemented - using that instead")?; } - let repo = git::open(repository)?.apply_environment(); let store = repo.objects.store_ref(); let stats = info::Statistics { path: store.path().into(), @@ -49,13 +48,11 @@ pub fn info( Ok(()) } -pub fn entries(repository: PathBuf, format: OutputFormat, mut out: impl io::Write) -> anyhow::Result<()> { +pub fn entries(repo: git::Repository, format: OutputFormat, mut out: impl io::Write) -> anyhow::Result<()> { if format != OutputFormat::Human { bail!("Only human output format is supported at the moment"); } - let repo = git::open(repository)?.apply_environment(); - for object in repo.objects.iter()? { let object = object?; writeln!(out, "{}", object)?; diff --git a/gitoxide-core/src/repository/tree.rs b/gitoxide-core/src/repository/tree.rs index 83dfcc81af..57618d7345 100644 --- a/gitoxide-core/src/repository/tree.rs +++ b/gitoxide-core/src/repository/tree.rs @@ -1,4 +1,4 @@ -use std::{borrow::Cow, io, path::PathBuf}; +use std::{borrow::Cow, io}; use anyhow::bail; use git_repository as git; @@ -117,7 +117,7 @@ mod entries { #[cfg_attr(not(feature = "serde1"), allow(unused_variables))] pub fn info( - repository: PathBuf, + repo: git::Repository, treeish: Option<&str>, extended: bool, format: OutputFormat, @@ -128,7 +128,6 @@ pub fn info( writeln!(err, "Only JSON is implemented - using that instead")?; } - let repo = git::open(repository)?.apply_environment(); let tree = treeish_to_tree(treeish, &repo)?; let mut delegate = entries::Traverse::new(extended.then(|| &repo), None); @@ -144,7 +143,7 @@ pub fn info( } pub fn entries( - repository: PathBuf, + repo: git::Repository, treeish: Option<&str>, recursive: bool, extended: bool, @@ -155,7 +154,6 @@ pub fn entries( bail!("Only human output format is supported at the moment"); } - let repo = git::open(repository)?.apply_environment(); let tree = treeish_to_tree(treeish, &repo)?; if recursive { diff --git a/gitoxide-core/src/repository/verify.rs b/gitoxide-core/src/repository/verify.rs index 42052e8e82..77677ff519 100644 --- a/gitoxide-core/src/repository/verify.rs +++ b/gitoxide-core/src/repository/verify.rs @@ -1,4 +1,4 @@ -use std::{path::PathBuf, sync::atomic::AtomicBool}; +use std::sync::atomic::AtomicBool; use git_repository as git; use git_repository::Progress; @@ -20,7 +20,7 @@ pub struct Context { pub const PROGRESS_RANGE: std::ops::RangeInclusive = 1..=3; pub fn integrity( - repo: PathBuf, + repo: git::Repository, mut out: impl std::io::Write, progress: impl Progress, should_interrupt: &AtomicBool, @@ -31,7 +31,6 @@ pub fn integrity( algorithm, }: Context, ) -> anyhow::Result<()> { - let repo = git_repository::open(repo)?; #[cfg_attr(not(feature = "serde1"), allow(unused))] let mut outcome = repo.objects.store_ref().verify_integrity( progress, diff --git a/src/plumbing/main.rs b/src/plumbing/main.rs index f8b391a56b..c30b69fced 100644 --- a/src/plumbing/main.rs +++ b/src/plumbing/main.rs @@ -9,6 +9,7 @@ use std::{ use anyhow::Result; use clap::Parser; +use git_repository::bstr::io::BufReadExt; use gitoxide_core as core; use gitoxide_core::pack::verify; @@ -155,133 +156,181 @@ pub fn main() -> Result<()> { }, ), }, - Subcommands::Repository(repo::Platform { repository, cmd }) => match cmd { - repo::Subcommands::Commit { cmd } => match cmd { - repo::commit::Subcommands::Describe { - annotated_tags, - all_refs, - first_parent, - always, - long, - statistics, - max_candidates, - rev_spec, + Subcommands::Repository(repo::Platform { repository, cmd }) => { + use git_repository as git; + let repository = git::ThreadSafeRepository::discover(repository)?; + match cmd { + repo::Subcommands::Commit { cmd } => match cmd { + repo::commit::Subcommands::Describe { + annotated_tags, + all_refs, + first_parent, + always, + long, + statistics, + max_candidates, + rev_spec, + } => prepare_and_run( + "repository-commit-describe", + verbose, + progress, + progress_keep_open, + None, + move |_progress, out, err| { + core::repository::commit::describe( + repository.into(), + rev_spec.as_deref(), + out, + err, + core::repository::commit::describe::Options { + all_tags: !annotated_tags, + all_refs, + long_format: long, + first_parent, + statistics, + max_candidates, + always, + }, + ) + }, + ), + }, + repo::Subcommands::Exclude { cmd } => match cmd { + repo::exclude::Subcommands::Query { + patterns, + pathspecs, + show_ignore_patterns, + } => prepare_and_run( + "repository-exclude-query", + verbose, + progress, + progress_keep_open, + None, + move |_progress, out, _err| { + use git::bstr::ByteSlice; + core::repository::exclude::query( + repository.into(), + if pathspecs.is_empty() { + Box::new( + stdin_or_bail()? + .byte_lines() + .filter_map(Result::ok) + .filter_map(|line| git::path::Spec::from_bytes(line.as_bstr())), + ) as Box> + } else { + Box::new(pathspecs.into_iter()) + }, + out, + core::repository::exclude::query::Options { + format, + show_ignore_patterns, + overrides: patterns, + }, + ) + }, + ), + }, + repo::Subcommands::Mailmap { cmd } => match cmd { + repo::mailmap::Subcommands::Entries => prepare_and_run( + "repository-mailmap-entries", + verbose, + progress, + progress_keep_open, + None, + move |_progress, out, err| { + core::repository::mailmap::entries(repository.into(), format, out, err) + }, + ), + }, + repo::Subcommands::Odb { cmd } => match cmd { + repo::odb::Subcommands::Entries => prepare_and_run( + "repository-odb-entries", + verbose, + progress, + progress_keep_open, + None, + move |_progress, out, _err| core::repository::odb::entries(repository.into(), format, out), + ), + repo::odb::Subcommands::Info => prepare_and_run( + "repository-odb-info", + verbose, + progress, + progress_keep_open, + None, + move |_progress, out, err| core::repository::odb::info(repository.into(), format, out, err), + ), + }, + repo::Subcommands::Tree { cmd } => match cmd { + repo::tree::Subcommands::Entries { + treeish, + recursive, + extended, + } => prepare_and_run( + "repository-tree-entries", + verbose, + progress, + progress_keep_open, + None, + move |_progress, out, _err| { + core::repository::tree::entries( + repository.into(), + treeish.as_deref(), + recursive, + extended, + format, + out, + ) + }, + ), + repo::tree::Subcommands::Info { treeish, extended } => prepare_and_run( + "repository-tree-info", + verbose, + progress, + progress_keep_open, + None, + move |_progress, out, err| { + core::repository::tree::info( + repository.into(), + treeish.as_deref(), + extended, + format, + out, + err, + ) + }, + ), + }, + repo::Subcommands::Verify { + args: + pack::VerifyOptions { + statistics, + algorithm, + decode, + re_encode, + }, } => prepare_and_run( - "repository-commit-describe", + "repository-verify", verbose, progress, progress_keep_open, - None, - move |_progress, out, err| { - core::repository::commit::describe( - repository, - rev_spec.as_deref(), + core::repository::verify::PROGRESS_RANGE, + move |progress, out, _err| { + core::repository::verify::integrity( + repository.into(), out, - err, - core::repository::commit::describe::Options { - all_tags: !annotated_tags, - all_refs, - long_format: long, - first_parent, - statistics, - max_candidates, - always, + progress, + &should_interrupt, + core::repository::verify::Context { + output_statistics: statistics.then(|| format), + algorithm, + verify_mode: verify_mode(decode, re_encode), + thread_limit, }, ) }, ), - }, - repo::Subcommands::Mailmap { cmd } => match cmd { - repo::mailmap::Subcommands::Entries => prepare_and_run( - "repository-mailmap-entries", - verbose, - progress, - progress_keep_open, - None, - move |_progress, out, err| core::repository::mailmap::entries(repository, format, out, err), - ), - }, - repo::Subcommands::Odb { cmd } => match cmd { - repo::odb::Subcommands::Entries => prepare_and_run( - "repository-odb-entries", - verbose, - progress, - progress_keep_open, - None, - move |_progress, out, _err| core::repository::odb::entries(repository, format, out), - ), - repo::odb::Subcommands::Info => prepare_and_run( - "repository-odb-info", - verbose, - progress, - progress_keep_open, - None, - move |_progress, out, err| core::repository::odb::info(repository, format, out, err), - ), - }, - repo::Subcommands::Tree { cmd } => match cmd { - repo::tree::Subcommands::Entries { - treeish, - recursive, - extended, - } => prepare_and_run( - "repository-tree-entries", - verbose, - progress, - progress_keep_open, - None, - move |_progress, out, _err| { - core::repository::tree::entries( - repository, - treeish.as_deref(), - recursive, - extended, - format, - out, - ) - }, - ), - repo::tree::Subcommands::Info { treeish, extended } => prepare_and_run( - "repository-tree-info", - verbose, - progress, - progress_keep_open, - None, - move |_progress, out, err| { - core::repository::tree::info(repository, treeish.as_deref(), extended, format, out, err) - }, - ), - }, - repo::Subcommands::Verify { - args: - pack::VerifyOptions { - statistics, - algorithm, - decode, - re_encode, - }, - } => prepare_and_run( - "repository-verify", - verbose, - progress, - progress_keep_open, - core::repository::verify::PROGRESS_RANGE, - move |progress, out, _err| { - core::repository::verify::integrity( - repository, - out, - progress, - &should_interrupt, - core::repository::verify::Context { - output_statistics: statistics.then(|| format), - algorithm, - verify_mode: verify_mode(decode, re_encode), - thread_limit, - }, - ) - }, - ), - }, + } + } Subcommands::Pack(subcommands) => match subcommands { pack::Subcommands::Create { repository, @@ -303,16 +352,7 @@ pub fn main() -> Result<()> { progress_keep_open, core::pack::create::PROGRESS_RANGE, move |progress, out, _err| { - let input = if has_tips { - None - } else { - if atty::is(atty::Stream::Stdin) { - anyhow::bail!( - "Refusing to read from standard input as no path is given, but it's a terminal." - ) - } - Some(BufReader::new(stdin())) - }; + let input = if has_tips { None } else { stdin_or_bail()?.into() }; let repository = repository.unwrap_or_else(|| PathBuf::from(".")); let context = core::pack::create::Context { thread_limit, @@ -379,7 +419,7 @@ pub fn main() -> Result<()> { directory, refs_directory, refs.into_iter().map(|r| r.into()).collect(), - git_features::progress::DoOrDiscard::from(progress), + progress, core::pack::receive::Context { thread_limit, format, @@ -568,7 +608,7 @@ pub fn main() -> Result<()> { core::remote::refs::list( protocol, &url, - git_features::progress::DoOrDiscard::from(progress), + progress, core::remote::refs::Context { thread_limit, format, @@ -603,6 +643,13 @@ pub fn main() -> Result<()> { Ok(()) } +fn stdin_or_bail() -> anyhow::Result> { + if atty::is(atty::Stream::Stdin) { + anyhow::bail!("Refusing to read from standard input while a terminal is connected") + } + Ok(BufReader::new(stdin())) +} + fn verify_mode(decode: bool, re_encode: bool) -> verify::Mode { match (decode, re_encode) { (true, false) => verify::Mode::HashCrc32Decode, diff --git a/src/plumbing/options.rs b/src/plumbing/options.rs index 80d3a9647b..0807c9f0ed 100644 --- a/src/plumbing/options.rs +++ b/src/plumbing/options.rs @@ -370,6 +370,36 @@ pub mod repo { #[clap(subcommand)] cmd: mailmap::Subcommands, }, + /// Interact with the exclude files like .gitignore. + Exclude { + #[clap(subcommand)] + cmd: exclude::Subcommands, + }, + } + + pub mod exclude { + use git_repository as git; + use std::ffi::OsString; + + #[derive(Debug, clap::Subcommand)] + pub enum Subcommands { + /// Check if path-specs are excluded and print the result similar to `git check-ignore`. + Query { + /// Show actual ignore patterns instead of un-excluding an entry. + /// + /// That way one can understand why an entry might not be excluded. + #[clap(long, short = 'i')] + show_ignore_patterns: bool, + /// Additional patterns to use for exclusions. They have the highest priority. + /// + /// Useful for undoing previous patterns using the '!' prefix. + #[clap(long, short = 'p')] + patterns: Vec, + /// The git path specifications to check for exclusion, or unset to read from stdin one per line. + #[clap(parse(try_from_os_str = std::convert::TryFrom::try_from))] + pathspecs: Vec, + }, + } } pub mod mailmap { diff --git a/src/porcelain/main.rs b/src/porcelain/main.rs index 98b668620c..af1c7d8b9a 100644 --- a/src/porcelain/main.rs +++ b/src/porcelain/main.rs @@ -53,7 +53,7 @@ pub fn main() -> Result<()> { hours::estimate( &working_dir, &refname, - git_features::progress::DoOrDiscard::from(progress), + progress, hours::Context { show_pii, omit_unify_identities, @@ -75,7 +75,7 @@ pub fn main() -> Result<()> { organize::discover( root.unwrap_or_else(|| [std::path::Component::CurDir].iter().collect()), out, - git_features::progress::DoOrDiscard::from(progress), + progress, debug, ) }, @@ -102,7 +102,7 @@ pub fn main() -> Result<()> { }, repository_source.unwrap_or_else(|| [std::path::Component::CurDir].iter().collect()), destination_directory.unwrap_or_else(|| [std::path::Component::CurDir].iter().collect()), - git_features::progress::DoOrDiscard::from(progress), + progress, ) }, ) diff --git a/src/shared.rs b/src/shared.rs index 9d708c3966..d9b5ae05af 100644 --- a/src/shared.rs +++ b/src/shared.rs @@ -52,19 +52,62 @@ pub mod pretty { use crate::shared::ProgressRange; + #[cfg(feature = "small")] + pub fn prepare_and_run( + name: &str, + verbose: bool, + progress: bool, + #[cfg_attr(not(feature = "prodash-render-tui"), allow(unused_variables))] progress_keep_open: bool, + range: impl Into>, + run: impl FnOnce( + progress::DoOrDiscard, + &mut dyn std::io::Write, + &mut dyn std::io::Write, + ) -> Result, + ) -> Result { + crate::shared::init_env_logger(); + + match (verbose, progress) { + (false, false) => { + let stdout = stdout(); + let mut stdout_lock = stdout.lock(); + let stderr = stderr(); + let mut stderr_lock = stderr.lock(); + run(progress::DoOrDiscard::from(None), &mut stdout_lock, &mut stderr_lock) + } + (true, false) => { + let progress = crate::shared::progress_tree(); + let sub_progress = progress.add_child(name); + + use crate::shared::{self, STANDARD_RANGE}; + let handle = shared::setup_line_renderer_range(&progress, range.into().unwrap_or(STANDARD_RANGE)); + + let mut out = Vec::::new(); + let res = run(progress::DoOrDiscard::from(Some(sub_progress)), &mut out, &mut stderr()); + handle.shutdown_and_wait(); + std::io::Write::write_all(&mut stdout(), &out)?; + res + } + #[cfg(not(feature = "prodash-render-tui"))] + (true, true) | (false, true) => { + unreachable!("BUG: This branch can't be run without a TUI built-in") + } + } + } + + #[cfg(not(feature = "small"))] pub fn prepare_and_run( name: &str, verbose: bool, progress: bool, #[cfg_attr(not(feature = "prodash-render-tui"), allow(unused_variables))] progress_keep_open: bool, - #[cfg_attr(not(feature = "prodash-render-line"), allow(unused_variables))] range: impl Into>, + range: impl Into>, run: impl FnOnce( progress::DoOrDiscard, &mut dyn std::io::Write, &mut dyn std::io::Write, ) -> Result + Send - + std::panic::UnwindSafe + 'static, ) -> Result { crate::shared::init_env_logger(); diff --git a/tests/tools/src/lib.rs b/tests/tools/src/lib.rs index 97e35da8f7..04dc792819 100644 --- a/tests/tools/src/lib.rs +++ b/tests/tools/src/lib.rs @@ -13,7 +13,7 @@ use once_cell::sync::Lazy; use parking_lot::Mutex; pub use tempfile; -type Result = std::result::Result>; +pub type Result = std::result::Result>; static SCRIPT_IDENTITY: Lazy>> = Lazy::new(|| Mutex::new(BTreeMap::new()));