diff --git a/Cargo.lock b/Cargo.lock
index 6d876a0a1d8..0d7576ebd70 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 90d0bbd4996..c10e07e54dc 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 55f2e4d034f..e8c4d364de7 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 36a8d5c801f..43e4374f738 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 1da6fbc4bfc..3a2db8351f9 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 bf27419b3bf..04fcf792bb9 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 1f2a941c85a..26276a82c59 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<T: match_group::Tag> {
+#[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<T: match_group::Pattern = Attributes> {
     /// 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<T: match_group::Tag> {
     pub patterns: Vec<PatternList<T>>,
 }
 
-/// A list of patterns with an optional names, for matching against it.
-#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)]
-pub struct PatternList<T: match_group::Tag> {
-    /// 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<T: match_group::Pattern> {
+    /// 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<PatternMapping<T::Value>>,
 
-    /// 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<BString>,
-}
-
-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<PathBuf>,
 
-    /// 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<Ignore> {
-        /// See [PatternList::<Ignore>::from_overrides()] for details.
-        pub fn from_overrides(patterns: impl IntoIterator<Item = impl Into<OsString>>) -> Self {
-            MatchGroup {
-                patterns: vec![PatternList::<Ignore>::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<BString>,
+}
 
-    impl PatternList<Ignore> {
-        /// Parse a list of patterns, using slashes as path separators
-        pub fn from_overrides(patterns: impl IntoIterator<Item = impl Into<OsString>>) -> 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<T> {
+    pub pattern: git_glob::Pattern,
+    pub value: T,
+    pub sequence_number: usize,
 }
-pub use match_group::{Attributes, Ignore, Tag};
 
-pub type Files = MatchGroup<Attributes>;
-pub type IgnoreFiles = MatchGroup<Ignore>;
+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 00000000000..8fd5b045999
--- /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<StateRef<'a>> 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<Item = Result<(&'a BStr, StateRef<'a>), crate::parse::Error>>,
+) -> Result<Vec<Assignment>, 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<PatternMapping<Self::Value>>;
+
+    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<PatternMapping<Self::Value>> {
+        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<Assignment>),
+    Attributes(Vec<Assignment>),
+}
+
+/// 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<PatternMapping<Self::Value>> {
+        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<T> MatchGroup<T>
+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<bool>,
+        case: git_glob::pattern::Case,
+    ) -> Option<Match<'_, T::Value>> {
+        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<Ignore> {
+    /// 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<Path>,
+        excludes_file: Option<PathBuf>,
+        buf: &mut Vec<u8>,
+    ) -> std::io::Result<Self> {
+        let mut group = Self::default();
+
+        let follow_symlinks = true;
+        // order matters! More important ones first.
+        group.patterns.extend(
+            excludes_file
+                .map(|file| PatternList::<Ignore>::from_file(file, None, follow_symlinks, buf))
+                .transpose()?
+                .flatten(),
+        );
+        group.patterns.extend(PatternList::<Ignore>::from_file(
+            git_dir.as_ref().join("info").join("exclude"),
+            None,
+            follow_symlinks,
+            buf,
+        )?);
+        Ok(group)
+    }
+
+    /// See [PatternList::<Ignore>::from_overrides()] for details.
+    pub fn from_overrides(patterns: impl IntoIterator<Item = impl Into<OsString>>) -> Self {
+        MatchGroup {
+            patterns: vec![PatternList::<Ignore>::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<PathBuf>,
+        follow_symlinks: bool,
+        root: Option<&Path>,
+        buf: &mut Vec<u8>,
+    ) -> std::io::Result<bool> {
+        let previous_len = self.patterns.len();
+        self.patterns.extend(PatternList::<Ignore>::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<PathBuf>, root: Option<&Path>) {
+        self.patterns
+            .push(PatternList::<Ignore>::from_bytes(bytes, source.into(), root));
+    }
+}
+
+fn read_in_full_ignore_missing(path: &Path, follow_symlinks: bool, buf: &mut Vec<u8>) -> std::io::Result<bool> {
+    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<T> PatternList<T>
+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<PathBuf>, 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<PathBuf>,
+        root: Option<&Path>,
+        follow_symlinks: bool,
+        buf: &mut Vec<u8>,
+    ) -> std::io::Result<Option<Self>> {
+        let source = source.into();
+        Ok(read_in_full_ignore_missing(&source, follow_symlinks, buf)?.then(|| Self::from_bytes(buf, source, root)))
+    }
+}
+
+impl<T> PatternList<T>
+where
+    T: Pattern,
+{
+    pub fn pattern_matching_relative_path(
+        &self,
+        relative_path: &BStr,
+        basename_pos: Option<usize>,
+        is_dir: Option<bool>,
+        case: git_glob::pattern::Case,
+    ) -> Option<Match<'_, T::Value>> {
+        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<usize>,
+        is_dir: Option<bool>,
+        case: git_glob::pattern::Case,
+    ) -> Option<usize> {
+        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<usize>,
+    ) -> Option<(&'a BStr, Option<usize>)> {
+        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<Ignore> {
+    /// Parse a list of patterns, using slashes as path separators
+    pub fn from_overrides(patterns: impl IntoIterator<Item = impl Into<OsString>>) -> 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 8d044e933a1..064e78a4a17 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<Self::Item> {
         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 c25a606d73b..36d782c5c94 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 00000000000..195d47f4886
--- /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 <<EOF >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 <<EOF >.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 <<EOF >.gitignore
+# a sample .gitignore
+top-level-local-file-anywhere
+EOF
+
+  mkdir dir-with-ignore
+  cat <<EOF >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 <<EOF >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 32d84a473d2..a8b661eb1a5 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<Self::Item> {
+            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::<Ignore>::from_overrides(input).patterns;
-        assert_eq!(patterns.len(), 1);
+        let group = git_attributes::MatchGroup::<Ignore>::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::<Ignore>::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 a5889dfe134..c4306c70cd0 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<usize>) -> parse::Kind {
diff --git a/git-attributes/tests/parse/ignore.rs b/git-attributes/tests/parse/ignore.rs
index 2594e5b5e9d..f3c94e059ac 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 18a5d9dd077..f4c67d83591 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 9113473e94b..00000000000
--- 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 2d2662736e2..ba119e8bd4f 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<P: AsRef<Path>>(path: P) -> Result<Self, parser::ParserOrIoError<'static>> {
         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<PathBuf>, options: &from_paths::Options) -> Result<Self, from_paths::Error> {
         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<Option<Self>, 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<Cow<'event, [u8]>>>(
+    pub fn value<T: TryFrom<Cow<'event, [u8]>>>(
         &'event self,
-        section_name: &'lookup str,
-        subsection_name: Option<&'lookup str>,
-        key: &'lookup str,
-    ) -> Result<T, GitConfigError<'lookup>> {
-        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, lookup::Error<T::Error>> {
+        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<T: TryFrom<Cow<'event, [u8]>>>(
+        &'event self,
+        section_name: &str,
+        subsection_name: Option<&str>,
+        key: &str,
+    ) -> Option<Result<T, T::Error>> {
+        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<Cow<'event, BStr>> {
+        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<values::Path<'event>> {
+        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<Result<bool, value::parse::Error>> {
+        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<Bytes> = git_config.multi_value("core", None, "c")?;
     /// assert_eq!(c_value, vec![Bytes { value: Cow::Borrowed(b"g") }]);
-    /// # Ok::<(), GitConfigError>(())
+    /// # Ok::<(), Box<dyn std::error::Error>>(())
     /// ```
     ///
     /// # Errors
@@ -518,18 +548,17 @@ impl<'event> GitConfig<'event> {
     ///
     /// [`values`]: crate::values
     /// [`TryFrom`]: std::convert::TryFrom
-    #[inline]
     pub fn multi_value<'lookup, T: TryFrom<Cow<'event, [u8]>>>(
         &'event self,
         section_name: &'lookup str,
         subsection_name: Option<&'lookup str>,
         key: &'lookup str,
-    ) -> Result<Vec<T>, GitConfigError<'lookup>> {
-        self.get_raw_multi_value(section_name, subsection_name, key)?
+    ) -> Result<Vec<T>, lookup::Error<T::Error>> {
+        self.raw_multi_value(section_name, subsection_name, key)?
             .into_iter()
             .map(T::try_from)
             .collect::<Result<Vec<_>, _>>()
-            .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<MutableSection<'_, '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(MutableSection::new(self.sections.get_mut(id).expect(
-            "Section did not have id from lookup, internal invariant violated",
-        )))
+    ) -> Result<MutableSection<'_, '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(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<&'lookup str>>,
     ) -> Option<SectionBody> {
         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<Option<&'lookup str>>,
         new_section_name: impl Into<SectionHeaderName<'event>>,
         new_subsection_name: impl Into<Option<Cow<'event, str>>>,
-    ) -> 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<Cow<'_, [u8]>, GitConfigError<'lookup>> {
+    ) -> Result<Cow<'_, [u8]>, 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<MutableValue<'_, 'lookup, 'event>, GitConfigError<'lookup>> {
-        let section_ids = self.get_section_ids_by_name_and_subname(section_name, subsection_name)?;
+    ) -> Result<MutableValue<'_, 'lookup, 'event>, 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<Vec<Cow<'_, [u8]>>, GitConfigError<'lookup>> {
+        section_name: &str,
+        subsection_name: Option<&str>,
+        key: &str,
+    ) -> Result<Vec<Cow<'_, [u8]>>, 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(&section_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<MutableMultiValue<'_, 'lookup, 'event>, GitConfigError<'lookup>> {
-        let section_ids = self.get_section_ids_by_name_and_subname(section_name, subsection_name)?;
+    ) -> Result<MutableMultiValue<'_, 'lookup, 'event>, 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<dyn std::error::Error>>(())
     /// ```
     ///
     /// # Errors
@@ -1166,8 +1190,8 @@ impl<'event> GitConfig<'event> {
         subsection_name: Option<&'lookup str>,
         key: &'lookup str,
         new_value: Vec<u8>,
-    ) -> 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<Item = Cow<'event, [u8]>>,
-    ) -> 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<SectionHeaderName<'lookup>>,
         subsection_name: Option<&'lookup str>,
-    ) -> Result<Vec<SectionId>, GitConfigError<'lookup>> {
+    ) -> Result<Vec<SectionId>, lookup::existing::Error> {
         let section_name = section_name.into();
         let section_ids = self
             .section_lookup_tree
             .get(&section_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<SectionHeaderName<'lookup>>,
-    ) -> Result<Vec<SectionId>, GitConfigError<'lookup>> {
+    ) -> Result<Vec<SectionId>, lookup::existing::Error> {
         let section_name = section_name.into();
         self.section_lookup_tree
             .get(&section_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<GitConfig<'a>, 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<GitConfig<'a>, Self::Error> {
         parse_from_bytes(value).map(GitConfig::from)
     }
@@ -1408,7 +1430,6 @@ impl<'a> TryFrom<&'a Vec<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 Vec<u8>) -> Result<GitConfig<'a>, Self::Error> {
         parse_from_bytes(value).map(GitConfig::from)
     }
