diff --git a/Cargo.lock b/Cargo.lock
index 6af4fe2515b..7e986db4455 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -709,11 +709,10 @@ dependencies = [
 
 [[package]]
 name = "crossbeam-channel"
-version = "0.5.10"
+version = "0.5.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "82a9b73a36529d9c47029b9fb3a6f0ea3cc916a261195352ba19e770fc1748b2"
+checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2"
 dependencies = [
- "cfg-if",
  "crossbeam-utils",
 ]
 
@@ -2181,6 +2180,7 @@ dependencies = [
 name = "gix-odb-tests"
 version = "0.0.0"
 dependencies = [
+ "crossbeam-channel",
  "filetime",
  "gix-actor 0.31.2",
  "gix-date 0.8.6",
diff --git a/gitoxide-core/src/repository/attributes/validate_baseline.rs b/gitoxide-core/src/repository/attributes/validate_baseline.rs
index 77eeb258a49..686c668add6 100644
--- a/gitoxide-core/src/repository/attributes/validate_baseline.rs
+++ b/gitoxide-core/src/repository/attributes/validate_baseline.rs
@@ -305,13 +305,13 @@ pub(crate) mod function {
     }
 
     fn parse_exclude(line: &str) -> Option<(String, Baseline)> {
-        let (left, value) = line.split_at(line.find(|c| c == '\t')?);
+        let (left, value) = line.split_at(line.find('\t')?);
         let value = &value[1..];
 
         let location = if left == "::" {
             None
         } else {
-            let mut tokens = left.split(|b| b == ':');
+            let mut tokens = left.split(':');
             let source = tokens.next()?;
             let line_number: usize = tokens.next()?.parse().ok()?;
             let pattern = tokens.next()?;
@@ -363,8 +363,8 @@ pub(crate) mod function {
                 "unspecified" => StateRef::Unspecified,
                 _ => StateRef::from_bytes(info.as_bytes()),
             };
-            path = path.trim_end_matches(|b| b == ':');
-            let attr = attr.trim_end_matches(|b| b == ':');
+            path = path.trim_end_matches(':');
+            let attr = attr.trim_end_matches(':');
             let assignment = gix::attrs::AssignmentRef {
                 name: gix::attrs::NameRef::try_from(attr.as_bytes().as_bstr()).ok()?,
                 state,
diff --git a/gitoxide-core/src/repository/index/entries.rs b/gitoxide-core/src/repository/index/entries.rs
index 4485bad5c3e..20a21e3a38b 100644
--- a/gitoxide-core/src/repository/index/entries.rs
+++ b/gitoxide-core/src/repository/index/entries.rs
@@ -319,7 +319,7 @@ pub(crate) mod function {
 
     #[cfg(feature = "serde")]
     fn to_json(
-        mut out: &mut impl std::io::Write,
+        out: &mut impl std::io::Write,
         index: &gix::index::File,
         entry: &gix::index::Entry,
         attrs: Option<Attrs>,
@@ -338,7 +338,7 @@ pub(crate) mod function {
         }
 
         serde_json::to_writer(
-            &mut out,
+            &mut *out,
             &Entry {
                 stat: &entry.stat,
                 hex_id: entry.id.to_hex().to_string(),
diff --git a/gix-config/src/file/section/mod.rs b/gix-config/src/file/section/mod.rs
index f07a145e3d4..1dd4bd15ad3 100644
--- a/gix-config/src/file/section/mod.rs
+++ b/gix-config/src/file/section/mod.rs
@@ -75,7 +75,7 @@ impl<'a> Section<'a> {
     /// Stream ourselves to the given `out`, in order to reproduce this section mostly losslessly
     /// as it was parsed.
     pub fn write_to(&self, mut out: &mut dyn std::io::Write) -> std::io::Result<()> {
-        self.header.write_to(&mut out)?;
+        self.header.write_to(&mut *out)?;
 
         if self.body.0.is_empty() {
             return Ok(());
diff --git a/gix-config/src/file/write.rs b/gix-config/src/file/write.rs
index 772054f9529..f08f5db57a4 100644
--- a/gix-config/src/file/write.rs
+++ b/gix-config/src/file/write.rs
@@ -18,7 +18,7 @@ impl File<'_> {
     pub fn write_to_filter(
         &self,
         mut out: &mut dyn std::io::Write,
-        mut filter: &mut dyn FnMut(&Section<'_>) -> bool,
+        filter: &mut dyn FnMut(&Section<'_>) -> bool,
     ) -> std::io::Result<()> {
         let nl = self.detect_newline_style();
 
@@ -27,7 +27,8 @@ impl File<'_> {
                 event.write_to(&mut out)?;
             }
 
-            if !ends_with_newline(self.frontmatter_events.as_ref(), nl, true) && self.sections.values().any(&mut filter)
+            if !ends_with_newline(self.frontmatter_events.as_ref(), nl, true)
+                && self.sections.values().any(&mut *filter)
             {
                 out.write_all(nl)?;
             }
diff --git a/gix-config/src/parse/event.rs b/gix-config/src/parse/event.rs
index d88ace7ce6c..653a0c67751 100644
--- a/gix-config/src/parse/event.rs
+++ b/gix-config/src/parse/event.rs
@@ -33,7 +33,7 @@ impl Event<'_> {
 
     /// Stream ourselves to the given `out`, in order to reproduce this event mostly losslessly
     /// as it was parsed.
-    pub fn write_to(&self, mut out: &mut dyn std::io::Write) -> std::io::Result<()> {
+    pub fn write_to(&self, out: &mut dyn std::io::Write) -> std::io::Result<()> {
         match self {
             Self::ValueNotDone(e) => {
                 out.write_all(e.as_ref())?;
@@ -42,8 +42,8 @@ impl Event<'_> {
             Self::Whitespace(e) | Self::Newline(e) | Self::Value(e) | Self::ValueDone(e) => out.write_all(e.as_ref()),
             Self::KeyValueSeparator => out.write_all(b"="),
             Self::SectionKey(k) => out.write_all(k.0.as_ref()),
-            Self::SectionHeader(h) => h.write_to(&mut out),
-            Self::Comment(c) => c.write_to(&mut out),
+            Self::SectionHeader(h) => h.write_to(out),
+            Self::Comment(c) => c.write_to(out),
         }
     }
 
diff --git a/gix-config/src/parse/events.rs b/gix-config/src/parse/events.rs
index 5e0c8f06bf1..feda7a7ca6c 100644
--- a/gix-config/src/parse/events.rs
+++ b/gix-config/src/parse/events.rs
@@ -28,26 +28,26 @@ pub type FrontMatterEvents<'a> = SmallVec<[Event<'a>; 8]>;
 ///
 /// For concrete examples, some notable differences are:
 /// - `git-config` sections permit subsections via either a quoted string
-/// (`[some-section "subsection"]`) or via the deprecated dot notation
-/// (`[some-section.subsection]`). Successful parsing these section names is not
-/// well defined in typical `.ini` parsers. This parser will handle these cases
-/// perfectly.
+///   (`[some-section "subsection"]`) or via the deprecated dot notation
+///   (`[some-section.subsection]`). Successful parsing these section names is not
+///   well defined in typical `.ini` parsers. This parser will handle these cases
+///   perfectly.
 /// - Comment markers are not strictly defined either. This parser will always
-/// and only handle a semicolon or octothorpe (also known as a hash or number
-/// sign).
+///   and only handle a semicolon or octothorpe (also known as a hash or number
+///   sign).
 /// - Global properties may be allowed in `.ini` parsers, but is strictly
-/// disallowed by this parser.
+///   disallowed by this parser.
 /// - Only `\t`, `\n`, `\b` `\\` are valid escape characters.
 /// - Quoted and semi-quoted values will be parsed (but quotes will be included
-/// in event outputs). An example of a semi-quoted value is `5"hello world"`,
-/// which should be interpreted as `5hello world` after
-/// [normalization][crate::value::normalize()].
+///   in event outputs). An example of a semi-quoted value is `5"hello world"`,
+///   which should be interpreted as `5hello world` after
+///   [normalization][crate::value::normalize()].
 /// - Line continuations via a `\` character is supported (inside or outside of quotes)
 /// - Whitespace handling similarly follows the `git-config` specification as
-/// closely as possible, where excess whitespace after a non-quoted value are
-/// trimmed, and line continuations onto a new line with excess spaces are kept.
+///   closely as possible, where excess whitespace after a non-quoted value are
+///   trimmed, and line continuations onto a new line with excess spaces are kept.
 /// - Only equal signs (optionally padded by spaces) are valid name/value
-/// delimiters.
+///   delimiters.
 ///
 /// Note that things such as case-sensitivity or duplicate sections are
 /// _not_ handled. This parser is a low level _syntactic_ interpreter
@@ -62,8 +62,8 @@ pub type FrontMatterEvents<'a> = SmallVec<[Event<'a>; 8]>;
 /// # Trait Implementations
 ///
 /// - This struct does _not_ implement [`FromStr`] due to lifetime
-/// constraints implied on the required `from_str` method. Instead, it provides
-/// [`From<&'_ str>`].
+///   constraints implied on the required `from_str` method. Instead, it provides
+///   [`From<&'_ str>`].
 ///
 /// # Idioms
 ///
diff --git a/gix-fs/src/symlink.rs b/gix-fs/src/symlink.rs
index 8dd64406cbb..ce639c48f65 100644
--- a/gix-fs/src/symlink.rs
+++ b/gix-fs/src/symlink.rs
@@ -1,17 +1,17 @@
 use std::{io, io::ErrorKind::AlreadyExists, path::Path};
 
-#[cfg(not(windows))]
 /// Create a new symlink at `link` which points to `original`.
 ///
 /// Note that `original` doesn't have to exist.
+#[cfg(not(windows))]
 pub fn create(original: &Path, link: &Path) -> io::Result<()> {
     std::os::unix::fs::symlink(original, link)
 }
 
-#[cfg(not(windows))]
 /// Remove a symlink.
 ///
 /// Note that on only on windows this is special.
+#[cfg(not(windows))]
 pub fn remove(path: &Path) -> io::Result<()> {
     std::fs::remove_file(path)
 }
@@ -31,12 +31,12 @@ pub fn remove(path: &Path) -> io::Result<()> {
     }
 }
 
-#[cfg(windows)]
 /// Create a new symlink at `link` which points to `original`.
 ///
 /// Note that if a symlink target (the `original`) isn't present on disk, it's assumed to be a
 /// file, creating a dangling file symlink. This is similar to a dangling symlink on Unix,
 /// which doesn't have to care about the target type though.
+#[cfg(windows)]
 pub fn create(original: &Path, link: &Path) -> io::Result<()> {
     use std::os::windows::fs::{symlink_dir, symlink_file};
     // TODO: figure out if links to links count as files or whatever they point at
@@ -53,20 +53,20 @@ pub fn create(original: &Path, link: &Path) -> io::Result<()> {
     }
 }
 
-#[cfg(not(windows))]
 /// Return true if `err` indicates that a file collision happened, i.e. a symlink couldn't be created as the `link`
 /// already exists as filesystem object.
+#[cfg(not(windows))]
 pub fn is_collision_error(err: &std::io::Error) -> bool {
     // TODO: use ::IsDirectory as well when stabilized instead of raw_os_error(), and ::FileSystemLoop respectively
     err.kind() == AlreadyExists
-            || err.raw_os_error() == Some(if cfg!(windows) { 5 } else { 21 })
+            || err.raw_os_error() == Some(21)
             || err.raw_os_error() == Some(62) // no-follow on symlnk on mac-os
             || err.raw_os_error() == Some(40) // no-follow on symlnk on ubuntu
 }
 
-#[cfg(windows)]
 /// Return true if `err` indicates that a file collision happened, i.e. a symlink couldn't be created as the `link`
 /// already exists as filesystem object.
+#[cfg(windows)]
 pub fn is_collision_error(err: &std::io::Error) -> bool {
     err.kind() == AlreadyExists || err.kind() == std::io::ErrorKind::PermissionDenied
 }
diff --git a/gix-ignore/src/search.rs b/gix-ignore/src/search.rs
index a527b447221..9b5a2766b61 100644
--- a/gix-ignore/src/search.rs
+++ b/gix-ignore/src/search.rs
@@ -55,7 +55,7 @@ impl Search {
                 .transpose()?,
         );
         group.patterns.extend(pattern::List::<Ignore>::from_file(
-            &git_dir.join("info").join("exclude"),
+            git_dir.join("info").join("exclude"),
             None,
             follow_symlinks,
             buf,
diff --git a/gix-odb/tests/Cargo.toml b/gix-odb/tests/Cargo.toml
index 2c26eb6b801..d42d1775af6 100644
--- a/gix-odb/tests/Cargo.toml
+++ b/gix-odb/tests/Cargo.toml
@@ -24,9 +24,10 @@ gix-date = { path = "../../gix-date" }
 gix-object = { path = "../../gix-object" }
 gix-pack = { path = "../../gix-pack" }
 
-gix-testtools = { path = "../../tests/tools"}
+gix-testtools = { path = "../../tests/tools" }
 gix-actor = { path = "../../gix-actor" }
 pretty_assertions = "1.0.0"
 filetime = "0.2.15"
 maplit = "1.0.2"
+crossbeam-channel = "0.5.13"
 
diff --git a/gix-odb/tests/fixtures/generated-archives/make_repo_multi_index.tar.xz b/gix-odb/tests/fixtures/generated-archives/make_repo_multi_index.tar.xz
index 2489b1a3bc0..03c2e9c071f 100644
Binary files a/gix-odb/tests/fixtures/generated-archives/make_repo_multi_index.tar.xz and b/gix-odb/tests/fixtures/generated-archives/make_repo_multi_index.tar.xz differ
diff --git a/gix-odb/tests/odb/regression/mod.rs b/gix-odb/tests/odb/regression/mod.rs
index 1b8f5de0b0a..c6bffa61125 100644
--- a/gix-odb/tests/odb/regression/mod.rs
+++ b/gix-odb/tests/odb/regression/mod.rs
@@ -17,12 +17,13 @@ mod repo_with_small_packs {
     }
 
     #[test]
-    #[cfg(feature = "internal-testing-gix-features-parallel")]
+    #[cfg(feature = "gix-features-parallel")]
     fn multi_threaded_access_will_not_panic() -> crate::Result {
         for arg in ["no", "without-multi-index"] {
-            let base = gix_testtools::scripted_fixture_read_only_with_args("make_repo_multi_index.sh", Some(arg))?
-                .join(".git")
-                .join("objects");
+            let base =
+                gix_testtools::scripted_fixture_read_only_with_args_standalone("make_repo_multi_index.sh", Some(arg))?
+                    .join(".git")
+                    .join("objects");
             let store = gix_odb::at(base)?;
             let (tx, barrier) = crossbeam_channel::unbounded::<()>();
             let handles = (0..std::thread::available_parallelism()?.get()).map(|tid| {
@@ -36,7 +37,7 @@ mod repo_with_small_packs {
                         for id in store.iter()? {
                             let id = id?;
                             assert!(
-                                store.try_find(id, &mut buf).is_ok(),
+                                store.try_find(&id, &mut buf).is_ok(),
                                 "Thread {} could not find {}",
                                 tid,
                                 id
diff --git a/gix-pack/src/bundle/write/mod.rs b/gix-pack/src/bundle/write/mod.rs
index 9ddd1b9d3a5..88a56858b57 100644
--- a/gix-pack/src/bundle/write/mod.rs
+++ b/gix-pack/src/bundle/write/mod.rs
@@ -51,8 +51,8 @@ impl crate::Bundle {
     /// * `progress` provides detailed progress information which can be discarded with [`gix_features::progress::Discard`].
     /// * `should_interrupt` is checked regularly and when true, the whole operation will stop.
     /// * `thin_pack_base_object_lookup` If set, we expect to see a thin-pack with objects that reference their base object by object id which is
-    /// expected to exist in the object database the bundle is contained within.
-    /// `options` further configure how the task is performed.
+    ///    expected to exist in the object database the bundle is contained within.
+    ///    `options` further configure how the task is performed.
     ///
     /// # Note
     ///
@@ -300,31 +300,37 @@ impl crate::Bundle {
                 )?;
                 drop(pack_entries_iter);
 
-                let data_path = directory.join(format!("pack-{}.pack", outcome.data_hash.to_hex()));
-                let index_path = data_path.with_extension("idx");
-                let keep_path = data_path.with_extension("keep");
+                if outcome.num_objects == 0 {
+                    WriteOutcome {
+                        outcome,
+                        data_path: None,
+                        index_path: None,
+                        keep_path: None,
+                    }
+                } else {
+                    let data_path = directory.join(format!("pack-{}.pack", outcome.data_hash.to_hex()));
+                    let index_path = data_path.with_extension("idx");
+                    let keep_path = data_path.with_extension("keep");
 
-                std::fs::write(&keep_path, b"")?;
-                Arc::try_unwrap(data_file)
-                    .expect("only one handle left after pack was consumed")
-                    .into_inner()
-                    .into_inner()
-                    .map_err(|err| Error::from(err.into_error()))?
-                    .persist(&data_path)?;
-                index_file
-                    .persist(&index_path)
-                    .map_err(|err| {
-                        progress.info(format!(
-                            "pack file at {} is retained despite failing to move the index file into place. You can use plumbing to make it usable.",
-                            data_path.display()
-                        ));
-                        err
-                    })?;
-                WriteOutcome {
-                    outcome,
-                    data_path: Some(data_path),
-                    index_path: Some(index_path),
-                    keep_path: Some(keep_path),
+                    std::fs::write(&keep_path, b"")?;
+                    Arc::try_unwrap(data_file)
+                        .expect("only one handle left after pack was consumed")
+                        .into_inner()
+                        .into_inner()
+                        .map_err(|err| Error::from(err.into_error()))?
+                        .persist(&data_path)?;
+                    index_file
+                        .persist(&index_path)
+                        .map_err(|err| {
+                            gix_features::trace::warn!("pack file at \"{}\" is retained despite failing to move the index file into place. You can use plumbing to make it usable.",data_path.display());
+                            err
+                        })?;
+                    WriteOutcome {
+                        outcome,
+                        data_path: Some(data_path),
+                        index_path: Some(index_path),
+                        keep_path: Some(keep_path),
+                    }
                 }
             }
             None => WriteOutcome {
diff --git a/gix-pack/src/cache/delta/from_offsets.rs b/gix-pack/src/cache/delta/from_offsets.rs
index fc807264d77..4b05bed4e56 100644
--- a/gix-pack/src/cache/delta/from_offsets.rs
+++ b/gix-pack/src/cache/delta/from_offsets.rs
@@ -31,13 +31,13 @@ const PACK_HEADER_LEN: usize = 12;
 impl<T> Tree<T> {
     /// Create a new `Tree` from any data sorted by offset, ascending as returned by the `data_sorted_by_offsets` iterator.
     /// * `get_pack_offset(item: &T) -> data::Offset` is a function returning the pack offset of the given item, which can be used
-    /// for obtaining the objects entry within the pack.
+    ///   for obtaining the objects entry within the pack.
     /// * `pack_path` is the path to the pack file itself and from which to read the entry data, which is a pack file matching the offsets
-    /// returned by `get_pack_offset(…)`.
+    ///   returned by `get_pack_offset(…)`.
     /// * `progress` is used to track progress when creating the tree.
     /// * `resolve_in_pack_id(gix_hash::oid) -> Option<data::Offset>` takes an object ID and tries to resolve it to an object within this pack if
-    /// possible. Failing to do so aborts the operation, and this function is not expected to be called in usual packs. It's a theoretical
-    /// possibility though as old packs might have referred to their objects using the 20 bytes hash, instead of their encoded offset from the base.
+    ///   possible. Failing to do so aborts the operation, and this function is not expected to be called in usual packs. It's a theoretical
+    ///   possibility though as old packs might have referred to their objects using the 20 bytes hash, instead of their encoded offset from the base.
     ///
     /// Note that the sort order is ascending. The given pack file path must match the provided offsets.
     pub fn from_offsets_in_pack(
diff --git a/gix-pack/src/data/output/entry/iter_from_counts.rs b/gix-pack/src/data/output/entry/iter_from_counts.rs
index c65f29ced49..5760bbb22fe 100644
--- a/gix-pack/src/data/output/entry/iter_from_counts.rs
+++ b/gix-pack/src/data/output/entry/iter_from_counts.rs
@@ -40,7 +40,7 @@ pub(crate) mod function {
     ///
     /// * ~~currently there is no way to easily write the pack index, even though the state here is uniquely positioned to do
     ///   so with minimal overhead (especially compared to `gix index-from-pack`)~~ Probably works now by chaining Iterators
-    ///  or keeping enough state to write a pack and then generate an index with recorded data.
+    ///   or keeping enough state to write a pack and then generate an index with recorded data.
     ///
     pub fn iter_from_counts<Find>(
         mut counts: Vec<output::Count>,
diff --git a/gix-pack/src/index/write/mod.rs b/gix-pack/src/index/write/mod.rs
index 2247d8a1ebe..ca3d8348088 100644
--- a/gix-pack/src/index/write/mod.rs
+++ b/gix-pack/src/index/write/mod.rs
@@ -78,9 +78,9 @@ impl crate::index::File {
     ///
     /// * neither in-pack nor out-of-pack Ref Deltas are supported here, these must have been resolved beforehand.
     /// * `make_resolver()` will only be called after the iterator stopped returning elements and produces a function that
-    /// provides all bytes belonging to a pack entry writing them to the given mutable output `Vec`.
-    /// It should return `None` if the entry cannot be resolved from the pack that produced the `entries` iterator, causing
-    /// the write operation to fail.
+    ///   provides all bytes belonging to a pack entry writing them to the given mutable output `Vec`.
+    ///   It should return `None` if the entry cannot be resolved from the pack that produced the `entries` iterator, causing
+    ///   the write operation to fail.
     #[allow(clippy::too_many_arguments)]
     pub fn write_data_iter_to_stream<F, F2, R>(
         version: crate::index::Version,
diff --git a/gix-pack/tests/pack/data/output/count_and_entries.rs b/gix-pack/tests/pack/data/output/count_and_entries.rs
index c20bdf52727..edb4783224a 100644
--- a/gix-pack/tests/pack/data/output/count_and_entries.rs
+++ b/gix-pack/tests/pack/data/output/count_and_entries.rs
@@ -322,13 +322,18 @@ fn traversals() -> crate::Result {
 
 #[test]
 fn empty_pack_is_allowed() {
-    write_and_verify(
-        db(DbKind::DeterministicGeneratedContent).unwrap(),
-        vec![],
-        hex_to_id("029d08823bd8a8eab510ad6ac75c823cfd3ed31e"),
-        None,
-    )
-    .unwrap();
+    assert_eq!(
+        write_and_verify(
+            db(DbKind::DeterministicGeneratedContent).unwrap(),
+            vec![],
+            hex_to_id("029d08823bd8a8eab510ad6ac75c823cfd3ed31e"),
+            None,
+        )
+        .unwrap_err()
+        .to_string(),
+        "pack data directory should be set",
+        "empty packs are not actually written as they would be useless"
+    );
 }
 
 fn write_and_verify(
@@ -392,7 +397,7 @@ fn write_and_verify(
             pack::bundle::write::Options::default(),
         )?
         .data_path
-        .expect("directory set"),
+        .ok_or("pack data directory should be set")?,
         object_hash,
     )?;
     // TODO: figure out why these hashes change, also depending on the machine, even though they are indeed stable.
diff --git a/gix-pack/tests/pack/index.rs b/gix-pack/tests/pack/index.rs
index e554f50611d..67f991d0f8d 100644
--- a/gix-pack/tests/pack/index.rs
+++ b/gix-pack/tests/pack/index.rs
@@ -108,7 +108,7 @@ mod version {
         }
     }
 
-    #[cfg(feature = "internal-testing-gix-features-parallel")]
+    #[cfg(feature = "gix-features-parallel")]
     mod any {
         use std::{fs, io, sync::atomic::AtomicBool};
 
@@ -133,7 +133,7 @@ mod version {
                 index_path: &&str,
                 data_path: &&str,
             ) -> Result<(), Box<dyn std::error::Error>> {
-                let pack_iter = pack::data::input::BytesToEntriesIter::new_from_header(
+                let mut pack_iter = pack::data::input::BytesToEntriesIter::new_from_header(
                     io::BufReader::new(fs::File::open(fixture_path(data_path))?),
                     *mode,
                     *compressed,
@@ -148,12 +148,12 @@ mod version {
                     desired_kind,
                     || {
                         let file = std::fs::File::open(fixture_path(data_path))?;
-                        let map = unsafe { memmap2::MmapOptions::map_copy_read_only(&file)? };
+                        let map = unsafe { memmap2::MmapOptions::new().map_copy_read_only(&file)? };
                         Ok((slice_map, map))
                     },
-                    pack_iter,
+                    &mut pack_iter,
                     None,
-                    progress::Discard,
+                    &mut progress::Discard,
                     &mut actual,
                     &AtomicBool::new(false),
                     gix_hash::Kind::Sha1,
@@ -210,7 +210,7 @@ mod version {
                 assert_eq!(outcome.index_version, desired_kind);
                 assert_eq!(
                     outcome.index_hash,
-                    gix_hash::ObjectId::from(&expected[end_of_pack_hash..end_of_index_hash])
+                    gix_hash::ObjectId::try_from(&expected[end_of_pack_hash..end_of_index_hash])?
                 );
                 Ok(())
             }
@@ -227,7 +227,7 @@ mod version {
         #[test]
         fn lookup_missing() {
             let file = index::File::at(&fixture_path(INDEX_V2), gix_hash::Kind::Sha1).unwrap();
-            let prefix = gix_hash::Prefix::new(gix_hash::ObjectId::null(gix_hash::Kind::Sha1), 7).unwrap();
+            let prefix = gix_hash::Prefix::new(&gix_hash::Kind::Sha1.null(), 7).unwrap();
             assert!(file.lookup_prefix(prefix, None).is_none());
 
             let mut candidates = 1..1;
diff --git a/gix-ref/fuzz/fuzz_targets/fuzz_log.rs b/gix-ref/fuzz/fuzz_targets/fuzz_log.rs
index 423da5479de..204ab6e7ba2 100644
--- a/gix-ref/fuzz/fuzz_targets/fuzz_log.rs
+++ b/gix-ref/fuzz/fuzz_targets/fuzz_log.rs
@@ -20,10 +20,10 @@ fn fuzz(ctx: Ctx) -> Result<()> {
 
     let mut buf = [0u8; 1024];
     let read = std::io::Cursor::new(ctx.multi_line_reverse);
-    let mut iter = gix_ref::file::log::iter::reverse(read, &mut buf)?;
+    let iter = gix_ref::file::log::iter::reverse(read, &mut buf)?;
     _ = black_box(iter.map(|x| black_box(x)).count());
 
-    let mut iter = gix_ref::file::log::iter::forward(ctx.multi_line_forward);
+    let iter = gix_ref::file::log::iter::forward(ctx.multi_line_forward);
     _ = black_box(iter.map(|x| black_box(x)).count());
 
     Ok(())
diff --git a/gix-ref/fuzz/fuzz_targets/fuzz_names.rs b/gix-ref/fuzz/fuzz_targets/fuzz_names.rs
index 651a28fb23e..6729f2d5899 100644
--- a/gix-ref/fuzz/fuzz_targets/fuzz_names.rs
+++ b/gix-ref/fuzz/fuzz_targets/fuzz_names.rs
@@ -3,7 +3,7 @@
 use anyhow::Result;
 use arbitrary::Arbitrary;
 use bstr::{BStr, BString};
-use gix_ref::{namespace::expand, FullName, FullNameRef, PartialName, PartialNameCow, PartialNameRef};
+use gix_ref::{namespace::expand, FullName, FullNameRef, PartialName};
 use libfuzzer_sys::fuzz_target;
 use std::hint::black_box;
 
diff --git a/gix-ref/src/lib.rs b/gix-ref/src/lib.rs
index 29028b38d62..6059fd2d675 100644
--- a/gix-ref/src/lib.rs
+++ b/gix-ref/src/lib.rs
@@ -23,8 +23,6 @@
 #![cfg_attr(all(doc, feature = "document-features"), feature(doc_cfg, doc_auto_cfg))]
 #![deny(missing_docs, rust_2018_idioms, unsafe_code)]
 
-use std::borrow::Cow;
-
 use gix_hash::{oid, ObjectId};
 pub use gix_object::bstr;
 use gix_object::bstr::{BStr, BString};
@@ -133,10 +131,6 @@ pub struct FullName(pub(crate) BString);
 #[repr(transparent)]
 pub struct FullNameRef(BStr);
 
-/// A validated and potentially partial reference name, safe to use for common operations.
-#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)]
-pub struct PartialNameCow<'a>(Cow<'a, BStr>);
-
 /// A validated and potentially partial reference name, safe to use for common operations.
 #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd)]
 #[repr(transparent)]
diff --git a/gix-ref/tests/store/mod.rs b/gix-ref/tests/store/mod.rs
index d6263358735..9f6ae14b7d9 100644
--- a/gix-ref/tests/store/mod.rs
+++ b/gix-ref/tests/store/mod.rs
@@ -1,12 +1,15 @@
 #[test]
-#[cfg(feature = "internal-testing-gix-features-parallel")]
+#[cfg(feature = "gix-features-parallel")]
 fn is_send_and_sync() {
     pub fn store_at(name: &str) -> crate::Result<gix_ref::file::Store> {
         let path = gix_testtools::scripted_fixture_read_only_standalone(name)?;
         Ok(gix_ref::file::Store::at(
             path.join(".git"),
-            gix_ref::store::WriteReflog::Normal,
-            gix_hash::Kind::Sha1,
+            gix_ref::store::init::Options {
+                write_reflog: gix_ref::store::WriteReflog::Normal,
+                object_hash: gix_hash::Kind::Sha1,
+                ..Default::default()
+            },
         ))
     }
 
diff --git a/gix-status/src/index_as_worktree_with_renames/types.rs b/gix-status/src/index_as_worktree_with_renames/types.rs
index 76e9287e6d2..84ba2e24ec5 100644
--- a/gix-status/src/index_as_worktree_with_renames/types.rs
+++ b/gix-status/src/index_as_worktree_with_renames/types.rs
@@ -325,10 +325,10 @@ pub struct Context<'a> {
     ///
     /// * `attr_stack`
     ///     - A stack pre-configured to allow accessing attributes for each entry, as required for `filter`
-    ///      and possibly pathspecs.
-    ///      It *may* also allow accessing `.gitignore` information for use in the directory walk.
-    ///      If no excludes information is present, the directory walk will identify ignored files as untracked, which
-    ///      might be desirable under certain circumstances.
+    ///       and possibly pathspecs.
+    ///       It *may* also allow accessing `.gitignore` information for use in the directory walk.
+    ///       If no excludes information is present, the directory walk will identify ignored files as untracked, which
+    ///       might be desirable under certain circumstances.
     /// * `filter`
     ///     - A filter to be able to perform conversions from and to the worktree format.
     ///       It is needed to potentially refresh the index with data read from the worktree, which needs to be converted back
diff --git a/gix/src/remote/connection/fetch/error.rs b/gix/src/remote/connection/fetch/error.rs
index 29326b3a763..129a2e6d345 100644
--- a/gix/src/remote/connection/fetch/error.rs
+++ b/gix/src/remote/connection/fetch/error.rs
@@ -47,6 +47,11 @@ pub enum Error {
     NegotiationAlgorithmConfig(#[from] config::key::GenericErrorWithValue),
     #[error("Failed to read remaining bytes in stream")]
     ReadRemainingBytes(#[source] std::io::Error),
+    #[error("None of the refspec(s) {} matched any of the {num_remote_refs} refs on the remote", refspecs.iter().map(|r| r.to_ref().instruction().to_bstring().to_string()).collect::<Vec<_>>().join(", "))]
+    NoMapping {
+        refspecs: Vec<gix_refspec::RefSpec>,
+        num_remote_refs: usize,
+    },
 }
 
 impl gix_protocol::transport::IsSpuriousError for Error {
diff --git a/gix/src/remote/connection/fetch/negotiate.rs b/gix/src/remote/connection/fetch/negotiate.rs
index f5b6a031c4c..42b578ee490 100644
--- a/gix/src/remote/connection/fetch/negotiate.rs
+++ b/gix/src/remote/connection/fetch/negotiate.rs
@@ -61,7 +61,7 @@ pub(crate) enum Action {
 /// Finally, we also mark tips in the `negotiator` in one go to avoid traversing all refs twice, since we naturally encounter all tips during
 /// our own walk.
 ///
-/// Return whether or not we should negotiate, along with a queue for later use.
+/// Return whether we should negotiate, along with a queue for later use.
 pub(crate) fn mark_complete_and_common_ref(
     repo: &crate::Repository,
     negotiator: &mut dyn gix_negotiate::Negotiator,
@@ -71,6 +71,9 @@ pub(crate) fn mark_complete_and_common_ref(
     mapping_is_ignored: impl Fn(&fetch::Mapping) -> bool,
 ) -> Result<Action, Error> {
     let _span = gix_trace::detail!("mark_complete_and_common_ref", mappings = ref_map.mappings.len());
+    if ref_map.mappings.is_empty() {
+        return Ok(Action::NoChange);
+    }
     if let fetch::Shallow::Deepen(0) = shallow {
         // Avoid deepening (relative) with zero as it seems to upset the server. Git also doesn't actually
         // perform the negotiation for some reason (couldn't find it in code).
diff --git a/gix/src/remote/connection/fetch/receive_pack.rs b/gix/src/remote/connection/fetch/receive_pack.rs
index e0231f51a0a..ac48473e1bc 100644
--- a/gix/src/remote/connection/fetch/receive_pack.rs
+++ b/gix/src/remote/connection/fetch/receive_pack.rs
@@ -91,6 +91,15 @@ where
         let _span = gix_trace::coarse!("fetch::Prepare::receive()");
         let mut con = self.con.take().expect("receive() can only be called once");
 
+        if self.ref_map.mappings.is_empty() && !self.ref_map.remote_refs.is_empty() {
+            let mut specs = con.remote.fetch_specs.clone();
+            specs.extend(self.ref_map.extra_refspecs.clone());
+            return Err(Error::NoMapping {
+                refspecs: specs,
+                num_remote_refs: self.ref_map.remote_refs.len(),
+            });
+        }
+
         let handshake = &self.ref_map.handshake;
         let protocol_version = handshake.server_protocol_version;
 
diff --git a/gix/src/remote/connection/ref_map.rs b/gix/src/remote/connection/ref_map.rs
index bf53cf35fb0..c33471d5416 100644
--- a/gix/src/remote/connection/ref_map.rs
+++ b/gix/src/remote/connection/ref_map.rs
@@ -145,6 +145,7 @@ where
                 }
             }))
             .validated()?;
+
         let mappings = res.mappings;
         let mappings = mappings
             .into_iter()
diff --git a/src/porcelain/main.rs b/src/porcelain/main.rs
index 9024c193775..95645d843d5 100644
--- a/src/porcelain/main.rs
+++ b/src/porcelain/main.rs
@@ -62,11 +62,11 @@ pub fn main() -> Result<()> {
                     progress,
                     progress_keep_open,
                     crate::shared::STANDARD_RANGE,
-                    move |mut progress, out, mut err| {
+                    move |mut progress, out, err| {
                         let engine = query::prepare(
                             &repo_dir,
                             &mut progress,
-                            &mut err,
+                            &mut *err,
                             query::Options {
                                 object_cache_size_mb,
                                 find_copies_harder,