From 9193b05b2528f62d829447ccc50314bd4cffc415 Mon Sep 17 00:00:00 2001
From: Sebastian Thiel <sebastian.thiel@icloud.com>
Date: Thu, 9 Jan 2025 18:47:24 +0100
Subject: [PATCH 1/5] thanks clippy

---
 Cargo.toml                                       | 1 +
 gitoxide-core/src/repository/attributes/query.rs | 2 +-
 gitoxide-core/src/repository/clean.rs            | 4 ++--
 gitoxide-core/src/repository/config.rs           | 9 +++++----
 gitoxide-core/src/repository/exclude.rs          | 2 +-
 gitoxide-core/src/repository/fetch.rs            | 2 +-
 gitoxide-core/src/repository/index/entries.rs    | 2 +-
 gitoxide-core/src/repository/remote.rs           | 2 +-
 gitoxide-core/src/repository/revision/list.rs    | 2 +-
 gix-dir/src/walk/classify.rs                     | 2 +-
 gix-filter/examples/arrow.rs                     | 2 +-
 gix-filter/src/driver/process/mod.rs             | 2 +-
 gix-pack/src/multi_index/chunk.rs                | 4 ++--
 gix-packetline-blocking/src/line/async_io.rs     | 8 ++++----
 gix-packetline/src/line/async_io.rs              | 8 ++++----
 gix-ref/src/store/file/transaction/commit.rs     | 2 +-
 gix-submodule/src/lib.rs                         | 4 +---
 gix-validate/src/path.rs                         | 2 +-
 gix/src/config/tree/sections/mod.rs              | 1 +
 gix/src/remote/url/scheme_permission.rs          | 2 +-
 tests/tools/src/lib.rs                           | 3 +--
 21 files changed, 33 insertions(+), 33 deletions(-)

diff --git a/Cargo.toml b/Cargo.toml
index b1b353eb1fb..cb92685da1d 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -394,3 +394,4 @@ stable_sort_primitive = "allow"  # x1
 no_effect_underscore_binding = "allow"  # x1
 empty_docs = "allow"
 too_long_first_doc_paragraph = "allow"