@@ -1458,7 +1479,6 @@ impl<'a> From<Parser<'a>> for GitConfig<'a> {
 }
 
 impl From<GitConfig<'_>> for Vec<u8> {
-    #[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 27c4bd4d97e..c133b572ad6 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 e97c1f5cf3d..42334dac0c5 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<P: AsRef<Path>>(path: P) -> Result<Self, parser::ParserOrIoError<'static>> {
         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, Self::Error> {
         Self::open(path)
     }
 }
 
 impl<'data> From<GitConfig<'data>> 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 ada8b479e48..a8aadcc5f98 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<Cow<'event, [u8]>> {
-        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<Cow<'event, [u8]>> {
-        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<Cow<'_, [u8]>, GitConfigError<'key>> {
+    ) -> Result<Cow<'_, [u8]>, 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<Cow<'event, [u8]>> {
-        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<T: TryFrom<Cow<'event, [u8]>>>(&self, key: &Key) -> Result<T, GitConfigError<'event>> {
-        T::try_from(self.value(key).ok_or(GitConfigError::KeyDoesNotExist)?)
-            .map_err(|_| GitConfigError::FailedConversion)
+    pub fn value_as<T: TryFrom<Cow<'event, [u8]>>>(&self, key: &Key) -> Result<T, lookup::Error<T::Error>> {
+        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<T: TryFrom<Cow<'event, [u8]>>>(&self, key: &Key) -> Result<Vec<T>, GitConfigError<'event>> {
+    pub fn values_as<T: TryFrom<Cow<'event, [u8]>>>(&self, key: &Key) -> Result<Vec<T>, lookup::Error<T::Error>> {
         self.values(key)
             .into_iter()
             .map(T::try_from)
             .collect::<Result<Vec<T>, _>>()
-            .map_err(|_| GitConfigError::FailedConversion)
+            .map_err(lookup::Error::FailedConversion)
     }
 
     /// Returns an iterator visiting all keys in order.
-    #[inline]
     pub fn keys(&self) -> impl Iterator<Item = &Key<'event>> {
         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<usize> {
+    fn value_range_by_key(&self, key: &Key<'event>) -> Range<usize> {
         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<Vec<Event<'event>>> for SectionBody<'event> {
-    #[inline]
     fn from(e: Vec<Event<'event>>) -> Self {
         Self(e)
     }
diff --git a/git-config/src/file/value.rs b/git-config/src/file/value.rs
index 1935ff253f7..63b3e07b3e8 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<Cow<'_, [u8]>, GitConfigError> {
+    pub fn get(&self) -> Result<Cow<'_, [u8]>, 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<Vec<Cow<'_, [u8]>>, GitConfigError> {
+    pub fn get(&self) -> Result<Vec<Cow<'_, [u8]>>, 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<u8>) {
         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<Item = Cow<'a, [u8]>>) {
         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<SectionId, Vec<usize>>,
         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<SectionId, Vec<usize>>,
         section_id: SectionId,
diff --git a/git-config/src/fs.rs b/git-config/src/fs.rs
index fcd0904f1a8..62db5f5cbfe 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<T: TryFrom<Cow<'config, [u8]>>>(
         &'config self,
@@ -177,13 +176,12 @@ impl<'config> Config<'config> {
         None
     }
 
-    #[inline]
     pub fn try_value<'lookup, T: TryFrom<Cow<'config, [u8]>>>(
         &'config self,
         section_name: &'lookup str,
         subsection_name: Option<&'lookup str>,
         key: &'lookup str,
-    ) -> Result<Option<T>, GitConfigError<'lookup>> {
+    ) -> Result<Option<T>, lookup::Error<T::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<Option<(T, ConfigSource)>, GitConfigError<'lookup>> {
+    ) -> Result<Option<(T, ConfigSource)>, lookup::Error<T::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 238b250344f..b7e89f1259d 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<E> {
+        #[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<std::str::Utf8Error>,
+        }
+
+        impl Error {
+            pub(crate) fn new(message: &'static str, input: impl Into<BString>) -> 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 1c6800d5882..3d59ff26e26 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<u8> {
         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<Event<'_>> for Vec<u8> {
-    #[inline]
     fn from(event: Event) -> Self {
         event.into()
     }
 }
 
 impl From<&Event<'_>> for Vec<u8> {
-    #[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<std::cmp::Ordering> {
                 self.0
                     .to_ascii_lowercase()
@@ -249,21 +239,18 @@ macro_rules! generate_case_insensitive {
         }
 
         impl std::hash::Hash for $name<'_> {
-            #[inline]
             fn hash<H: std::hash::Hasher>(&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<Cow<'a, str>> 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<u8> {
         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<ParsedSectionHeader<'_>> for Vec<u8> {
-    #[inline]
     fn from(header: ParsedSectionHeader) -> Self {
         header.into()
     }
 }
 
 impl From<&ParsedSectionHeader<'_>> for Vec<u8> {
-    #[inline]
     fn from(header: &ParsedSectionHeader) -> Self {
         header.to_string().into_bytes()
     }
 }
 
 impl<'a> From<ParsedSectionHeader<'a>> 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<ParsedComment<'_>> for Vec<u8> {
-    #[inline]
     fn from(c: ParsedComment) -> Self {
         c.into()
     }
 }
 
 impl From<&ParsedComment<'_>> for Vec<u8> {
-    #[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<std::io::Error> 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<Event<'a>> {
         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<ParsedSection<'a>> {
         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<Event<'a>> {
         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<Self, Self::Error> {
         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<Self, Self::Error> {
         parse_from_bytes(value)
     }
@@ -925,7 +890,6 @@ pub fn parse_from_path<P: AsRef<Path>>(path: P) -> Result<Parser<'static>, 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<Parser, Error> {
     parse_from_bytes(input.as_bytes())
 }
diff --git a/git-config/src/values.rs b/git-config/src/values.rs
index 80620d23785..b6752b2f245 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<u8>) -> 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<Vec<u8>> for Bytes<'_> {
 }
 
 impl<'a> From<Cow<'a, [u8]>> 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<Cow<'a, [u8]>> 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<BStr> for Path<'a> {
+    fn as_ref(&self) -> &BStr {
+        self.value.as_ref()
+    }
+}
+
 impl<'a> From<Cow<'a, [u8]>> 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<u8> {
         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<BString>) -> 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<Self, Self::Error> {
         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<Vec<u8>> for Boolean<'_> {
-    type Error = BooleanError;
+    type Error = value::parse::Error;
 
     fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
         if value.eq_ignore_ascii_case(b"no")
@@ -424,7 +425,7 @@ impl TryFrom<Vec<u8>> for Boolean<'_> {
 }
 
 impl<'a> TryFrom<Cow<'a, [u8]>> for Boolean<'a> {
-    type Error = BooleanError;
+    type Error = value::parse::Error;
     fn try_from(c: Cow<'a, [u8]>) -> Result<Self, Self::Error> {
         match c {
             Cow::Borrowed(c) => Self::try_from(c),
@@ -434,7 +435,6 @@ impl<'a> TryFrom<Cow<'a, [u8]>> 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<Boolean<'_>> for bool {
-    #[inline]
     fn from(b: Boolean) -> Self {
         match b {
             Boolean::True(_) => true,
@@ -454,7 +453,6 @@ impl From<Boolean<'_>> 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<Boolean<'_>> for Vec<u8> {
-    #[inline]
     fn from(b: Boolean) -> Self {
         b.into()
     }
 }
 
 impl From<&Boolean<'_>> for Vec<u8> {
-    #[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<Self, Self::Error> {
         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<Vec<u8>> for TrueVariant<'_> {
-    type Error = BooleanError;
+    type Error = value::parse::Error;
 
     fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
         if value.eq_ignore_ascii_case(b"yes")
@@ -536,7 +532,7 @@ impl TryFrom<Vec<u8>> 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<BString>) -> 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<Self, Self::Error> {
-        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<Vec<u8>> for Integer {
-    type Error = IntegerError;
+    type Error = value::parse::Error;
 
-    #[inline]
     fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
         Self::try_from(value.as_ref())
     }
 }
 
 impl TryFrom<Cow<'_, [u8]>> for Integer {
-    type Error = IntegerError;
+    type Error = value::parse::Error;
 
-    #[inline]
     fn try_from(c: Cow<'_, [u8]>) -> Result<Self, Self::Error> {
         match c {
             Cow::Borrowed(c) => Self::try_from(c),
@@ -710,14 +690,12 @@ impl TryFrom<Cow<'_, [u8]>> for Integer {
 }
 
 impl From<Integer> for Vec<u8> {
-    #[inline]
     fn from(i: Integer) -> Self {
         i.into()
     }
 }
 
 impl From<&Integer> for Vec<u8> {
-    #[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<Self, Self::Err> {
         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, Self::Error> {
-        Self::from_str(std::str::from_utf8(s)?)
+        Self::from_str(std::str::from_utf8(s).map_err(|_| ())?)
     }
 }
 
 impl TryFrom<Vec<u8>> for IntegerSuffix {
-    type Error = IntegerError;
+    type Error = ();
 
-    #[inline]
     fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
         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<u8> {
         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<BString>) -> 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<Self, Self::Error> {
-        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<Vec<u8>> for Color {
-    type Error = ColorError;
+    type Error = value::parse::Error;
 
-    #[inline]
     fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
         Self::try_from(value.as_ref())
     }
 }
 
 impl TryFrom<Cow<'_, [u8]>> for Color {
-    type Error = ColorError;
+    type Error = value::parse::Error;
 
-    #[inline]
     fn try_from(c: Cow<'_, [u8]>) -> Result<Self, Self::Error> {
         match c {
             Cow::Borrowed(c) => Self::try_from(c),
@@ -948,14 +905,12 @@ impl TryFrom<Cow<'_, [u8]>> for Color {
 }
 
 impl From<Color> for Vec<u8> {
-    #[inline]
     fn from(c: Color) -> Self {
         c.into()
     }
 }
 
 impl From<&Color> for Vec<u8> {
-    #[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<Self, Self::Err> {
         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, Self::Error> {
-        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<Self, Self::Err> {
         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, Self::Error> {
-        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 904df0f6296..4fe3ed6a85c 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 c5b4076a481..82ca1597afb 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::<Boolean>("core", None, "bool-explicit")?,
         Boolean::False(Cow::Borrowed("false"))
     );
+    assert!(!file.boolean("core", None, "bool-explicit").expect("exists")?);
 
     assert_eq!(
         file.value::<Boolean>("core", None, "bool-implicit")?,
         Boolean::True(TrueVariant::Implicit)
     );
+    assert_eq!(
+        file.try_value::<Boolean>("core", None, "bool-implicit")
+            .expect("exists")?,
+        Boolean::True(TrueVariant::Implicit)
+    );
+
+    assert!(file.boolean("core", None, "bool-implicit").expect("present")?);
+    assert_eq!(file.try_value::<String>("doesnt", None, "exist"), None);
 
     assert_eq!(
         file.value::<Integer>("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::<git_config::values::Path>("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::<Boolean>("core", None, "bool-implicit").is_ok());
     assert_eq!(
-        file.value::<Boolean>("core", None, "bool-implicit"),
-        file.value::<Boolean>("CORE", None, "bool-implicit")
+        file.value::<Boolean>("core", None, "bool-implicit").unwrap(),
+        file.value::<Boolean>("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::<Boolean>("core", None, "a")?.len(), 2);
     assert_eq!(
-        file.value::<Boolean>("core", None, "a"),
-        file.value::<Boolean>("core", None, "A")
+        file.value::<Boolean>("core", None, "a").unwrap(),
+        file.value::<Boolean>("core", None, "A").unwrap()
     );
 
     Ok(())
diff --git a/git-features/Cargo.toml b/git-features/Cargo.toml
index 518d914d803..0402a2eba41 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 83e9a9116bf..f8af9589ddd 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 819cc891807..00000000000
--- 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<Cow<'a, Path>>) -> Result<Cow<'a, [u8]>, 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, Path>>) -> 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<Cow<'a, [u8]>>) -> Result<Cow<'a, Path>, 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<Cow<'a, bstr::BStr>>) -> Result<Cow<'a, Path>, 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<Vec<u8>>) -> Result<PathBuf, Utf8Error> {
-    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<Vec<u8>>) -> 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<Cow<'a, [u8]>>, 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]>>) -> 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]>>) -> 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]>>) -> 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 76d3f13f665..00000000000
--- 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 847600a71a2..cfafeac7c3a 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> {
-    Pattern::from_bytes(text)
+///
+/// Note that
+pub fn parse(text: impl AsRef<[u8]>) -> Option<Pattern> {
+    Pattern::from_bytes(text.as_ref())
 }
diff --git a/git-glob/src/parse.rs b/git-glob/src/parse.rs
index d39140438c7..3693f88efcb 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<usize>)
     }
     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<usize>)
         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 14fb4f6658f..78c771e91c9 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<Self> {
@@ -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<usize>,
-        is_dir: bool,
+        is_dir: Option<bool>,
         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<usize>,
-        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 b919a42dd69..5c62e7b8533 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 e5325749636..5787ff64ce1 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 <<EOF >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 <<EOF >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 c7452ed5f32..3a90f1d5101 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 80384880303..8b137891791 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<Self::Item> {
-        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<usize> {
-    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 b95f854e1fc..2d77d4f732c 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 00000000000..4dddf804ecf
--- /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<Self::Item> {
+        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<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<usize> {
+    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 00000000000..1a66b98fa8d
--- /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 9d6a034c5d9..5b0962f7533 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<usize> {
     path.rfind('/').map(|pos| pos + 1)
diff --git a/git-index/src/access.rs b/git-index/src/access.rs
index 92ae7309b20..c72fd75ad38 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<T> + 'a,
+    ) -> impl Iterator<Item = (&'a BStr, T)> + '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<Item = (&'state mut Entry, &'backing BStr)> {
+        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 36262430eb8..82a62296818 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<usize>,
     /// 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 f165127fe3f..6ca28e484fe 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 8151618edc6..364b6d0d5a6 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<u8>;
+
 /// 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<Entry>,
     /// A memory area keeping all index paths, in full length, independently of the index version.
-    path_backing: Vec<u8>,
+    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 0ea6f4ce5dd..5590570f296 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 a244076fdcb..5ca2f4353d3 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<Vec<PathBuf>, 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 bbd4f144ef9..5eaf30b4497 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 8e266dc1a28..80982bfecff 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 00000000000..8f962e3406b
--- /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)
+
+ - <csr-id-54801592488416ef2bb0f34c5061b62189c35c5e/> various name changes for more convenient API
+
+### Commit Statistics
+
+<csr-read-only-do-not-edit/>
+
+ - 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
+
+<csr-read-only-do-not-edit/>
+
+<details><summary>view details</summary>
+
+ * **[#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))
+</details>
+
+## 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 00000000000..2c9331f508d
--- /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 <sebastian.thiel@icloud.com>"]
+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 00000000000..5c2853aae45
--- /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<Cow<'a, Path>>) -> Result<Cow<'a, BStr>, 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, Path>>) -> 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<Cow<'a, BStr>>) -> Result<Cow<'a, Path>, 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, BStr>>) -> 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<BString>) -> Result<PathBuf, Utf8Error> {
+    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<BString>) -> 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<Cow<'a, BStr>>, 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>>) -> 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, BStr>>) -> 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>>) -> 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>>) -> 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>>) -> 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 00000000000..e7f301a66a7
--- /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 00000000000..4c41e40fb28
--- /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<Self, Self::Error> {
+        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<Self> {
+        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<Item = &BStr> {
+        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 00000000000..b1ad512bc62
--- /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 5b73b955a3c..44e1462b291 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 7d89e9ef925..d07a441bba8 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 9ad9ddb86a1..f273a149756 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<Self, Self::Error> {
-        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("<unknown encoding>".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 d2bb21705bd..47c628ed248 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<Path>) -> 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 deccd32eb6d..78e7281f5e9 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 1545e7bc374..48c1240b44c 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 ec3bf22a7c2..591e26a4a03 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, Path>>) -> 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 c3865265e90..ca3f7f323ba 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 19de3b86538..3c262e7e943 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<usize>,
-    /// 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<std::path::PathBuf>,
+    /// 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<Self, Error> {
+        pub fn new(
+            git_dir: &std::path::Path,
+            xdg_config_home_env: EnvVarResourcePermission,
+            home_env: EnvVarResourcePermission,
+            git_install_dir: Option<&std::path::Path>,
+        ) -> Result<Self, Error> {
             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::<Integer>("core", None, "repositoryFormatVersion")
                 .map_or(0, |v| v.value);
-            let object_hash = if repo_format_version == 1 {
-                if let Ok(format) = config.value::<Cow<'_, [u8]>>("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::<values::String<'_>>("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<Option<PathBuf>, git_sec::permission::Error<PathBuf, git_sec::Permission>> {
+            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<bool, Error> {
         let (section, key) = key.split_once('.').expect("valid section.key format");
         config
-            .value::<Boolean<'_>>(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 07da578a390..caba6dec2ac 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<std::path::PathBuf>) -> Result<crate::Repositor
     ThreadSafeRepository::open(directory).map(Into::into)
 }
 
+///
+pub mod permission {
+    use git_sec::permission::Resource;
+    use git_sec::Access;
+
+    /// A permission to control access to the resource pointed to an environment variable.
+    pub type EnvVarResourcePermission = Access<Resource, git_sec::Permission>;
+}
+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 0312fe30814..69f4276c6f8 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<Self, Error> {
-        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 1ebd0184ff8..087c7273c98 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<Option<(crate::Path, git_sec::Trust)>, 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<Option<git_sec::Trust>, 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 e6570f4d6d3..c39f6ef4112 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<Path>) -> 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 bc0ab7ce30a..3668fa3dd07 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<PathBuf>, 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::path::PathBuf> {
+    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 00000000000..f674492c036
--- /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<std::path::PathBuf>,
+        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 f3998dbbf25..4eab54d8f37 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::path::PathBuf> {
-        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<std::io::Result<std::path::PathBuf>> {
+        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 633036b3605..4947ec0ab8a 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<Resource, git_sec::ReadWrite>,
-    }
-
-    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<std::path::PathBuf>,
-            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 00000000000..5f0713abbec
--- /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<Resource, git_sec::ReadWrite>,
+    /// 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 63c53fafa5f..b6624b533b9 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 00000000000..5571aafa5c0
--- /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<Worktree<'repo>> {
+        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 ae35f24daf0..6ffbc39bd24 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<u8>,
-    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<u8>,
-    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<u8>,
-    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<u8>,
-    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 00000000000..421cea89e21
--- /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<PathBuf, git_sec::Permission>),
+    }
+}
+
+/// 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<git_attributes::MatchGroup<git_attributes::Ignore>>,
+    ) -> Result<git_worktree::fs::Cache<'a>, 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::<git_attributes::Ignore>::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<git_index::File, crate::worktree::open_index::Error> {
+        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 69735a7d222..a03fdf3ad27 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 7a40bcb7b82..0a5b1ad1f6a 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 47d074bfd3f..bea8a7d8e6e 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<P> Access<Config, P> {
+    impl<P: Debug + Display + Clone> Access<Config, P> {
         /// 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<P> Access<Resource, P> {
+    impl<P: Debug + Display + Clone> Access<Resource, P> {
         /// 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<R: Debug, P: Debug + Display> {
+        /// 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<R: Debug>(&self, resource: R) -> Result<Option<R>, permission::Error<R, Self>> {
+        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<T: permission::Tag, P> {
+#[derive(Debug, Clone)]
+pub struct Access<T: permission::Tag, P: Debug + Display + Clone> {
     /// The access permission itself.
     permission: P,
     _data: PhantomData<T>,
 }
 
-impl<T: permission::Tag, P> Deref for Access<T, P> {
+impl<T: permission::Tag, P: Debug + Display + Clone> Display for Access<T, P> {
+    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+        Display::fmt(&self.permission, f)
+    }
+}
+
+impl<T: permission::Tag, P: Debug + Display + Clone> Deref for Access<T, P> {
     type Target = P;
 
     fn deref(&self) -> &Self::Target {
diff --git a/git-url/Cargo.toml b/git-url/Cargo.toml
index c19bf176212..49b1caec3f2 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 3546c52a27a..59cab95b36d 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 cf6b63dc526..2c4f2af0a59 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 7a304675437..00000000000
--- 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<PathBuf>,
-    },
-    /// 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<PathBuf>,
-        /// An additional per-user attributes file, similar to `$GIT_DIR/info/attributes`
-        attributes_file: Option<PathBuf>,
-    },
-}
-
-impl Mode {
-    /// Configure a mode to be suitable for checking out files.
-    pub fn checkout(unlink_on_collision: bool, attributes_file: Option<PathBuf>) -> 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<PathBuf>, attributes_file: Option<PathBuf>) -> 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<PathBuf>, 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<Path>,
-        mode: git_index::entry::Mode,
-    ) -> std::io::Result<Platform<'_>> {
-        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<std::path::Components<'_>>,
-    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 00000000000..bd098ad4d48
--- /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<bool>,
+}
+
+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<PathBuf>,
+        state: State,
+        case: git_glob::pattern::Case,
+        buf: Vec<u8>,
+        attribute_files_in_index: Vec<PathOidMapping<'paths>>,
+    ) -> 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<Find, E>(
+        &mut self,
+        relative: impl AsRef<Path>,
+        is_dir: Option<bool>,
+        find: Find,
+    ) -> std::io::Result<Platform<'_, 'paths>>
+    where
+        Find: for<'a> FnMut(&oid, &'a mut Vec<u8>) -> Result<git_object::BlobRef<'a>, 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<bool>,
+        find: Find,
+    ) -> std::io::Result<Platform<'_, 'paths>>
+    where
+        Find: for<'a> FnMut(&oid, &'a mut Vec<u8>) -> Result<git_object::BlobRef<'a>, 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 00000000000..4ee5cfa13b5
--- /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<git_attributes::Match<'_, ()>> {
+        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<u8>,
+    pub is_dir: bool,
+    pub attribute_files_in_index: &'a Vec<PathOidMapping<'paths>>,
+    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<u8>) -> Result<git_object::BlobRef<'b>, 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 00000000000..273c3dacb3a
--- /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<git_attributes::Attributes>;
+type IgnoreMatchGroup = git_attributes::MatchGroup<git_attributes::Ignore>;
+
+/// 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<Option<(usize, usize, usize)>>,
+    ///  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<bool>,
+        case: Case,
+    ) -> Option<git_attributes::Match<'_, ()>> {
+        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<bool>,
+        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<Find, E>(
+        &mut self,
+        root: &Path,
+        dir: &Path,
+        buf: &mut Vec<u8>,
+        attribute_files_in_index: &[PathOidMapping<'_>],
+        mut find: Find,
+    ) -> std::io::Result<()>
+    where
+        Find: for<'b> FnMut(&oid, &'b mut Vec<u8>) -> Result<git_object::BlobRef<'b>, 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<AttributeMatchGroup> 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<PathOidMapping<'paths>> {
+        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 b95afcc6d7f..4092f56f71b 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<u8>,
+    /// 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<PathOidMapping<'paths>>,
 }
 
+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 793e2fa86b2..6ccf84769a2 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<Path>,
-        mut push_comp: impl FnMut(&mut std::iter::Peekable<std::path::Components<'_>>, &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 3fa8e0ed1c5..d30291e6dc5 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<PathBuf>,
+    /// A group of attribute patterns that are applied globally, i.e. aren't rooted within the repository itself.
+    pub attribute_globals: git_attributes::MatchGroup<Attributes>,
 }
 
 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 e959369ab14..c5ba0093929 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<u8>,
 }
 
@@ -17,7 +17,7 @@ pub struct Context<'a, Find> {
 pub fn checkout<Find, E>(
     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<u8>) -> Result<git_object::BlobRef<'a>, 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 872e1792cf1..6851517fe5b 100644
--- a/git-worktree/src/index/mod.rs
+++ b/git-worktree/src/index/mod.rs
@@ -21,22 +21,36 @@ pub fn checkout<Find, E>(
     should_interrupt: &AtomicBool,
     options: checkout::Options,
 ) -> Result<checkout::Outcome, checkout::Error<E>>
+where
+    Find: for<'a> FnMut(&oid, &'a mut Vec<u8>) -> Result<git_object::BlobRef<'a>, 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<Find, E>(
+    index: &mut git_index::State,
+    paths: &git_index::PathStorage,
+    dir: impl Into<std::path::PathBuf>,
+    find: Find,
+    files: &mut impl Progress,
+    bytes: &mut impl Progress,
+    should_interrupt: &AtomicBool,
+    options: checkout::Options,
+) -> Result<checkout::Outcome, checkout::Error<E>>
 where
     Find: for<'a> FnMut(&oid, &'a mut Vec<u8>) -> Result<git_object::BlobRef<'a>, 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<u8>,
         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<Item = (&'entry mut git_index::Entry, &'entry BStr)>,
         files: &mut impl Progress,
         bytes: &mut impl Progress,
-        ctx: &mut Context<'_, Find>,
+        ctx: &mut Context<'_, '_, Find>,
     ) -> Result<Outcome<'entry>, checkout::Error<E>>
     where
-        Find: for<'a> FnMut(&oid, &'a mut Vec<u8>) -> Result<git_object::BlobRef<'a>, E>,
+        Find: for<'a> FnMut(&oid, &'a mut Vec<u8>) -> Result<git_object::BlobRef<'a>, 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<usize, checkout::Error<E>>
     where
-        Find: for<'a> FnMut(&oid, &'a mut Vec<u8>) -> Result<git_object::BlobRef<'a>, E>,
+        Find: for<'a> FnMut(&oid, &'a mut Vec<u8>) -> Result<git_object::BlobRef<'a>, 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 00000000000..2eb265bd062
--- /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 00000000000..e176e6c8140
--- /dev/null
+++ b/git-worktree/tests/fixtures/make_ignore_and_attributes_setup.sh
@@ -0,0 +1,112 @@
+#!/bin/bash
+set -eu -o pipefail
+
+cat <<EOF >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 <<EOF >.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 <<EOF >.gitignore
+# a sample .gitignore
+top-level-local-file-anywhere
+EOF
+
+  mkdir dir-with-ignore
+  cat <<EOF >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 <<EOF >"$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 <<EOF >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 5a53f9c26f6..cdae7eb15e4 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 17bbcf4474d..bb92087715f 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<u8>,
+    ) -> std::io::Result<git_object::BlobRef<'buf>> {
+        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<Self::Item> {
+            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 b300f17c2b7..65064b1a507 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 00000000000..2be7f9b519d
--- /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<PathBuf>,
+    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 17f312212a4..8f63f94246f 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 80cfb96ff9c..88cf827d1bc 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 1e449121361..ec7c3c8aad2 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<PathBuf>,
+    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 00000000000..a3b5e4c2bcd
--- /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<OsString>,
+        pub show_ignore_patterns: bool,
+    }
+}
+
+pub fn query(
+    repo: git::Repository,
+    pathspecs: impl Iterator<Item = git::path::Spec>,
+    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::<git::attrs::Ignore>::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 b513de5d23f..c00540842e3 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<Entry<'a>> 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 92cd2151284..a4acbb0b058 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 21b329b4284..835874432df 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 83dfcc81af0..57618d73455 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 42052e8e82c..77677ff5197 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<u8> = 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 f8b391a56b4..c30b69fced1 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<dyn Iterator<Item = git::path::Spec>>
+                                } 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<std::io::BufReader<std::io::Stdin>> {
+    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 80d3a9647ba..0807c9f0ed2 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<OsString>,
+                /// 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<git::path::Spec>,
+            },
+        }
     }
 
     pub mod mailmap {
diff --git a/src/porcelain/main.rs b/src/porcelain/main.rs
index 98b668620c8..af1c7d8b9a5 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 9d708c39665..d9b5ae05af9 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<T>(
+        name: &str,
+        verbose: bool,
+        progress: bool,
+        #[cfg_attr(not(feature = "prodash-render-tui"), allow(unused_variables))] progress_keep_open: bool,
+        range: impl Into<Option<ProgressRange>>,
+        run: impl FnOnce(
+            progress::DoOrDiscard<prodash::tree::Item>,
+            &mut dyn std::io::Write,
+            &mut dyn std::io::Write,
+        ) -> Result<T>,
+    ) -> Result<T> {
+        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::<u8>::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<T: Send + 'static>(
         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<Option<ProgressRange>>,
+        range: impl Into<Option<ProgressRange>>,
         run: impl FnOnce(
                 progress::DoOrDiscard<prodash::tree::Item>,
                 &mut dyn std::io::Write,
                 &mut dyn std::io::Write,
             ) -> Result<T>
             + Send
-            + std::panic::UnwindSafe
             + 'static,
     ) -> Result<T> {
         crate::shared::init_env_logger();
diff --git a/tests/tools/src/lib.rs b/tests/tools/src/lib.rs
index 97e35da8f73..04dc7928193 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<T> = std::result::Result<T, Box<dyn std::error::Error>>;
+pub type Result<T = ()> = std::result::Result<T, Box<dyn std::error::Error>>;
 
 static SCRIPT_IDENTITY: Lazy<Mutex<BTreeMap<PathBuf, u32>>> = Lazy::new(|| Mutex::new(BTreeMap::new()));