+large_stack_arrays = "allow"
diff --git a/gitoxide-core/src/repository/attributes/query.rs b/gitoxide-core/src/repository/attributes/query.rs
index 9081eb9ed78..72fac97ac2c 100644
--- a/gitoxide-core/src/repository/attributes/query.rs
+++ b/gitoxide-core/src/repository/attributes/query.rs
@@ -89,7 +89,7 @@ pub(crate) mod function {
                         let entry = cache.at_entry(
                             path,
                             Some(is_dir_to_mode(
-                                workdir.map_or(false, |wd| wd.join(gix::path::from_bstr(path)).is_dir())
+                                workdir.is_some_and(|wd| wd.join(gix::path::from_bstr(path)).is_dir())
                                     || pattern.signature.contains(gix::pathspec::MagicSignature::MUST_BE_DIR),
                             )),
                         )?;
diff --git a/gitoxide-core/src/repository/clean.rs b/gitoxide-core/src/repository/clean.rs
index 8ccc8a90dfb..6d858182e72 100644
--- a/gitoxide-core/src/repository/clean.rs
+++ b/gitoxide-core/src/repository/clean.rs
@@ -128,10 +128,10 @@ pub(crate) mod function {
             let pathspec_includes_entry = match pathspec.as_mut() {
                 None => entry
                     .pathspec_match
-                    .map_or(false, |m| m != gix::dir::entry::PathspecMatch::Excluded),
+                    .is_some_and(|m| m != gix::dir::entry::PathspecMatch::Excluded),
                 Some(pathspec) => pathspec
                     .pattern_matching_relative_path(entry.rela_path.as_bstr(), entry.disk_kind.map(|k| k.is_dir()))
-                    .map_or(false, |m| !m.is_excluded()),
+                    .is_some_and(|m| !m.is_excluded()),
             };
             pruned_entries += usize::from(!pathspec_includes_entry);
             if !pathspec_includes_entry && debug {
diff --git a/gitoxide-core/src/repository/config.rs b/gitoxide-core/src/repository/config.rs
index 48640688c05..c39272a2ebe 100644
--- a/gitoxide-core/src/repository/config.rs
+++ b/gitoxide-core/src/repository/config.rs
@@ -32,7 +32,7 @@ pub fn list(
         }
 
         let meta = section.meta();
-        if last_meta.map_or(true, |last| last != meta) {
+        if last_meta != Some(meta) {
             write_meta(meta, &mut out)?;
         }
         last_meta = Some(meta);
@@ -41,9 +41,10 @@ pub fn list(
         for event in matter {
             event.write_to(&mut out)?;
         }
-        if it.peek().map_or(false, |(next_section, _)| {
-            next_section.header().name() != section.header().name()
-        }) {
+        if it
+            .peek()
+            .is_some_and(|(next_section, _)| next_section.header().name() != section.header().name())
+        {
             writeln!(&mut out)?;
         }
     }
diff --git a/gitoxide-core/src/repository/exclude.rs b/gitoxide-core/src/repository/exclude.rs
index a0cd212d08e..197e6d5d3b7 100644
--- a/gitoxide-core/src/repository/exclude.rs
+++ b/gitoxide-core/src/repository/exclude.rs
@@ -93,7 +93,7 @@ pub fn query(
                     let entry = cache.at_entry(
                         path,
                         Some(is_dir_to_mode(
-                            workdir.map_or(false, |wd| wd.join(gix::path::from_bstr(path)).is_dir())
+                            workdir.is_some_and(|wd| wd.join(gix::path::from_bstr(path)).is_dir())
                                 || pattern.signature.contains(gix::pathspec::MagicSignature::MUST_BE_DIR),
                         )),
                     )?;
diff --git a/gitoxide-core/src/repository/fetch.rs b/gitoxide-core/src/repository/fetch.rs
index bf1cbcce21d..e8d229765bc 100644
--- a/gitoxide-core/src/repository/fetch.rs
+++ b/gitoxide-core/src/repository/fetch.rs
@@ -305,7 +305,7 @@ pub(crate) mod function {
             for fix in &map.fixes {
                 match fix {
                     Fix::MappingWithPartialDestinationRemoved { name, spec } => {
-                        if prev_spec.map_or(true, |prev_spec| prev_spec != spec) {
+                        if prev_spec.is_some_and(|prev_spec| prev_spec != spec) {
                             prev_spec = spec.into();
                             spec.to_ref().write_to(&mut err)?;
                             writeln!(err)?;
diff --git a/gitoxide-core/src/repository/index/entries.rs b/gitoxide-core/src/repository/index/entries.rs
index 5eaad0a1e56..74057464cf6 100644
--- a/gitoxide-core/src/repository/index/entries.rs
+++ b/gitoxide-core/src/repository/index/entries.rs
@@ -198,7 +198,7 @@ pub(crate) mod function {
                         sms_by_path
                             .iter()
                             .find_map(|(path, sm)| (path == entry_path).then_some(sm))
-                            .filter(|sm| sm.git_dir_try_old_form().map_or(false, |dot_git| dot_git.exists()))
+                            .filter(|sm| sm.git_dir_try_old_form().is_ok_and(|dot_git| dot_git.exists()))
                     })
                 {
                     let sm_path = gix::path::to_unix_separators_on_windows(sm.path()?);
diff --git a/gitoxide-core/src/repository/remote.rs b/gitoxide-core/src/repository/remote.rs
index 5a1cb061a34..eeb634566c2 100644
--- a/gitoxide-core/src/repository/remote.rs
+++ b/gitoxide-core/src/repository/remote.rs
@@ -190,7 +190,7 @@ mod refs_impl {
             for fix in &map.fixes {
                 match fix {
                     Fix::MappingWithPartialDestinationRemoved { name, spec } => {
-                        if prev_spec.map_or(true, |prev_spec| prev_spec != spec) {
+                        if prev_spec.is_some_and(|prev_spec| prev_spec != spec) {
                             prev_spec = spec.into();
                             spec.to_ref().write_to(&mut err)?;
                             writeln!(err)?;
diff --git a/gitoxide-core/src/repository/revision/list.rs b/gitoxide-core/src/repository/revision/list.rs
index 3ad5e7a9574..1698a29d494 100644
--- a/gitoxide-core/src/repository/revision/list.rs
+++ b/gitoxide-core/src/repository/revision/list.rs
@@ -108,7 +108,7 @@ pub(crate) mod function {
                 }
             }
             progress.inc();
-            if limit.map_or(false, |limit| limit == progress.step()) {
+            if limit.is_some_and(|limit| limit == progress.step()) {
                 break;
             }
         }
diff --git a/gix-dir/src/walk/classify.rs b/gix-dir/src/walk/classify.rs
index 2fbae8753aa..d5fcbcafcd4 100644
--- a/gix-dir/src/walk/classify.rs
+++ b/gix-dir/src/walk/classify.rs
@@ -215,7 +215,7 @@ pub fn path(
 
     let is_dir = if symlinks_to_directories_are_ignored_like_directories
         && ctx.excludes.is_some()
-        && kind.map_or(false, |ft| ft == entry::Kind::Symlink)
+        && kind == Some(entry::Kind::Symlink)
     {
         path.metadata().ok().map(|md| is_dir_to_mode(md.is_dir()))
     } else {
diff --git a/gix-filter/examples/arrow.rs b/gix-filter/examples/arrow.rs
index 6f6d5a1c074..616084deb09 100644
--- a/gix-filter/examples/arrow.rs
+++ b/gix-filter/examples/arrow.rs
@@ -19,7 +19,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
 
     match sub_command.as_str() {
         "process" => {
-            let disallow_delay = next_arg.as_deref().map_or(false, |arg| arg == "disallow-delay");
+            let disallow_delay = next_arg.as_deref() == Some("disallow-delay");
             let mut srv = gix_filter::driver::process::Server::handshake(
                 stdin(),
                 stdout(),
diff --git a/gix-filter/src/driver/process/mod.rs b/gix-filter/src/driver/process/mod.rs
index c2f62ddd260..b4e19c47a83 100644
--- a/gix-filter/src/driver/process/mod.rs
+++ b/gix-filter/src/driver/process/mod.rs
@@ -83,7 +83,7 @@ impl Status {
 
     /// Returns true if this is an `abort` status.
     pub fn is_abort(&self) -> bool {
-        self.message().map_or(false, |m| m == "abort")
+        self.message() == Some("abort")
     }
 
     /// Return true if the status is explicitly set to indicated delayed output processing
diff --git a/gix-pack/src/multi_index/chunk.rs b/gix-pack/src/multi_index/chunk.rs
index 95d3f21f97e..b0a1bddba78 100644
--- a/gix-pack/src/multi_index/chunk.rs
+++ b/gix-pack/src/multi_index/chunk.rs
@@ -69,7 +69,7 @@ pub mod index_names {
                 ascii_path.is_ascii(),
                 "must use ascii bytes for correct size computation"
             );
-            count += (ascii_path.as_bytes().len() + 1/* null byte */) as u64;
+            count += (ascii_path.len() + 1/* null byte */) as u64;
         }
 
         let needed_alignment = CHUNK_ALIGNMENT - (count % CHUNK_ALIGNMENT);
@@ -89,7 +89,7 @@ pub mod index_names {
             let path = path.as_ref().to_str().expect("UTF-8 path");
             out.write_all(path.as_bytes())?;
             out.write_all(&[0])?;
-            written_bytes += path.as_bytes().len() as u64 + 1;
+            written_bytes += path.len() as u64 + 1;
         }
 
         let needed_alignment = CHUNK_ALIGNMENT - (written_bytes % CHUNK_ALIGNMENT);
diff --git a/gix-packetline-blocking/src/line/async_io.rs b/gix-packetline-blocking/src/line/async_io.rs
index 3a631c44c9a..83cf072023f 100644
--- a/gix-packetline-blocking/src/line/async_io.rs
+++ b/gix-packetline-blocking/src/line/async_io.rs
@@ -6,7 +6,7 @@ use futures_io::AsyncWrite;
 
 use crate::{encode, BandRef, Channel, ErrorRef, PacketLineRef, TextRef};
 
-impl<'a> BandRef<'a> {
+impl BandRef<'_> {
     /// Serialize this instance to `out`, returning the amount of bytes written.
     ///
     /// The data written to `out` can be decoded with [`Borrowed::decode_band()]`.
@@ -20,14 +20,14 @@ impl<'a> BandRef<'a> {
     }
 }
 
-impl<'a> TextRef<'a> {
+impl TextRef<'_> {
     /// Serialize this instance to `out`, appending a newline if there is none, returning the amount of bytes written.
     pub async fn write_to(&self, out: impl AsyncWrite + Unpin) -> io::Result<usize> {
         encode::text_to_write(self.0, out).await
     }
 }
 
-impl<'a> ErrorRef<'a> {
+impl ErrorRef<'_> {
     /// Serialize this line as error to `out`.
     ///
     /// This includes a marker to allow decoding it outside of a side-band channel, returning the amount of bytes written.
@@ -36,7 +36,7 @@ impl<'a> ErrorRef<'a> {
     }
 }
 
-impl<'a> PacketLineRef<'a> {
+impl PacketLineRef<'_> {
     /// Serialize this instance to `out` in git `packetline` format, returning the amount of bytes written to `out`.
     pub async fn write_to(&self, out: impl AsyncWrite + Unpin) -> io::Result<usize> {
         match self {
diff --git a/gix-packetline/src/line/async_io.rs b/gix-packetline/src/line/async_io.rs
index 7f20aa030b7..c2526326bd5 100644
--- a/gix-packetline/src/line/async_io.rs
+++ b/gix-packetline/src/line/async_io.rs
@@ -4,7 +4,7 @@ use futures_io::AsyncWrite;
 
 use crate::{encode, BandRef, Channel, ErrorRef, PacketLineRef, TextRef};
 
-impl<'a> BandRef<'a> {
+impl BandRef<'_> {
     /// Serialize this instance to `out`, returning the amount of bytes written.
     ///
     /// The data written to `out` can be decoded with [`Borrowed::decode_band()]`.
@@ -18,14 +18,14 @@ impl<'a> BandRef<'a> {
     }
 }
 
-impl<'a> TextRef<'a> {
+impl TextRef<'_> {
     /// Serialize this instance to `out`, appending a newline if there is none, returning the amount of bytes written.
     pub async fn write_to(&self, out: impl AsyncWrite + Unpin) -> io::Result<usize> {
         encode::text_to_write(self.0, out).await
     }
 }
 
-impl<'a> ErrorRef<'a> {
+impl ErrorRef<'_> {
     /// Serialize this line as error to `out`.
     ///
     /// This includes a marker to allow decoding it outside of a side-band channel, returning the amount of bytes written.
@@ -34,7 +34,7 @@ impl<'a> ErrorRef<'a> {
     }
 }
 
-impl<'a> PacketLineRef<'a> {
+impl PacketLineRef<'_> {
     /// Serialize this instance to `out` in git `packetline` format, returning the amount of bytes written to `out`.
     pub async fn write_to(&self, out: impl AsyncWrite + Unpin) -> io::Result<usize> {
         match self {
diff --git a/gix-ref/src/store/file/transaction/commit.rs b/gix-ref/src/store/file/transaction/commit.rs
index 89894f475bf..c9247dc4bcc 100644
--- a/gix-ref/src/store/file/transaction/commit.rs
+++ b/gix-ref/src/store/file/transaction/commit.rs
@@ -72,7 +72,7 @@ impl Transaction<'_, '_> {
                             }
                         };
                         if let Some((previous, new_oid)) = log_update {
-                            let do_update = previous.as_ref().map_or(true, |previous| previous != new_oid);
+                            let do_update = previous.as_ref() != Some(new_oid);
                             if do_update {
                                 self.store.reflog_create_or_append(
                                     change.update.name.as_ref(),
diff --git a/gix-submodule/src/lib.rs b/gix-submodule/src/lib.rs
index 639af30fae3..72d4482d419 100644
--- a/gix-submodule/src/lib.rs
+++ b/gix-submodule/src/lib.rs
@@ -4,8 +4,6 @@
 
 use std::{borrow::Cow, collections::BTreeMap};
 
-use bstr::BStr;
-
 /// All relevant information about a git module, typically from `.gitmodules` files.
 ///
 /// Note that overrides from other configuration might be relevant, which is why this type
@@ -64,7 +62,7 @@ impl File {
         let mut config_to_append = gix_config::File::new(config.meta_owned());
         let mut prev_name = None;
         for ((module_name, field), values) in values {
-            if prev_name.map_or(true, |pn: &BStr| pn != module_name) {
+            if prev_name != Some(module_name) {
                 config_to_append
                     .new_section("submodule", Some(Cow::Owned(module_name.to_owned())))
                     .expect("all names come from valid configuration, so remain valid");
diff --git a/gix-validate/src/path.rs b/gix-validate/src/path.rs
index 060f094d906..6cf4bb615c8 100644
--- a/gix-validate/src/path.rs
+++ b/gix-validate/src/path.rs
@@ -185,7 +185,7 @@ fn check_win_devices_and_illegal_characters(input: &BStr) -> Option<component::E
 }
 
 fn is_symlink(mode: Option<component::Mode>) -> bool {
-    mode.map_or(false, |m| m == component::Mode::Symlink)
+    mode == Some(component::Mode::Symlink)
 }
 
 fn is_dot_hfs(input: &BStr, search_case_insensitive: &str) -> bool {
diff --git a/gix/src/config/tree/sections/mod.rs b/gix/src/config/tree/sections/mod.rs
index e4e8db0773f..85958d431ad 100644
--- a/gix/src/config/tree/sections/mod.rs
+++ b/gix/src/config/tree/sections/mod.rs
@@ -1,3 +1,4 @@
+#![allow(clippy::unnecessary_literal_bound)]
 #![allow(missing_docs)]
 
 /// The `author` top-level section.
diff --git a/gix/src/remote/url/scheme_permission.rs b/gix/src/remote/url/scheme_permission.rs
index 47fbd351b8a..936c2ddd0a7 100644
--- a/gix/src/remote/url/scheme_permission.rs
+++ b/gix/src/remote/url/scheme_permission.rs
@@ -63,7 +63,7 @@ impl SchemePermission {
             .map(|value| Protocol::ALLOW.try_into_allow(value, None))
             .transpose()?;
 
-        let mut saw_user = allow.map_or(false, |allow| allow == Allow::User);
+        let mut saw_user = allow == Some(Allow::User);
         let allow_per_scheme = match config.sections_by_name_and_filter("protocol", &mut filter) {
             Some(it) => {
                 let mut map = BTreeMap::default();
diff --git a/tests/tools/src/lib.rs b/tests/tools/src/lib.rs
index a03ed9c5c91..15dbea49775 100644
--- a/tests/tools/src/lib.rs
+++ b/tests/tools/src/lib.rs
@@ -685,8 +685,7 @@ fn is_lfs_pointer_file(path: &Path) -> bool {
     std::fs::OpenOptions::new()
         .read(true)
         .open(path)
-        .and_then(|mut f| f.read_exact(&mut buf))
-        .map_or(false, |_| buf.starts_with(PREFIX))
+        .is_ok_and(|mut f| f.read_exact(&mut buf).is_ok_and(|_| buf.starts_with(PREFIX)))
 }
 
 /// The `script_identity` will be baked into the soon to be created `archive` as it identifies the script

From 7379aaf7db3f9e7e498e3fc9cd5c66d50dfdb98a Mon Sep 17 00:00:00 2001
From: Sebastian Thiel <sebastian.thiel@icloud.com>
Date: Thu, 9 Jan 2025 19:02:58 +0100
Subject: [PATCH 2/5] Fix CI after Rust update

---
 .github/workflows/ci.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index afc1cea684a..ae35ff33c71 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -318,7 +318,7 @@ jobs:
 
     strategy:
       matrix:
-        target: [ wasm32-unknown-unknown, wasm32-wasi ]
+        target: [ wasm32-unknown-unknown, wasm32-wasip1 ]
 
     env:
       TARGET: ${{ matrix.target }}

From 4ef3a8d34512b97039e8335a34f4e4256a4ff7d0 Mon Sep 17 00:00:00 2001
From: Sebastian Thiel <sebastian.thiel@icloud.com>
Date: Thu, 9 Jan 2025 17:28:52 +0100
Subject: [PATCH 3/5] refactor git-path tests

---
 gix-path/tests/path.rs                        | 80 -------------------
 gix-path/tests/{ => path}/convert/mod.rs      |  0
 .../tests/{ => path}/convert/normalize.rs     |  0
 gix-path/tests/path/env.rs                    | 61 ++++++++++++++
 gix-path/tests/path/main.rs                   | 18 +++++
 .../{realpath/mod.rs => path/realpath.rs}     |  0
 gix-path/tests/{util/mod.rs => path/util.rs}  |  0
 7 files changed, 79 insertions(+), 80 deletions(-)
 delete mode 100644 gix-path/tests/path.rs
 rename gix-path/tests/{ => path}/convert/mod.rs (100%)
 rename gix-path/tests/{ => path}/convert/normalize.rs (100%)
 create mode 100644 gix-path/tests/path/env.rs
 create mode 100644 gix-path/tests/path/main.rs
 rename gix-path/tests/{realpath/mod.rs => path/realpath.rs} (100%)
 rename gix-path/tests/{util/mod.rs => path/util.rs} (100%)

diff --git a/gix-path/tests/path.rs b/gix-path/tests/path.rs
deleted file mode 100644
index 72cf2547b89..00000000000
--- a/gix-path/tests/path.rs
+++ /dev/null
@@ -1,80 +0,0 @@
-pub type Result<T = ()> = std::result::Result<T, Box<dyn std::error::Error>>;
-
-mod convert;
-mod realpath;
-mod home_dir {
-    #[test]
-    fn returns_existing_directory() {
-        if let Some(home) = gix_path::env::home_dir() {
-            assert!(
-                home.is_dir(),
-                "the home directory would typically exist, even though on unix we don't test for that."
-            );
-        }
-    }
-}
-
-mod env {
-    #[test]
-    fn exe_invocation() {
-        let actual = gix_path::env::exe_invocation();
-        assert!(
-            !actual.as_os_str().is_empty(),
-            "it finds something as long as git is installed somewhere on the system (or a default location)"
-        );
-    }
-
-    #[test]
-    fn installation_config() {
-        assert_ne!(
-            gix_path::env::installation_config().map(|p| p.components().count()),
-            gix_path::env::installation_config_prefix().map(|p| p.components().count()),
-            "the prefix is a bit shorter than the installation config path itself"
-        );
-    }
-
-    #[test]
-    fn system_prefix() {
-        assert_ne!(
-            gix_path::env::system_prefix(),
-            None,
-            "git should be present when running tests"
-        );
-    }
-
-    #[test]
-    fn home_dir() {
-        assert_ne!(
-            gix_path::env::home_dir(),
-            None,
-            "we find a home on every system these tests execute"
-        );
-    }
-
-    mod xdg_config {
-        use std::ffi::OsStr;
-
-        #[test]
-        fn prefers_xdg_config_bases() {
-            let actual = gix_path::env::xdg_config("test", &mut |n| {
-                (n == OsStr::new("XDG_CONFIG_HOME")).then(|| "marker".into())
-            })
-            .expect("set");
-            #[cfg(unix)]
-            assert_eq!(actual.to_str(), Some("marker/git/test"));
-            #[cfg(windows)]
-            assert_eq!(actual.to_str(), Some("marker\\git\\test"));
-        }
-
-        #[test]
-        fn falls_back_to_home() {
-            let actual = gix_path::env::xdg_config("test", &mut |n| (n == OsStr::new("HOME")).then(|| "marker".into()))
-                .expect("set");
-            #[cfg(unix)]
-            assert_eq!(actual.to_str(), Some("marker/.config/git/test"));
-            #[cfg(windows)]
-            assert_eq!(actual.to_str(), Some("marker\\.config\\git\\test"));
-        }
-    }
-}
-mod util;
diff --git a/gix-path/tests/convert/mod.rs b/gix-path/tests/path/convert/mod.rs
similarity index 100%
rename from gix-path/tests/convert/mod.rs
rename to gix-path/tests/path/convert/mod.rs
diff --git a/gix-path/tests/convert/normalize.rs b/gix-path/tests/path/convert/normalize.rs
similarity index 100%
rename from gix-path/tests/convert/normalize.rs
rename to gix-path/tests/path/convert/normalize.rs
diff --git a/gix-path/tests/path/env.rs b/gix-path/tests/path/env.rs
new file mode 100644
index 00000000000..69beb85b6e7
--- /dev/null
+++ b/gix-path/tests/path/env.rs
@@ -0,0 +1,61 @@
+#[test]
+fn exe_invocation() {
+    let actual = gix_path::env::exe_invocation();
+    assert!(
+        !actual.as_os_str().is_empty(),
+        "it finds something as long as git is installed somewhere on the system (or a default location)"
+    );
+}
+
+#[test]
+fn installation_config() {
+    assert_ne!(
+        gix_path::env::installation_config().map(|p| p.components().count()),
+        gix_path::env::installation_config_prefix().map(|p| p.components().count()),
+        "the prefix is a bit shorter than the installation config path itself"
+    );
+}
+
+#[test]
+fn system_prefix() {
+    assert_ne!(
+        gix_path::env::system_prefix(),
+        None,
+        "git should be present when running tests"
+    );
+}
+
+#[test]
+fn home_dir() {
+    assert_ne!(
+        gix_path::env::home_dir(),
+        None,
+        "we find a home on every system these tests execute"
+    );
+}
+
+mod xdg_config {
+    use std::ffi::OsStr;
+
+    #[test]
+    fn prefers_xdg_config_bases() {
+        let actual = gix_path::env::xdg_config("test", &mut |n| {
+            (n == OsStr::new("XDG_CONFIG_HOME")).then(|| "marker".into())
+        })
+        .expect("set");
+        #[cfg(unix)]
+        assert_eq!(actual.to_str(), Some("marker/git/test"));
+        #[cfg(windows)]
+        assert_eq!(actual.to_str(), Some("marker\\git\\test"));
+    }
+
+    #[test]
+    fn falls_back_to_home() {
+        let actual = gix_path::env::xdg_config("test", &mut |n| (n == OsStr::new("HOME")).then(|| "marker".into()))
+            .expect("set");
+        #[cfg(unix)]
+        assert_eq!(actual.to_str(), Some("marker/.config/git/test"));
+        #[cfg(windows)]
+        assert_eq!(actual.to_str(), Some("marker\\.config\\git\\test"));
+    }
+}
diff --git a/gix-path/tests/path/main.rs b/gix-path/tests/path/main.rs
new file mode 100644
index 00000000000..dae33bb7746
--- /dev/null
+++ b/gix-path/tests/path/main.rs
@@ -0,0 +1,18 @@
+pub type Result<T = ()> = std::result::Result<T, Box<dyn std::error::Error>>;
+
+mod convert;
+mod realpath;
+mod home_dir {
+    #[test]
+    fn returns_existing_directory() {
+        if let Some(home) = gix_path::env::home_dir() {
+            assert!(
+                home.is_dir(),
+                "the home directory would typically exist, even though on unix we don't test for that."
+            );
+        }
+    }
+}
+
+mod env;
+mod util;
diff --git a/gix-path/tests/realpath/mod.rs b/gix-path/tests/path/realpath.rs
similarity index 100%
rename from gix-path/tests/realpath/mod.rs
rename to gix-path/tests/path/realpath.rs
diff --git a/gix-path/tests/util/mod.rs b/gix-path/tests/path/util.rs
similarity index 100%
rename from gix-path/tests/util/mod.rs
rename to gix-path/tests/path/util.rs

From 840c71dfb5cda32fcaa6f744e23751c53a8281b1 Mon Sep 17 00:00:00 2001
From: Sebastian Thiel <sebastian.thiel@icloud.com>
Date: Thu, 9 Jan 2025 17:14:50 +0100
Subject: [PATCH 4/5] feat: add `env::git_shell()` to obtain the shell Git
 would be using.

This is particularly useful to execute Git hooks.
---
 gix-path/src/env/mod.rs    | 17 +++++++++++++++++
 gix-path/tests/path/env.rs | 10 ++++++++++
 2 files changed, 27 insertions(+)

diff --git a/gix-path/src/env/mod.rs b/gix-path/src/env/mod.rs
index 154a0d1ddfa..cdc092dcb25 100644
--- a/gix-path/src/env/mod.rs
+++ b/gix-path/src/env/mod.rs
@@ -28,6 +28,23 @@ pub fn installation_config_prefix() -> Option<&'static Path> {
     installation_config().map(git::config_to_base_path)
 }
 
+/// Return the shell that Git would prefer as login shell, the shell to execute Git commands from.
+///
+/// On Windows, this is the `bash.exe` bundled with it, and on Unix it's the shell specified by `SHELL`,
+/// or `None` if it is truly unspecified.
+pub fn login_shell() -> Option<&'static Path> {
+    static PATH: Lazy<Option<PathBuf>> = Lazy::new(|| {
+        if cfg!(windows) {
+            installation_config_prefix()
+                .and_then(|p| p.parent())
+                .map(|p| p.join("usr").join("bin").join("bash.exe"))
+        } else {
+            std::env::var_os("SHELL").map(PathBuf::from)
+        }
+    });
+    PATH.as_deref()
+}
+
 /// Return the name of the Git executable to invoke it.
 /// If it's in the `PATH`, it will always be a short name.
 ///
diff --git a/gix-path/tests/path/env.rs b/gix-path/tests/path/env.rs
index 69beb85b6e7..d2c4f9fd265 100644
--- a/gix-path/tests/path/env.rs
+++ b/gix-path/tests/path/env.rs
@@ -7,6 +7,16 @@ fn exe_invocation() {
     );
 }
 
+#[test]
+fn login_shell() {
+    // On CI, the $SHELL variable isn't necessarily set. Maybe other ways to get the login shell should be used then.
+    if !gix_testtools::is_ci::cached() {
+        assert!(gix_path::env::login_shell()
+            .expect("There should always be the notion of a shell used by git")
+            .exists());
+    }
+}
+
 #[test]
 fn installation_config() {
     assert_ne!(

From 3aff1e57ba25b39774db4b1dd051bfe9c110911e Mon Sep 17 00:00:00 2001
From: Sebastian Thiel <sebastian.thiel@icloud.com>
Date: Thu, 9 Jan 2025 18:22:12 +0100
Subject: [PATCH 5/5] Make a note to use `env::git_login_shell()` in testtools
 when available.

---
 tests/tools/src/lib.rs | 1 +
 1 file changed, 1 insertion(+)

diff --git a/tests/tools/src/lib.rs b/tests/tools/src/lib.rs
index 15dbea49775..e3ce748cc5c 100644
--- a/tests/tools/src/lib.rs
+++ b/tests/tools/src/lib.rs
@@ -652,6 +652,7 @@ fn configure_command<'a, I: IntoIterator<Item = S>, S: AsRef<OsStr>>(
 }
 
 fn bash_program() -> &'static Path {
+    // TODO: use `gix_path::env::login_shell()` when available.
     if cfg!(windows) {
         static GIT_BASH: Lazy<Option<PathBuf>> = Lazy::new(|| {
             GIT_CORE_DIR