From 449db9122f51a1546a0420f3b61db7967583e0e9 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 20 Aug 2026 17:05:18 +0200 Subject: [PATCH 01/51] change(gix-packetline)!: migrate errors to gix-error Rubber-stamp after quick review. --- Cargo.lock | 3 +- gix-packetline/Cargo.toml | 2 +- gix-packetline/src/async_io/encode.rs | 12 ++-- gix-packetline/src/async_io/read.rs | 8 +-- gix-packetline/src/blocking_io/encode.rs | 8 +-- gix-packetline/src/blocking_io/read.rs | 8 +-- gix-packetline/src/decode.rs | 58 +++++++------------ gix-packetline/src/encode.rs | 11 +--- gix-packetline/src/lib.rs | 14 ++++- gix-packetline/tests/packetline/decode.rs | 13 +++-- gix-packetline/tests/packetline/main-async.rs | 2 +- .../tests/packetline/main-blocking.rs | 2 +- gix-packetline/tests/packetline/read/mod.rs | 8 +-- gix-protocol/Cargo.toml | 1 + gix-protocol/src/handshake/refs/async_io.rs | 6 +- .../src/handshake/refs/blocking_io.rs | 16 ++++- gix-protocol/src/handshake/refs/mod.rs | 2 +- 17 files changed, 83 insertions(+), 91 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ce1b7d60c73..7af8bee06c3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2263,6 +2263,7 @@ dependencies = [ "faster-hex", "futures-io", "futures-lite", + "gix-error", "gix-hash", "gix-macros", "gix-odb", @@ -2270,7 +2271,6 @@ dependencies = [ "gix-trace", "pin-project-lite", "serde", - "thiserror 2.0.18", ] [[package]] @@ -2328,6 +2328,7 @@ dependencies = [ "futures-lite", "gix-credentials", "gix-date", + "gix-error", "gix-features", "gix-hash", "gix-lock", diff --git a/gix-packetline/Cargo.toml b/gix-packetline/Cargo.toml index 10c62030223..96514518794 100644 --- a/gix-packetline/Cargo.toml +++ b/gix-packetline/Cargo.toml @@ -40,9 +40,9 @@ required-features = ["blocking-io"] [dependencies] gix-trace = { version = "^0.1.21", path = "../gix-trace" } +gix-error = { version = "^0.3.0", path = "../gix-error" } serde = { version = "1.0.114", optional = true, default-features = false, features = ["std", "derive"] } -thiserror = "2.0.18" faster-hex = { version = "0.10.1", default-features = false, features = ["std"] } bstr = { version = "1.12.0", default-features = false, features = ["std"] } # async support diff --git a/gix-packetline/src/async_io/encode.rs b/gix-packetline/src/async_io/encode.rs index 7a452c04729..da2c2fb413d 100644 --- a/gix-packetline/src/async_io/encode.rs +++ b/gix-packetline/src/async_io/encode.rs @@ -59,13 +59,11 @@ impl AsyncWrite for LineWriter<'_, W> { State::Idle => { let data_len = this.prefix.len() + data.len() + this.suffix.len(); if data_len > MAX_DATA_LEN { - let err = Error::DataLengthLimitExceeded { - length_in_bytes: data_len, - }; + let err = Error::new(format!("Cannot encode more than {MAX_DATA_LEN} bytes, got {data_len}")); return Poll::Ready(Err(io::Error::other(err))); } if data.is_empty() { - let err = Error::DataIsEmpty; + let err = Error::new("Empty lines are invalid"); return Poll::Ready(Err(io::Error::other(err))); } let data_len = data_len + 4; @@ -148,13 +146,11 @@ async fn prefixed_and_suffixed_data_to_write( ) -> io::Result { let data_len = prefix.len() + data.len() + suffix.len(); if data_len > MAX_DATA_LEN { - let err = Error::DataLengthLimitExceeded { - length_in_bytes: data_len, - }; + let err = Error::new(format!("Cannot encode more than {MAX_DATA_LEN} bytes, got {data_len}")); return Err(io::Error::other(err)); } if data.is_empty() { - let err = Error::DataIsEmpty; + let err = Error::new("Empty lines are invalid"); return Err(io::Error::other(err)); } diff --git a/gix-packetline/src/async_io/read.rs b/gix-packetline/src/async_io/read.rs index 999f269c978..5ff2a64b938 100644 --- a/gix-packetline/src/async_io/read.rs +++ b/gix-packetline/src/async_io/read.rs @@ -40,9 +40,7 @@ where buf: &'a mut [u8], ) -> io::Result, decode::Error>> { if buf.len() < U16_HEX_BYTES { - return Ok(Err(decode::Error::NotEnoughData { - bytes_needed: U16_HEX_BYTES - buf.len(), - })); + return Ok(Err(decode::not_enough_data(U16_HEX_BYTES - buf.len()))); } let (hex_bytes, data_bytes) = buf.split_at_mut(U16_HEX_BYTES); reader.read_exact(hex_bytes).await?; @@ -52,9 +50,7 @@ where Err(err) => return Ok(Err(err)), }; if num_data_bytes > data_bytes.len() { - return Ok(Err(decode::Error::DataLengthLimitExceeded { - length_in_bytes: num_data_bytes + U16_HEX_BYTES, - })); + return Ok(Err(decode::data_length_limit_exceeded(num_data_bytes + U16_HEX_BYTES))); } let (data_bytes, _) = data_bytes.split_at_mut(num_data_bytes); diff --git a/gix-packetline/src/blocking_io/encode.rs b/gix-packetline/src/blocking_io/encode.rs index 72485d82fe9..15380bfa53c 100644 --- a/gix-packetline/src/blocking_io/encode.rs +++ b/gix-packetline/src/blocking_io/encode.rs @@ -86,12 +86,12 @@ fn prefixed_and_suffixed_data_to_write( ) -> io::Result { let data_len = prefix.len() + data.len() + suffix.len(); if data_len > MAX_DATA_LEN { - return Err(io::Error::other(Error::DataLengthLimitExceeded { - length_in_bytes: data_len, - })); + return Err(io::Error::other(Error::new(format!( + "Cannot encode more than {MAX_DATA_LEN} bytes, got {data_len}" + )))); } if data.is_empty() { - return Err(io::Error::other(Error::DataIsEmpty)); + return Err(io::Error::other(Error::new("Empty lines are invalid"))); } let data_len = data_len + 4; diff --git a/gix-packetline/src/blocking_io/read.rs b/gix-packetline/src/blocking_io/read.rs index 9806656e942..0bc09fc8d2c 100644 --- a/gix-packetline/src/blocking_io/read.rs +++ b/gix-packetline/src/blocking_io/read.rs @@ -35,9 +35,7 @@ where fn read_line_inner<'a>(reader: &mut T, buf: &'a mut [u8]) -> io::Result, decode::Error>> { if buf.len() < U16_HEX_BYTES { - return Ok(Err(decode::Error::NotEnoughData { - bytes_needed: U16_HEX_BYTES - buf.len(), - })); + return Ok(Err(decode::not_enough_data(U16_HEX_BYTES - buf.len()))); } let (hex_bytes, data_bytes) = buf.split_at_mut(U16_HEX_BYTES); reader.read_exact(hex_bytes)?; @@ -47,9 +45,7 @@ where Err(err) => return Ok(Err(err)), }; if num_data_bytes > data_bytes.len() { - return Ok(Err(decode::Error::DataLengthLimitExceeded { - length_in_bytes: num_data_bytes + U16_HEX_BYTES, - })); + return Ok(Err(decode::data_length_limit_exceeded(num_data_bytes + U16_HEX_BYTES))); } let (data_bytes, _) = data_bytes.split_at_mut(num_data_bytes); diff --git a/gix-packetline/src/decode.rs b/gix-packetline/src/decode.rs index d3c2530ef62..b216ed87928 100644 --- a/gix-packetline/src/decode.rs +++ b/gix-packetline/src/decode.rs @@ -1,38 +1,24 @@ -use bstr::BString; - use crate::{DELIMITER_LINE, FLUSH_LINE, MAX_DATA_LEN, MAX_LINE_LEN, PacketLineRef, RESPONSE_END_LINE, U16_HEX_BYTES}; /// The error used in the [`decode`][mod@crate::decode] module -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] -pub enum Error { - #[error("Failed to decode the first four hex bytes indicating the line length: {err}")] - HexDecode { err: String }, - #[error( +pub type Error = gix_error::ValidationError; + +pub(crate) fn data_length_limit_exceeded(length_in_bytes: usize) -> Error { + Error::new(format!( "The data received claims to be larger than the maximum allowed size: got {length_in_bytes}, exceeds {MAX_DATA_LEN}" - )] - DataLengthLimitExceeded { length_in_bytes: usize }, - #[error("Received an invalid empty line")] - DataIsEmpty, - #[error("Received an invalid line of length 3")] - InvalidLineLength, - #[error("{data:?} - consumed {bytes_consumed} bytes")] - Line { data: BString, bytes_consumed: usize }, - #[error("Needing {bytes_needed} additional bytes to decode the line successfully")] - NotEnoughData { bytes_needed: usize }, + )) +} + +pub(crate) fn not_enough_data(bytes_needed: usize) -> Error { + Error::new(format!( + "Needing {bytes_needed} additional bytes to decode the line successfully" + )) } /// pub mod band { /// The error used in [`PacketLineRef::decode_band()`][super::PacketLineRef::decode_band()]. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error("attempt to decode a non-side channel line or input was malformed: {band_id}")] - InvalidSideBand { band_id: u8 }, - #[error("attempt to decode a non-data line into a side-channel band")] - NonDataLine, - } + pub type Error = gix_error::ValidationError; } /// A utility return type to support incremental parsing of packet lines. @@ -74,14 +60,18 @@ pub fn hex_prefix(four_bytes: &[u8]) -> Result, Error } let mut buf = [0u8; U16_HEX_BYTES / 2]; - faster_hex::hex_decode(four_bytes, &mut buf).map_err(|err| Error::HexDecode { err: err.to_string() })?; + faster_hex::hex_decode(four_bytes, &mut buf).map_err(|err| { + Error::new(format!( + "Failed to decode the first four hex bytes indicating the line length: {err}" + )) + })?; let wanted_bytes = u16::from_be_bytes(buf); if wanted_bytes == 3 { - return Err(Error::InvalidLineLength); + return Err(Error::new("Received an invalid line of length 3")); } if wanted_bytes == 4 { - return Err(Error::DataIsEmpty); + return Err(Error::new("Received an invalid empty line")); } debug_assert!( wanted_bytes as usize > U16_HEX_BYTES, @@ -93,9 +83,7 @@ pub fn hex_prefix(four_bytes: &[u8]) -> Result, Error /// Obtain a `PacketLine` from `data` after assuring `data` is small enough to fit. pub fn to_data_line(data: &[u8]) -> Result, Error> { if data.len() > MAX_LINE_LEN { - return Err(Error::DataLengthLimitExceeded { - length_in_bytes: data.len(), - }); + return Err(data_length_limit_exceeded(data.len())); } Ok(PacketLineRef::Data(data)) @@ -119,9 +107,7 @@ pub fn streaming(data: &[u8]) -> Result, Error> { } } + U16_HEX_BYTES; if wanted_bytes > MAX_LINE_LEN { - return Err(Error::DataLengthLimitExceeded { - length_in_bytes: wanted_bytes, - }); + return Err(data_length_limit_exceeded(wanted_bytes)); } if data_len < wanted_bytes { return Ok(Stream::Incomplete { @@ -142,6 +128,6 @@ pub fn streaming(data: &[u8]) -> Result, Error> { pub fn all_at_once(data: &[u8]) -> Result, Error> { match streaming(data)? { Stream::Complete { line, .. } => Ok(line), - Stream::Incomplete { bytes_needed } => Err(Error::NotEnoughData { bytes_needed }), + Stream::Incomplete { bytes_needed } => Err(not_enough_data(bytes_needed)), } } diff --git a/gix-packetline/src/encode.rs b/gix-packetline/src/encode.rs index eef8e172bab..01f5784c7e9 100644 --- a/gix-packetline/src/encode.rs +++ b/gix-packetline/src/encode.rs @@ -1,14 +1,5 @@ -use super::MAX_DATA_LEN; - /// The error returned by most functions in the [`encode`](crate::encode) module -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] -pub enum Error { - #[error("Cannot encode more than {MAX_DATA_LEN} bytes, got {length_in_bytes}")] - DataLengthLimitExceeded { length_in_bytes: usize }, - #[error("Empty lines are invalid")] - DataIsEmpty, -} +pub type Error = gix_error::ValidationError; pub(crate) fn u16_to_hex(value: u16) -> [u8; 4] { let mut buf = [0u8; 4]; diff --git a/gix-packetline/src/lib.rs b/gix-packetline/src/lib.rs index 5d601fc0794..bc95c71f9c2 100644 --- a/gix-packetline/src/lib.rs +++ b/gix-packetline/src/lib.rs @@ -163,13 +163,21 @@ impl<'a> PacketLineRef<'a> { /// Decode the band of this [`slice`](PacketLineRef::as_slice()) pub fn decode_band(&self) -> Result, decode::band::Error> { - let d = self.as_slice().ok_or(decode::band::Error::NonDataLine)?; - let (&band_id, d) = d.split_first().ok_or(decode::band::Error::NonDataLine)?; + let d = self + .as_slice() + .ok_or_else(|| decode::band::Error::new("attempt to decode a non-data line into a side-channel band"))?; + let (&band_id, d) = d + .split_first() + .ok_or_else(|| decode::band::Error::new("attempt to decode a non-data line into a side-channel band"))?; Ok(match band_id { 1 => BandRef::Data(d), 2 => BandRef::Progress(d), 3 => BandRef::Error(d), - band => return Err(decode::band::Error::InvalidSideBand { band_id: band }), + band => { + return Err(decode::band::Error::new(format!( + "attempt to decode a non-side channel line or input was malformed: {band}" + ))); + } }) } } diff --git a/gix-packetline/tests/packetline/decode.rs b/gix-packetline/tests/packetline/decode.rs index af406d1cda8..0b8ac5b929d 100644 --- a/gix-packetline/tests/packetline/decode.rs +++ b/gix-packetline/tests/packetline/decode.rs @@ -10,7 +10,7 @@ mod streaming { res: Result, expected_consumed: usize, expected_value: PacketLineRef, - ) -> crate::Result { + ) -> Result<(), decode::Error> { match res? { Stream::Complete { line, bytes_consumed } => { assert_eq!(bytes_consumed, expected_consumed); @@ -108,17 +108,20 @@ mod streaming { #[test] fn flush() -> crate::Result { - assert_complete(streaming(b"0000someotherstuff"), 4, PacketLineRef::Flush) + assert_complete(streaming(b"0000someotherstuff"), 4, PacketLineRef::Flush)?; + Ok(()) } #[test] fn trailing_line_feeds_are_not_removed_automatically() -> crate::Result { - assert_complete(streaming(b"0006a\n"), 6, PacketLineRef::Data(b"a\n")) + assert_complete(streaming(b"0006a\n"), 6, PacketLineRef::Data(b"a\n"))?; + Ok(()) } #[test] fn ignore_extra_bytes() -> crate::Result { - assert_complete(streaming(b"0006a\nhello"), 6, PacketLineRef::Data(b"a\n")) + assert_complete(streaming(b"0006a\nhello"), 6, PacketLineRef::Data(b"a\n"))?; + Ok(()) } #[test] @@ -160,7 +163,7 @@ mod streaming { mod incomplete { use gix_packetline::decode::{self, Stream, streaming}; - fn assert_incomplete(res: Result, expected_missing: usize) -> crate::Result { + fn assert_incomplete(res: Result, expected_missing: usize) -> Result<(), decode::Error> { match res? { Stream::Complete { .. } => { panic!("expected parsing to be partial, not complete"); diff --git a/gix-packetline/tests/packetline/main-async.rs b/gix-packetline/tests/packetline/main-async.rs index 9f004fe5410..a299dee441b 100644 --- a/gix-packetline/tests/packetline/main-async.rs +++ b/gix-packetline/tests/packetline/main-async.rs @@ -2,7 +2,7 @@ pub mod bisync { pub use gix_macros::{discard as only_sync, keep as bisync, keep as only_async}; } -pub type Result = std::result::Result<(), Box>; +pub type Result = gix_error::TestResult; pub fn assert_err_display( res: std::result::Result, diff --git a/gix-packetline/tests/packetline/main-blocking.rs b/gix-packetline/tests/packetline/main-blocking.rs index 3a98e202d86..6d54d0a4404 100644 --- a/gix-packetline/tests/packetline/main-blocking.rs +++ b/gix-packetline/tests/packetline/main-blocking.rs @@ -2,7 +2,7 @@ pub mod bisync { pub use gix_macros::{discard as only_async, keep as only_sync, sync as bisync}; } -pub type Result = std::result::Result<(), Box>; +pub type Result = gix_error::TestResult; pub fn assert_err_display( res: std::result::Result, diff --git a/gix-packetline/tests/packetline/read/mod.rs b/gix-packetline/tests/packetline/read/mod.rs index 1d89e00a515..4d701f2721c 100644 --- a/gix-packetline/tests/packetline/read/mod.rs +++ b/gix-packetline/tests/packetline/read/mod.rs @@ -181,10 +181,10 @@ pub mod streaming_peek_iter { .expect("a decode error instead of EOF") .expect("no IO error expected") .expect_err("decode should fail for oversized lengths"); - assert!(matches!( - err, - gix_packetline::decode::Error::DataLengthLimitExceeded { length_in_bytes: 65535 } - )); + assert_eq!( + err.to_string(), + "The data received claims to be larger than the maximum allowed size: got 65535, exceeds 65516" + ); Ok(()) } diff --git a/gix-protocol/Cargo.toml b/gix-protocol/Cargo.toml index e0c2de19e89..d91ef6a3b2b 100644 --- a/gix-protocol/Cargo.toml +++ b/gix-protocol/Cargo.toml @@ -72,6 +72,7 @@ path = "tests/async-protocol.rs" required-features = ["async-client"] [dependencies] +gix-error = { version = "^0.3.0", path = "../gix-error" } gix-features = { version = "^0.49.1", path = "../gix-features", features = [ "progress", ] } diff --git a/gix-protocol/src/handshake/refs/async_io.rs b/gix-protocol/src/handshake/refs/async_io.rs index 07a6877504a..b338149111e 100644 --- a/gix-protocol/src/handshake/refs/async_io.rs +++ b/gix-protocol/src/handshake/refs/async_io.rs @@ -10,7 +10,8 @@ pub async fn from_v2_refs(in_refs: &mut dyn ReadlineBufRead) -> Result, .readline() .await .transpose()? - .transpose()? + .transpose() + .map_err(|err| Error::DecodePacketline(gix_error::Error::from_error(err)))? .and_then(|l| l.as_bstr()) { out_refs.push(refs::shared::parse_v2(line)?); @@ -39,7 +40,8 @@ pub async fn from_v1_refs_received_as_part_of_handshake_and_capabilities<'a>( .readline() .await .transpose()? - .transpose()? + .transpose() + .map_err(|err| Error::DecodePacketline(gix_error::Error::from_error(err)))? .and_then(|l| l.as_bstr()) { refs::shared::parse_v1( diff --git a/gix-protocol/src/handshake/refs/blocking_io.rs b/gix-protocol/src/handshake/refs/blocking_io.rs index a0eab0b2b26..3c9257ca050 100644 --- a/gix-protocol/src/handshake/refs/blocking_io.rs +++ b/gix-protocol/src/handshake/refs/blocking_io.rs @@ -7,7 +7,13 @@ use crate::{ /// Parse refs from the given input line by line. Protocol V2 is required for this to succeed. pub fn from_v2_refs(in_refs: &mut dyn ReadlineBufRead) -> Result, Error> { let mut out_refs = Vec::new(); - while let Some(line) = in_refs.readline().transpose()?.transpose()?.and_then(|l| l.as_bstr()) { + while let Some(line) = in_refs + .readline() + .transpose()? + .transpose() + .map_err(|err| Error::DecodePacketline(gix_error::Error::from_error(err)))? + .and_then(|l| l.as_bstr()) + { out_refs.push(refs::shared::parse_v2(line)?); } Ok(out_refs) @@ -29,7 +35,13 @@ pub fn from_v1_refs_received_as_part_of_handshake_and_capabilities<'a>( let mut out_shallow = Vec::new(); let number_of_possible_symbolic_refs_for_lookup = out_refs.len(); - while let Some(line) = in_refs.readline().transpose()?.transpose()?.and_then(|l| l.as_bstr()) { + while let Some(line) = in_refs + .readline() + .transpose()? + .transpose() + .map_err(|err| Error::DecodePacketline(gix_error::Error::from_error(err)))? + .and_then(|l| l.as_bstr()) + { refs::shared::parse_v1( number_of_possible_symbolic_refs_for_lookup, &mut out_refs, diff --git a/gix-protocol/src/handshake/refs/mod.rs b/gix-protocol/src/handshake/refs/mod.rs index 77c04ac74d7..0261c7dfbaa 100644 --- a/gix-protocol/src/handshake/refs/mod.rs +++ b/gix-protocol/src/handshake/refs/mod.rs @@ -13,7 +13,7 @@ pub mod parse { #[error(transparent)] Io(#[from] std::io::Error), #[error(transparent)] - DecodePacketline(#[from] gix_transport::packetline::decode::Error), + DecodePacketline(gix_error::Error), #[error(transparent)] Id(#[from] gix_hash::decode::Error), #[error("{symref:?} could not be parsed. A symref is expected to look like :.")] From de1716ab2191f05d9342800ddd7b1d482781e2da Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 20 Aug 2026 17:07:10 +0200 Subject: [PATCH 02/51] change(gix-config-value)!: migrate errors to gix-error Rubber stamp --- Cargo.lock | 4 +- gix-config-value/Cargo.toml | 2 +- .../fuzz/fuzz_targets/fuzz_value.rs | 10 +- gix-config-value/src/boolean.rs | 12 +- gix-config-value/src/color.rs | 23 ++-- gix-config-value/src/integer.rs | 13 +- gix-config-value/src/lib.rs | 26 +--- gix-config-value/src/path.rs | 51 +++----- gix-config-value/tests/value/boolean.rs | 19 +-- gix-config-value/tests/value/color.rs | 114 ++++++++++-------- gix-config-value/tests/value/main.rs | 2 +- gix-config-value/tests/value/path.rs | 75 +++++++----- gix-config/Cargo.toml | 1 + gix-config/src/file/access/comfort.rs | 12 +- gix-config/src/file/access/read_only.rs | 20 +-- gix-config/src/file/includes/mod.rs | 23 ++-- gix-config/src/file/includes/types.rs | 4 +- gix-config/src/file/init/from_env.rs | 8 +- gix-config/src/file/init/types.rs | 8 +- .../tests/config/file/access/read_only.rs | 74 ++++++++---- .../init/from_paths/includes/unconditional.rs | 32 ++--- .../tests/config/file/init/from_paths/mod.rs | 6 +- gix-pathspec/Cargo.toml | 1 + gix-pathspec/src/defaults.rs | 8 +- gix-submodule/src/config.rs | 8 +- gix-tix/src/enrich.rs | 4 + gix/src/commit/mod.rs | 2 +- gix/src/commit/sign.rs | 2 +- gix/src/commit/verify.rs | 2 +- gix/src/config/cache/access.rs | 7 +- gix/src/config/mod.rs | 29 ++--- gix/src/config/snapshot/access.rs | 15 +-- gix/src/config/snapshot/credential_helpers.rs | 27 +---- gix/src/config/tree/keys.rs | 22 ++-- gix/src/config/tree/sections/checkout.rs | 10 +- gix/src/config/tree/sections/core.rs | 14 ++- gix/src/config/tree/sections/diff.rs | 11 +- gix/src/config/tree/sections/http.rs | 2 +- gix/src/config/tree/sections/pack.rs | 7 +- gix/src/config/tree/sections/protocol.rs | 3 +- gix/src/filter.rs | 7 +- gix/src/lib.rs | 32 ----- gix/src/mailmap.rs | 2 +- gix/src/object/tree/diff/mod.rs | 4 - gix/src/open/options.rs | 4 - gix/src/open/repository.rs | 5 +- gix/src/repository/config/transport.rs | 5 +- gix/src/repository/worktree.rs | 4 - gix/src/submodule/errors.rs | 2 +- gix/src/submodule/mod.rs | 26 ++-- gix/tests/gix/config/tree.rs | 19 ++- gix/tests/gix/remote/fetch.rs | 2 - .../config_snapshot/credential_helpers.rs | 36 +++++- gix/tests/gix/repository/config/identity.rs | 4 +- 54 files changed, 461 insertions(+), 404 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7af8bee06c3..b166e0452e0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1744,6 +1744,7 @@ dependencies = [ "document-features", "gix-config", "gix-config-value", + "gix-error", "gix-features", "gix-glob", "gix-path", @@ -1766,10 +1767,10 @@ dependencies = [ "bitflags 2.13.0", "bstr", "document-features", + "gix-error", "gix-path", "libc", "serde", - "thiserror 2.0.18", ] [[package]] @@ -2295,6 +2296,7 @@ dependencies = [ "bstr", "gix-attributes", "gix-config-value", + "gix-error", "gix-glob", "gix-path", "gix-testtools", diff --git a/gix-config-value/Cargo.toml b/gix-config-value/Cargo.toml index 730bc421b2e..2c93a2ee23a 100644 --- a/gix-config-value/Cargo.toml +++ b/gix-config-value/Cargo.toml @@ -20,8 +20,8 @@ serde = ["dep:serde", "bstr/serde"] [dependencies] gix-path = { version = "^0.12.4", path = "../gix-path" } +gix-error = { version = "^0.3.0", path = "../gix-error" } -thiserror = "2.0.18" bstr = { version = "1.12.0", default-features = false, features = ["std"] } serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"] } bitflags = "2" diff --git a/gix-config-value/fuzz/fuzz_targets/fuzz_value.rs b/gix-config-value/fuzz/fuzz_targets/fuzz_value.rs index f80d64ba319..664c2e5a712 100644 --- a/gix-config-value/fuzz/fuzz_targets/fuzz_value.rs +++ b/gix-config-value/fuzz/fuzz_targets/fuzz_value.rs @@ -24,19 +24,19 @@ struct Ctx<'a> { } fn fuzz(ctx: Ctx) -> Result<()> { - let b = Boolean::try_from(BStr::new(ctx.bool_str))?; + let b = Boolean::try_from(BStr::new(ctx.bool_str)).map_err(|err| err.into_error())?; _ = black_box(b.is_true()); - _ = black_box(Color::try_from(BStr::new(ctx.color_str)))?; + _ = black_box(Color::try_from(BStr::new(ctx.color_str))).map_err(|err| err.into_error())?; let mut buf = String::with_capacity(128); - let a = Attribute::from_str(ctx.attribute_str)?; + let a = Attribute::from_str(ctx.attribute_str).map_err(|err| err.into_error())?; _ = black_box(write!(&mut buf, "{a}")); - let name = Name::from_str(ctx.name_str)?; + let name = Name::from_str(ctx.name_str).map_err(|err| err.into_error())?; _ = black_box(write!(&mut buf, "{name}")); - let i = Integer::try_from(BStr::new(ctx.integer_str))?; + let i = Integer::try_from(BStr::new(ctx.integer_str)).map_err(|err| err.into_error())?; _ = black_box(i.to_decimal()); let p = Path::from(BStr::new(ctx.path_str)); diff --git a/gix-config-value/src/boolean.rs b/gix-config-value/src/boolean.rs index 444b929e827..4e0d440e1a9 100644 --- a/gix-config-value/src/boolean.rs +++ b/gix-config-value/src/boolean.rs @@ -1,11 +1,12 @@ use std::{borrow::Cow, ffi::OsString, fmt::Display}; use bstr::{BStr, BString}; +use gix_error::{ErrorExt, ResultExt, ValidationError}; use crate::{Boolean, Error, Integer}; -fn bool_err(input: impl Into) -> Error { - Error::new( +fn bool_err(input: impl Into) -> ValidationError { + ValidationError::new_with_input( "Booleans need to be 'no', 'off', 'false', '' or 'yes', 'on', 'true' or any number", input, ) @@ -15,8 +16,9 @@ impl TryFrom for Boolean { type Error = Error; fn try_from(value: OsString) -> Result { - let value = gix_path::os_str_into_bstr(&value) - .map_err(|_| Error::new("Illformed UTF-8", std::path::Path::new(&value).display().to_string()))?; + let value = gix_path::os_str_into_bstr(&value).or_raise(|| { + ValidationError::new_with_input("Illformed UTF-8", std::path::Path::new(&value).display().to_string()) + })?; Self::try_from(value) } } @@ -46,7 +48,7 @@ impl TryFrom<&BStr> for Boolean { } else if let Some(integer) = Integer::try_from(value).ok().and_then(|integer| integer.to_decimal()) { Ok(Boolean(integer != 0)) } else { - Err(bool_err(value)) + Err(bool_err(value).raise()) } } } diff --git a/gix-config-value/src/color.rs b/gix-config-value/src/color.rs index c8da8b2f30c..c6255f9f581 100644 --- a/gix-config-value/src/color.rs +++ b/gix-config-value/src/color.rs @@ -1,6 +1,7 @@ use std::{borrow::Cow, fmt::Display, str::FromStr}; use bstr::{BStr, BString}; +use gix_error::{ErrorExt, ResultExt, ValidationError}; use crate::{Color, Error}; @@ -30,8 +31,8 @@ impl Display for Color { } } -fn color_err(input: impl Into) -> Error { - Error::new( +fn color_err(input: impl Into) -> ValidationError { + ValidationError::new_with_input( "Colors are specific color values and their attributes, like 'brightred', or 'blue'", input, ) @@ -41,7 +42,7 @@ impl TryFrom<&BStr> for Color { type Error = Error; fn try_from(s: &BStr) -> Result { - let s = std::str::from_utf8(s).map_err(|err| color_err(s).with_err(err))?; + let s = std::str::from_utf8(s).or_raise(|| color_err(s))?; enum ColorItem { Value(Name), Attr(Attribute), @@ -71,12 +72,12 @@ impl TryFrom<&BStr> for Color { } else if background.is_none() { background = Some(v); } else { - return Err(color_err(s)); + return Err(color_err(s).raise()); } } ColorItem::Attr(a) => attributes |= a, }, - Err(_) => return Err(color_err(s)), + Err(_) => return Err(color_err(s).raise()), } } @@ -260,7 +261,7 @@ impl FromStr for Name { } if is_bright { - return Err(color_err(s)); + return Err(color_err(s).raise()); } if s.eq_ignore_ascii_case("normal") || s == "-1" { @@ -281,7 +282,7 @@ impl FromStr for Name { return Ok(Self::Rgb(r, g, b)); } - Err(color_err(s)) + Err(color_err(s).raise()) } } @@ -289,7 +290,7 @@ impl TryFrom<&BStr> for Name { type Error = Error; fn try_from(s: &BStr) -> Result { - Self::from_str(std::str::from_utf8(s).map_err(|err| color_err(s).with_err(err))?) + Self::from_str(std::str::from_utf8(s).or_raise(|| color_err(s))?) } } @@ -395,7 +396,7 @@ impl FromStr for Attribute { if s.eq_ignore_ascii_case("reset") { return if inverted { - Err(color_err(s)) + Err(color_err(s).raise()) } else { Ok(Attribute::RESET) }; @@ -416,7 +417,7 @@ impl FromStr for Attribute { "italic" if inverted => Ok(Attribute::NO_ITALIC), "strike" if !inverted => Ok(Attribute::STRIKE), "strike" if inverted => Ok(Attribute::NO_STRIKE), - _ => Err(color_err(s)), + _ => Err(color_err(s).raise()), } } } @@ -425,6 +426,6 @@ impl TryFrom<&BStr> for Attribute { type Error = Error; fn try_from(s: &BStr) -> Result { - Self::from_str(std::str::from_utf8(s).map_err(|err| color_err(s).with_err(err))?) + Self::from_str(std::str::from_utf8(s).or_raise(|| color_err(s))?) } } diff --git a/gix-config-value/src/integer.rs b/gix-config-value/src/integer.rs index 81c7fd33e07..0d0cd265163 100644 --- a/gix-config-value/src/integer.rs +++ b/gix-config-value/src/integer.rs @@ -1,6 +1,7 @@ use std::{borrow::Cow, fmt::Display, str::FromStr}; use bstr::{BStr, BString}; +use gix_error::{ErrorExt, ResultExt, ValidationError}; use crate::{Error, Integer}; @@ -47,8 +48,8 @@ impl serde::Serialize for Integer { } } -fn int_err(input: impl Into) -> Error { - Error::new( +fn int_err(input: impl Into) -> ValidationError { + ValidationError::new_with_input( "Integers needs to be positive or negative numbers which may have a suffix like 1k, 42, or 50G", input, ) @@ -88,18 +89,18 @@ impl TryFrom<&BStr> for Integer { type Error = Error; fn try_from(s: &BStr) -> Result { - let s = std::str::from_utf8(s).map_err(|err| int_err(s).with_err(err))?; + let s = std::str::from_utf8(s).or_raise(|| int_err(s))?; if let Some(value) = parse_like_git(s) { return Ok(Self { value, suffix: None }); } if s.len() <= 1 { - return Err(int_err(s)); + return Err(int_err(s).raise()); } let last_idx = s.len() - 1; if !s.is_char_boundary(last_idx) { - return Err(int_err(s)); + return Err(int_err(s).raise()); } let (number, suffix) = s.split_at(s.len() - 1); @@ -109,7 +110,7 @@ impl TryFrom<&BStr> for Integer { suffix: Some(suffix), }) } else { - Err(int_err(s)) + Err(int_err(s).raise()) } } } diff --git a/gix-config-value/src/lib.rs b/gix-config-value/src/lib.rs index 763661a7e55..5c3193eff53 100644 --- a/gix-config-value/src/lib.rs +++ b/gix-config-value/src/lib.rs @@ -26,31 +26,7 @@ #![deny(missing_docs, unsafe_code)] /// The error returned when any config value couldn't be instantiated due to malformed input. -#[derive(Debug, thiserror::Error, Eq, PartialEq)] -#[expect(missing_docs)] -#[error("Could not decode '{input}': {message}")] -pub struct Error { - pub message: &'static str, - pub input: bstr::BString, - #[source] - pub utf8_err: Option, -} - -impl Error { - /// Create a new value error from `message`, with `input` being what's causing the error. - pub fn new(message: &'static str, input: impl Into) -> Self { - Error { - message, - input: input.into(), - utf8_err: None, - } - } - - pub(crate) fn with_err(mut self, err: std::str::Utf8Error) -> Self { - self.utf8_err = Some(err); - self - } -} +pub type Error = gix_error::Exn; mod boolean; /// Color value parsing and the supported color names and attributes. diff --git a/gix-config-value/src/path.rs b/gix-config-value/src/path.rs index f26fe5d872c..d3d1003b9bd 100644 --- a/gix-config-value/src/path.rs +++ b/gix-config-value/src/path.rs @@ -1,6 +1,7 @@ use std::{borrow::Cow, path::PathBuf}; use bstr::{BStr, BString, ByteSlice}; +use gix_error::{ErrorExt, NotFoundError, OptionExt, ResultExt, ValidationError}; use crate::Path; @@ -31,20 +32,7 @@ pub mod interpolate { } /// The error returned by [`Path::interpolate()`][crate::Path::interpolate()]. - #[derive(Debug, thiserror::Error)] - #[expect(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: gix_path::Utf8Error, - }, - #[error("Ill-formed UTF-8 in username")] - UsernameConversion(#[from] std::str::Utf8Error), - } + pub type Error = gix_error::Exn; /// Obtain the home directory for the given user `name` or return `None` if the user wasn't found /// or any other error occurred. @@ -168,21 +156,20 @@ impl Path { }: interpolate::Context<'_>, ) -> Result { if self.is_empty() { - return Err(interpolate::Error::Missing { what: "path" }); + return Err(NotFoundError::new("path is missing").raise_erased()); } const PREFIX: &[u8] = b"%(prefix)/"; if self.starts_with(PREFIX) { - let git_install_dir = git_install_dir.ok_or(interpolate::Error::Missing { - what: "git install dir", - })?; + let git_install_dir = + git_install_dir.ok_or_raise_erased(|| NotFoundError::new("git install dir is missing"))?; let (_prefix, path_without_trailing_slash) = self.split_at(PREFIX.len()); let path_without_trailing_slash = - gix_path::try_from_bstring(path_without_trailing_slash).map_err(|err| { - interpolate::Error::Utf8Conversion { - what: "path past %(prefix)", - err, - } + gix_path::try_from_bstring(path_without_trailing_slash).or_raise_erased(|| { + ValidationError::new_with_input( + "Ill-formed UTF-8 in path past %(prefix)", + path_without_trailing_slash, + ) })?; Ok(git_install_dir.join(path_without_trailing_slash)) } else if let Some(val) = self.strip_prefix(b"~") { @@ -193,7 +180,7 @@ impl Path { let (mut home, what) = if username.is_empty() { ( home_dir - .ok_or(interpolate::Error::Missing { what: "home dir" })? + .ok_or_raise_erased(|| NotFoundError::new("home dir is missing"))? .to_path_buf(), "path past ~/", ) @@ -201,18 +188,15 @@ impl Path { ( Self::home_for_username( username, - home_for_user.ok_or(interpolate::Error::Missing { - what: "home for user lookup", - })?, + home_for_user.ok_or_raise_erased(|| NotFoundError::new("home for user lookup is missing"))?, )?, "path past ~user/", ) }; if let Some(path) = path { - home.push( - gix_path::try_from_byte_slice(path) - .map_err(|err| interpolate::Error::Utf8Conversion { what, err })?, - ); + home.push(gix_path::try_from_byte_slice(path).or_raise_erased(|| { + ValidationError::new_with_input(format!("Ill-formed UTF-8 in {what}"), path) + })?); } Ok(home) } else { @@ -224,7 +208,8 @@ impl Path { username: &[u8], home_for_user: fn(&str) -> Option, ) -> Result { - let username = std::str::from_utf8(username)?; - home_for_user(username).ok_or(interpolate::Error::Missing { what: "pwd user info" }) + let username = std::str::from_utf8(username) + .or_raise_erased(|| ValidationError::new_with_input("Ill-formed UTF-8 in username", username))?; + home_for_user(username).ok_or_raise_erased(|| NotFoundError::new("pwd user info is missing")) } } diff --git a/gix-config-value/tests/value/boolean.rs b/gix-config-value/tests/value/boolean.rs index f201dcbcedc..d51ea991931 100644 --- a/gix-config-value/tests/value/boolean.rs +++ b/gix-config-value/tests/value/boolean.rs @@ -22,9 +22,9 @@ fn from_str_false() -> crate::Result { #[test] fn from_str_true() -> crate::Result { - assert_eq!(Boolean::try_from("yes").map(Into::into), Ok(true)); - assert_eq!(Boolean::try_from("on"), Ok(Boolean(true))); - assert_eq!(Boolean::try_from("true"), Ok(Boolean(true))); + assert!(Boolean::try_from("yes")?.0); + assert!(Boolean::try_from("on")?.0); + assert!(Boolean::try_from("true")?.0); assert!(Boolean::try_from("1")?.0); assert!(Boolean::try_from("+10")?.0); assert!(Boolean::try_from("-1")?.0); @@ -35,14 +35,16 @@ fn from_str_true() -> crate::Result { fn ignores_case() { // Random subset for word in &["no", "yes", "on", "off", "true", "false"] { - let first: bool = Boolean::try_from(*word).unwrap().into(); - let second: bool = Boolean::try_from(word.to_uppercase().as_str()).unwrap().into(); + let first: bool = Boolean::try_from(*word).expect("valid boolean").into(); + let second: bool = Boolean::try_from(word.to_uppercase().as_str()) + .expect("valid boolean") + .into(); assert_eq!(first, second); } } #[test] -fn numbers_are_parsed_as_integers() { +fn numbers_are_parsed_as_integers() -> crate::Result { // Use the same bases, suffixes, and full `i64` range as `Integer`. for (input, expected) in [ ("0x10", true), @@ -68,11 +70,12 @@ fn numbers_are_parsed_as_integers() { ("-8589934592g", true), // i64::MIN after applying the suffix ] { assert_eq!( - Boolean::try_from(input).map(Into::into), - Ok(expected), + Boolean::try_from(input)?.0, + expected, "{input:?}: zero integers are false and nonzero integers are true" ); } + Ok(()) } #[test] diff --git a/gix-config-value/tests/value/color.rs b/gix-config-value/tests/value/color.rs index 3fea7d4d5f0..eace46a7f5b 100644 --- a/gix-config-value/tests/value/color.rs +++ b/gix-config-value/tests/value/color.rs @@ -15,31 +15,35 @@ mod name { use gix_config_value::color::Name; + fn name(input: &str) -> Name { + Name::from_str(input).expect("valid color name") + } + #[test] fn non_bright() { - assert_eq!(Name::from_str("normal"), Ok(Name::Normal)); - assert_eq!(Name::from_str("-1"), Ok(Name::Normal)); - assert_eq!(Name::from_str("default"), Ok(Name::Default)); - assert_eq!(Name::from_str("black"), Ok(Name::Black)); - assert_eq!(Name::from_str("red"), Ok(Name::Red)); - assert_eq!(Name::from_str("green"), Ok(Name::Green)); - assert_eq!(Name::from_str("yellow"), Ok(Name::Yellow)); - assert_eq!(Name::from_str("blue"), Ok(Name::Blue)); - assert_eq!(Name::from_str("magenta"), Ok(Name::Magenta)); - assert_eq!(Name::from_str("cyan"), Ok(Name::Cyan)); - assert_eq!(Name::from_str("white"), Ok(Name::White)); + assert_eq!(name("normal"), Name::Normal); + assert_eq!(name("-1"), Name::Normal); + assert_eq!(name("default"), Name::Default); + assert_eq!(name("black"), Name::Black); + assert_eq!(name("red"), Name::Red); + assert_eq!(name("green"), Name::Green); + assert_eq!(name("yellow"), Name::Yellow); + assert_eq!(name("blue"), Name::Blue); + assert_eq!(name("magenta"), Name::Magenta); + assert_eq!(name("cyan"), Name::Cyan); + assert_eq!(name("white"), Name::White); } #[test] fn bright() { - assert_eq!(Name::from_str("brightblack"), Ok(Name::BrightBlack)); - assert_eq!(Name::from_str("brightred"), Ok(Name::BrightRed)); - assert_eq!(Name::from_str("brightgreen"), Ok(Name::BrightGreen)); - assert_eq!(Name::from_str("brightyellow"), Ok(Name::BrightYellow)); - assert_eq!(Name::from_str("brightblue"), Ok(Name::BrightBlue)); - assert_eq!(Name::from_str("brightmagenta"), Ok(Name::BrightMagenta)); - assert_eq!(Name::from_str("brightcyan"), Ok(Name::BrightCyan)); - assert_eq!(Name::from_str("brightwhite"), Ok(Name::BrightWhite)); + assert_eq!(name("brightblack"), Name::BrightBlack); + assert_eq!(name("brightred"), Name::BrightRed); + assert_eq!(name("brightgreen"), Name::BrightGreen); + assert_eq!(name("brightyellow"), Name::BrightYellow); + assert_eq!(name("brightblue"), Name::BrightBlue); + assert_eq!(name("brightmagenta"), Name::BrightMagenta); + assert_eq!(name("brightcyan"), Name::BrightCyan); + assert_eq!(name("brightwhite"), Name::BrightWhite); } #[test] @@ -53,8 +57,8 @@ mod name { ("BRIGHTWHITE", Name::BrightWhite), ] { assert_eq!( - Name::from_str(input), - Ok(expected), + name(input), + expected, "{input:?}: color names and the 'bright' prefix are case-insensitive, like in Git" ); } @@ -72,16 +76,16 @@ mod name { #[test] fn ansi() { - assert_eq!(Name::from_str("255"), Ok(Name::Ansi(255))); - assert_eq!(Name::from_str("0"), Ok(Name::Ansi(0))); + assert_eq!(name("255"), Name::Ansi(255)); + assert_eq!(name("0"), Name::Ansi(0)); } #[test] fn hex() { - assert_eq!(Name::from_str("#ff0010"), Ok(Name::Rgb(255, 0, 16))); - assert_eq!(Name::from_str("#ffffff"), Ok(Name::Rgb(255, 255, 255))); - assert_eq!(Name::from_str("#000000"), Ok(Name::Rgb(0, 0, 0))); - assert_eq!(Name::from_str("#FF0010"), Ok(Name::Rgb(255, 0, 16))); + assert_eq!(name("#ff0010"), Name::Rgb(255, 0, 16)); + assert_eq!(name("#ffffff"), Name::Rgb(255, 255, 255)); + assert_eq!(name("#000000"), Name::Rgb(0, 0, 0)); + assert_eq!(name("#FF0010"), Name::Rgb(255, 0, 16)); } #[test] @@ -95,15 +99,15 @@ mod name { ("#fff", Name::Rgb(0xff, 0xff, 0xff), "#ffffff"), ("#aBc", Name::Rgb(0xaa, 0xbb, 0xcc), "#aabbcc"), ] { - let actual = Name::from_str(input); - assert_eq!(actual, Ok(expected), "{input:?}"); + let actual = name(input); + assert_eq!(actual, expected, "{input:?}"); assert_eq!( actual, - Name::from_str(long_form), + name(long_form), "{input:?}: the shorthand and the long form it stands for are the same color" ); assert_eq!( - actual.expect("the shorthand parses, as asserted above").to_string(), + actual.to_string(), long_form, "{input:?}: a shorthand renders back as the long form, since `Name::Rgb` keeps no record of which spelling it came from" ); @@ -144,38 +148,42 @@ mod attribute { use gix_config_value::color::Attribute; + fn attribute(input: &str) -> Attribute { + Attribute::from_str(input).expect("valid color attribute") + } + #[test] fn non_inverted() { - assert_eq!(Attribute::from_str("reset"), Ok(Attribute::RESET)); - assert_eq!(Attribute::from_str("bold"), Ok(Attribute::BOLD)); - assert_eq!(Attribute::from_str("dim"), Ok(Attribute::DIM)); - assert_eq!(Attribute::from_str("ul"), Ok(Attribute::UL)); - assert_eq!(Attribute::from_str("blink"), Ok(Attribute::BLINK)); - assert_eq!(Attribute::from_str("reverse"), Ok(Attribute::REVERSE)); - assert_eq!(Attribute::from_str("italic"), Ok(Attribute::ITALIC)); - assert_eq!(Attribute::from_str("strike"), Ok(Attribute::STRIKE)); + assert_eq!(attribute("reset"), Attribute::RESET); + assert_eq!(attribute("bold"), Attribute::BOLD); + assert_eq!(attribute("dim"), Attribute::DIM); + assert_eq!(attribute("ul"), Attribute::UL); + assert_eq!(attribute("blink"), Attribute::BLINK); + assert_eq!(attribute("reverse"), Attribute::REVERSE); + assert_eq!(attribute("italic"), Attribute::ITALIC); + assert_eq!(attribute("strike"), Attribute::STRIKE); } #[test] fn inverted_no_dash() { - assert_eq!(Attribute::from_str("nobold"), Ok(Attribute::NO_BOLD)); - assert_eq!(Attribute::from_str("nodim"), Ok(Attribute::NO_DIM)); - assert_eq!(Attribute::from_str("noul"), Ok(Attribute::NO_UL)); - assert_eq!(Attribute::from_str("noblink"), Ok(Attribute::NO_BLINK)); - assert_eq!(Attribute::from_str("noreverse"), Ok(Attribute::NO_REVERSE)); - assert_eq!(Attribute::from_str("noitalic"), Ok(Attribute::NO_ITALIC)); - assert_eq!(Attribute::from_str("nostrike"), Ok(Attribute::NO_STRIKE)); + assert_eq!(attribute("nobold"), Attribute::NO_BOLD); + assert_eq!(attribute("nodim"), Attribute::NO_DIM); + assert_eq!(attribute("noul"), Attribute::NO_UL); + assert_eq!(attribute("noblink"), Attribute::NO_BLINK); + assert_eq!(attribute("noreverse"), Attribute::NO_REVERSE); + assert_eq!(attribute("noitalic"), Attribute::NO_ITALIC); + assert_eq!(attribute("nostrike"), Attribute::NO_STRIKE); } #[test] fn inverted_dashed() { - assert_eq!(Attribute::from_str("no-bold"), Ok(Attribute::NO_BOLD)); - assert_eq!(Attribute::from_str("no-dim"), Ok(Attribute::NO_DIM)); - assert_eq!(Attribute::from_str("no-ul"), Ok(Attribute::NO_UL)); - assert_eq!(Attribute::from_str("no-blink"), Ok(Attribute::NO_BLINK)); - assert_eq!(Attribute::from_str("no-reverse"), Ok(Attribute::NO_REVERSE)); - assert_eq!(Attribute::from_str("no-italic"), Ok(Attribute::NO_ITALIC)); - assert_eq!(Attribute::from_str("no-strike"), Ok(Attribute::NO_STRIKE)); + assert_eq!(attribute("no-bold"), Attribute::NO_BOLD); + assert_eq!(attribute("no-dim"), Attribute::NO_DIM); + assert_eq!(attribute("no-ul"), Attribute::NO_UL); + assert_eq!(attribute("no-blink"), Attribute::NO_BLINK); + assert_eq!(attribute("no-reverse"), Attribute::NO_REVERSE); + assert_eq!(attribute("no-italic"), Attribute::NO_ITALIC); + assert_eq!(attribute("no-strike"), Attribute::NO_STRIKE); } #[test] diff --git a/gix-config-value/tests/value/main.rs b/gix-config-value/tests/value/main.rs index 937346a2774..d554c601782 100644 --- a/gix-config-value/tests/value/main.rs +++ b/gix-config-value/tests/value/main.rs @@ -1,4 +1,4 @@ -type Result = std::result::Result>; +type Result = gix_error::Result; mod boolean; mod color; diff --git a/gix-config-value/tests/value/path.rs b/gix-config-value/tests/value/path.rs index be09051ed51..fd7b34590fe 100644 --- a/gix-config-value/tests/value/path.rs +++ b/gix-config-value/tests/value/path.rs @@ -1,7 +1,9 @@ mod interpolate { use std::path::{Path, PathBuf}; + use bstr::BString; use gix_config_value::path; + use gix_error::{NotFoundError, ValidationError}; #[test] fn backslash_is_not_special_and_they_are_not_escaping_anything() -> crate::Result { @@ -14,10 +16,8 @@ mod interpolate { #[test] fn empty_path_is_error() { - assert!(matches!( - interpolate_without_context(""), - Err(path::interpolate::Error::Missing { what: "path" }) - )); + let err = interpolate_without_context("").expect_err("empty paths are invalid"); + assert!(err.downcast_any_ref::().is_some()); } #[test] @@ -32,7 +32,7 @@ mod interpolate { git_install_dir: Path::new(git_install_dir).into(), ..Default::default() }) - .unwrap(), + .expect("valid interpolation"), expected, "prefix interpolation keeps separators as they are" ); @@ -50,33 +50,35 @@ mod interpolate { git_install_dir: Path::new(git_install_dir).into(), ..Default::default() }) - .unwrap(), + .expect("valid interpolation"), Path::new(path) ); } #[test] fn tilde_alone_substitutes_current_user() -> crate::Result { - let home = std::env::current_dir()?; + let home = std::env::current_dir().expect("current directory is available"); assert_eq!( - gix_config_value::Path::from("~") - .interpolate(path::interpolate::Context { - home_dir: Some(&home), - ..Default::default() - }) - .unwrap(), + gix_config_value::Path::from("~").interpolate(path::interpolate::Context { + home_dir: Some(&home), + ..Default::default() + })?, home ); - assert!(matches!( - interpolate_without_context("~"), - Err(path::interpolate::Error::Missing { what: "home dir" }) - )); + let err = interpolate_without_context("~").expect_err("tilde expansion needs the current user's home"); + assert_eq!( + err.downcast_any_ref::() + .expect("missing home directories are classified as not found") + .to_string(), + "home dir is missing", + "tilde expansion reports the missing home directory" + ); Ok(()) } #[test] fn tilde_slash_substitutes_current_user() -> crate::Result { - let home = std::env::current_dir()?; + let home = std::env::current_dir().expect("current directory is available"); for suffix in ["", "user/bar", r"user\bar", "/user/bar"] { let actual = gix_config_value::Path::from(format!("~/{suffix}").as_str()).interpolate( path::interpolate::Context { @@ -96,7 +98,7 @@ mod interpolate { #[test] fn tilde_with_given_user() -> crate::Result { - let home = std::env::current_dir()?; + let home = std::env::current_dir().expect("current directory is available"); for path_suffix in &["foo/bar", r"foo\bar", ""] { let path = format!("~user/{path_suffix}"); @@ -114,17 +116,31 @@ mod interpolate { home.join("user"), "~user without trailing slash is expanded like git does" ); - assert!(matches!( - interpolate_without_context("~nonexistent"), - Err(path::interpolate::Error::Missing { what: "pwd user info" }) - )); - assert!(matches!( - interpolate_without_context("~nonexistent/foo"), - Err(path::interpolate::Error::Missing { what: "pwd user info" }) - )); + for path in ["~nonexistent", "~nonexistent/foo"] { + let err = interpolate_without_context(path).expect_err("the named user does not exist"); + assert_eq!( + err.downcast_any_ref::() + .expect("missing users are classified as not found") + .to_string(), + "pwd user info is missing", + "named-user expansion reports the missing user" + ); + } Ok(()) } + #[test] + fn malformed_usernames_are_validation_errors_with_the_utf8_cause() { + let err = gix_config_value::Path::from(BString::from(vec![b'~', 0xff, b'/', b'x'])) + .interpolate(path::interpolate::Context { + home_for_user: Some(home_for_user), + ..Default::default() + }) + .expect_err("the username is not UTF-8"); + assert!(err.downcast_any_ref::().is_some()); + assert!(err.downcast_any_ref::().is_some()); + } + fn interpolate_without_context( path: impl AsRef, ) -> Result { @@ -138,7 +154,10 @@ mod interpolate { if name == "nonexistent" { return None; } - std::env::current_dir().unwrap().join(name).into() + std::env::current_dir() + .expect("current directory is available") + .join(name) + .into() } } diff --git a/gix-config/Cargo.toml b/gix-config/Cargo.toml index 3a99d2aba68..f280ee1e86d 100644 --- a/gix-config/Cargo.toml +++ b/gix-config/Cargo.toml @@ -24,6 +24,7 @@ serde = ["dep:serde", "bstr/serde", "gix-sec/serde", "gix-ref/serde", "gix-glob/ [dependencies] gix-features = { version = "^0.49.1", path = "../gix-features" } gix-config-value = { version = "^0.19.1", path = "../gix-config-value" } +gix-error = { version = "^0.3.0", path = "../gix-error" } gix-path = { version = "^0.12.6", path = "../gix-path" } gix-sec = { version = "^0.14.2", path = "../gix-sec" } gix-ref = { version = "^0.67.0", path = "../gix-ref" } diff --git a/gix-config/src/file/access/comfort.rs b/gix-config/src/file/access/comfort.rs index 9ba42d581f0..1066d54e9ee 100644 --- a/gix-config/src/file/access/comfort.rs +++ b/gix-config/src/file/access/comfort.rs @@ -1,4 +1,5 @@ use bstr::{BStr, BString}; +use gix_error::{ErrorExt, ValidationError}; use crate::{AsBStrOpt, AsKey, File, file::Metadata, value}; @@ -183,7 +184,10 @@ impl File { return Ok(None); }; crate::Integer::try_from(BStr::new(&int)) - .and_then(|b| b.to_decimal().ok_or_else(|| value::Error::new("Integer overflow", int))) + .and_then(|b| { + b.to_decimal() + .ok_or_else(|| ValidationError::new_with_input("Integer overflow", int).raise()) + }) .map(Some) } @@ -267,8 +271,10 @@ impl File { values .into_iter() .map(|v| { - crate::Integer::try_from(BStr::new(&v)) - .and_then(|int| int.to_decimal().ok_or_else(|| value::Error::new("Integer overflow", v))) + crate::Integer::try_from(BStr::new(&v)).and_then(|int| { + int.to_decimal() + .ok_or_else(|| ValidationError::new_with_input("Integer overflow", v).raise()) + }) }) .collect::, _>>() .map(Some) diff --git a/gix-config/src/file/access/read_only.rs b/gix-config/src/file/access/read_only.rs index 10fbfc45f5b..17f6d263e4a 100644 --- a/gix-config/src/file/access/read_only.rs +++ b/gix-config/src/file/access/read_only.rs @@ -37,12 +37,12 @@ impl File { /// a = 10k /// c = false /// "#; - /// let git_config = gix_config::File::try_from(config)?; + /// let git_config = gix_config::File::try_from(config).expect("valid config"); /// // You can either use the turbofish to determine the type... /// let a_value = git_config.value::("core.a")?; /// // ... or explicitly declare the type to avoid the turbofish /// let c_value: Boolean = git_config.value("core.c")?; - /// # Ok::<(), Box>(()) + /// # Ok::<(), gix_config::lookup::Error>(()) /// ``` pub fn value>(&self, key: impl AsKey) -> Result> { let key = key.as_key(); @@ -71,12 +71,12 @@ impl File { /// a = 10k /// c = false /// "#; - /// let git_config = gix_config::File::try_from(config)?; + /// let git_config = gix_config::File::try_from(config).expect("valid config"); /// // You can either use the turbofish to determine the type... /// let a_value = git_config.value_by::("core", None, "a")?; /// // ... or explicitly declare the type to avoid the turbofish /// let c_value: Boolean = git_config.value_by("core", None, "c")?; - /// # Ok::<(), Box>(()) + /// # Ok::<(), gix_config::lookup::Error>(()) /// ``` pub fn value_by>( &self, @@ -162,7 +162,7 @@ impl File { /// a /// a = false /// "#; - /// let git_config = gix_config::File::try_from(config).unwrap(); + /// let git_config = gix_config::File::try_from(config).expect("valid config"); /// // You can either use the turbofish to determine the type... /// let a_value = git_config.values::("core.a")?; /// assert_eq!( @@ -174,9 +174,9 @@ impl File { /// ] /// ); /// // ... or explicitly declare the type to avoid the turbofish - /// let c_value: Vec = git_config.values("core.c").unwrap(); + /// let c_value: Vec = git_config.values("core.c")?; /// assert_eq!(c_value, vec![Boolean(false)]); - /// # Ok::<(), Box>(()) + /// # Ok::<(), gix_config::lookup::Error>(()) /// ``` /// /// [`value`]: crate::value @@ -216,7 +216,7 @@ impl File { /// a /// a = false /// "#; - /// let git_config = gix_config::File::try_from(config).unwrap(); + /// let git_config = gix_config::File::try_from(config).expect("valid config"); /// // You can either use the turbofish to determine the type... /// let a_value = git_config.values_by::("core", None, "a")?; /// assert_eq!( @@ -228,9 +228,9 @@ impl File { /// ] /// ); /// // ... or explicitly declare the type to avoid the turbofish - /// let c_value: Vec = git_config.values_by("core", None, "c").unwrap(); + /// let c_value: Vec = git_config.values_by("core", None, "c")?; /// assert_eq!(c_value, vec![Boolean(false)]); - /// # Ok::<(), Box>(()) + /// # Ok::<(), gix_config::lookup::Error>(()) /// ``` /// /// [`value`]: crate::value diff --git a/gix-config/src/file/includes/mod.rs b/gix-config/src/file/includes/mod.rs index 073f4ee8793..aec8f06eef3 100644 --- a/gix-config/src/file/includes/mod.rs +++ b/gix-config/src/file/includes/mod.rs @@ -1,6 +1,7 @@ use std::path::{Path, PathBuf}; use bstr::{BStr, BString, ByteSlice, ByteVec}; +use gix_error::ValidationError; use gix_features::threading::OwnShared; use gix_ref::Category; @@ -250,7 +251,9 @@ fn gitdir_matches( let mut pattern_path = match check_interpolation_result( err_on_interpolation_failure, crate::Path::from(condition_path.to_owned()).interpolate(context), - )? { + ) + .map_err(|err| Error::Interpolate(err.into_error()))? + { Some(path) => gix_path::into_bstr(path).into_owned(), // Git keeps the original condition pattern when interpolation fails. None => condition_path.to_owned(), @@ -290,9 +293,9 @@ fn gitdir_matches( return Ok(true); } - let expanded_git_dir = gix_path::to_unix_separators_on_windows(gix_path::into_bstr(gix_path::realpath( - gix_path::from_byte_slice(&git_dir), - )?)); + let expanded_git_dir = gix_path::to_unix_separators_on_windows(gix_path::into_bstr( + gix_path::realpath(gix_path::from_byte_slice(&git_dir)).map_err(|err| Error::Realpath(err.into_error()))?, + )); Ok(gix_glob::wildmatch( pattern_path.as_bstr(), expanded_git_dir.as_ref(), @@ -309,12 +312,8 @@ fn check_interpolation_result( } match res { Ok(good) => Ok(Some(good.into())), - Err(err) => match err { - path::interpolate::Error::Missing { .. } => Ok(None), - path::interpolate::Error::UsernameConversion(_) | path::interpolate::Error::Utf8Conversion { .. } => { - Err(err) - } - }, + Err(err) if err.downcast_any_ref::().is_some() => Err(err), + Err(_) => Ok(None), } } @@ -328,7 +327,9 @@ fn resolve_path( .. }: includes::Options<'_>, ) -> Result, Error> { - let path = match check_interpolation_result(err_on_interpolation_failure, path.interpolate(context))? { + let path = match check_interpolation_result(err_on_interpolation_failure, path.interpolate(context)) + .map_err(|err| Error::Interpolate(err.into_error()))? + { Some(p) => p, None => return Ok(None), }; diff --git a/gix-config/src/file/includes/types.rs b/gix-config/src/file/includes/types.rs index 66aa6752b19..16496ad9d29 100644 --- a/gix-config/src/file/includes/types.rs +++ b/gix-config/src/file/includes/types.rs @@ -15,7 +15,7 @@ pub enum Error { #[error(transparent)] Span(#[from] parse::span::Error), #[error(transparent)] - Interpolate(#[from] interpolate::Error), + Interpolate(gix_error::Error), #[error("The maximum allowed length {} of the file include chain built by following nested resolve_includes is exceeded", .max_depth)] IncludeDepthExceeded { max_depth: u8 }, #[error("Include paths from environment variables must not be relative as no config file paths exists as root")] @@ -23,7 +23,7 @@ pub enum Error { #[error("The git directory must be provided to support `gitdir:` conditional includes")] MissingGitDir, #[error(transparent)] - Realpath(#[from] gix_path::realpath::Error), + Realpath(gix_error::Error), } /// Options to handle includes, like `include.path` or `includeIf..path`, diff --git a/gix-config/src/file/init/from_env.rs b/gix-config/src/file/init/from_env.rs index 32effaa2418..d8f16ff662a 100644 --- a/gix-config/src/file/init/from_env.rs +++ b/gix-config/src/file/init/from_env.rs @@ -17,7 +17,7 @@ pub enum Error { #[error("GIT_CONFIG_VALUE_{} was not set", .value_id)] InvalidValueId { value_id: usize }, #[error(transparent)] - PathInterpolationError(#[from] interpolate::Error), + PathInterpolationError(gix_error::Error), #[error(transparent)] Includes(#[from] init::includes::Error), #[error(transparent)] @@ -26,6 +26,12 @@ pub enum Error { SectionValue(#[from] file::section::value::Error), } +impl From for Error { + fn from(err: interpolate::Error) -> Self { + Error::PathInterpolationError(err.into_error()) + } +} + /// Instantiation from environment variables impl File { /// Generates a config from `GIT_CONFIG_*` environment variables or returns `Ok(None)` if no configuration was found. diff --git a/gix-config/src/file/init/types.rs b/gix-config/src/file/init/types.rs index c1b4f32e518..2fdb38f5a51 100644 --- a/gix-config/src/file/init/types.rs +++ b/gix-config/src/file/init/types.rs @@ -7,13 +7,19 @@ pub enum Error { #[error(transparent)] Parse(#[from] parse::Error), #[error(transparent)] - Interpolate(#[from] interpolate::Error), + Interpolate(gix_error::Error), #[error(transparent)] Includes(#[from] init::includes::Error), #[error(transparent)] Span(#[from] parse::span::Error), } +impl From for Error { + fn from(err: interpolate::Error) -> Self { + Error::Interpolate(err.into_error()) + } +} + /// Options when loading git config using [`File::from_paths_metadata()`][crate::File::from_paths_metadata()]. #[derive(Clone, Copy, Default)] pub struct Options<'a> { diff --git a/gix-config/tests/config/file/access/read_only.rs b/gix-config/tests/config/file/access/read_only.rs index 4bd3fabd4c1..84d37c16e22 100644 --- a/gix-config/tests/config/file/access/read_only.rs +++ b/gix-config/tests/config/file/access/read_only.rs @@ -9,6 +9,13 @@ use gix_config::{ use crate::file::bstring; +fn lookup_error(err: gix_config::lookup::Error>) -> gix_error::Error { + match err { + gix_config::lookup::Error::ValueMissing(err) => gix_error::Error::from_error(err), + gix_config::lookup::Error::FailedConversion(err) => err.into_error(), + } +} + #[test] fn parsed_section_header_legacy_check_uses_backing_buffer() -> crate::Result { let config = File::try_from( @@ -53,7 +60,7 @@ fn get_value_for_all_provided_values() -> crate::Result { }, )?; - assert!(!config.value::("core.bool-explicit")?.0); + assert!(!config.value::("core.bool-explicit").map_err(lookup_error)?.0); assert!(!config.boolean("core.bool-explicit")?.expect("exists")); assert!(!config.boolean("core.bool-explicit")?.expect("exists")); @@ -105,7 +112,9 @@ fn get_value_for_all_provided_values() -> crate::Result { assert_eq!(config.string("doesn't.exist"), None); assert_eq!( - config.value::("core.integer-no-prefix")?, + config + .value::("core.integer-no-prefix") + .map_err(lookup_error)?, Integer { value: 10, suffix: None @@ -113,7 +122,9 @@ fn get_value_for_all_provided_values() -> crate::Result { ); assert_eq!( - config.value::("core.integer-no-prefix")?, + config + .value::("core.integer-no-prefix") + .map_err(lookup_error)?, Integer { value: 10, suffix: None @@ -121,7 +132,7 @@ fn get_value_for_all_provided_values() -> crate::Result { ); assert_eq!( - config.value::("core.integer-prefix")?, + config.value::("core.integer-prefix").map_err(lookup_error)?, Integer { value: 10, suffix: Some(integer::Suffix::Gibi), @@ -129,7 +140,7 @@ fn get_value_for_all_provided_values() -> crate::Result { ); assert_eq!( - config.value::("core.color")?, + config.value::("core.color").map_err(lookup_error)?, Color { foreground: Some(color::Name::BrightGreen), background: Some(color::Name::Red), @@ -201,7 +212,7 @@ fn get_value_looks_up_all_sections_before_failing() -> crate::Result { // Checks that we check the last entry first still assert!( - !file.value::("core.bool-implicit")?.0, + !file.value::("core.bool-implicit").map_err(lookup_error)?.0, "implicit bool is invisible to `value` and boolean is the only value we want. Would have to special case it." ); assert!( @@ -210,7 +221,7 @@ fn get_value_looks_up_all_sections_before_failing() -> crate::Result { ); assert!( - !file.value::("core.bool-explicit")?.0, + !file.value::("core.bool-explicit").map_err(lookup_error)?.0, "explicit values always work" ); @@ -228,20 +239,27 @@ fn interpreted_values_can_be_returned_with_their_sections() -> crate::Result { )?; let section_ids: Vec<_> = file.sections().map(|section| section.id()).collect(); - let (value, section) = file.value_with_section::("core.a")?; + let (value, section) = file.value_with_section::("core.a").map_err(lookup_error)?; assert_eq!(value.value, 3); assert_eq!(section.id(), section_ids[1]); - let values = file.values_with_sections::("core.a")?; + let values = file.values_with_sections::("core.a").map_err(lookup_error)?; let actual: Vec<_> = values .into_iter() .map(|(value, section)| (value.value, section.id())) .collect(); assert_eq!(actual, [(1, section_ids[0]), (2, section_ids[0]), (3, section_ids[1])]); - let (value, section) = file.value_with_section_by::("core", None, "a")?; + let (value, section) = file + .value_with_section_by::("core", None, "a") + .map_err(lookup_error)?; assert_eq!((value.value, section.id()), (3, section_ids[1])); - assert_eq!(file.values_with_sections_by::("core", None, "a")?.len(), 3); + assert_eq!( + file.values_with_sections_by::("core", None, "a") + .map_err(lookup_error)? + .len(), + 3 + ); Ok(()) } @@ -250,8 +268,8 @@ fn section_names_are_case_insensitive() -> crate::Result { let config = "[core] a=true"; let file = File::try_from(config)?; assert_eq!( - file.value::("core.a").unwrap(), - file.value::("CORE.a").unwrap() + file.value::("core.a").map_err(lookup_error)?, + file.value::("CORE.a").map_err(lookup_error)? ); Ok(()) @@ -263,10 +281,10 @@ fn value_names_are_case_insensitive() -> crate::Result { a = true A = false"; let file = File::try_from(config)?; - assert_eq!(file.values::("core.a")?.len(), 2); + assert_eq!(file.values::("core.a").map_err(lookup_error)?.len(), 2); assert_eq!( - file.value::("core.a").unwrap(), - file.value::("core.A").unwrap() + file.value::("core.a").map_err(lookup_error)?, + file.value::("core.A").map_err(lookup_error)? ); Ok(()) @@ -522,8 +540,12 @@ fn overrides_with_implicit_booleans_work_in_single_section() { b = false b "#; - let config = File::try_from(config).unwrap(); - assert_eq!(config.boolean("a.b"), Ok(Some(true)), "empty implicit booleans "); + let config = File::try_from(config).expect("valid config"); + assert_eq!( + config.boolean("a.b").expect("valid boolean"), + Some(true), + "empty implicit booleans " + ); } #[test] @@ -539,8 +561,8 @@ fn implicit_booleans_may_be_followed_by_whitespace() -> crate::Result { ] { let file = File::try_from(config)?; assert_eq!( - file.boolean("a.b"), - Ok(Some(true)), + file.boolean("a.b")?, + Some(true), "Git sees no separator in {config:?}, so the value is an implicit boolean and thus true" ); assert_eq!( @@ -553,8 +575,8 @@ fn implicit_booleans_may_be_followed_by_whitespace() -> crate::Result { for config in ["[a]\n\tb =\n", "[a]\n\tb = \n", "[a]\n\tb=\"\"\n", "[a]\n\tb ="] { let file = File::try_from(config)?; assert_eq!( - file.boolean("a.b"), - Ok(Some(false)), + file.boolean("a.b")?, + Some(false), "a separator in {config:?} makes the value explicitly empty, and an empty value is false" ); assert_eq!( @@ -575,6 +597,10 @@ fn overrides_with_implicit_booleans_work_across_sections() { [a] b "#; - let config = File::try_from(config).unwrap(); - assert_eq!(config.boolean("a.b"), Ok(Some(true)), "empty implicit booleans "); + let config = File::try_from(config).expect("valid config"); + assert_eq!( + config.boolean("a.b").expect("valid boolean"), + Some(true), + "empty implicit booleans " + ); } diff --git a/gix-config/tests/config/file/init/from_paths/includes/unconditional.rs b/gix-config/tests/config/file/init/from_paths/includes/unconditional.rs index 2a1697b5350..94f8c5a2b93 100644 --- a/gix-config/tests/config/file/init/from_paths/includes/unconditional.rs +++ b/gix-config/tests/config/file/init/from_paths/includes/unconditional.rs @@ -77,10 +77,10 @@ fn multiple() -> crate::Result { let config = File::from_paths_metadata(into_meta(vec![c_path]), follow_options())?.expect("non-empty"); assert_eq!(config.string_by("core", None, "c"), Some(bstring("12"))); - assert_eq!(config.integer_by("core", None, "d"), Ok(Some(41))); - assert_eq!(config.boolean_by("http", None, "sslVerify"), Ok(Some(false))); - assert_eq!(config.boolean_by("diff", None, "renames"), Ok(Some(true))); - assert_eq!(config.boolean_by("core", None, "a"), Ok(Some(false))); + assert_eq!(config.integer_by("core", None, "d")?, Some(41)); + assert_eq!(config.boolean_by("http", None, "sslVerify")?, Some(false)); + assert_eq!(config.boolean_by("diff", None, "renames")?, Some(true)); + assert_eq!(config.boolean_by("core", None, "a")?, Some(false)); Ok(()) } @@ -121,8 +121,8 @@ fn respect_max_depth() -> crate::Result { let config = File::from_paths_metadata(into_meta(vec![dir.path().join("0")]), follow_options())?.expect("non-empty"); - assert_eq!(config.integers_by("core", None, "i"), Ok(Some(vec![0, 1, 2, 3, 4]))); - assert_eq!(config.integers("core.i"), Ok(Some(vec![0, 1, 2, 3, 4]))); + assert_eq!(config.integers_by("core", None, "i")?, Some(vec![0, 1, 2, 3, 4])); + assert_eq!(config.integers("core.i")?, Some(vec![0, 1, 2, 3, 4])); fn make_options(max_depth: u8, error_on_max_depth_exceeded: bool) -> init::Options<'static> { init::Options { @@ -139,8 +139,8 @@ fn respect_max_depth() -> crate::Result { // this is equivalent to running git with --no-includes option let options = make_options(1, false); let config = File::from_paths_metadata(into_meta(vec![dir.path().join("0")]), options)?.expect("non-empty"); - assert_eq!(config.integer_by("core", None, "i"), Ok(Some(1))); - assert_eq!(config.integer("core.i"), Ok(Some(1))); + assert_eq!(config.integer_by("core", None, "i")?, Some(1)); + assert_eq!(config.integer("core.i")?, Some(1)); // with default max_allowed_depth of 10 and 4 levels of includes, last level is read let options = init::Options { @@ -148,12 +148,12 @@ fn respect_max_depth() -> crate::Result { ..Default::default() }; let config = File::from_paths_metadata(into_meta(vec![dir.path().join("0")]), options)?.expect("non-empty"); - assert_eq!(config.integer_by("core", None, "i"), Ok(Some(4))); + assert_eq!(config.integer_by("core", None, "i")?, Some(4)); // with max_allowed_depth of 5, the base and 4 levels of includes, last level is read let options = make_options(5, false); let config = File::from_paths_metadata(into_meta(vec![dir.path().join("0")]), options)?.expect("non-empty"); - assert_eq!(config.integer_by("core", None, "i"), Ok(Some(4))); + assert_eq!(config.integer_by("core", None, "i")?, Some(4)); // with max_allowed_depth of 2 and 4 levels of includes, max_allowed_depth is exceeded and error is returned let options = make_options(2, true); @@ -168,7 +168,7 @@ fn respect_max_depth() -> crate::Result { // with max_allowed_depth of 2 and 4 levels of includes and error_on_max_depth_exceeded: false , max_allowed_depth is exceeded and the value of level 2 is returned let options = make_options(2, false); let config = File::from_paths_metadata(into_meta(vec![dir.path().join("0")]), options)?.expect("non-empty"); - assert_eq!(config.integer_by("core", None, "i"), Ok(Some(2))); + assert_eq!(config.integer_by("core", None, "i")?, Some(2)); // with max_allowed_depth of 0 and 4 levels of includes, max_allowed_depth is exceeded and error is returned let options = make_options(0, true); @@ -214,7 +214,7 @@ fn simple() -> crate::Result { )?; let config = File::from_paths_metadata(into_meta(vec![a_path]), follow_options())?.expect("non-empty"); - assert_eq!(config.boolean_by("core", None, "b"), Ok(Some(false))); + assert_eq!(config.boolean_by("core", None, "b")?, Some(false)); Ok(()) } @@ -274,7 +274,7 @@ fn cycle_detection() -> crate::Result { ..Default::default() }; let config = File::from_paths_metadata(into_meta(vec![a_path]), options)?.expect("non-empty"); - assert_eq!(config.integers_by("core", None, "b"), Ok(Some(vec![0, 1, 0, 1, 0]))); + assert_eq!(config.integers_by("core", None, "b")?, Some(vec![0, 1, 0, 1, 0])); Ok(()) } @@ -319,8 +319,8 @@ fn nested() -> crate::Result { let config = File::from_paths_metadata(into_meta(vec![c_path]), follow_options())?.expect("non-empty"); - assert_eq!(config.integer_by("core", None, "c"), Ok(Some(1))); - assert_eq!(config.boolean_by("core", None, "b"), Ok(Some(true))); - assert_eq!(config.boolean_by("core", None, "a"), Ok(Some(false))); + assert_eq!(config.integer_by("core", None, "c")?, Some(1)); + assert_eq!(config.boolean_by("core", None, "b")?, Some(true)); + assert_eq!(config.boolean_by("core", None, "a")?, Some(false)); Ok(()) } diff --git a/gix-config/tests/config/file/init/from_paths/mod.rs b/gix-config/tests/config/file/init/from_paths/mod.rs index 9cdf3526db7..6aa735c31c2 100644 --- a/gix-config/tests/config/file/init/from_paths/mod.rs +++ b/gix-config/tests/config/file/init/from_paths/mod.rs @@ -54,9 +54,9 @@ fn multiple_paths_single_value() -> crate::Result { let paths = vec![a_path, b_path, c_path, d_path]; let config = File::from_paths_metadata(into_meta(paths), Default::default())?.expect("non-empty"); - assert_eq!(config.boolean("core.a"), Ok(Some(false))); - assert_eq!(config.boolean("core.b"), Ok(Some(true))); - assert_eq!(config.boolean("core.c"), Ok(Some(true))); + assert_eq!(config.boolean("core.a")?, Some(false)); + assert_eq!(config.boolean("core.b")?, Some(true)); + assert_eq!(config.boolean("core.c")?, Some(true)); assert_eq!(config.num_values(), 4); assert_eq!(config.sections().count(), 4, "each value is in a dedicated section"); diff --git a/gix-pathspec/Cargo.toml b/gix-pathspec/Cargo.toml index 1466bb5021e..5ea44504fc3 100644 --- a/gix-pathspec/Cargo.toml +++ b/gix-pathspec/Cargo.toml @@ -23,6 +23,7 @@ gix-glob = { version = "^0.27.1", path = "../gix-glob" } gix-path = { version = "^0.12.6", path = "../gix-path" } gix-attributes = { version = "^0.35.0", path = "../gix-attributes" } gix-config-value = { version = "^0.19.1", path = "../gix-config-value" } +gix-error = { version = "^0.3.0", path = "../gix-error" } bstr = { version = "1.12.0", default-features = false, features = ["std"] } bitflags = "2" diff --git a/gix-pathspec/src/defaults.rs b/gix-pathspec/src/defaults.rs index d48c259575b..f479c44058f 100644 --- a/gix-pathspec/src/defaults.rs +++ b/gix-pathspec/src/defaults.rs @@ -9,10 +9,16 @@ pub mod from_environment { #[expect(missing_docs)] pub enum Error { #[error(transparent)] - ParseValue(#[from] gix_config_value::Error), + ParseValue(gix_error::Error), #[error("Glob and no-glob settings are mutually exclusive")] MixedGlobAndNoGlob, } + + impl From for Error { + fn from(err: gix_config_value::Error) -> Self { + Error::ParseValue(err.into_error()) + } + } } impl Defaults { diff --git a/gix-submodule/src/config.rs b/gix-submodule/src/config.rs index 29466dafcb9..593d17d49fd 100644 --- a/gix-submodule/src/config.rs +++ b/gix-submodule/src/config.rs @@ -64,8 +64,12 @@ impl FetchRecurse { }), Ok(None) => None, Err(err) => { - if err.input != "on-demand" { - return Err(err.input); + let input = err + .into_inner() + .input + .expect("gix-config-value validation errors retain their input"); + if input != "on-demand" { + return Err(input); } Some(FetchRecurse::OnDemand) } diff --git a/gix-tix/src/enrich.rs b/gix-tix/src/enrich.rs index 39513867774..ed510e98322 100644 --- a/gix-tix/src/enrich.rs +++ b/gix-tix/src/enrich.rs @@ -63,6 +63,7 @@ pub(crate) fn load(notes: &mut gix::note::Platform, change_id: ChangeId) -> Resu Ok(Enrichment { todo: config .boolean("commit.todo") + .map_err(gix::Exn::into_error) .context("commit.todo is not a boolean")? .unwrap_or(false), note: config.string("commit.note").filter(|note| !note.is_empty()), @@ -76,6 +77,7 @@ pub(crate) fn load_tree(notes: &mut gix::note::Platform, tree_id: ObjectId) -> R Ok(TreeEnrichment { checks_pass: config .boolean("tree.checks-pass") + .map_err(gix::Exn::into_error) .context("tree.checks-pass is not a boolean")? .unwrap_or(false), }) @@ -103,6 +105,7 @@ pub(crate) fn toggle(repo: &gix::Repository, commit_id: ObjectId) -> Result update_tree(repo, tree_id, |config| { let enabled = !config .boolean("tree.checks-pass") + .map_err(gix::Exn::into_error) .context("tree.checks-pass is not a boolean")? .unwrap_or(false); set_checks_pass(config, enabled) diff --git a/gix/src/commit/mod.rs b/gix/src/commit/mod.rs index 5e7e8cad9a7..d84f4397e19 100644 --- a/gix/src/commit/mod.rs +++ b/gix/src/commit/mod.rs @@ -14,7 +14,7 @@ pub mod verify; fn signature_program( config: &crate::config::Snapshot<'_>, format: gix_object::signature::Format, -) -> Result { +) -> Result { use crate::config::tree::{Gpg, Key, gpg}; use gix_object::signature::Format; diff --git a/gix/src/commit/sign.rs b/gix/src/commit/sign.rs index 724f13c05e0..ebd1f66557f 100644 --- a/gix/src/commit/sign.rs +++ b/gix/src/commit/sign.rs @@ -45,7 +45,7 @@ pub mod options { #[error("user.signingKey or gpg.ssh.defaultKeyCommand must provide an SSH signing key")] MissingSshSigningKey, #[error("Could not interpolate a configured commit-signing path")] - ConfiguredPath(#[from] gix_config::path::interpolate::Error), + ConfiguredPath(#[from] gix_error::Error), #[error("Could not execute gpg.ssh.defaultKeyCommand {program:?}")] DefaultKeyCommand { program: OsString, diff --git a/gix/src/commit/verify.rs b/gix/src/commit/verify.rs index 0fcb617c2a1..cce980c7fb5 100644 --- a/gix/src/commit/verify.rs +++ b/gix/src/commit/verify.rs @@ -18,7 +18,7 @@ pub enum Error { #[error(transparent)] InvalidTrustLevel(#[from] crate::config::key::GenericErrorWithValue), #[error("Could not interpolate a configured signature-verification path")] - ConfiguredPath(#[from] gix_config::path::interpolate::Error), + ConfiguredPath(#[from] gix_error::Error), #[error("gpg.ssh.allowedSignersFile must be configured for SSH signature verification")] MissingAllowedSigners, #[error(transparent)] diff --git a/gix/src/config/cache/access.rs b/gix/src/config/cache/access.rs index 4c3b88da631..266faafe973 100644 --- a/gix/src/config/cache/access.rs +++ b/gix/src/config/cache/access.rs @@ -407,7 +407,7 @@ impl Cache { source: gix_worktree::stack::state::ignore::Source, buf: &mut Vec, ) -> Result { - let excludes_file = match self.excludes_file()? { + let excludes_file = match self.excludes_file().map_err(gix_error::Exn::into_error)? { Some(user_path) => Some(user_path), None => self.xdg_config_path("ignore")?, }; @@ -429,7 +429,10 @@ impl Cache { attributes: crate::open::permissions::Attributes, ) -> Result<(gix_worktree::stack::state::Attributes, Vec), config::attribute_stack::Error> { use gix_attributes::Source; - let configured_or_user_attributes = match self.trusted_file_path(Core::ATTRIBUTES_FILE)? { + let configured_or_user_attributes = match self + .trusted_file_path(Core::ATTRIBUTES_FILE) + .map_err(gix_error::Exn::into_error)? + { Some(attributes) => Some(attributes), None => { if attributes.git { diff --git a/gix/src/config/mod.rs b/gix/src/config/mod.rs index 405dfc3bb4a..066c114eecc 100644 --- a/gix/src/config/mod.rs +++ b/gix/src/config/mod.rs @@ -123,10 +123,7 @@ pub enum Error { #[error(transparent)] FromEnv(#[from] gix_config::file::init::from_env::Error), #[error("The path {path:?} at the 'core.worktree' configuration could not be interpolated")] - PathInterpolation { - path: BString, - source: gix_config::path::interpolate::Error, - }, + PathInterpolation { path: BString, source: gix_error::Error }, #[error("{source:?} configuration overrides at open or init time could not be applied.")] ConfigOverrides { #[source] @@ -256,7 +253,7 @@ pub mod command_context { #[error(transparent)] Boolean(#[from] config::boolean::Error), #[error(transparent)] - ParseBool(#[from] gix_config::value::Error), + ParseBool(#[from] gix_error::Error), } } @@ -274,7 +271,7 @@ pub mod exclude_stack { #[error(transparent)] EnvironmentPermission(#[from] gix_sec::permission::Error), #[error("The value for `core.excludesFile` could not be read from configuration")] - ExcludesFilePathInterpolation(#[from] gix_config::path::interpolate::Error), + ExcludesFilePathInterpolation(#[from] gix_error::Error), #[error(transparent)] ParsePreciousEnabled(#[from] config::boolean::Error), } @@ -289,7 +286,7 @@ pub mod attribute_stack { #[error("An attribute file could not be read")] Io(#[from] std::io::Error), #[error("Failed to interpolate the attribute file configured at `core.attributesFile`")] - AttributesFileInterpolation(#[from] gix_config::path::interpolate::Error), + AttributesFileInterpolation(#[from] gix_error::Error), } } @@ -412,10 +409,10 @@ pub mod key { } /// A generic key error for use when it doesn't seem worth it say more than 'key is invalid' along with meta-data. - pub type GenericError = Error; + pub type GenericError = Error; /// A generic key error which will also contain a value. - pub type GenericErrorWithValue = Error; + pub type GenericErrorWithValue = Error; } /// @@ -442,7 +439,7 @@ pub mod checkout { use crate::config; /// The error produced when failing to parse the `checkout.workers` key. - pub type Error = config::key::Error; + pub type Error = config::key::Error; } } @@ -494,25 +491,25 @@ pub mod commit_signature { /// pub mod lock_timeout { /// The error produced when failing to parse timeout for locks. - pub type Error = super::key::Error; + pub type Error = super::key::Error; } /// pub mod duration { /// The error produced when failing to parse durations (in milliseconds). - pub type Error = super::key::Error; + pub type Error = super::key::Error; } /// pub mod boolean { /// The error produced when failing to parse time from configuration. - pub type Error = super::key::Error; + pub type Error = super::key::Error; } /// pub mod unsigned_integer { /// The error produced when failing to parse a signed integer from configuration. - pub type Error = super::key::Error; + pub type Error = super::key::Error; } /// @@ -563,12 +560,12 @@ pub mod transport { }, #[error("Could not interpret configuration key {key:?}")] ConfigValue { - source: gix_config::value::Error, + source: gix_error::Error, key: &'static str, }, #[error("Could not interpolate path at key {key:?}")] InterpolatePath { - source: gix_config::path::interpolate::Error, + source: gix_error::Error, key: &'static str, }, #[error("Could not decode value at key {key:?} as UTF-8 string")] diff --git a/gix/src/config/snapshot/access.rs b/gix/src/config/snapshot/access.rs index 0e0a2438f9d..47ecccb3ee1 100644 --- a/gix/src/config/snapshot/access.rs +++ b/gix/src/config/snapshot/access.rs @@ -25,8 +25,8 @@ impl Snapshot<'_> { } /// Like [`boolean()`][Self::boolean()], but it will report an error if the value couldn't be interpreted as boolean. - pub fn try_boolean(&self, key: impl gix_config::AsKey) -> Result, gix_config::value::Error> { - self.repo.config.resolved.boolean(key) + pub fn try_boolean(&self, key: impl gix_config::AsKey) -> Result, gix_error::Error> { + self.repo.config.resolved.boolean(key).map_err(Into::into) } /// Return the resolved integer at `key`, or `None` if there is no such value or if the value can't be interpreted as @@ -40,8 +40,8 @@ impl Snapshot<'_> { } /// Like [`integer()`][Self::integer()], but it will report an error if the value couldn't be interpreted as boolean. - pub fn try_integer(&self, key: impl gix_config::AsKey) -> Result, gix_config::value::Error> { - self.repo.config.resolved.integer(key) + pub fn try_integer(&self, key: impl gix_config::AsKey) -> Result, gix_error::Error> { + self.repo.config.resolved.integer(key).map_err(Into::into) } /// Return the string at `key`, or `None` if there is no such value. @@ -60,11 +60,8 @@ impl Snapshot<'_> { /// The path can be prefixed with `:(optional)` which means it won't be returned if the interpolated /// path couldn't be accessed. Note also that this is different from Git, which ignores it only if /// it doesn't exist. - pub fn trusted_path( - &self, - key: impl gix_config::AsKey, - ) -> Result, gix_config::path::interpolate::Error> { - self.repo.config.trusted_file_path(key) + pub fn trusted_path(&self, key: impl gix_config::AsKey) -> Result, gix_error::Error> { + self.repo.config.trusted_file_path(key).map_err(Into::into) } /// Return the trusted string at `key` for launching using [command::prepare()](gix_command::prepare()), diff --git a/gix/src/config/snapshot/credential_helpers.rs b/gix/src/config/snapshot/credential_helpers.rs index e3b07f1fe65..6b7dda36254 100644 --- a/gix/src/config/snapshot/credential_helpers.rs +++ b/gix/src/config/snapshot/credential_helpers.rs @@ -10,12 +10,9 @@ mod error { #[expect(missing_docs)] pub enum Error { #[error("Could not parse 'useHttpPath' key in section {section}")] - InvalidUseHttpPath { - section: BString, - source: gix_config::value::Error, - }, + InvalidUseHttpPath { section: BString, source: gix_error::Error }, #[error("core.askpass could not be read")] - CoreAskpass(#[from] gix_config::path::interpolate::Error), + CoreAskpass(#[from] gix_error::Error), #[error(transparent)] BooleanConfig(#[from] crate::config::boolean::Error), } @@ -169,7 +166,7 @@ pub(super) mod function { .map(|val| { gix_config::Boolean::try_from(val) .map_err(|err| Error::InvalidUseHttpPath { - source: err, + source: err.into_error(), section: section.header().to_bstring(), }) .map(|b| b.0) @@ -200,10 +197,10 @@ pub(super) mod function { config, Core::ASKPASS, &mut filter, - is_lenient_config, + true, // An empty core.askpass is allowed even with strict configuration. environment, ) - .ignore_empty()?, + .map_err(gix_error::Exn::into_error)?, mode: Credentials::TERMINAL_PROMPT .enrich_error(config.boolean(Credentials::TERMINAL_PROMPT)) .with_leniency(is_lenient_config)? @@ -275,18 +272,4 @@ pub(super) mod function { } gix_url::parse(pattern).ok() } - - trait IgnoreEmptyPath { - fn ignore_empty(self) -> Self; - } - - impl IgnoreEmptyPath for Result, gix_config::path::interpolate::Error> { - fn ignore_empty(self) -> Self { - match self { - Ok(maybe_path) => Ok(maybe_path), - Err(gix_config::path::interpolate::Error::Missing { .. }) => Ok(None), - Err(err) => Err(err), - } - } - } } diff --git a/gix/src/config/tree/keys.rs b/gix/src/config/tree/keys.rs index f93fc153078..91099077aba 100644 --- a/gix/src/config/tree/keys.rs +++ b/gix/src/config/tree/keys.rs @@ -260,7 +260,8 @@ mod duration { &'static self, value: Result, gix_config::value::Error>, ) -> Result, config::duration::Error> { - let Some(value) = value.map_err(|err| config::duration::Error::from(self).with_source(err))? else { + let Some(value) = value.map_err(|err| config::duration::Error::from(self).with_source(err.into_error()))? + else { return Ok(None); }; Ok(Some(match value { @@ -292,7 +293,9 @@ mod lock_timeout { &'static self, value: Result, gix_config::value::Error>, ) -> Result, config::lock_timeout::Error> { - let Some(value) = value.map_err(|err| config::lock_timeout::Error::from(self).with_source(err))? else { + let Some(value) = + value.map_err(|err| config::lock_timeout::Error::from(self).with_source(err.into_error()))? + else { return Ok(None); }; Ok(Some(match value { @@ -324,7 +327,9 @@ mod compression { &'static self, value: Result, gix_config::value::Error>, ) -> Result, config::key::GenericError> { - let Some(value) = value.map_err(|err| config::key::GenericError::from(self).with_source(err))? else { + let Some(value) = + value.map_err(|err| config::key::GenericError::from(self).with_source(err.into_error()))? + else { return Ok(None); }; match value { @@ -427,7 +432,8 @@ mod workers { &'static self, value: Result, gix_config::value::Error>, ) -> Result, crate::config::unsigned_integer::Error> { - let value = value.map_err(|err| crate::config::unsigned_integer::Error::from(self).with_source(err))?; + let value = value + .map_err(|err| crate::config::unsigned_integer::Error::from(self).with_source(err.into_error()))?; value .map(|value| { value @@ -442,7 +448,8 @@ mod workers { &'static self, value: Result, gix_config::value::Error>, ) -> Result, crate::config::unsigned_integer::Error> { - let value = value.map_err(|err| crate::config::unsigned_integer::Error::from(self).with_source(err))?; + let value = value + .map_err(|err| crate::config::unsigned_integer::Error::from(self).with_source(err.into_error()))?; value .map(|value| { value @@ -457,7 +464,8 @@ mod workers { &'static self, value: Result, gix_config::value::Error>, ) -> Result, crate::config::unsigned_integer::Error> { - let value = value.map_err(|err| crate::config::unsigned_integer::Error::from(self).with_source(err))?; + let value = value + .map_err(|err| crate::config::unsigned_integer::Error::from(self).with_source(err.into_error()))?; value .map(|value| { value @@ -525,7 +533,7 @@ mod boolean { &'static self, value: Result, gix_config::value::Error>, ) -> Result, config::boolean::Error> { - value.map_err(|err| config::boolean::Error::from(self).with_source(err)) + value.map_err(|err| config::boolean::Error::from(self).with_source(err.into_error())) } } } diff --git a/gix/src/config/tree/sections/checkout.rs b/gix/src/config/tree/sections/checkout.rs index b3490e37674..0a59d858ab0 100644 --- a/gix/src/config/tree/sections/checkout.rs +++ b/gix/src/config/tree/sections/checkout.rs @@ -36,7 +36,9 @@ mod workers { Ok(Some(v)) if v < 0 => Ok(Some(0)), Ok(Some(v)) => Ok(Some(v.try_into().expect("positive i64 can always be usize on 64 bit"))), Ok(None) => Ok(None), - Err(err) => Err(crate::config::key::Error::from(&super::Checkout::WORKERS).with_source(err)), + Err(err) => { + Err(crate::config::key::Error::from(&super::Checkout::WORKERS).with_source(err.into_error())) + } } } } @@ -45,6 +47,7 @@ mod workers { /// pub mod validate { use crate::{bstr::BStr, config::tree::keys}; + use gix_error::{ErrorExt, ValidationError}; pub struct Workers; impl keys::Validate for Workers { @@ -52,8 +55,9 @@ pub mod validate { super::Checkout::WORKERS.try_from_workers( gix_config::Integer::try_from(value) .and_then(|i| { - i.to_decimal() - .ok_or_else(|| gix_config::value::Error::new("Integer overflow", value.to_owned())) + i.to_decimal().ok_or_else(|| { + ValidationError::new_with_input("Integer overflow", value.to_owned()).raise() + }) }) .map(Some), )?; diff --git a/gix/src/config/tree/sections/core.rs b/gix/src/config/tree/sections/core.rs index f3f72107f4a..20444705a6f 100644 --- a/gix/src/config/tree/sections/core.rs +++ b/gix/src/config/tree/sections/core.rs @@ -281,7 +281,7 @@ mod filter { return Ok(CrlfRoundTripCheck::Warn); } let value = gix_config::Boolean::try_from(value.as_bstr()).map_err(|err| { - config::key::GenericErrorWithValue::from_value(self, value.into()).with_source(err) + config::key::GenericErrorWithValue::from_value(self, value.into()).with_source(err.into_error()) })?; Ok(if value.into() { CrlfRoundTripCheck::Fail @@ -308,7 +308,7 @@ mod filter { return Ok(eol::AutoCrlf::Input); } let value = gix_config::Boolean::try_from(value.as_bstr()).map_err(|err| { - config::key::GenericErrorWithValue::from_value(self, value.into()).with_source(err) + config::key::GenericErrorWithValue::from_value(self, value.into()).with_source(err.into_error()) })?; Ok(if value.into() { eol::AutoCrlf::Enabled @@ -356,7 +356,9 @@ mod shared_repository { gix_config::Boolean::try_from(value) .map(|value| if value.0 { 0o660 } else { 0 }) - .map_err(|err| config::key::GenericErrorWithValue::from_value(self, value.into()).with_source(err)) + .map_err(|err| { + config::key::GenericErrorWithValue::from_value(self, value.into()).with_source(err.into_error()) + }) } } } @@ -403,7 +405,11 @@ mod log_all_ref_updates { } else { gix_ref::store::WriteReflog::Disable })), - Err(err) => match err.input { + Err(err) => match err + .into_inner() + .input + .expect("gix-config-value validation errors retain their input") + { val if val.eq_ignore_ascii_case(b"always") => Ok(Some(gix_ref::store::WriteReflog::Always)), val => Err(config::key::GenericErrorWithValue::from_value(self, val)), }, diff --git a/gix/src/config/tree/sections/diff.rs b/gix/src/config/tree/sections/diff.rs index d4a4c9802e5..484b54d99c5 100644 --- a/gix/src/config/tree/sections/diff.rs +++ b/gix/src/config/tree/sections/diff.rs @@ -148,7 +148,7 @@ mod binary { .map(|b| b.0) .map_err(|err| { crate::config::key::GenericErrorWithValue::from_value(self, value.into()) - .with_source(err) + .with_source(err.into_error()) })?, ) } @@ -184,10 +184,15 @@ mod renames { Ok(Some(false)) => Some(Tracking::Disabled), Ok(None) => None, Err(err) => { - let value = &err.input; + let value = err + .error() + .input + .as_ref() + .expect("gix-config-value validation errors retain their input") + .clone(); match value.as_bytes() { b"copy" | b"copies" => Some(Tracking::RenamesAndCopies), - _ => return Err(GenericError::from_value(self, value.clone()).with_source(err)), + _ => return Err(GenericError::from_value(self, value).with_source(err.into_error())), } } }) diff --git a/gix/src/config/tree/sections/http.rs b/gix/src/config/tree/sections/http.rs index c57ba4cf2c5..c88e3b5767c 100644 --- a/gix/src/config/tree/sections/http.rs +++ b/gix/src/config/tree/sections/http.rs @@ -134,7 +134,7 @@ mod key_impls { Ok(if value.as_bstr().as_bytes() == b"initial" { FollowRedirects::Initial } else if let Some(value) = boolean().map_err(|err| { - crate::config::key::GenericErrorWithValue::from_value(self, value.into()).with_source(err) + crate::config::key::GenericErrorWithValue::from_value(self, value.into()).with_source(err.into_error()) })? { if value { FollowRedirects::All diff --git a/gix/src/config/tree/sections/pack.rs b/gix/src/config/tree/sections/pack.rs index 9ab23b52fd9..0845c578937 100644 --- a/gix/src/config/tree/sections/pack.rs +++ b/gix/src/config/tree/sections/pack.rs @@ -29,7 +29,9 @@ mod index_version { &'static self, value: Result, gix_config::value::Error>, ) -> Result, config::key::GenericError> { - let Some(value) = value.map_err(|err| config::key::GenericError::from(self).with_source(err))? else { + let Some(value) = + value.map_err(|err| config::key::GenericError::from(self).with_source(err.into_error()))? + else { return Ok(None); }; Ok(Some(match value { @@ -53,6 +55,7 @@ impl Section for Pack { mod validate { use crate::{bstr::BStr, config::tree::keys}; + use gix_error::{ErrorExt, ValidationError}; #[derive(Clone, Copy)] pub struct IndexVersion; @@ -62,7 +65,7 @@ mod validate { gix_config::Integer::try_from(value) .and_then(|int| { int.to_decimal() - .ok_or_else(|| gix_config::value::Error::new("integer out of range", value)) + .ok_or_else(|| ValidationError::new_with_input("integer out of range", value).raise()) }) .map(Some), )?; diff --git a/gix/src/config/tree/sections/protocol.rs b/gix/src/config/tree/sections/protocol.rs index f7b558fa62c..99def73fa8b 100644 --- a/gix/src/config/tree/sections/protocol.rs +++ b/gix/src/config/tree/sections/protocol.rs @@ -88,7 +88,8 @@ mod key_impls { Ok(Some(value)) => value, Err(err) => { return Err( - crate::config::key::GenericErrorWithValue::from_value(self, "unknown".into()).with_source(err), + crate::config::key::GenericErrorWithValue::from_value(self, "unknown".into()) + .with_source(err.into_error()), ); } }; diff --git a/gix/src/filter.rs b/gix/src/filter.rs index 2f2be0926d1..b3d0c520be4 100644 --- a/gix/src/filter.rs +++ b/gix/src/filter.rs @@ -27,10 +27,7 @@ pub mod pipeline { #[error(transparent)] SafeCrlf(#[from] config::key::GenericErrorWithValue), #[error("Could not interpret 'filter.{name}.required' configuration")] - Driver { - name: BString, - source: gix_config::value::Error, - }, + Driver { name: BString, source: gix_error::Error }, #[error(transparent)] CommandContext(#[from] config::command_context::Error), } @@ -327,7 +324,7 @@ fn extract_drivers(repo: &Repository) -> Result, pipelin driver.required = gix_config::Boolean::try_from(BStr::new(&value)) .map_err(|source| pipeline::options::Error::Driver { name: name.to_owned(), - source, + source: source.into_error(), })? .into(); } diff --git a/gix/src/lib.rs b/gix/src/lib.rs index 77fab36e8a9..4c652620275 100644 --- a/gix/src/lib.rs +++ b/gix/src/lib.rs @@ -255,10 +255,6 @@ pub mod merge; /// assert!(repo.workdir_path("this").expect("non-bare").is_file()); /// # Ok(()) } /// ``` -#[expect( - clippy::result_large_err, - reason = "will be removed once `gix-error` is used consistently" -)] pub fn discover(directory: impl AsRef) -> Result { ThreadSafeRepository::discover(directory).map(Into::into) } @@ -266,10 +262,6 @@ pub fn discover(directory: impl AsRef) -> Result, options: discover::upwards::Options<'_>, @@ -289,10 +281,6 @@ pub fn discover_opts( /// Try to discover a git repository directly from the environment. /// /// For details, see [`ThreadSafeRepository::discover_with_environment_overrides_opts()`]. -#[expect( - clippy::result_large_err, - reason = "will be removed once `gix-error` is used consistently" -)] pub fn discover_with_environment_overrides( directory: impl AsRef, ) -> Result { @@ -302,10 +290,6 @@ pub fn discover_with_environment_overrides( /// Try to open a git repository directly from the environment. /// /// See [`ThreadSafeRepository::open_with_environment_overrides()`]. -#[expect( - clippy::result_large_err, - reason = "will be removed once `gix-error` is used consistently" -)] pub fn open_with_environment_overrides(directory: impl Into) -> Result { ThreadSafeRepository::open_with_environment_overrides(directory, Default::default()).map(Into::into) } @@ -325,19 +309,11 @@ pub fn open_with_environment_overrides(directory: impl Into) /// assert!(repo.head()?.is_unborn()); /// # Ok(()) } /// ``` -#[expect( - clippy::result_large_err, - reason = "will be removed once `gix-error` is used consistently" -)] pub fn init(directory: impl AsRef) -> Result { ThreadSafeRepository::init(directory, create::Kind::WithWorktree, create::Options::default()).map(Into::into) } /// See [`ThreadSafeRepository::init()`], but returns a [`Repository`] instead. -#[expect( - clippy::result_large_err, - reason = "will be removed once `gix-error` is used consistently" -)] pub fn init_bare(directory: impl AsRef) -> Result { ThreadSafeRepository::init(directory, create::Kind::Bare, create::Options::default()).map(Into::into) } @@ -410,20 +386,12 @@ fn open_opts_with_git_binary_config() -> open::Options { /// assert_eq!(repo.head_commit()?.decode()?.message, "c2\n"); /// # Ok(()) } /// ``` -#[expect( - clippy::result_large_err, - reason = "will be removed once `gix-error` is used consistently" -)] #[doc(alias = "git2")] pub fn open(directory: impl Into) -> Result { ThreadSafeRepository::open(directory).map(Into::into) } /// See [`ThreadSafeRepository::open_opts()`], but returns a [`Repository`] instead. -#[expect( - clippy::result_large_err, - reason = "will be removed once `gix-error` is used consistently" -)] #[doc(alias = "open_ext", alias = "git2")] pub fn open_opts(directory: impl Into, options: open::Options) -> Result { ThreadSafeRepository::open_opts(directory, options).map(Into::into) diff --git a/gix/src/mailmap.rs b/gix/src/mailmap.rs index fcc78c0c1a0..c040cdb7bd7 100644 --- a/gix/src/mailmap.rs +++ b/gix/src/mailmap.rs @@ -11,7 +11,7 @@ pub mod load { #[error("The configured mailmap.blob could not be parsed")] BlobSpec(#[from] crate::revision::spec::parse::single::Error), #[error(transparent)] - PathInterpolate(#[from] gix_config::path::interpolate::Error), + PathInterpolate(#[from] gix_error::Error), #[error("Could not find object configured in `mailmap.blob`")] FindExisting(#[from] crate::object::find::existing::Error), } diff --git a/gix/src/object/tree/diff/mod.rs b/gix/src/object/tree/diff/mod.rs index 44fadfc390c..cd0d246e8f9 100644 --- a/gix/src/object/tree/diff/mod.rs +++ b/gix/src/object/tree/diff/mod.rs @@ -124,10 +124,6 @@ impl<'repo> Tree<'repo> { /// Note that if a clone with `--filter=blob=none` was created, rename tracking may fail as it might /// try to access blobs to compute a similarity metric. Thus, it's more compatible to turn rewrite tracking off /// using [`Options::track_rewrites()`](crate::diff::Options::track_rewrites()). - #[expect( - clippy::result_large_err, - reason = "will be removed once `gix-error` is used consistently" - )] #[doc(alias = "diff_tree_to_tree", alias = "git2")] pub fn changes<'a>(&'a self) -> Result, crate::diff::options::init::Error> { Ok(Platform { diff --git a/gix/src/open/options.rs b/gix/src/open/options.rs index bb3a995cd99..35ae5742e2b 100644 --- a/gix/src/open/options.rs +++ b/gix/src/open/options.rs @@ -168,10 +168,6 @@ impl Options { } /// Open a repository at `path` with the options set so far. - #[expect( - clippy::result_large_err, - reason = "will be removed once `gix-error` is used consistently" - )] pub fn open(self, path: impl Into) -> Result { ThreadSafeRepository::open_opts(path, self) } diff --git a/gix/src/open/repository.rs b/gix/src/open/repository.rs index e57c965aa02..adbe89f70f9 100644 --- a/gix/src/open/repository.rs +++ b/gix/src/open/repository.rs @@ -1,5 +1,6 @@ #![allow(clippy::result_large_err)] use gix_config::file::Metadata; +use gix_error::ErrorExt; use gix_features::threading::OwnShared; use gix_object::bstr::ByteSlice; use gix_path::RelativePath; @@ -284,7 +285,7 @@ impl ThreadSafeRepository { if worktree.is_empty() { return Err(config::Error::PathInterpolation { path: worktree, - source: gix_config::path::interpolate::Error::Missing { what: "path" }, + source: gix_error::NotFoundError::new("path is missing").raise().into_error(), } .into()); } @@ -321,7 +322,7 @@ impl ThreadSafeRepository { }) .map_err(|err| { Error::from(config::Error::ConfigTypedString( - config::key::GenericErrorWithValue::from(&Core::WORKTREE).with_source(err), + config::key::GenericErrorWithValue::from(&Core::WORKTREE).with_source(err.into_error()), )) })? .is_some() diff --git a/gix/src/repository/config/transport.rs b/gix/src/repository/config/transport.rs index 16345c968b7..8a70492fcde 100644 --- a/gix/src/repository/config/transport.rs +++ b/gix/src/repository/config/transport.rs @@ -325,7 +325,10 @@ impl crate::Repository { }) .transpose() .with_leniency(lenient) - .map_err(|err| config::transport::Error::InterpolatePath { source: err, key })?; + .map_err(|err| config::transport::Error::InterpolatePath { + source: err.into_error(), + key, + })?; } { diff --git a/gix/src/repository/worktree.rs b/gix/src/repository/worktree.rs index c136f94c5dd..4fc627ffea8 100644 --- a/gix/src/repository/worktree.rs +++ b/gix/src/repository/worktree.rs @@ -73,10 +73,6 @@ impl crate::Repository { /// /// Note that it might be the one that is currently open if this repository doesn't point to a linked worktree. /// Also note that the main repo might be bare. - #[expect( - clippy::result_large_err, - reason = "will be removed once `gix-error` is used consistently" - )] pub fn main_repo(&self) -> Result { let options = match (self.kind(), self.options.clone()) { (crate::repository::Kind::LinkedWorkTree, opts) => opts.without_repository_environment_overrides(), diff --git a/gix/src/submodule/errors.rs b/gix/src/submodule/errors.rs index 7ffb0eb0a3d..362779967a6 100644 --- a/gix/src/submodule/errors.rs +++ b/gix/src/submodule/errors.rs @@ -43,7 +43,7 @@ pub mod is_active { #[error(transparent)] InitIsActivePlatform(#[from] gix_submodule::is_active_platform::Error), #[error(transparent)] - QueryIsActive(#[from] gix_config::value::Error), + QueryIsActive(#[from] gix_error::Error), #[error(transparent)] InitAttributes(#[from] crate::config::attribute_stack::Error), #[error(transparent)] diff --git a/gix/src/submodule/mod.rs b/gix/src/submodule/mod.rs index 724b5de83b8..56016736763 100644 --- a/gix/src/submodule/mod.rs +++ b/gix/src/submodule/mod.rs @@ -149,8 +149,8 @@ impl Submodule<'_> { /// Return the `shallow` field from this submodule's configuration, if present, or `None`. /// /// If `true`, the submodule will be checked out with `depth = 1`. If unset, `false` is assumed. - pub fn shallow(&self) -> Result, gix_config::value::Error> { - self.state.modules.shallow(self.name()) + pub fn shallow(&self) -> Result, gix_error::Error> { + self.state.modules.shallow(self.name()).map_err(Into::into) } /// Returns true if this submodule is considered active and can thus participate in an operation. @@ -158,16 +158,18 @@ impl Submodule<'_> { /// Please see the [plumbing crate documentation](gix_submodule::IsActivePlatform::is_active()) for details. pub fn is_active(&self) -> Result { let (mut platform, mut attributes) = self.state.active_state_mut()?; - let is_active = platform.is_active( - &self.state.repo.config.resolved, - self.name.as_ref(), - &mut |relative_path, case, is_dir, out| { - attributes - .set_case(case) - .at_entry(relative_path, Some(is_dir_to_mode(is_dir)), &self.state.repo.objects) - .is_ok_and(|platform| platform.matching_attributes(out)) - }, - )?; + let is_active = platform + .is_active( + &self.state.repo.config.resolved, + self.name.as_ref(), + &mut |relative_path, case, is_dir, out| { + attributes + .set_case(case) + .at_entry(relative_path, Some(is_dir_to_mode(is_dir)), &self.state.repo.objects) + .is_ok_and(|platform| platform.matching_attributes(out)) + }, + ) + .map_err(|err| is_active::Error::QueryIsActive(err.into_error()))?; Ok(is_active) } diff --git a/gix/tests/gix/config/tree.rs b/gix/tests/gix/config/tree.rs index 83ce08fe319..c1ae640de1a 100644 --- a/gix/tests/gix/config/tree.rs +++ b/gix/tests/gix/config/tree.rs @@ -1,3 +1,8 @@ +fn config_value_error(message: &'static str, input: &'static str) -> gix_config::value::Error { + use gix_error::ErrorExt; + gix_error::ValidationError::new_with_input(message, input).raise() +} + mod keys { use gix::config::tree::{Key, Section}; use gix_object::bstr::{BStr, ByteSlice}; @@ -117,7 +122,7 @@ mod keys { .validate(out_of_bounds.as_bytes().into()) .unwrap_err() .to_string(), - "Could not decode '9223372036854775808': Integers needs to be positive or negative numbers which may have a suffix like 1k, 42, or 50G" + "Integers needs to be positive or negative numbers which may have a suffix like 1k, 42, or 50G: \"9223372036854775808\"" ); } } @@ -358,19 +363,19 @@ mod diff { ); assert!(Diff::RENAMES.validate("0".into()).is_ok()); assert_eq!( - Diff::RENAMES.try_into_renames(Err(gix_config::value::Error::new("err", "copy")))?, + Diff::RENAMES.try_into_renames(Err(crate::config::tree::config_value_error("err", "copy")))?, Some(Tracking::RenamesAndCopies) ); assert!(Diff::RENAMES.validate("copy".into()).is_ok()); assert_eq!( - Diff::RENAMES.try_into_renames(Err(gix_config::value::Error::new("err", "copies")))?, + Diff::RENAMES.try_into_renames(Err(crate::config::tree::config_value_error("err", "copies")))?, Some(Tracking::RenamesAndCopies) ); assert!(Diff::RENAMES.validate("copies".into()).is_ok()); assert_eq!( Diff::RENAMES - .try_into_renames(Err(gix_config::value::Error::new("err", "foo"))) + .try_into_renames(Err(crate::config::tree::config_value_error("err", "foo"))) .unwrap_err() .to_string(), "The value of key \"diff.renames=foo\" was invalid" @@ -545,7 +550,7 @@ mod core { assert!(Core::FILES_REF_LOCK_TIMEOUT.validate("2500".into()).is_ok()); assert_eq!( Core::FILES_REF_LOCK_TIMEOUT - .try_into_lock_timeout(Err(gix_config::value::Error::new("err", "bogus"))) + .try_into_lock_timeout(Err(crate::config::tree::config_value_error("err", "bogus"))) .unwrap_err() .to_string(), "The timeout at key \"core.filesRefLockTimeout\" was invalid" @@ -1190,7 +1195,9 @@ mod http { assert_eq!( Http::FOLLOW_REDIRECTS - .try_into_follow_redirects("something", || Err(gix_config::value::Error::new("invalid", "value"))) + .try_into_follow_redirects("something", || { + Err(crate::config::tree::config_value_error("invalid", "value")) + }) .unwrap_err() .to_string(), "The key \"http.followRedirects=something\" was invalid", diff --git a/gix/tests/gix/remote/fetch.rs b/gix/tests/gix/remote/fetch.rs index 2e7bad54038..96f9ee79f7f 100644 --- a/gix/tests/gix/remote/fetch.rs +++ b/gix/tests/gix/remote/fetch.rs @@ -81,7 +81,6 @@ mod blocking_and_async_io { dir.join(name) } - #[expect(clippy::result_large_err)] pub(crate) fn try_repo_rw( name: &str, ) -> Result<(gix::Repository, gix_testtools::tempfile::TempDir), gix::open::Error> { @@ -93,7 +92,6 @@ mod blocking_and_async_io { CloneWithShallowSupport, } - #[expect(clippy::result_large_err)] pub(crate) fn try_repo_rw_args>( name: &str, args: impl IntoIterator, diff --git a/gix/tests/gix/repository/config/config_snapshot/credential_helpers.rs b/gix/tests/gix/repository/config/config_snapshot/credential_helpers.rs index 7ec3faa2b64..21cb3d6b252 100644 --- a/gix/tests/gix/repository/config/config_snapshot/credential_helpers.rs +++ b/gix/tests/gix/repository/config/config_snapshot/credential_helpers.rs @@ -240,9 +240,37 @@ fn invalid_urls_are_rejected_early() { #[test] fn empty_core_askpass_is_ignored() -> crate::Result { - let repo = remote::repo("empty-core-askpass"); - let _ = repo - .config_snapshot() - .credential_helpers("does-not-matter".try_into()?)?; + for strict in [false, true] { + let repo = gix::open_opts( + remote::repo_path("empty-core-askpass"), + gix::open::Options::isolated().strict_config(strict), + )?; + let (_, _, prompt) = repo + .config_snapshot() + .credential_helpers("does-not-matter".try_into()?)?; + assert!(prompt.askpass.is_none(), "an empty askpass path is always ignored"); + } + Ok(()) +} + +#[test] +fn core_askpass_interpolation_errors_are_not_ignored() -> crate::Result { + for strict in [false, true] { + let repo = gix::open_opts( + remote::repo_path("empty-core-askpass"), + gix::open::Options::isolated() + .strict_config(strict) + .config_overrides(["core.askpass=~/askpass"]), + )?; + let err = repo + .config_snapshot() + .credential_helpers("does-not-matter".try_into()?) + .err() + .expect("the isolated repository cannot resolve the home directory"); + assert!( + gix_error::Error::from_error(err).is_not_found(), + "the missing interpolation input remains available in the error chain" + ); + } Ok(()) } diff --git a/gix/tests/gix/repository/config/identity.rs b/gix/tests/gix/repository/config/identity.rs index 394edeffa05..05cdca97809 100644 --- a/gix/tests/gix/repository/config/identity.rs +++ b/gix/tests/gix/repository/config/identity.rs @@ -154,7 +154,7 @@ fn author_and_committer_and_fallback() -> crate::Result { assert_eq!(config.boolean("core.bare"), Some(false)); assert_eq!(config.boolean("a.bad-bool"), None); - assert_eq!(config.try_boolean("core.bare"), Ok(Some(false))); + assert_eq!(config.try_boolean("core.bare")?, Some(false)); assert!(config.try_boolean("a.bad-bool").is_err()); assert_eq!(config.integer("a.int"), Some(42)); @@ -171,7 +171,7 @@ fn author_and_committer_and_fallback() -> crate::Result { assert_eq!(config.string("a.env-override").expect("present"), "from-c.config"); assert_eq!(config.boolean("core.missing"), None); - assert_eq!(config.try_boolean("core.missing"), Ok(None)); + assert_eq!(config.try_boolean("core.missing")?, None); let relative_path_key = "a.relative-path"; if trust == gix_sec::Trust::Full { From 8c101c91b8c8ebeae0d890d8d4ac3923a01a23cf Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 20 Aug 2026 17:07:29 +0200 Subject: [PATCH 03/51] change(gix-object)!: migrate errors to gix-error rubberstamp --- Cargo.lock | 8 +- gitoxide-core/src/pack/explode.rs | 9 +- gix-blame/src/error.rs | 14 +- gix-blame/src/file/function.rs | 44 +++-- gix-diff/Cargo.toml | 1 + gix-diff/src/blob/pipeline.rs | 23 ++- gix-diff/src/rewrites/tracker.rs | 8 +- gix-diff/src/tree/function.rs | 14 +- gix-diff/src/tree/mod.rs | 8 +- gix-filter/src/pipeline/convert.rs | 2 +- .../tests/filter/pipeline/convert_to_git.rs | 4 +- gix-index/Cargo.toml | 1 + gix-index/src/extension/tree/verify.rs | 8 +- gix-merge/Cargo.toml | 1 + gix-merge/src/blob/pipeline.rs | 23 +-- gix-merge/src/commit/mod.rs | 8 +- gix-merge/src/commit/virtual_merge_base.rs | 8 +- gix-merge/src/tree/function/change.rs | 4 +- gix-merge/src/tree/function/resolve.rs | 4 +- gix-merge/src/tree/mod.rs | 12 +- gix-merge/tests/merge/blob/pipeline.rs | 4 +- gix-note/src/lib.rs | 3 +- gix-note/tests/note.rs | 5 +- gix-object/Cargo.toml | 2 +- gix-object/src/commit/decode.rs | 4 +- gix-object/src/data.rs | 16 +- gix-object/src/encode.rs | 24 +-- gix-object/src/find.rs | 60 +------ gix-object/src/kind.rs | 15 +- gix-object/src/lib.rs | 54 ++---- gix-object/src/object/mod.rs | 31 ++-- gix-object/src/parse.rs | 26 +-- gix-object/src/signature/sign.rs | 106 ++++------- gix-object/src/signature/verify.rs | 167 +++++++++--------- gix-object/src/tag/decode.rs | 16 +- gix-object/src/tag/write.rs | 32 ++-- gix-object/src/traits/find.rs | 91 ++++------ gix-object/src/traits/mod.rs | 4 +- gix-object/src/tree/editor.rs | 22 +-- gix-object/src/tree/ref_iter.rs | 4 +- gix-object/src/tree/write.rs | 31 ++-- gix-object/tests/object/encode.rs | 20 +-- gix-object/tests/object/tree/iter.rs | 2 +- gix-odb/src/memory.rs | 11 +- gix-odb/src/sink.rs | 19 +- gix-odb/src/store_impls/dynamic/find.rs | 5 +- gix-odb/src/store_impls/dynamic/header.rs | 4 +- gix-odb/src/store_impls/dynamic/write.rs | 7 +- gix-odb/src/store_impls/loose/find.rs | 8 +- gix-odb/src/store_impls/loose/verify.rs | 8 +- gix-odb/src/store_impls/loose/write.rs | 96 +++++----- gix-odb/src/traits.rs | 7 +- .../src/data/output/count/objects/types.rs | 8 +- gix-pack/src/find_traits.rs | 46 ++--- gix-pack/src/index/traverse/error.rs | 2 +- gix-pack/src/index/traverse/mod.rs | 2 +- gix-pack/tests/pack/bundle.rs | 2 +- gix-ref/src/store/packed/transaction.rs | 4 +- gix-revwalk/src/graph/mod.rs | 40 +++-- gix-status/Cargo.toml | 1 + gix-status/src/index_as_worktree/function.rs | 15 +- gix-status/src/index_as_worktree/types.rs | 2 +- gix-tix/src/edit/rebase.rs | 5 +- gix-tix/src/history.rs | 1 + gix-traverse/Cargo.toml | 1 + gix-traverse/src/commit/simple.rs | 8 +- gix-traverse/src/commit/topo/mod.rs | 8 +- gix-traverse/src/tree/breadthfirst.rs | 8 +- gix-worktree-state/Cargo.toml | 1 + gix-worktree-state/src/checkout/entry.rs | 4 +- gix-worktree-state/src/checkout/mod.rs | 2 +- gix-worktree-stream/tests/stream.rs | 3 +- gix-worktree/src/stack/state/attributes.rs | 4 +- gix-worktree/src/stack/state/ignore.rs | 4 +- gix-worktree/tests/worktree/stack/ignore.rs | 6 +- gix/src/commit/sign.rs | 4 +- gix/src/commit/verify.rs | 24 ++- gix/src/mailmap.rs | 2 +- gix/src/object/commit.rs | 9 +- gix/src/object/errors.rs | 2 +- gix/src/object/tree/editor.rs | 17 +- gix/src/repository/impls.rs | 17 +- gix/src/repository/mailmap.rs | 6 +- gix/src/repository/object.rs | 19 +- gix/src/repository/worktree.rs | 2 +- gix/tests/gix/commit.rs | 10 +- gix/tests/gix/revision/spec/from_bytes/mod.rs | 55 ++++-- tests/it/src/commands/blame_copy_royal.rs | 6 +- tests/it/src/commands/create_diff_cases.rs | 12 +- tests/it/src/commands/git_to_sh.rs | 9 +- 90 files changed, 762 insertions(+), 717 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b166e0452e0..774629f36b6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1819,6 +1819,7 @@ dependencies = [ "getrandom 0.4.3", "gix-attributes", "gix-command", + "gix-error", "gix-filter", "gix-fs", "gix-hash", @@ -2040,6 +2041,7 @@ dependencies = [ "filetime", "fnv", "gix-bitmap", + "gix-error", "gix-features", "gix-fs", "gix-hash", @@ -2109,6 +2111,7 @@ dependencies = [ "document-features", "gix-command", "gix-diff", + "gix-error", "gix-filter", "gix-fs", "gix-hash", @@ -2171,6 +2174,7 @@ dependencies = [ "gix-actor", "gix-command", "gix-date", + "gix-error", "gix-features", "gix-hash", "gix-hashtable", @@ -2187,7 +2191,6 @@ dependencies = [ "serde", "smallvec", "termtree", - "thiserror 2.0.18", ] [[package]] @@ -2481,6 +2484,7 @@ dependencies = [ "filetime", "gix-diff", "gix-dir", + "gix-error", "gix-features", "gix-filter", "gix-fs", @@ -2633,6 +2637,7 @@ dependencies = [ "bitflags 2.13.0", "gix-commitgraph", "gix-date", + "gix-error", "gix-hash", "gix-hashtable", "gix-object", @@ -2712,6 +2717,7 @@ name = "gix-worktree-state" version = "0.34.1" dependencies = [ "bstr", + "gix-error", "gix-features", "gix-filter", "gix-fs", diff --git a/gitoxide-core/src/pack/explode.rs b/gitoxide-core/src/pack/explode.rs index 68ab02000b2..19766213ff0 100644 --- a/gitoxide-core/src/pack/explode.rs +++ b/gitoxide-core/src/pack/explode.rs @@ -9,7 +9,7 @@ use anyhow::{Result, anyhow}; use gix::{ NestedProgress, hash::ObjectId, - object, objs, odb, + object, odb, odb::{loose, pack}, prelude::Write, }; @@ -77,7 +77,7 @@ enum Error { id: ObjectId, }, #[error("Object didn't verify after right after writing it")] - Verify(#[from] objs::data::verify::Error), + Verify(#[source] std::io::Error), #[error("{kind} object wasn't re-encoded without change")] ObjectEncodeMismatch { #[source] @@ -229,7 +229,7 @@ pub fn pack_or_pack_index( let mut read_buf = Vec::new(); move |object_kind, buf, index_entry, progress| { let written_id = out.write_buf(object_kind, buf).map_err(|err| Error::Write { - source: err, + source: err.into(), kind: object_kind, id: index_entry.oid, })?; @@ -254,7 +254,8 @@ pub fn pack_or_pack_index( id: written_id, })? .ok_or(Error::WrittenFileMissing { id: written_id })?; - obj.verify_checksum(&written_id)?; + obj.verify_checksum(&written_id) + .map_err(|err| Error::Verify(std::io::Error::other(err.into_error())))?; } Ok(()) } diff --git a/gix-blame/src/error.rs b/gix-blame/src/error.rs index 9cedec7b600..c5584247562 100644 --- a/gix-blame/src/error.rs +++ b/gix-blame/src/error.rs @@ -20,9 +20,9 @@ pub enum Error { #[error("Couldn't find commit or tree in the object database")] FindObject(#[from] gix_object::find::Error), #[error("Could not find existing blob or commit")] - FindExistingObject(#[from] gix_object::find::existing_object::Error), + FindExistingObject(#[source] gix_error::Error), #[error("Could not find existing iterator over a tree")] - FindExistingIter(#[from] gix_object::find::existing_iter::Error), + FindExistingIter(#[source] gix_error::Error), #[error("Failed to obtain the next commit in the commit-graph traversal")] Traverse(#[source] Box), #[error(transparent)] @@ -38,3 +38,13 @@ pub enum Error { #[error("Failed to get parent from commitgraph during traversal")] GetParentFromCommitGraph(#[from] gix_error::Message), } + +impl Error { + pub(crate) fn find_existing_object(err: gix_object::find::existing_object::Error) -> Self { + Error::FindExistingObject(err.into_error()) + } + + pub(crate) fn find_existing_iter(err: gix_object::find::existing_iter::Error) -> Self { + Error::FindExistingIter(err.into_error()) + } +} diff --git a/gix-blame/src/file/function.rs b/gix-blame/src/file/function.rs index 047ed5a3384..64f19bfad57 100644 --- a/gix-blame/src/file/function.rs +++ b/gix-blame/src/file/function.rs @@ -107,7 +107,7 @@ pub fn file( gix_revwalk::PriorityQueue::new(); if let Some(first_suspect) = first_suspect { - let commit = find_commit(cache.as_ref(), &odb, &first_suspect, &mut buf)?; + let commit = find_commit(cache.as_ref(), &odb, &first_suspect, &mut buf).map_err(Error::find_existing_iter)?; queue.insert(commit.commit_time()?, first_suspect); } @@ -137,7 +137,7 @@ pub fn file( .clone() .unwrap_or_else(|| file_path.to_owned()); - let commit = find_commit(cache.as_ref(), &odb, &suspect, &mut buf)?; + let commit = find_commit(cache.as_ref(), &odb, &suspect, &mut buf).map_err(Error::find_existing_iter)?; let commit_time = commit.commit_time()?; if let Some(since) = options.since @@ -208,7 +208,11 @@ pub fn file( // identical to the corresponding lines in the *Source File*. #[cfg(debug_assertions)] { - let source_blob = odb.find_blob(&entry_id, &mut buf)?.data.to_vec(); + let source_blob = odb + .find_blob(&entry_id, &mut buf) + .map_err(Error::find_existing_object)? + .data + .to_vec(); let mut source_interner = gix_diff::blob::Interner::new(source_blob.len() / 100); let source_lines_as_tokens: Vec<_> = tokens_for_diffing(&source_blob) .tokenize() @@ -567,14 +571,22 @@ fn tree_diff_at_file_path( rhs_tree_buf: &mut Vec, rewrites: Option, ) -> Result, Error> { - let parent_tree_id = find_commit(cache, &odb, &parent_id, commit_buf)?.tree_id()?; + let parent_tree_id = find_commit(cache, &odb, &parent_id, commit_buf) + .map_err(Error::find_existing_iter)? + .tree_id()?; - let parent_tree_iter = odb.find_tree_iter(&parent_tree_id, lhs_tree_buf)?; + let parent_tree_iter = odb + .find_tree_iter(&parent_tree_id, lhs_tree_buf) + .map_err(Error::find_existing_iter)?; stats.trees_decoded += 1; - let tree_id = find_commit(cache, &odb, &id, commit_buf)?.tree_id()?; + let tree_id = find_commit(cache, &odb, &id, commit_buf) + .map_err(Error::find_existing_iter)? + .tree_id()?; - let tree_iter = odb.find_tree_iter(&tree_id, rhs_tree_buf)?; + let tree_iter = odb + .find_tree_iter(&tree_id, rhs_tree_buf) + .map_err(Error::find_existing_iter)?; stats.trees_decoded += 1; let result = tree_diff_without_rewrites_at_file_path(&odb, file_path, stats, state, parent_tree_iter, tree_iter)?; @@ -844,8 +856,10 @@ fn find_path_entry_in_commit( buf2: &mut Vec, stats: &mut Statistics, ) -> Result, Error> { - let tree_id = find_commit(cache, odb, commit, buf)?.tree_id()?; - let tree_iter = odb.find_tree_iter(&tree_id, buf)?; + let tree_id = find_commit(cache, odb, commit, buf) + .map_err(Error::find_existing_iter)? + .tree_id()?; + let tree_iter = odb.find_tree_iter(&tree_id, buf).map_err(Error::find_existing_iter)?; stats.trees_decoded += 1; let res = tree_iter.lookup_entry( @@ -925,7 +939,11 @@ fn initial_state( file_path: file_path.to_owned(), commit_id: suspect, })?; - let blamed_file_blob = odb.find_blob(&blamed_file_entry_id, buf)?.data.to_vec(); + let blamed_file_blob = odb + .find_blob(&blamed_file_entry_id, buf) + .map_err(Error::find_existing_object)? + .data + .to_vec(); let num_lines_in_blamed = tokens_for_diffing(&blamed_file_blob).tokenize().count() as u32; // Binary or otherwise empty? @@ -995,7 +1013,11 @@ fn initial_state( }); }; - let first_suspect_blob = odb.find_blob(&first_suspect_entry_id, buf)?.data.to_vec(); + let first_suspect_blob = odb + .find_blob(&first_suspect_entry_id, buf) + .map_err(Error::find_existing_object)? + .data + .to_vec(); let changes = blob_changes_from_data(&first_suspect_blob, &blamed_file_blob, options.diff_algorithm, stats); hunks_to_blame = process_changes(hunks_to_blame, changes, null_id, first_suspect); diff --git a/gix-diff/Cargo.toml b/gix-diff/Cargo.toml index 8084d285656..957a2ebd52b 100644 --- a/gix-diff/Cargo.toml +++ b/gix-diff/Cargo.toml @@ -56,6 +56,7 @@ gix-index = { version = "^0.55.0", path = "../gix-index", optional = true } gix-pathspec = { version = "^0.20.0", path = "../gix-pathspec", optional = true } gix-attributes = { version = "^0.35.0", path = "../gix-attributes", optional = true } gix-hash = { version = "^0.26.1", path = "../gix-hash" } +gix-error = { version = "^0.3.0", path = "../gix-error" } gix-object = { version = "^0.64.0", path = "../gix-object" } gix-filter = { version = "^0.34.0", path = "../gix-filter", optional = true } gix-worktree = { version = "^0.56.0", path = "../gix-worktree", default-features = false, features = ["attributes"], optional = true } diff --git a/gix-diff/src/blob/pipeline.rs b/gix-diff/src/blob/pipeline.rs index 72e43457b07..00717d78b21 100644 --- a/gix-diff/src/blob/pipeline.rs +++ b/gix-diff/src/blob/pipeline.rs @@ -5,6 +5,7 @@ use std::{ }; use bstr::{BStr, ByteSlice}; +use gix_error::{ErrorExt, NotFoundError}; use gix_filter::{ driver::apply::{Delay, MaybeDelayed}, pipeline::convert::{ToGitOutcome, ToWorktreeOutcome, to_worktree}, @@ -158,7 +159,7 @@ pub mod convert_to_diffable { stderr: BString, }, #[error(transparent)] - FindObject(#[from] gix_object::find::existing_object::Error), + FindObject(gix_error::Error), #[error(transparent)] ConvertToWorktree(#[from] gix_filter::pipeline::convert::to_worktree::Error), #[error(transparent)] @@ -166,6 +167,12 @@ pub mod convert_to_diffable { #[error("Memory allocation failed")] OutOfMemory(#[from] TryReserveError), } + + impl From for Error { + fn from(err: gix_object::find::existing_object::Error) -> Self { + Error::FindObject(err.into_error()) + } + } } /// Lifecycle @@ -406,10 +413,9 @@ impl Pipeline { let data = if id.is_null() { None } else { - let header = objects - .try_header(id) - .map_err(gix_object::find::existing_object::Error::Find)? - .ok_or_else(|| gix_object::find::existing_object::Error::NotFound { oid: id.to_owned() })?; + let header = objects.try_header(id)?.ok_or_else(|| { + NotFoundError::new(format!("An object with id {id} could not be found")).raise_erased() + })?; if is_binary.is_none() && self.options.large_file_threshold_bytes > 0 && header.size > self.options.large_file_threshold_bytes @@ -419,10 +425,9 @@ impl Pipeline { let data = if is_binary == Some(true) { Data::Binary { size: header.size } } else { - objects - .try_find(id, out) - .map_err(gix_object::find::existing_object::Error::Find)? - .ok_or_else(|| gix_object::find::existing_object::Error::NotFound { oid: id.to_owned() })?; + objects.try_find(id, out)?.ok_or_else(|| { + NotFoundError::new(format!("An object with id {id} could not be found")).raise_erased() + })?; let mut is_derived = false; if matches!(mode, EntryKind::Blob | EntryKind::BlobExecutable) && convert == Mode::ToWorktreeAndBinaryToText diff --git a/gix-diff/src/rewrites/tracker.rs b/gix-diff/src/rewrites/tracker.rs index df5b440adee..72849f8d36c 100644 --- a/gix-diff/src/rewrites/tracker.rs +++ b/gix-diff/src/rewrites/tracker.rs @@ -140,7 +140,7 @@ pub mod emit { #[expect(missing_docs)] pub enum Error { #[error("Could not find blob for similarity checking")] - FindExistingBlob(#[from] gix_object::find::existing_object::Error), + FindExistingBlob(#[source] gix_error::Error), #[error("Could not obtain exhaustive item set to use as possible sources for copy detection")] GetItemsForExhaustiveCopyDetection(#[source] Box), #[error(transparent)] @@ -148,6 +148,12 @@ pub mod emit { #[error(transparent)] PrepareDiff(#[from] crate::blob::platform::prepare_diff::Error), } + + impl From for Error { + fn from(err: gix_object::find::existing_object::Error) -> Self { + Error::FindExistingBlob(err.into_error()) + } + } } /// Lifecycle diff --git a/gix-diff/src/tree/function.rs b/gix-diff/src/tree/function.rs index 88e334b22fa..f35f42e25d2 100644 --- a/gix-diff/src/tree/function.rs +++ b/gix-diff/src/tree/function.rs @@ -225,7 +225,12 @@ fn catchup_rhs_with_lhs( break; } }, - Some(Err(err)) => return Err(Error::EntriesDecode(err.to_owned())), + Some(Err(_)) => { + let Err(err) = rhs_entries.next().expect("the peeked item to be present") else { + unreachable!("peeked error changed before it was consumed") + }; + return Err(Error::EntriesDecode(err)); + } None => { delegate.pop_path_component(); delete_entry_schedule_recursion(lhs, queue, change_id, relation_to_propagate, delegate)?; @@ -274,7 +279,12 @@ fn catchup_lhs_with_rhs( break; } }, - Some(Err(err)) => return Err(Error::EntriesDecode(err.to_owned())), + Some(Err(_)) => { + let Err(err) = lhs_entries.next().expect("the peeked item to be present") else { + unreachable!("peeked error changed before it was consumed") + }; + return Err(Error::EntriesDecode(err)); + } None => { delegate.pop_path_component(); add_entry_schedule_recursion(rhs, queue, change_id, relation_to_propagate, delegate)?; diff --git a/gix-diff/src/tree/mod.rs b/gix-diff/src/tree/mod.rs index 52064b1c309..ccc7d3c3ed8 100644 --- a/gix-diff/src/tree/mod.rs +++ b/gix-diff/src/tree/mod.rs @@ -11,13 +11,19 @@ use crate::tree::visit::Relation; #[expect(missing_docs)] pub enum Error { #[error(transparent)] - Find(#[from] gix_object::find::existing_iter::Error), + Find(gix_error::Error), #[error("The delegate cancelled the operation")] Cancelled, #[error(transparent)] EntriesDecode(#[from] gix_object::decode::Error), } +impl From for Error { + fn from(err: gix_object::find::existing_iter::Error) -> Self { + Error::Find(err.into_error()) + } +} + /// A trait to allow responding to a traversal designed to figure out the [changes](visit::Change) /// to turn tree A into tree B. pub trait Visit { diff --git a/gix-filter/src/pipeline/convert.rs b/gix-filter/src/pipeline/convert.rs index 608aa91b350..8d3615913fe 100644 --- a/gix-filter/src/pipeline/convert.rs +++ b/gix-filter/src/pipeline/convert.rs @@ -170,7 +170,7 @@ impl Pipeline { &self.bufs.src, digest, &mut self.bufs.dest, - &mut |buf| index_object(buf), + &mut |buf| index_object(buf).map_err(Into::into), eol::convert_to_git::Options { round_trip_check: self.options.crlf_roundtrip_check.to_eol_roundtrip_check(rela_path), config: self.options.eol_config, diff --git a/gix-filter/tests/filter/pipeline/convert_to_git.rs b/gix-filter/tests/filter/pipeline/convert_to_git.rs index e9ec68b42f7..fd56b134f25 100644 --- a/gix-filter/tests/filter/pipeline/convert_to_git.rs +++ b/gix-filter/tests/filter/pipeline/convert_to_git.rs @@ -145,10 +145,10 @@ fn no_filter_means_reader_is_returned_unchanged() -> gix_testtools::Result { Ok(()) } -fn no_call(_buf: &mut Vec) -> Result, Box> { +fn no_call(_buf: &mut Vec) -> Result, gix_object::find::Error> { unreachable!("index function will not be called") } -fn no_object_in_index(_buf: &mut Vec) -> Result, Box> { +fn no_object_in_index(_buf: &mut Vec) -> Result, gix_object::find::Error> { Ok(None) } diff --git a/gix-index/Cargo.toml b/gix-index/Cargo.toml index 459a26150de..f0093873263 100644 --- a/gix-index/Cargo.toml +++ b/gix-index/Cargo.toml @@ -37,6 +37,7 @@ gix-features = { version = "^0.49.1", path = "../gix-features", features = [ "progress", ] } gix-hash = { version = "^0.26.1", path = "../gix-hash" } +gix-error = { version = "^0.3.0", path = "../gix-error" } gix-bitmap = { version = "^0.4.0", path = "../gix-bitmap" } gix-object = { version = "^0.64.0", path = "../gix-object" } gix-validate = { version = "^0.11.4", path = "../gix-validate" } diff --git a/gix-index/src/extension/tree/verify.rs b/gix-index/src/extension/tree/verify.rs index 5ae094a5c75..04598a66727 100644 --- a/gix-index/src/extension/tree/verify.rs +++ b/gix-index/src/extension/tree/verify.rs @@ -18,7 +18,7 @@ pub enum Error { name: BString, }, #[error(transparent)] - TreeNodeNotFound(#[from] gix_object::find::existing_iter::Error), + TreeNodeNotFound(gix_error::Error), #[error( "The tree with id {oid} should have {expected_childcount} children, but its cached representation had {actual_childcount} of them" )] @@ -51,6 +51,12 @@ pub enum Error { }, } +impl From for Error { + fn from(err: gix_object::find::existing_iter::Error) -> Self { + Error::TreeNodeNotFound(err.into_error()) + } +} + impl Tree { /// Validate the correctness of this instance. If `use_objects` is true, then `objects` will be used to access all objects. pub fn verify(&self, use_objects: bool, objects: impl gix_object::Find) -> Result<(), Error> { diff --git a/gix-merge/Cargo.toml b/gix-merge/Cargo.toml index a8446a22009..99475ecb461 100644 --- a/gix-merge/Cargo.toml +++ b/gix-merge/Cargo.toml @@ -42,6 +42,7 @@ gix-revision = { version = "^0.49.1", path = "../gix-revision", default-features gix-revwalk = { version = "^0.35.0", path = "../gix-revwalk" } gix-diff = { version = "^0.67.1", path = "../gix-diff", default-features = false, features = ["blob"] } gix-index = { version = "^0.55.0", path = "../gix-index" } +gix-error = { version = "^0.3.0", path = "../gix-error" } imara-diff = { package = "gix-imara-diff", version = "^0.2.5", path = "../gix-imara-diff" } thiserror = "2.0.18" diff --git a/gix-merge/src/blob/pipeline.rs b/gix-merge/src/blob/pipeline.rs index f16bf57a98d..61de3575787 100644 --- a/gix-merge/src/blob/pipeline.rs +++ b/gix-merge/src/blob/pipeline.rs @@ -4,11 +4,12 @@ use std::{ }; use bstr::BStr; +use gix_error::{ErrorExt, NotFoundError}; use gix_filter::{ driver::apply::{Delay, MaybeDelayed}, pipeline::convert::{ToGitOutcome, ToWorktreeOutcome, to_worktree}, }; -use gix_object::tree::EntryKind; +use gix_object::{FindExt, tree::EntryKind}; use super::{Pipeline, ResourceKind}; @@ -130,7 +131,7 @@ pub mod convert_to_mergeable { #[error("Entry at '{rela_path}' could not be copied from a filter process to a memory buffer")] StreamCopy { rela_path: BString, source: std::io::Error }, #[error(transparent)] - FindObject(#[from] gix_object::find::existing_object::Error), + FindObject(#[from] gix_error::Error), #[error(transparent)] ConvertToWorktree(#[from] gix_filter::pipeline::convert::to_worktree::Error), #[error(transparent)] @@ -138,6 +139,12 @@ pub mod convert_to_mergeable { #[error("Memory allocation failed")] OutOfMemory(#[from] TryReserveError), } + + impl From for Error { + fn from(err: gix_object::find::existing_object::Error) -> Self { + Error::FindObject(err.into_error()) + } + } } /// Conversion @@ -263,19 +270,15 @@ impl Pipeline { let data = if id.is_null() { None } else { - let header = objects - .try_header(id) - .map_err(gix_object::find::existing_object::Error::Find)? - .ok_or_else(|| gix_object::find::existing_object::Error::NotFound { oid: id.to_owned() })?; + let header = objects.try_header(id)?.ok_or_else(|| { + NotFoundError::new(format!("An object with id {id} could not be found")).raise_erased() + })?; let is_binary = self.options.large_file_threshold_bytes > 0 && header.size > self.options.large_file_threshold_bytes; let data = if is_binary { Data::TooLarge { size: header.size } } else { - objects - .try_find(id, out) - .map_err(gix_object::find::existing_object::Error::Find)? - .ok_or_else(|| gix_object::find::existing_object::Error::NotFound { oid: id.to_owned() })?; + objects.find(id, out)?; if convert == Mode::Renormalize { { diff --git a/gix-merge/src/commit/mod.rs b/gix-merge/src/commit/mod.rs index 0a32c818ac8..9667691bea1 100644 --- a/gix-merge/src/commit/mod.rs +++ b/gix-merge/src/commit/mod.rs @@ -16,7 +16,13 @@ pub enum Error { their_commit_id: gix_hash::ObjectId, }, #[error("Could not find ancestor, our or their commit to extract tree from")] - FindCommit(#[from] gix_object::find::existing_object::Error), + FindCommit(#[source] gix_error::Error), +} + +impl From for Error { + fn from(err: gix_object::find::existing_object::Error) -> Self { + Error::FindCommit(err.into_error()) + } } /// A way to configure [`commit()`](crate::commit()). diff --git a/gix-merge/src/commit/virtual_merge_base.rs b/gix-merge/src/commit/virtual_merge_base.rs index 09a08cff473..679df033e11 100644 --- a/gix-merge/src/commit/virtual_merge_base.rs +++ b/gix-merge/src/commit/virtual_merge_base.rs @@ -26,7 +26,13 @@ pub enum Error { )] VirtualMergeBaseConflict, #[error("Could not find commit to use as basis for a virtual commit")] - FindCommit(#[from] gix_object::find::existing_object::Error), + FindCommit(#[source] gix_error::Error), +} + +impl From for Error { + fn from(err: gix_object::find::existing_object::Error) -> Self { + Error::FindCommit(err.into_error()) + } } pub(super) mod function { diff --git a/gix-merge/src/tree/function/change.rs b/gix-merge/src/tree/function/change.rs index 8cad4ba0f84..2ee0a1a3bad 100644 --- a/gix-merge/src/tree/function/change.rs +++ b/gix-merge/src/tree/function/change.rs @@ -106,7 +106,9 @@ pub(super) fn collect( ) -> Result { let mut changes = Vec::new(); if base_tree != side_tree { - let side_tree = objects.find_tree_iter(side_tree, side_buf)?; + let side_tree = objects + .find_tree_iter(side_tree, side_buf) + .map_err(|err| Error::FindTreeIter(err.into_error()))?; gix_diff::tree_with_rewrites( gix_object::TreeRefIter::from_bytes(base_buf, base_tree.kind()), side_tree, diff --git a/gix-merge/src/tree/function/resolve.rs b/gix-merge/src/tree/function/resolve.rs index cd691d6c22d..e0d27ceb96d 100644 --- a/gix-merge/src/tree/function/resolve.rs +++ b/gix-merge/src/tree/function/resolve.rs @@ -105,7 +105,9 @@ where let _span = gix_trace::coarse!("gix_merge::tree", ?base_tree, ?our_tree, ?their_tree, ?labels); let (mut base_buf, mut side_buf) = (Vec::new(), Vec::new()); let mut editor = { - let ancestor_tree = objects.find_tree(base_tree, &mut base_buf)?; + let ancestor_tree = objects + .find_tree(base_tree, &mut base_buf) + .map_err(|err| Error::FindTree(err.into_error()))?; tree::Editor::new(ancestor_tree.to_owned(), objects, base_tree.kind()) }; let resolve_tree_conflicts = options.tree_conflicts; diff --git a/gix-merge/src/tree/mod.rs b/gix-merge/src/tree/mod.rs index a9808343416..344d37859b8 100644 --- a/gix-merge/src/tree/mod.rs +++ b/gix-merge/src/tree/mod.rs @@ -6,13 +6,13 @@ use gix_diff::{Rewrites, tree_with_rewrites::Change}; #[expect(missing_docs)] pub enum Error { #[error("Could not find ancestor, our or their tree to get started")] - FindTree(#[from] gix_object::find::existing_object::Error), + FindTree(#[source] gix_error::Error), #[error("Could not find ancestor, our or their tree iterator to get started")] - FindTreeIter(#[from] gix_object::find::existing_iter::Error), + FindTreeIter(#[source] gix_error::Error), #[error("Failed to diff our side or their side")] DiffTree(#[from] gix_diff::tree_with_rewrites::Error), #[error("Could not apply merge result to base tree")] - TreeEdit(#[from] gix_object::tree::editor::Error), + TreeEdit(#[source] gix_error::Error), #[error("Failed to load resource to prepare for blob merge")] BlobMergeSetResource(#[from] crate::blob::platform::set_resource::Error), #[error(transparent)] @@ -25,6 +25,12 @@ pub enum Error { MergeResourceNotFound, } +impl From for Error { + fn from(err: gix_object::tree::editor::Error) -> Self { + Error::TreeEdit(err.into_error()) + } +} + /// The outcome produced by [`tree()`](crate::tree()). #[derive(Clone)] pub struct Outcome<'a> { diff --git a/gix-merge/tests/merge/blob/pipeline.rs b/gix-merge/tests/merge/blob/pipeline.rs index 6ada5a8045f..d50fd60b0c6 100644 --- a/gix-merge/tests/merge/blob/pipeline.rs +++ b/gix-merge/tests/merge/blob/pipeline.rs @@ -300,8 +300,8 @@ fn non_existing() -> crate::Result { matches!( err, gix_merge::blob::pipeline::convert_to_mergeable::Error::FindObject( - gix_object::find::existing_object::Error::NotFound { .. } - ), + err + ) if err.is_not_found(), ), "missing object database ids are always an error (even though missing objects on disk are allowed)" ); diff --git a/gix-note/src/lib.rs b/gix-note/src/lib.rs index 9120a5e1f1f..2366b4da374 100644 --- a/gix-note/src/lib.rs +++ b/gix-note/src/lib.rs @@ -570,7 +570,7 @@ impl InternalNode { .or_raise_erased(|| message("Could not add a note tree entry"))?; } editor - .write(|tree| objects.write(tree).map_err(gix_error::Error::from_boxed)) + .write(|tree| objects.write(tree)) .or_raise_erased(|| message("Could not write the notes tree")) } @@ -667,7 +667,6 @@ fn write_note_entries(notes: &[TreeEntry], level: usize, objects: &impl Write) - objects .write(&Tree { entries }) - .map_err(gix_error::Error::from_boxed) .or_raise_erased(|| message("Could not write the notes tree")) } diff --git a/gix-note/tests/note.rs b/gix-note/tests/note.rs index 87b1a49e2ab..eb04c7af958 100644 --- a/gix-note/tests/note.rs +++ b/gix-note/tests/note.rs @@ -4,6 +4,7 @@ use std::{ io::{self, Read}, }; +use gix_error::ErrorExt; use gix_hash::{Kind, ObjectId, oid}; use gix_object::{ FindExt, Tree, Write, @@ -66,7 +67,7 @@ impl CountingObjectDb { fn maybe_fail_write(&self) -> Result<(), gix_object::write::Error> { if self.fail_next_write.replace(false) { - return Err(io::Error::other("injected write failure").into()); + return Err(io::Error::other("injected write failure").raise_erased()); } Ok(()) } @@ -80,7 +81,7 @@ impl gix_object::Find for CountingObjectDb { ) -> Result>, gix_object::find::Error> { self.reads.set(self.reads.get() + 1); if self.fail_next_read.replace(false) { - return Err(io::Error::other("injected read failure").into()); + return Err(io::Error::other("injected read failure").raise_erased()); } self.inner.try_find(id, buffer) } diff --git a/gix-object/Cargo.toml b/gix-object/Cargo.toml index 0b24abfefdc..94ecbe373ff 100644 --- a/gix-object/Cargo.toml +++ b/gix-object/Cargo.toml @@ -52,10 +52,10 @@ gix-date = { version = "^0.16.0", path = "../gix-date" } gix-utils = { version = "^0.3.6", path = "../gix-utils" } gix-command = { version = "^0.10.1", path = "../gix-command", optional = true } gix-path = { version = "^0.12.6", path = "../gix-path", optional = true } +gix-error = { version = "^0.3.0", path = "../gix-error" } gix-tempfile = { version = "^24.0.0", path = "../gix-tempfile", optional = true } itoa = "1.0.17" -thiserror = "2.0.18" bstr = { version = "1.12.0", default-features = false, features = [ "std", "unicode", diff --git a/gix-object/src/commit/decode.rs b/gix-object/src/commit/decode.rs index ae3d9750637..7f2ed670289 100644 --- a/gix-object/src/commit/decode.rs +++ b/gix-object/src/commit/decode.rs @@ -16,7 +16,7 @@ pub fn message<'a>(i: &mut &'a [u8]) -> ParseResult<&'a BStr> { *i = &[]; Ok(rest.as_bstr()) } else { - Err(crate::decode::Error) + Err(crate::decode::empty_error()) } } @@ -83,7 +83,7 @@ pub fn commit<'a>(i: &mut &'a [u8], object_hash: gix_hash::Kind) -> ParseResult< let message = message(i)?; if !i.is_empty() { - return Err(crate::decode::Error); + return Err(crate::decode::empty_error()); } Ok(CommitRef { diff --git a/gix-object/src/data.rs b/gix-object/src/data.rs index 93d9ed352c6..9559f3b0b91 100644 --- a/gix-object/src/data.rs +++ b/gix-object/src/data.rs @@ -55,23 +55,19 @@ impl<'a> Data<'a> { /// Types supporting object hash verification pub mod verify { + use gix_error::{CorruptionError, ErrorExt, ResultExt}; + /// Returned by [`crate::Data::verify_checksum()`] - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error("Failed to hash object")] - Hasher(#[from] gix_hash::hasher::Error), - #[error(transparent)] - Verify(gix_hash::verify::Error), - } + pub type Error = gix_error::Exn; impl crate::Data<'_> { /// Compute the checksum of `self` and compare it with the `expected` hash. /// If the hashes do not match, an [`Error`] is returned, containing the actual /// hash of `self`. pub fn verify_checksum(&self, expected: &gix_hash::oid) -> Result { - let actual = crate::compute_hash(expected.kind(), self.kind, self.data)?; - actual.verify(expected).map_err(Error::Verify)?; + let actual = crate::compute_hash(expected.kind(), self.kind, self.data) + .or_raise(|| CorruptionError::new("Failed to hash object"))?; + actual.verify(expected).map_err(ErrorExt::raise)?; Ok(actual) } } diff --git a/gix-object/src/encode.rs b/gix-object/src/encode.rs index 842fbe87696..68b0958b38a 100644 --- a/gix-object/src/encode.rs +++ b/gix-object/src/encode.rs @@ -1,17 +1,10 @@ //! Encoding utilities use std::io::{self, Write}; -use bstr::{BString, ByteSlice}; +use bstr::ByteSlice; /// An error returned when object encoding fails. -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] -pub enum Error { - #[error("Newlines are not allowed in header values: {value:?}")] - NewlineInHeaderValue { value: BString }, - #[error("Header values must not be empty")] - EmptyValue, -} +pub type Error = gix_error::ValidationError; macro_rules! check { ($e: expr) => { @@ -28,12 +21,6 @@ pub fn loose_header(kind: crate::Kind, size: u64) -> smallvec::SmallVec<[u8; 28] v } -impl From for io::Error { - fn from(other: Error) -> io::Error { - io::Error::other(other) - } -} - pub(crate) fn header_field_multi_line(name: &[u8], value: &[u8], out: &mut dyn io::Write) -> io::Result<()> { let mut lines = value.as_bstr().lines_with_terminator(); out.write_all(name)?; @@ -82,10 +69,13 @@ pub(crate) fn trusted_header_id( pub(crate) fn header_field(name: &[u8], value: &[u8], out: &mut dyn io::Write) -> io::Result<()> { if value.is_empty() { - return Err(Error::EmptyValue.into()); + return Err(io::Error::other(Error::new("Header values must not be empty"))); } if value.find(NL).is_some() { - return Err(Error::NewlineInHeaderValue { value: value.into() }.into()); + return Err(io::Error::other(Error::new_with_input( + "Newlines are not allowed in header values", + value, + ))); } trusted_header_field(name, value, out) } diff --git a/gix-object/src/find.rs b/gix-object/src/find.rs index 3892883630c..74750d77e90 100644 --- a/gix-object/src/find.rs +++ b/gix-object/src/find.rs @@ -1,65 +1,23 @@ +use gix_error::ResultExt; + /// The error type returned by the [`Find`](crate::Find) trait. -pub type Error = Box; +pub type Error = gix_error::Exn; /// pub mod existing { - use gix_hash::ObjectId; - /// The error returned by the [`find(…)`][crate::FindExt::find()] trait methods. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - Find(crate::find::Error), - #[error("An object with id {} could not be found", .oid)] - NotFound { oid: ObjectId }, - } + pub type Error = gix_error::Exn; } /// pub mod existing_object { - use gix_hash::ObjectId; - /// The error returned by the various [`find_*()`][crate::FindExt::find_commit()] trait methods. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - Find(crate::find::Error), - #[error("Could not decode object at {oid}")] - Decode { - oid: ObjectId, - source: crate::decode::Error, - }, - #[error("An object with id {oid} could not be found")] - NotFound { oid: ObjectId }, - #[error("Expected object of kind {expected} but got {actual} at {oid}")] - ObjectKind { - oid: ObjectId, - actual: crate::Kind, - expected: crate::Kind, - }, - } + pub type Error = gix_error::Exn; } /// pub mod existing_iter { - use gix_hash::ObjectId; - /// The error returned by the various [`find_*_iter()`][crate::FindExt::find_commit_iter()] trait methods. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - Find(crate::find::Error), - #[error("An object with id {oid} could not be found")] - NotFound { oid: ObjectId }, - #[error("Expected object of kind {expected} but got {actual} at {oid}")] - ObjectKind { - oid: ObjectId, - actual: crate::Kind, - expected: crate::Kind, - }, - } + pub type Error = gix_error::Exn; } /// An implementation of object access traits that stores nothing and finds nothing. @@ -152,7 +110,7 @@ impl super::Exists for Never { impl super::Write for Never { fn write_buf(&self, object: crate::Kind, from: &[u8]) -> Result { - crate::compute_hash(gix_hash::Kind::default(), object, from).map_err(Into::into) + crate::compute_hash(gix_hash::Kind::default(), object, from).or_erased() } fn write_buf_with_known_id( @@ -178,7 +136,7 @@ impl super::Write for Never { &mut gix_features::progress::Discard, &std::sync::atomic::AtomicBool::new(false), ) - .map_err(|err| Box::new(err.into_error()) as crate::write::Error) + .or_erased() } fn write_stream_with_known_id( @@ -191,7 +149,7 @@ impl super::Write for Never { let mut buf = [0u8; u16::MAX as usize]; while size != 0 { let bytes = (size as usize).min(buf.len()); - from.read_exact(&mut buf[..bytes])?; + from.read_exact(&mut buf[..bytes]).or_erased()?; size -= bytes as u64; } Ok(id) diff --git a/gix-object/src/kind.rs b/gix-object/src/kind.rs index aeef9c4d72a..2b1baa06f3a 100644 --- a/gix-object/src/kind.rs +++ b/gix-object/src/kind.rs @@ -1,14 +1,7 @@ -use std::fmt; - use crate::Kind; /// The Error used in [`Kind::from_bytes()`]. -#[derive(Debug, Clone, thiserror::Error)] -#[expect(missing_docs)] -pub enum Error { - #[error("Unknown object kind: {kind:?}")] - InvalidObjectKind { kind: bstr::BString }, -} +pub type Error = gix_error::ValidationError; /// Initialization impl Kind { @@ -19,7 +12,7 @@ impl Kind { b"blob" => Kind::Blob, b"commit" => Kind::Commit, b"tag" => Kind::Tag, - _ => return Err(Error::InvalidObjectKind { kind: s.into() }), + _ => return Err(Error::new_with_input("Unknown object kind", s)), }) } } @@ -57,8 +50,8 @@ impl Kind { } } -impl fmt::Display for Kind { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { +impl std::fmt::Display for Kind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(std::str::from_utf8(self.as_bytes()).expect("Converting Kind name to utf8")) } } diff --git a/gix-object/src/lib.rs b/gix-object/src/lib.rs index efd9cffb8c7..08f89412a54 100644 --- a/gix-object/src/lib.rs +++ b/gix-object/src/lib.rs @@ -66,7 +66,7 @@ pub mod find; /// pub mod write { /// The error type returned by the [`Write`](crate::Write) trait. - pub type Error = Box; + pub type Error = gix_error::Exn; } mod traits; @@ -318,60 +318,36 @@ pub struct Header { pub mod decode { mod error { pub(crate) fn empty_error() -> Error { - Error + Error::new("object parsing failed") } /// A type to indicate any error occurred during parsing. - #[derive(Debug, Clone, Copy, Default)] - pub struct Error; - - impl std::fmt::Display for Error { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str("object parsing failed") - } - } - - impl std::error::Error for Error {} + pub type Error = gix_error::ValidationError; } pub use error::Error; pub(crate) use error::empty_error; /// Returned by [`loose_header()`] - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum LooseHeaderDecodeError { - #[error("{message}: {number:?}")] - ParseIntegerError { - source: gix_utils::btoi::ParseIntegerError, - message: &'static str, - number: bstr::BString, - }, - #[error("{message}")] - InvalidHeader { message: &'static str }, - #[error("The object header contained an unknown object kind.")] - ObjectHeader(#[from] super::kind::Error), - } + pub type LooseHeaderDecodeError = gix_error::Exn; use bstr::ByteSlice; + use gix_error::{ErrorExt, ResultExt, ValidationError}; /// Decode a loose object header, being ` \0`, returns /// ([`kind`](super::Kind), `size`, `consumed bytes`). /// /// `size` is the uncompressed size of the payload in bytes. pub fn loose_header(input: &[u8]) -> Result<(super::Kind, u64, usize), LooseHeaderDecodeError> { - use LooseHeaderDecodeError::*; - let kind_end = input.find_byte(0x20).ok_or(InvalidHeader { - message: "Expected ' '", - })?; - let kind = super::Kind::from_bytes(&input[..kind_end])?; - let size_end = input.find_byte(0x0).ok_or(InvalidHeader { - message: "Did not find 0 byte in header", - })?; + let kind_end = input + .find_byte(0x20) + .ok_or_else(|| ValidationError::new("Expected ' '").raise())?; + let kind = super::Kind::from_bytes(&input[..kind_end]) + .or_raise(|| ValidationError::new("The object header contained an unknown object kind."))?; + let size_end = input + .find_byte(0x0) + .ok_or_else(|| ValidationError::new("Did not find 0 byte in header").raise())?; let size_bytes = &input[kind_end + 1..size_end]; - let size = gix_utils::btoi::to_signed(size_bytes).map_err(|source| ParseIntegerError { - source, - message: "Object size in header could not be parsed", - number: size_bytes.into(), - })?; + let size = gix_utils::btoi::to_signed(size_bytes) + .or_raise(|| ValidationError::new_with_input("Object size in header could not be parsed", size_bytes))?; Ok((kind, size, size_end + 1)) } } diff --git a/gix-object/src/object/mod.rs b/gix-object/src/object/mod.rs index cac46323056..67c815b6303 100644 --- a/gix-object/src/object/mod.rs +++ b/gix-object/src/object/mod.rs @@ -185,33 +185,26 @@ impl Object { } } -use crate::{ - BlobRef, CommitRef, Kind, ObjectRef, TagRef, TreeRef, - decode::{Error as DecodeError, LooseHeaderDecodeError, loose_header}, -}; +use crate::{BlobRef, CommitRef, Kind, ObjectRef, TagRef, TreeRef, decode::loose_header}; +use gix_error::{ErrorExt, ResultExt, ValidationError}; -#[derive(Debug, thiserror::Error)] -pub enum LooseDecodeError { - #[error(transparent)] - InvalidHeader(#[from] LooseHeaderDecodeError), - #[error(transparent)] - InvalidContent(#[from] DecodeError), - #[error("Object sized {size} does not fit into memory - this can happen on 32 bit systems")] - OutOfMemory { size: u64 }, -} +pub type LooseDecodeError = gix_error::Exn; impl<'a> ObjectRef<'a> { /// Deserialize an object from a loose serialisation given `data`, parsing with the provided `object_hash`. pub fn from_loose(data: &'a [u8], hash_kind: gix_hash::Kind) -> Result, LooseDecodeError> { let (kind, size, offset) = loose_header(data)?; - let body = &data[offset..] - .get(..size.try_into().map_err(|_| LooseDecodeError::OutOfMemory { size })?) - .ok_or(LooseHeaderDecodeError::InvalidHeader { - message: "object data was shorter than its size declared in the header", - })?; + let size = usize::try_from(size).or_raise(|| { + ValidationError::new(format!( + "Object sized {size} does not fit into memory - this can happen on 32 bit systems" + )) + })?; + let body = &data[offset..].get(..size).ok_or_else(|| { + ValidationError::new("object data was shorter than its size declared in the header").raise() + })?; - Ok(Self::from_bytes(body, kind, hash_kind)?) + Self::from_bytes(body, kind, hash_kind).map_err(ErrorExt::raise) } /// Deserialize an object of `kind` from the given `data`, using `object_hash`. diff --git a/gix-object/src/parse.rs b/gix-object/src/parse.rs index 283c83afd6e..e8c3c54d807 100644 --- a/gix-object/src/parse.rs +++ b/gix-object/src/parse.rs @@ -24,25 +24,25 @@ pub(crate) fn any_header_field_multi_line<'a>(i: &mut &'a [u8]) -> ParseResult<( let name_end = c .find_byteset(SPACE_OR_NL) .filter(|pos| *pos > 0) - .ok_or(crate::decode::Error)?; + .ok_or_else(crate::decode::empty_error)?; if c.get(name_end) != Some(&b' ') { - return Err(crate::decode::Error); + return Err(crate::decode::empty_error()); } c = &c[name_end + 1..]; - let first_line_end = c.find_byte(b'\n').ok_or(crate::decode::Error)?; + let first_line_end = c.find_byte(b'\n').ok_or_else(crate::decode::empty_error)?; c = &c[first_line_end + 1..]; let mut continuation_end = name_end + 1 + first_line_end + 1; let mut continuation_count = 0usize; while c.first() == Some(&b' ') { - let line_end = c.find_byte(b'\n').ok_or(crate::decode::Error)?; + let line_end = c.find_byte(b'\n').ok_or_else(crate::decode::empty_error)?; continuation_end += line_end + 1; c = &c[line_end + 1..]; continuation_count += 1; } if continuation_count == 0 { - return Err(crate::decode::Error); + return Err(crate::decode::empty_error()); } let bytes = input[name_end + 1..continuation_end].as_bstr(); @@ -72,10 +72,10 @@ pub(crate) fn header_field<'a, T>( ) -> ParseResult { let c = *i; let Some(rest) = c.strip_prefix(name).and_then(|rest| rest.strip_prefix(SPACE)) else { - return Err(crate::decode::Error); + return Err(crate::decode::empty_error()); }; let Some(nl) = rest.find_byte(b'\n') else { - return Err(crate::decode::Error); + return Err(crate::decode::empty_error()); }; let value = parse_value(&rest[..nl])?; *i = &rest[nl + 1..]; @@ -94,9 +94,9 @@ pub(crate) fn any_header_field<'a>(i: &mut &'a [u8]) -> ParseResult<(&'a [u8], & let name_end = c .find_byteset(SPACE_OR_NL) .filter(|pos| *pos > 0) - .ok_or(crate::decode::Error)?; + .ok_or_else(crate::decode::empty_error)?; if c.get(name_end) != Some(&b' ') { - return Err(crate::decode::Error); + return Err(crate::decode::empty_error()); } c = &c[name_end + 1..]; if let Some(value_end) = c.find_byte(b'\n') { @@ -105,7 +105,7 @@ pub(crate) fn any_header_field<'a>(i: &mut &'a [u8]) -> ParseResult<(&'a [u8], & *i = rest; Ok((&input[..name_end], value)) } else { - Err(crate::decode::Error) + Err(crate::decode::empty_error()) } } @@ -116,7 +116,7 @@ pub(crate) fn any_header_field<'a>(i: &mut &'a [u8]) -> ParseResult<(&'a [u8], & /// match the expected object hash length. pub fn hex_hash(i: &[u8], hash_kind: gix_hash::Kind) -> ParseResult<&BStr> { if i.len() != hash_kind.len_in_hex() || !i.iter().all(u8::is_ascii_hexdigit) { - return Err(crate::decode::Error); + return Err(crate::decode::empty_error()); } Ok(i.as_bstr()) } @@ -127,11 +127,11 @@ pub fn hex_hash(i: &[u8], hash_kind: gix_hash::Kind) -> ParseResult<&BStr> { /// The entire input slice must be consumed by /// `gix_actor`'s signature parser; trailing bytes cause an error. pub(crate) fn signature(mut i: &[u8]) -> ParseResult> { - let signature = gix_actor::SignatureRef::from_bytes_consuming(&mut i).map_err(|_| crate::decode::Error)?; + let signature = gix_actor::SignatureRef::from_bytes_consuming(&mut i)?; if i.is_empty() { Ok(signature) } else { - Err(crate::decode::Error) + Err(crate::decode::empty_error()) } } diff --git a/gix-object/src/signature/sign.rs b/gix-object/src/signature/sign.rs index e3a2a743462..6bdc8e68610 100644 --- a/gix-object/src/signature/sign.rs +++ b/gix-object/src/signature/sign.rs @@ -6,6 +6,7 @@ use std::{ }; use bstr::{BString, ByteSlice}; +use gix_error::{CorruptionError, ErrorExt, ResultExt, ValidationError, message}; use crate::{Commit, CommitRef, Tag, TagRef, WriteTo}; @@ -29,41 +30,12 @@ pub struct Options { } /// The error returned when signing an object. -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] -pub enum Error { - #[error(transparent)] - Decode(#[from] crate::decode::Error), - #[error(transparent)] - Encode(#[from] std::io::Error), - #[error("A signing key is required")] - MissingSigningKey, - #[error("Could not create or write a temporary signing file")] - TemporaryFile(#[source] std::io::Error), - #[error("Could not execute signing program {program:?}")] - Spawn { - program: OsString, - #[source] - source: std::io::Error, - }, - #[error("Could not communicate with signing program {program:?}")] - Communicate { - program: OsString, - #[source] - source: std::io::Error, - }, - #[error("Signing program {program:?} failed: {output}")] - Failed { program: OsString, output: BString }, - #[error("The OpenPGP/X.509 signer did not report SIG_CREATED")] - MissingSignatureConfirmation, - #[error("The SSH signer produced no signature")] - MissingSshSignature(#[source] std::io::Error), -} +pub type Error = gix_error::Exn; impl CommitRef<'_> { /// Return an owned copy of this commit with its active signature replaced by a newly created one. pub fn sign(self, options: Options) -> Result { - self.into_owned()?.sign(options) + self.into_owned().or_erased()?.sign(options) } } @@ -73,7 +45,7 @@ impl Commit { let signature_field = crate::commit::signature_field_name(self.tree.kind()); self.extra_headers.retain(|(name, _)| name != signature_field); let mut payload = Vec::new(); - self.write_to(&mut payload)?; + self.write_to(&mut payload).or_erased()?; let signature = sign(&payload, &options)?; self.extra_headers.push((signature_field.into(), signature)); Ok(self) @@ -84,7 +56,7 @@ impl TagRef<'_> { /// Return an owned copy of this annotated tag with its in-body signature replaced by a newly created one /// according to `options`. pub fn sign(self, options: Options) -> Result { - self.into_owned()?.sign(options) + self.into_owned().or_erased()?.sign(options) } } @@ -93,7 +65,7 @@ impl Tag { pub fn sign(mut self, options: Options) -> Result { self.signature = None; let mut payload = Vec::new(); - self.write_to(&mut payload)?; + self.write_to(&mut payload).or_erased()?; // Tag signatures follow the message in the object body, separated by a newline which is itself signed. This // differs from commit signatures, which are inserted as a header after signing the commit without that header. payload.push(b'\n'); @@ -118,7 +90,7 @@ fn command(options: &Options) -> gix_command::Prepare { fn sign_gpg(payload: &[u8], options: &Options) -> Result { if options.signing_key.is_empty() { - return Err(Error::MissingSigningKey); + return Err(ValidationError::new("A signing key is required").raise_erased()); } let output = run( command(options) @@ -131,24 +103,26 @@ fn sign_gpg(payload: &[u8], options: &Options) -> Result { payload, )?; if !output.status.success() { - return Err(Error::Failed { - program: options.program.clone(), - output: output.stderr.into(), - }); + return Err(message!( + "Signing program {:?} failed: {}", + options.program, + output.stderr.as_bstr() + ) + .raise_erased()); } if !output .stderr .lines() .any(|line| line.starts_with(b"[GNUPG:] SIG_CREATED ")) { - return Err(Error::MissingSignatureConfirmation); + return Err(CorruptionError::new("The OpenPGP/X.509 signer did not report SIG_CREATED").raise_erased()); } Ok(strip_cr_before_lf(output.stdout).into()) } fn sign_ssh(payload: &[u8], options: &Options) -> Result { if options.signing_key.is_empty() { - return Err(Error::MissingSigningKey); + return Err(ValidationError::new("A signing key is required").raise_erased()); } let mut literal_key_file = None; let literal_key = options @@ -183,23 +157,20 @@ fn sign_ssh(payload: &[u8], options: &Options) -> Result { .stdout(Stdio::piped()) .stderr(Stdio::piped()) .spawn() - .map_err(|source| Error::Spawn { - program: options.program.clone(), - source, - })? + .or_raise_erased(|| message!("Could not execute signing program {:?}", options.program))? .wait_with_output() - .map_err(|source| Error::Communicate { - program: options.program.clone(), - source, - })?; + .or_raise_erased(|| message!("Could not communicate with signing program {:?}", options.program))?; drop(literal_key_file); if !output.status.success() { - return Err(Error::Failed { - program: options.program.clone(), - output: output.stderr.into(), - }); + return Err(message!( + "Signing program {:?} failed: {}", + options.program, + output.stderr.as_bstr() + ) + .raise_erased()); } - let signature = std::fs::read(&signature_path).map_err(Error::MissingSshSignature); + let signature = + std::fs::read(&signature_path).or_raise_erased(|| CorruptionError::new("The SSH signer produced no signature")); let _ = std::fs::remove_file(signature_path); Ok(strip_cr_before_lf(signature?).into()) } @@ -220,38 +191,33 @@ fn secure_temporary_file() -> Result, data: &[u8]) -> Result<(), Error> { file.with_mut(|file| file.write_all(data)) - .map_err(Error::TemporaryFile)? - .map_err(Error::TemporaryFile) + .or_raise_erased(|| message("Could not create or write a temporary signing file"))? + .or_raise_erased(|| message("Could not create or write a temporary signing file")) } fn temporary_path(file: &mut gix_tempfile::Handle) -> Result { file.with_mut(|file| file.path().to_owned()) - .map_err(Error::TemporaryFile) + .or_raise_erased(|| message("Could not create or write a temporary signing file")) } fn run(command: gix_command::Prepare, program: &OsStr, input: &[u8]) -> Result { - let mut child = command.spawn().map_err(|source| Error::Spawn { - program: program.to_owned(), - source, - })?; + let mut child = command + .spawn() + .or_raise_erased(|| message!("Could not execute signing program {program:?}"))?; child .stdin .take() .expect("configured as piped") .write_all(input) - .map_err(|source| Error::Communicate { - program: program.to_owned(), - source, - })?; - child.wait_with_output().map_err(|source| Error::Communicate { - program: program.to_owned(), - source, - }) + .or_raise_erased(|| message!("Could not communicate with signing program {program:?}"))?; + child + .wait_with_output() + .or_raise_erased(|| message!("Could not communicate with signing program {program:?}")) } /// Normalize signer-produced CRLF line endings to LF before embedding the signature in an object. diff --git a/gix-object/src/signature/verify.rs b/gix-object/src/signature/verify.rs index 9bb4993028f..8472b132688 100644 --- a/gix-object/src/signature/verify.rs +++ b/gix-object/src/signature/verify.rs @@ -7,6 +7,7 @@ use std::{ }; use bstr::{BStr, BString, ByteSlice}; +use gix_error::{CorruptionError, ErrorExt, OptionExt, ResultExt, ValidationError, message}; use super::SignedData; @@ -161,30 +162,13 @@ impl Outcome { } /// The error returned when verifying an object signature. -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] -pub enum Error { - #[error("The signature format is unsupported")] - UnsupportedFormat, - #[error("The configured program format {program_format:?} does not match signature format {signature_format:?}")] - FormatMismatch { - program_format: Format, - signature_format: Format, - }, - #[error("Could not create or write the temporary signature file")] - TemporaryFile(#[source] std::io::Error), - #[error("Could not execute signature verifier {program:?}")] - Spawn { program: OsString, source: std::io::Error }, - #[error("Could not communicate with signature verifier {program:?}")] - Communicate { program: OsString, source: std::io::Error }, - #[error("Signature time could not be formatted for SSH verification")] - CommitTime(#[source] Box), -} +pub type Error = gix_error::Exn; impl SignedData<'_> { /// Verify `signature` over these exact object bytes with fully resolved `options`. pub fn verify(&self, signature: &BStr, options: Options) -> Result { - let format = Format::from_signature(signature).ok_or(Error::UnsupportedFormat)?; + let format = Format::from_signature(signature) + .ok_or_raise_erased(|| CorruptionError::new("The signature format is unsupported"))?; match options { Options::OpenPgp { program, @@ -230,18 +214,21 @@ impl SignedData<'_> { verify_time, minimum_trust, ), - Options::OpenPgp { .. } => Err(Error::FormatMismatch { - program_format: Format::OpenPgp, - signature_format: format, - }), - Options::X509 { .. } => Err(Error::FormatMismatch { - program_format: Format::X509, - signature_format: format, - }), - Options::Ssh { .. } => Err(Error::FormatMismatch { - program_format: Format::Ssh, - signature_format: format, - }), + Options::OpenPgp { .. } => Err(ValidationError::new(format!( + "The configured program format {:?} does not match signature format {format:?}", + Format::OpenPgp + )) + .raise_erased()), + Options::X509 { .. } => Err(ValidationError::new(format!( + "The configured program format {:?} does not match signature format {format:?}", + Format::X509 + )) + .raise_erased()), + Options::Ssh { .. } => Err(ValidationError::new(format!( + "The configured program format {:?} does not match signature format {format:?}", + Format::Ssh + )) + .raise_erased()), } } @@ -302,7 +289,7 @@ impl SignedData<'_> { ) -> Result { let verify_time = verify_time .format(gix_date::time::CustomFormat::new("%Y%m%d%H%M%S")) - .map_err(|err| Error::CommitTime(Box::new(err)))?; + .or_raise_erased(|| message("Signature time could not be formatted for SSH verification"))?; let verify_time = format!("-Overify-time={verify_time}"); let mut signature_file = signature_file(signature)?; let signature_path = super::ssh_path_argument(&signature_path(&mut signature_file)?); @@ -393,27 +380,24 @@ impl SignedData<'_> { } fn run(&self, command: gix_command::Prepare, program: &OsStr) -> Result { - let mut child = command.spawn().map_err(|source| Error::Spawn { - program: program.to_owned(), - source, - })?; + let mut child = command + .spawn() + .or_raise_erased(|| message!("Could not execute signature verifier {program:?}"))?; let mut stdin = child.stdin.take().expect("configured as piped"); let [before, after] = self.segments(); if let Err(source) = stdin.write_all(before).and_then(|_| stdin.write_all(after)) { // A verifier may reject the invocation and exit without consuming all input. Its status and output are // still authoritative, whereas other write failures indicate an actual communication problem. if source.kind() != std::io::ErrorKind::BrokenPipe { - return Err(Error::Communicate { - program: program.to_owned(), - source, - }); + return Err(source + .and_raise(message!("Could not communicate with signature verifier {program:?}")) + .erased()); } } drop(stdin); - child.wait_with_output().map_err(|source| Error::Communicate { - program: program.to_owned(), - source, - }) + child + .wait_with_output() + .or_raise_erased(|| message!("Could not communicate with signature verifier {program:?}")) } fn run_prepared( @@ -455,23 +439,18 @@ fn run_prepared( .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()); - let mut child = command.spawn().map_err(|source| Error::Spawn { - program: program.to_owned(), - source, - })?; + let mut child = command + .spawn() + .or_raise_erased(|| message!("Could not execute signature verifier {program:?}"))?; child .stdin .take() .expect("configured as piped") .write_all(input) - .map_err(|source| Error::Communicate { - program: program.to_owned(), - source, - })?; - child.wait_with_output().map_err(|source| Error::Communicate { - program: program.to_owned(), - source, - }) + .or_raise_erased(|| message!("Could not communicate with signature verifier {program:?}"))?; + child + .wait_with_output() + .or_raise_erased(|| message!("Could not communicate with signature verifier {program:?}")) } fn signature_file(signature: &BStr) -> Result, Error> { @@ -486,35 +465,29 @@ fn temporary_file<'a>( gix_tempfile::ContainingDirectory::Exists, gix_tempfile::AutoRemove::Tempfile, ) - .map_err(Error::TemporaryFile)?; + .or_raise_erased(|| message("Could not create or write the temporary signature file"))?; file.with_mut(|file| { for data in data { file.write_all(data)?; } - Ok(()) + Ok::<_, std::io::Error>(()) }) - .map_err(Error::TemporaryFile)? - .map_err(Error::TemporaryFile)?; + .or_raise_erased(|| message("Could not create or write the temporary signature file"))? + .or_raise_erased(|| message("Could not create or write the temporary signature file"))?; Ok(file) } fn signature_path(file: &mut gix_tempfile::Handle) -> Result { file.with_mut(|file| file.path().to_owned()) - .map_err(Error::TemporaryFile) + .or_raise_erased(|| message("Could not create or write the temporary signature file")) } fn run_without_input(command: gix_command::Prepare, program: &OsStr) -> Result { command .spawn() - .map_err(|source| Error::Spawn { - program: program.to_owned(), - source, - })? + .or_raise_erased(|| message!("Could not execute signature verifier {program:?}"))? .wait_with_output() - .map_err(|source| Error::Communicate { - program: program.to_owned(), - source, - }) + .or_raise_erased(|| message!("Could not communicate with signature verifier {program:?}")) } fn parse_gpg_output(format: Format, output: BString, raw_output: BString) -> Outcome { @@ -754,19 +727,51 @@ mod tests { environment: Vec::new(), minimum_trust: TrustLevel::Undefined, }; - assert!(matches!( - signed.verify(BStr::new(b"not a signature"), options.clone()), - Err(Error::UnsupportedFormat) - )); + let unsupported = signed + .verify(BStr::new(b"not a signature"), options.clone()) + .expect_err("the unsupported signature is rejected"); assert!( - matches!( - signed.verify(BStr::new(b"-----BEGIN SSH SIGNATURE-----\n"), options), - Err(Error::FormatMismatch { - program_format: Format::X509, - signature_format: Format::Ssh, - }) - ), + unsupported.downcast_any_ref::().is_some(), + "an unrecognized object signature is corrupt" + ); + let mismatch = signed + .verify(BStr::new(b"-----BEGIN SSH SIGNATURE-----\n"), options) + .expect_err("the mismatched signature is rejected"); + assert!( + mismatch.downcast_any_ref::().is_some(), + "a configured verifier which cannot handle the signature is invalid" + ); + let mismatch = mismatch.to_string(); + assert!( + mismatch.contains("X509") && mismatch.contains("Ssh"), "the mismatch identifies both the configured program and detected signature formats" ); } + + #[test] + fn commit_time_keeps_the_formatting_error() { + let signed = SignedData::new(b"payloadsignature", 7..16); + let err = signed + .verify( + BStr::new(b"-----BEGIN SSH SIGNATURE-----\n"), + Options::Ssh { + program: "must-not-run".into(), + program_arguments: Vec::new(), + environment: Vec::new(), + allowed_signers: "unused".into(), + revocation_file: None, + verify_time: gix_date::Time { + seconds: i64::MAX, + offset: 0, + }, + minimum_trust: TrustLevel::Undefined, + }, + ) + .expect_err("the timestamp is outside jiff's supported range"); + assert_eq!( + err.to_string(), + "Signature time could not be formatted for SSH verification" + ); + assert!(err.iter().count() > 1, "the concrete formatting error is retained"); + } } diff --git a/gix-object/src/tag/decode.rs b/gix-object/src/tag/decode.rs index 1c9a9e6eca7..cf30d805c50 100644 --- a/gix-object/src/tag/decode.rs +++ b/gix-object/src/tag/decode.rs @@ -22,7 +22,7 @@ pub fn git_tag<'a>(i: &mut &'a [u8], hash_kind: gix_hash::Kind) -> ParseResult(i: &mut &'a [u8], hash_kind: gix_hash::Kind) -> ParseRe /// Typical inputs are `type commit\n`, `type tree\n`, `type blob\n`, and /// `type tag\n`. On success, `i` is advanced past the entire header line. pub(crate) fn kind(i: &mut &[u8]) -> ParseResult { - parse::header_field(i, b"type", |value| { - Kind::from_bytes(value).map_err(|_| crate::decode::Error) - }) + parse::header_field(i, b"type", Kind::from_bytes) } /// Parse the `tag \n` header and return the tag name. @@ -61,7 +59,9 @@ pub(crate) fn kind(i: &mut &[u8]) -> ParseResult { /// `i` is advanced past the entire header line. pub(crate) fn name<'a>(i: &mut &'a [u8]) -> ParseResult<&'a BStr> { parse::header_field(i, b"tag", |value| { - (!value.is_empty()).then(|| value.as_bstr()).ok_or(crate::decode::Error) + (!value.is_empty()) + .then(|| value.as_bstr()) + .ok_or_else(crate::decode::empty_error) }) } @@ -77,7 +77,7 @@ pub(crate) fn tagger_raw<'a>(i: &mut &'a [u8]) -> ParseResult> } parse::header_field(i, b"tagger", |raw| { let mut sig = raw; - gix_actor::SignatureRef::from_bytes_consuming(&mut sig).map_err(|_| crate::decode::Error)?; + gix_actor::SignatureRef::from_bytes_consuming(&mut sig)?; Ok(raw.as_bstr()) }) .map(Some) @@ -95,7 +95,7 @@ pub(crate) fn tagger<'a>(i: &mut &'a [u8]) -> ParseResult(i: &mut &'a [u8]) -> ParseResult<(&'a BStr, Option<&'a BStr>) } let Some(rest) = i.strip_prefix(parse::NL) else { - return Err(crate::decode::Error); + return Err(crate::decode::empty_error()); }; *i = &[]; diff --git a/gix-object/src/tag/write.rs b/gix-object/src/tag/write.rs index 6eb6654b8d5..e5f842d25cb 100644 --- a/gix-object/src/tag/write.rs +++ b/gix-object/src/tag/write.rs @@ -2,30 +2,22 @@ use std::io; use bstr::BStr; use gix_date::parse::TimeBuf; +use gix_error::{ErrorExt, ResultExt, ValidationError}; use crate::{Kind, Tag, TagRef, encode, encode::NL}; /// An Error used in [`Tag::write_to()`][crate::WriteTo::write_to()]. -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] -pub enum Error { - #[error("Tags must not start with a dash: '-'")] - StartsWithDash, - #[error("The tag name was no valid reference name")] - InvalidRefName(#[from] gix_validate::tag::name::Error), -} - -impl From for io::Error { - fn from(err: Error) -> Self { - io::Error::other(err) - } -} +pub type Error = gix_error::Exn; impl crate::WriteTo for Tag { fn write_to(&self, out: &mut dyn io::Write) -> io::Result<()> { encode::trusted_header_id(b"object", &self.target, out)?; encode::trusted_header_field(b"type", self.target_kind.as_bytes(), out)?; - encode::header_field(b"tag", validated_name(self.name.as_ref())?, out)?; + encode::header_field( + b"tag", + validated_name(self.name.as_ref()).map_err(|err| io::Error::other(err.into_error()))?, + out, + )?; if let Some(tagger) = &self.tagger { let mut buf = TimeBuf::default(); encode::trusted_header_signature(b"tagger", &tagger.to_ref(&mut buf), out)?; @@ -63,7 +55,11 @@ impl crate::WriteTo for TagRef<'_> { fn write_to(&self, mut out: &mut dyn io::Write) -> io::Result<()> { encode::trusted_header_field(b"object", self.target, &mut out)?; encode::trusted_header_field(b"type", self.target_kind.as_bytes(), &mut out)?; - encode::header_field(b"tag", validated_name(self.name)?, &mut out)?; + encode::header_field( + b"tag", + validated_name(self.name).map_err(|err| io::Error::other(err.into_error()))?, + &mut out, + )?; if let Some(tagger) = self.tagger { encode::trusted_header_field(b"tagger", tagger.as_ref(), &mut out)?; } @@ -96,9 +92,9 @@ impl crate::WriteTo for TagRef<'_> { } fn validated_name(name: &BStr) -> Result<&BStr, Error> { - gix_validate::tag::name(name)?; + gix_validate::tag::name(name).or_raise(|| ValidationError::new("The tag name was no valid reference name"))?; if name[0] == b'-' { - return Err(Error::StartsWithDash); + return Err(ValidationError::new("Tags must not start with a dash: '-'").raise()); } Ok(name) } diff --git a/gix-object/src/traits/find.rs b/gix-object/src/traits/find.rs index 0ab898d4540..a338e4c05ea 100644 --- a/gix-object/src/traits/find.rs +++ b/gix-object/src/traits/find.rs @@ -153,8 +153,18 @@ mod _impls { } mod ext { + use gix_error::{CorruptionError, ErrorExt, NotFoundError, ResultExt, ValidationError}; + use crate::{BlobRef, CommitRef, CommitRefIter, Kind, ObjectRef, TagRef, TagRefIter, TreeRef, TreeRefIter, find}; + fn not_found(id: &gix_hash::oid) -> gix_error::Exn { + NotFoundError::new(format!("An object with id {id} could not be found")).raise_erased() + } + + fn wrong_kind(id: &gix_hash::oid, actual: Kind, expected: Kind) -> gix_error::Exn { + ValidationError::new(format!("Expected object of kind {expected} but got {actual} at {id}")).raise_erased() + } + macro_rules! make_obj_lookup { ($method:ident, $object_variant:path, $object_kind:path, $object_type:ty) => { /// Like [`find(…)`][Self::find()], but flattens the `Result>` into a single `Result` making a non-existing object an error @@ -164,24 +174,15 @@ mod ext { id: &gix_hash::oid, buffer: &'a mut Vec, ) -> Result<$object_type, find::existing_object::Error> { - self.try_find(id, buffer) - .map_err(find::existing_object::Error::Find)? - .ok_or_else(|| find::existing_object::Error::NotFound { - oid: id.as_ref().to_owned(), - }) + self.try_find(id, buffer)? + .ok_or_else(|| not_found(id)) .and_then(|o| { - o.decode().map_err(|err| find::existing_object::Error::Decode { - source: err, - oid: id.as_ref().to_owned(), - }) + o.decode() + .or_raise_erased(|| CorruptionError::new(format!("Could not decode object at {id}"))) }) .and_then(|o| match o { $object_variant(o) => return Ok(o), - o => Err(find::existing_object::Error::ObjectKind { - oid: id.as_ref().to_owned(), - actual: o.kind(), - expected: $object_kind, - }), + o => Err(wrong_kind(id, o.kind(), $object_kind)), }) } }; @@ -196,19 +197,9 @@ mod ext { id: &gix_hash::oid, buffer: &'a mut Vec, ) -> Result<$object_type, find::existing_iter::Error> { - self.try_find(id, buffer) - .map_err(find::existing_iter::Error::Find)? - .ok_or_else(|| find::existing_iter::Error::NotFound { - oid: id.as_ref().to_owned(), - }) - .and_then(|o| { - o.$into_iter() - .ok_or_else(|| find::existing_iter::Error::ObjectKind { - oid: id.as_ref().to_owned(), - actual: o.kind, - expected: $object_kind, - }) - }) + self.try_find(id, buffer)? + .ok_or_else(|| not_found(id)) + .and_then(|o| o.$into_iter().ok_or_else(|| wrong_kind(id, o.kind, $object_kind))) } }; } @@ -217,9 +208,7 @@ mod ext { pub trait HeaderExt: super::Header { /// Like [`try_header(…)`](super::Header::try_header()), but flattens the `Result>` into a single `Result` making a non-existing header an error. fn header(&self, id: &gix_hash::oid) -> Result { - self.try_header(id) - .map_err(find::existing::Error::Find)? - .ok_or_else(|| find::existing::Error::NotFound { oid: id.to_owned() }) + self.try_header(id)?.ok_or_else(|| not_found(id)) } } @@ -231,9 +220,7 @@ mod ext { id: &gix_hash::oid, buffer: &'a mut Vec, ) -> Result, find::existing::Error> { - self.try_find(id, buffer) - .map_err(find::existing::Error::Find)? - .ok_or_else(|| find::existing::Error::NotFound { oid: id.to_owned() }) + self.try_find(id, buffer)?.ok_or_else(|| not_found(id)) } /// Like [`find(…)`][Self::find()], but flattens the `Result>` into a single `Result` making a non-existing object an error @@ -246,24 +233,15 @@ mod ext { if id == gix_hash::ObjectId::empty_blob(id.kind()) { return Ok(BlobRef { data: &[] }); } - self.try_find(id, buffer) - .map_err(find::existing_object::Error::Find)? - .ok_or_else(|| find::existing_object::Error::NotFound { - oid: id.as_ref().to_owned(), - }) + self.try_find(id, buffer)? + .ok_or_else(|| not_found(id)) .and_then(|o| { - o.decode().map_err(|err| find::existing_object::Error::Decode { - source: err, - oid: id.as_ref().to_owned(), - }) + o.decode() + .or_raise_erased(|| CorruptionError::new(format!("Could not decode object at {id}"))) }) .and_then(|o| match o { ObjectRef::Blob(o) => Ok(o), - o => Err(find::existing_object::Error::ObjectKind { - oid: id.as_ref().to_owned(), - actual: o.kind(), - expected: Kind::Blob, - }), + o => Err(wrong_kind(id, o.kind(), Kind::Blob)), }) } @@ -277,24 +255,15 @@ mod ext { if id == gix_hash::ObjectId::empty_tree(id.kind()) { return Ok(TreeRef { entries: Vec::new() }); } - self.try_find(id, buffer) - .map_err(find::existing_object::Error::Find)? - .ok_or_else(|| find::existing_object::Error::NotFound { - oid: id.as_ref().to_owned(), - }) + self.try_find(id, buffer)? + .ok_or_else(|| not_found(id)) .and_then(|o| { - o.decode().map_err(|err| find::existing_object::Error::Decode { - source: err, - oid: id.as_ref().to_owned(), - }) + o.decode() + .or_raise_erased(|| CorruptionError::new(format!("Could not decode object at {id}"))) }) .and_then(|o| match o { ObjectRef::Tree(o) => Ok(o), - o => Err(find::existing_object::Error::ObjectKind { - oid: id.as_ref().to_owned(), - actual: o.kind(), - expected: Kind::Tree, - }), + o => Err(wrong_kind(id, o.kind(), Kind::Tree)), }) } diff --git a/gix-object/src/traits/mod.rs b/gix-object/src/traits/mod.rs index 66aa0bc44fa..15e67b4e685 100644 --- a/gix-object/src/traits/mod.rs +++ b/gix-object/src/traits/mod.rs @@ -1,5 +1,7 @@ use std::io; +use gix_error::ResultExt; + use crate::Kind; /// Describe the capability to write git objects into an object store. @@ -8,7 +10,7 @@ pub trait Write { /// returning id to reference it in subsequent reads. fn write(&self, object: &dyn WriteTo) -> Result { let mut buf = Vec::with_capacity(2048); - object.write_to(&mut buf)?; + object.write_to(&mut buf).or_erased()?; self.write_stream(object.kind(), buf.len() as u64, &mut buf.as_slice()) } /// As [`write`](Write::write), but takes an [`object` kind](Kind) along with its encoded bytes. diff --git a/gix-object/src/tree/editor.rs b/gix-object/src/tree/editor.rs index edd31adfdda..5b967347b0f 100644 --- a/gix-object/src/tree/editor.rs +++ b/gix-object/src/tree/editor.rs @@ -5,6 +5,7 @@ use std::{ }; use bstr::{BStr, BString, ByteSlice, ByteVec}; +use gix_error::{ErrorExt, ValidationError}; use gix_hash::ObjectId; use crate::{ @@ -32,16 +33,7 @@ impl std::fmt::Debug for Editor<'_> { } /// The error returned by [Editor] or [Cursor] edit operation. -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] -pub enum Error { - #[error("Empty path components are not allowed")] - EmptyPathComponent, - #[error(transparent)] - FindExistingObject(#[from] crate::find::existing_object::Error), - #[error("Cannot remove '{rela_path}' as leaf entry because it is a tree")] - CannotRemoveNonLeaf { rela_path: BString }, -} +pub type Error = gix_error::Exn; /// Lifecycle impl<'a> Editor<'a> { @@ -278,7 +270,7 @@ impl Editor<'_> { while let Some(name) = rela_path.next() { let name = name.as_ref(); if name.is_empty() { - return Err(Error::EmptyPathComponent); + return Err(ValidationError::new("Empty path components are not allowed").raise_erased()); } let is_last = rela_path.peek().is_none(); let mut needs_sorting = false; @@ -304,9 +296,11 @@ impl Editor<'_> { EditMode::Remove(mode) => { if is_last { if mode == RemoveMode::LeafOnly && cursor.entries[idx].mode.is_tree() { - return Err(Error::CannotRemoveNonLeaf { - rela_path: path_with_component(path_buf.as_bstr(), name), - }); + let rela_path = path_with_component(path_buf.as_bstr(), name); + return Err(ValidationError::new(format!( + "Cannot remove '{rela_path}' as leaf entry because it is a tree" + )) + .raise_erased()); } cursor.entries.remove(idx); break; diff --git a/gix-object/src/tree/ref_iter.rs b/gix-object/src/tree/ref_iter.rs index 17c54d1c9c7..0e24f7ea8a5 100644 --- a/gix-object/src/tree/ref_iter.rs +++ b/gix-object/src/tree/ref_iter.rs @@ -180,7 +180,7 @@ impl<'a> Iterator for TreeRefIter<'a> { } None => { self.data = &[]; - Some(Err(crate::decode::Error)) + Some(Err(crate::decode::empty_error())) } } } @@ -234,7 +234,7 @@ mod decode { while !i.is_empty() { let Some((rest, entry)) = fast_entry(i, hash_len) else { - return Err(crate::decode::Error); + return Err(crate::decode::empty_error()); }; i = rest; out.push(entry); diff --git a/gix-object/src/tree/write.rs b/gix-object/src/tree/write.rs index 540eaa856ef..d5e41519cd9 100644 --- a/gix-object/src/tree/write.rs +++ b/gix-object/src/tree/write.rs @@ -1,6 +1,6 @@ use std::io; -use bstr::{BString, ByteSlice}; +use bstr::ByteSlice; use crate::{ Kind, Tree, TreeRef, @@ -9,18 +9,7 @@ use crate::{ }; /// The Error used in [`Tree::write_to()`][crate::WriteTo::write_to()]. -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] -pub enum Error { - #[error("Nullbytes are invalid in file paths as they are separators: {name:?}")] - NullbyteInFilename { name: BString }, -} - -impl From for io::Error { - fn from(err: Error) -> Self { - io::Error::other(err) - } -} +pub type Error = gix_error::ValidationError; /// Serialization impl crate::WriteTo for Tree { @@ -41,10 +30,10 @@ impl crate::WriteTo for Tree { out.write_all(SPACE)?; if filename.find_byte(0).is_some() { - return Err(Error::NullbyteInFilename { - name: (*filename).to_owned(), - } - .into()); + return Err(io::Error::other(Error::new_with_input( + "Nullbytes are invalid in file paths as they are separators", + filename.clone(), + ))); } out.write_all(filename)?; out.write_all(b"\0")?; @@ -88,10 +77,10 @@ impl crate::WriteTo for TreeRef<'_> { out.write_all(SPACE)?; if filename.find_byte(0).is_some() { - return Err(Error::NullbyteInFilename { - name: (*filename).to_owned(), - } - .into()); + return Err(io::Error::other(Error::new_with_input( + "Nullbytes are invalid in file paths as they are separators", + (*filename).to_owned(), + ))); } out.write_all(filename)?; out.write_all(b"\0")?; diff --git a/gix-object/tests/object/encode.rs b/gix-object/tests/object/encode.rs index c4b534e6ff1..94763ef3b0f 100644 --- a/gix-object/tests/object/encode.rs +++ b/gix-object/tests/object/encode.rs @@ -1,9 +1,5 @@ -/// Because the `TryFrom` implementations don't return proper errors -/// on failure -#[derive(Debug, thiserror::Error)] -enum Error { - #[error("")] - TryFromError, +fn try_from_error() -> std::io::Error { + std::io::Error::other("object type conversion failed") } /// Needed for roundtripping object types that take a `object_hash` parameter. @@ -54,7 +50,7 @@ macro_rules! round_trip_with_hash_kind { w.write_all(&item.loose_header())?; item.write_to(w)?; let parsed = ObjectRef::from_loose(&output, hash_kind)?; - let item2 = <$borrowed>::try_from(parsed).or(Err(super::Error::TryFromError))?; + let item2 = <$borrowed>::try_from(parsed).map_err(|_| super::try_from_error())?; assert_eq!(item2, item, "object-ref loose: {input_name} {:?}\n{:?}", output.as_bstr(), input.as_bstr()); } @@ -65,8 +61,8 @@ macro_rules! round_trip_with_hash_kind { w.write_all(&item.loose_header())?; item.write_to(w)?; let parsed = ObjectRef::from_loose(&output, hash_kind)?; - let parsed_borrowed = <$borrowed>::try_from(parsed).or(Err(super::Error::TryFromError))?; - let item2: $owned = parsed_borrowed.try_into().or(Err(super::Error::TryFromError))?; + let parsed_borrowed = <$borrowed>::try_from(parsed).map_err(|_| super::try_from_error())?; + let item2: $owned = parsed_borrowed.try_into().map_err(|_| super::try_from_error())?; assert_eq!(item2, item, "object-ref loose owned: {input_name} {:?}\n{:?}", output.as_bstr(), input.as_bstr()); } Ok(()) @@ -187,7 +183,7 @@ mod blob { w.write_all(&item.loose_header())?; item.write_to(w)?; let parsed = ObjectRef::from_loose(&output, gix_testtools::object_hash())?; - let item2 = BlobRef::try_from(parsed).or(Err(super::Error::TryFromError))?; + let item2 = BlobRef::try_from(parsed).map_err(|_| super::try_from_error())?; assert_eq!( item2, item, @@ -203,7 +199,7 @@ mod blob { w.write_all(&item.loose_header())?; item.write_to(w)?; let parsed = ObjectRef::from_loose(&output, gix_testtools::object_hash())?; - let parsed_borrowed = BlobRef::try_from(parsed).or(Err(super::Error::TryFromError))?; + let parsed_borrowed = BlobRef::try_from(parsed).map_err(|_| super::try_from_error())?; let item2: Blob = parsed_borrowed.into(); assert_eq!( item2, @@ -222,7 +218,7 @@ mod loose_header { use gix_object::{Kind, decode, encode}; #[test] - fn round_trip() -> Result<(), Box> { + fn round_trip() -> Result<(), Box> { for (kind, size, expected) in &[ (Kind::Tree, 1234, "tree 1234\0".as_bytes()), (Kind::Blob, 0, b"blob 0\0"), diff --git a/gix-object/tests/object/tree/iter.rs b/gix-object/tests/object/tree/iter.rs index 3a1973000fa..68388ec8262 100644 --- a/gix-object/tests/object/tree/iter.rs +++ b/gix-object/tests/object/tree/iter.rs @@ -158,7 +158,7 @@ mod lookup_entry { let root_tree = odb.find_tree_iter(&root_tree_id, &mut buf)?; let mut buf = Vec::new(); - root_tree.lookup_entry_by_path(&odb, &mut buf, path) + Ok(root_tree.lookup_entry_by_path(&odb, &mut buf, path)?) } } } diff --git a/gix-odb/src/memory.rs b/gix-odb/src/memory.rs index 9e6b9b21ec9..4883930a0bd 100644 --- a/gix-odb/src/memory.rs +++ b/gix-odb/src/memory.rs @@ -5,6 +5,7 @@ use std::{ sync::Arc, }; +use gix_error::ResultExt; use gix_object::Data; use crate::{Cache, find::Header}; @@ -216,9 +217,9 @@ where }; let mut buf = Vec::with_capacity(2048); - object.write_to(&mut buf)?; + object.write_to(&mut buf).or_erased()?; let kind = object.kind(); - let id = gix_object::compute_hash(self.object_hash, kind, &buf)?; + let id = gix_object::compute_hash(self.object_hash, kind, &buf).or_erased()?; map.borrow_mut().entry(id).or_insert((kind, buf)); Ok(id) } @@ -234,9 +235,9 @@ where }; let mut buf = Vec::new(); - from.read_to_end(&mut buf)?; + from.read_to_end(&mut buf).or_erased()?; - let id = gix_object::compute_hash(self.object_hash, kind, &buf)?; + let id = gix_object::compute_hash(self.object_hash, kind, &buf).or_erased()?; map.borrow_mut().entry(id).or_insert((kind, buf)); Ok(id) } @@ -267,7 +268,7 @@ where }; let mut buf = Vec::new(); - from.read_to_end(&mut buf)?; + from.read_to_end(&mut buf).or_erased()?; map.borrow_mut().entry(id).or_insert((kind, buf)); Ok(id) diff --git a/gix-odb/src/sink.rs b/gix-odb/src/sink.rs index 9fb395274d6..2e4cf1e74e8 100644 --- a/gix-odb/src/sink.rs +++ b/gix-odb/src/sink.rs @@ -3,6 +3,7 @@ use std::{ io::{self, Write}, }; +use gix_error::ResultExt; use gix_zlib::stream::deflate; use crate::Sink; @@ -43,22 +44,22 @@ impl gix_object::Write for Sink { let mut hasher = gix_hash::hasher(self.object_hash); hasher.update(&header); - possibly_compress(&header).map_err(Box::new)?; + possibly_compress(&header).or_erased()?; while size != 0 { let bytes = (size as usize).min(buf.len()); - from.read_exact(&mut buf[..bytes]).map_err(Box::new)?; + from.read_exact(&mut buf[..bytes]).or_erased()?; hasher.update(&buf[..bytes]); - possibly_compress(&buf[..bytes]).map_err(Box::new)?; + possibly_compress(&buf[..bytes]).or_erased()?; size -= bytes as u64; } if let Some(compressor) = self.compressor.as_ref() { let mut c = compressor.borrow_mut(); - c.flush().map_err(Box::new)?; + c.flush().or_erased()?; c.reset(); } - Ok(hasher.try_finalize()?) + hasher.try_finalize().or_erased() } fn write_stream_with_known_id( @@ -78,17 +79,17 @@ impl gix_object::Write for Sink { Ok(()) }; - possibly_compress(&header).map_err(Box::new)?; + possibly_compress(&header).or_erased()?; while size != 0 { let bytes = (size as usize).min(buf.len()); - from.read_exact(&mut buf[..bytes]).map_err(Box::new)?; - possibly_compress(&buf[..bytes]).map_err(Box::new)?; + from.read_exact(&mut buf[..bytes]).or_erased()?; + possibly_compress(&buf[..bytes]).or_erased()?; size -= bytes as u64; } if let Some(compressor) = self.compressor.as_ref() { let mut c = compressor.borrow_mut(); - c.flush().map_err(Box::new)?; + c.flush().or_erased()?; c.reset(); } diff --git a/gix-odb/src/store_impls/dynamic/find.rs b/gix-odb/src/store_impls/dynamic/find.rs index ac5de05dea6..d09eddd9946 100644 --- a/gix-odb/src/store_impls/dynamic/find.rs +++ b/gix-odb/src/store_impls/dynamic/find.rs @@ -1,5 +1,6 @@ use std::ops::Deref; +use gix_error::ResultExt; use gix_pack::cache::DecodeEntry; use crate::store::{handle, load_index}; @@ -354,7 +355,7 @@ where let mut snapshot = self.snapshot.borrow_mut(); let mut inflate = self.inflate.borrow_mut(); self.try_find_cached_inner(id, buffer, &mut inflate, pack_cache, &mut snapshot, None) - .map_err(|err| Box::new(err) as _) + .or_erased() } fn location_by_oid(&self, id: &gix_hash::oid, buf: &mut Vec) -> Option { @@ -534,7 +535,7 @@ where size: hdr.size(), }) }) - .map_err(|err| Box::new(err) as _) + .or_erased() } } diff --git a/gix-odb/src/store_impls/dynamic/header.rs b/gix-odb/src/store_impls/dynamic/header.rs index 8e568bd624f..14f13bad62f 100644 --- a/gix-odb/src/store_impls/dynamic/header.rs +++ b/gix-odb/src/store_impls/dynamic/header.rs @@ -1,5 +1,6 @@ use std::ops::Deref; +use gix_error::ResultExt; use gix_hash::oid; use super::find::Error; @@ -185,7 +186,6 @@ where fn try_header(&self, id: &oid) -> Result, gix_object::find::Error> { let mut snapshot = self.snapshot.borrow_mut(); let mut inflate = self.inflate.borrow_mut(); - self.try_header_inner(id, &mut inflate, &mut snapshot, None) - .map_err(|err| Box::new(err) as _) + self.try_header_inner(id, &mut inflate, &mut snapshot, None).or_erased() } } diff --git a/gix-odb/src/store_impls/dynamic/write.rs b/gix-odb/src/store_impls/dynamic/write.rs index b2ab84f2d4e..313ac4e5e9e 100644 --- a/gix-odb/src/store_impls/dynamic/write.rs +++ b/gix-odb/src/store_impls/dynamic/write.rs @@ -1,5 +1,6 @@ use std::{io::Read, ops::Deref}; +use gix_error::ResultExt; use gix_hash::ObjectId; use gix_object::Kind; @@ -36,7 +37,7 @@ where let new_snapshot = self .store .load_one_index(self.index_ctx(snapshot.marker)) - .map_err(Box::new)? + .or_erased()? .expect("there is always at least one ODB, and this code runs only once for initialization"); *snapshot = new_snapshot; snapshot.loose_dbs[0].write_stream(kind, size, from)? @@ -57,7 +58,7 @@ where let new_snapshot = self .store .load_one_index(self.index_ctx(snapshot.marker)) - .map_err(Box::new)? + .or_erased()? .expect("there is always at least one ODB, and this code runs only once for initialization"); *snapshot = new_snapshot; snapshot.loose_dbs[0].write_buf_with_known_id(kind, from, id)? @@ -79,7 +80,7 @@ where let new_snapshot = self .store .load_one_index(self.index_ctx(snapshot.marker)) - .map_err(Box::new)? + .or_erased()? .expect("there is always at least one ODB, and this code runs only once for initialization"); *snapshot = new_snapshot; snapshot.loose_dbs[0].write_stream_with_known_id(kind, size, from, id)? diff --git a/gix-odb/src/store_impls/loose/find.rs b/gix-odb/src/store_impls/loose/find.rs index 3f09ac6733d..5a014f98740 100644 --- a/gix-odb/src/store_impls/loose/find.rs +++ b/gix-odb/src/store_impls/loose/find.rs @@ -13,7 +13,7 @@ pub enum Error { #[error("file at '{path}' showed invalid size of inflated data, expected {expected}, got {actual}")] SizeMismatch { actual: u64, expected: u64, path: PathBuf }, #[error(transparent)] - Decode(#[from] gix_object::decode::LooseHeaderDecodeError), + Decode(GixError), #[error("Cannot store {size} in memory as it's not representable")] OutOfMemory { size: u64 }, #[error("Could not {action} data at '{path}'")] @@ -24,6 +24,12 @@ pub enum Error { }, } +impl From for Error { + fn from(err: gix_object::decode::LooseHeaderDecodeError) -> Self { + Error::Decode(err.into_error()) + } +} + /// Object lookup impl Store { const OPEN_OR_MAP_ACTION: &'static str = "open or map"; diff --git a/gix-odb/src/store_impls/loose/verify.rs b/gix-odb/src/store_impls/loose/verify.rs index 03292091432..924fb863526 100644 --- a/gix-odb/src/store_impls/loose/verify.rs +++ b/gix-odb/src/store_impls/loose/verify.rs @@ -71,9 +71,7 @@ impl Store { progress: &mut dyn DynNestedProgress, should_interrupt: &AtomicBool, ) -> Result { - use gix_object::Write; let mut buf = Vec::new(); - let sink = crate::sink(self.object_hash); let mut num_objects = 0; let start = Instant::now(); @@ -84,9 +82,9 @@ impl Store { .try_find(&id, &mut buf) .map_err(|_| integrity::Error::Retry)? .ok_or(integrity::Error::Retry)?; - sink.write_buf(object.kind, object.data) - .map_err(|err| integrity::Error::ObjectHasher { - source: *err.downcast().expect("sink can only fail in hasher"), + gix_object::compute_hash(self.object_hash, object.kind, object.data) + .map_err(|source| integrity::Error::ObjectHasher { + source, kind: object.kind, expected: id, })? diff --git a/gix-odb/src/store_impls/loose/write.rs b/gix-odb/src/store_impls/loose/write.rs index 2a8a96fe50a..39922054152 100644 --- a/gix-odb/src/store_impls/loose/write.rs +++ b/gix-odb/src/store_impls/loose/write.rs @@ -1,5 +1,6 @@ use std::{fs, io, io::Write, path::PathBuf}; +use gix_error::ResultExt; use gix_object::WriteTo; use gix_zlib::stream::deflate; use tempfile::NamedTempFile; @@ -28,40 +29,48 @@ pub enum Error { impl gix_object::Write for Store { fn write(&self, object: &dyn WriteTo) -> Result { - let mut to = self.dest()?; - to.write_all(&object.loose_header()).map_err(|err| Error::Io { - source: err, - message: "write header to tempfile in", - path: self.path.to_owned(), - })?; - object.write_to(&mut to).map_err(|err| Error::Io { - source: err, - message: "stream all data into tempfile in", - path: self.path.to_owned(), - })?; - to.flush().map_err(Box::new)?; - Ok(self.finalize_object(to).map_err(Box::new)?) + let mut to = self.dest().or_erased()?; + to.write_all(&object.loose_header()) + .map_err(|err| Error::Io { + source: err, + message: "write header to tempfile in", + path: self.path.to_owned(), + }) + .or_erased()?; + object + .write_to(&mut to) + .map_err(|err| Error::Io { + source: err, + message: "stream all data into tempfile in", + path: self.path.to_owned(), + }) + .or_erased()?; + to.flush().or_erased()?; + self.finalize_object(to).or_erased() } /// Write the given buffer in `from` to disk in one syscall at best. /// /// This will cost at least 4 IO operations. fn write_buf(&self, kind: gix_object::Kind, from: &[u8]) -> Result { - let mut to = self.dest().map_err(Box::new)?; + let mut to = self.dest().or_erased()?; to.write_all(&gix_object::encode::loose_header(kind, from.len() as u64)) .map_err(|err| Error::Io { source: err, message: "write header to tempfile in", path: self.path.to_owned(), - })?; + }) + .or_erased()?; - to.write_all(from).map_err(|err| Error::Io { - source: err, - message: "stream all data into tempfile in", - path: self.path.to_owned(), - })?; - to.flush()?; - Ok(self.finalize_object(to)?) + to.write_all(from) + .map_err(|err| Error::Io { + source: err, + message: "stream all data into tempfile in", + path: self.path.to_owned(), + }) + .or_erased()?; + to.flush().or_erased()?; + self.finalize_object(to).or_erased() } fn write_buf_with_known_id( @@ -70,21 +79,24 @@ impl gix_object::Write for Store { from: &[u8], id: gix_hash::ObjectId, ) -> Result { - let mut to = self.compressed_tempfile().map_err(Box::new)?; + let mut to = self.compressed_tempfile().or_erased()?; to.write_all(&gix_object::encode::loose_header(kind, from.len() as u64)) .map_err(|err| Error::Io { source: err, message: "write header to tempfile in", path: self.path.to_owned(), - })?; + }) + .or_erased()?; - to.write_all(from).map_err(|err| Error::Io { - source: err, - message: "stream all data into tempfile in", - path: self.path.to_owned(), - })?; - to.flush()?; - Ok(self.finalize_object_at(id, to)?) + to.write_all(from) + .map_err(|err| Error::Io { + source: err, + message: "stream all data into tempfile in", + path: self.path.to_owned(), + }) + .or_erased()?; + to.flush().or_erased()?; + self.finalize_object_at(id, to).or_erased() } /// Write the given stream in `from` to disk with at least one syscall. @@ -96,13 +108,14 @@ impl gix_object::Write for Store { size: u64, mut from: &mut dyn io::Read, ) -> Result { - let mut to = self.dest().map_err(Box::new)?; + let mut to = self.dest().or_erased()?; to.write_all(&gix_object::encode::loose_header(kind, size)) .map_err(|err| Error::Io { source: err, message: "write header to tempfile in", path: self.path.to_owned(), - })?; + }) + .or_erased()?; io::copy(&mut from, &mut to) .map_err(|err| Error::Io { @@ -110,9 +123,9 @@ impl gix_object::Write for Store { message: "stream all data into tempfile in", path: self.path.to_owned(), }) - .map_err(Box::new)?; - to.flush().map_err(Box::new)?; - Ok(self.finalize_object(to)?) + .or_erased()?; + to.flush().or_erased()?; + self.finalize_object(to).or_erased() } fn write_stream_with_known_id( @@ -122,13 +135,14 @@ impl gix_object::Write for Store { mut from: &mut dyn io::Read, id: gix_hash::ObjectId, ) -> Result { - let mut to = self.compressed_tempfile().map_err(Box::new)?; + let mut to = self.compressed_tempfile().or_erased()?; to.write_all(&gix_object::encode::loose_header(kind, size)) .map_err(|err| Error::Io { source: err, message: "write header to tempfile in", path: self.path.to_owned(), - })?; + }) + .or_erased()?; io::copy(&mut from, &mut to) .map_err(|err| Error::Io { @@ -136,9 +150,9 @@ impl gix_object::Write for Store { message: "stream all data into tempfile in", path: self.path.to_owned(), }) - .map_err(Box::new)?; - to.flush().map_err(Box::new)?; - Ok(self.finalize_object_at(id, to)?) + .or_erased()?; + to.flush().or_erased()?; + self.finalize_object_at(id, to).or_erased() } } diff --git a/gix-odb/src/traits.rs b/gix-odb/src/traits.rs index f3e5cec6403..bebc3ee8ad6 100644 --- a/gix-odb/src/traits.rs +++ b/gix-odb/src/traits.rs @@ -42,15 +42,16 @@ mod _impls { } mod ext { + use gix_error::{ErrorExt, NotFoundError}; + use crate::find; /// An extension trait with convenience functions. pub trait HeaderExt: super::Header { /// Like [`try_header(…)`][super::Header::try_header()], but flattens the `Result>` into a single `Result` making a non-existing object an error. fn header(&self, id: impl AsRef) -> Result { let id = id.as_ref(); - self.try_header(id) - .map_err(gix_object::find::existing::Error::Find)? - .ok_or_else(|| gix_object::find::existing::Error::NotFound { oid: id.to_owned() }) + self.try_header(id)? + .ok_or_else(|| NotFoundError::new(format!("An object with id {id} could not be found")).raise_erased()) } } diff --git a/gix-pack/src/data/output/count/objects/types.rs b/gix-pack/src/data/output/count/objects/types.rs index 117a22ccd6d..dba75ff9db5 100644 --- a/gix-pack/src/data/output/count/objects/types.rs +++ b/gix-pack/src/data/output/count/objects/types.rs @@ -84,7 +84,7 @@ pub enum Error { #[error(transparent)] CommitDecode(gix_object::decode::Error), #[error(transparent)] - FindExisting(#[from] gix_object::find::existing::Error), + FindExisting(gix_error::Error), #[error(transparent)] InputIteration(Box), #[error(transparent)] @@ -94,3 +94,9 @@ pub enum Error { #[error("Operation interrupted")] Interrupted, } + +impl From for Error { + fn from(err: gix_object::find::existing::Error) -> Self { + Error::FindExisting(err.into_error()) + } +} diff --git a/gix-pack/src/find_traits.rs b/gix-pack/src/find_traits.rs index 787ed5b635e..6d3b474b73d 100644 --- a/gix-pack/src/find_traits.rs +++ b/gix-pack/src/find_traits.rs @@ -64,8 +64,17 @@ pub trait Find { } mod ext { + use gix_error::{CorruptionError, ErrorExt, NotFoundError, ResultExt, ValidationError}; use gix_object::{BlobRef, CommitRef, CommitRefIter, Kind, ObjectRef, TagRef, TagRefIter, TreeRef, TreeRefIter}; + fn not_found(id: &gix_hash::oid) -> gix_error::Exn { + NotFoundError::new(format!("An object with id {id} could not be found")).raise_erased() + } + + fn wrong_kind(id: &gix_hash::oid, actual: Kind, expected: Kind) -> gix_error::Exn { + ValidationError::new(format!("Expected object of kind {expected} but got {actual} at {id}")).raise_erased() + } + macro_rules! make_obj_lookup { ($method:ident, $object_variant:path, $object_kind:path, $object_type:ty) => { /// Like [`find(…)`][Self::find()], but flattens the `Result>` into a single `Result` making a non-existing object an error @@ -77,26 +86,16 @@ mod ext { ) -> Result<($object_type, Option), gix_object::find::existing_object::Error> { let id = id.as_ref(); - self.try_find(id, buffer) - .map_err(gix_object::find::existing_object::Error::Find)? - .ok_or_else(|| gix_object::find::existing_object::Error::NotFound { - oid: id.as_ref().to_owned(), - }) + self.try_find(id, buffer)? + .ok_or_else(|| not_found(id)) .and_then(|(o, l)| { o.decode() - .map_err(|err| gix_object::find::existing_object::Error::Decode { - source: err, - oid: id.to_owned(), - }) + .or_raise_erased(|| CorruptionError::new(format!("Could not decode object at {id}"))) .map(|o| (o, l)) }) .and_then(|(o, l)| match o { $object_variant(o) => return Ok((o, l)), - o => Err(gix_object::find::existing_object::Error::ObjectKind { - oid: id.to_owned(), - actual: o.kind(), - expected: $object_kind, - }), + o => Err(wrong_kind(id, o.kind(), $object_kind)), }) } }; @@ -112,18 +111,11 @@ mod ext { buffer: &'a mut Vec, ) -> Result<($object_type, Option), gix_object::find::existing_iter::Error> { let id = id.as_ref(); - self.try_find(id, buffer) - .map_err(gix_object::find::existing_iter::Error::Find)? - .ok_or_else(|| gix_object::find::existing_iter::Error::NotFound { - oid: id.as_ref().to_owned(), - }) + self.try_find(id, buffer)? + .ok_or_else(|| not_found(id)) .and_then(|(o, l)| { o.$into_iter() - .ok_or_else(|| gix_object::find::existing_iter::Error::ObjectKind { - oid: id.to_owned(), - actual: o.kind, - expected: $object_kind, - }) + .ok_or_else(|| wrong_kind(id, o.kind, $object_kind)) .map(|i| (i, l)) }) } @@ -139,11 +131,7 @@ mod ext { buffer: &'a mut Vec, ) -> Result<(gix_object::Data<'a>, Option), gix_object::find::existing::Error> { - self.try_find(id, buffer) - .map_err(gix_object::find::existing::Error::Find)? - .ok_or_else(|| gix_object::find::existing::Error::NotFound { - oid: id.as_ref().to_owned(), - }) + self.try_find(id, buffer)?.ok_or_else(|| not_found(id)) } make_obj_lookup!(find_commit, ObjectRef::Commit, Kind::Commit, CommitRef<'a>); diff --git a/gix-pack/src/index/traverse/error.rs b/gix-pack/src/index/traverse/error.rs index b64b9a1a211..1dcd08dd3c3 100644 --- a/gix-pack/src/index/traverse/error.rs +++ b/gix-pack/src/index/traverse/error.rs @@ -28,7 +28,7 @@ pub enum Error { PackObjectVerify { offset: u64, #[source] - source: gix_object::data::verify::Error, + source: gix_error::Error, }, #[error( "The CRC32 of {kind} object at offset {offset} didn't match the checksum in the index file: expected {expected}, got {actual}" diff --git a/gix-pack/src/index/traverse/mod.rs b/gix-pack/src/index/traverse/mod.rs index 4450b76803b..ad6defc0600 100644 --- a/gix-pack/src/index/traverse/mod.rs +++ b/gix-pack/src/index/traverse/mod.rs @@ -225,7 +225,7 @@ where .verify_checksum(&index_entry.oid) .map_err(|source| Error::PackObjectVerify { offset: index_entry.pack_offset, - source, + source: source.into_error(), })?; if let Some(desired_crc32) = index_entry.crc32 { let actual_crc32 = pack_entry_crc32(); diff --git a/gix-pack/tests/pack/bundle.rs b/gix-pack/tests/pack/bundle.rs index f2c7a6d943d..56a0b3246b6 100644 --- a/gix-pack/tests/pack/bundle.rs +++ b/gix-pack/tests/pack/bundle.rs @@ -25,7 +25,7 @@ mod locate { use crate::{PACKS_AND_INDICES, fixture_path}; #[test] - fn all() -> Result<(), Box> { + fn all() -> Result<(), Box> { for (index_path, data_path) in PACKS_AND_INDICES { // both paths are equivalent pack::Bundle::at(fixture_path(index_path), gix_hash::Kind::Sha1)?; diff --git a/gix-ref/src/store/packed/transaction.rs b/gix-ref/src/store/packed/transaction.rs index 9915d39d7c2..07a6cfc550a 100644 --- a/gix-ref/src/store/packed/transaction.rs +++ b/gix-ref/src/store/packed/transaction.rs @@ -106,7 +106,9 @@ impl packed::Transaction { { let mut next_id = new; edit.peeled = loop { - let data = objects.try_find(&next_id, &mut buf)?; + let data = objects + .try_find(&next_id, &mut buf) + .map_err(|err| prepare::Error::Resolve(err.into()))?; match data { Some(gix_object::Data { kind: gix_object::Kind::Tag, diff --git a/gix-revwalk/src/graph/mod.rs b/gix-revwalk/src/graph/mod.rs index 76501ede34e..701cb6301c3 100644 --- a/gix-revwalk/src/graph/mod.rs +++ b/gix-revwalk/src/graph/mod.rs @@ -1,5 +1,6 @@ use std::{fmt::Formatter, ops::Index}; +use gix_error::ErrorExt; use gix_hash::oid; use smallvec::SmallVec; @@ -21,12 +22,18 @@ mod errors { #[expect(missing_docs)] pub enum Error { #[error(transparent)] - Lookup(#[from] gix_object::find::existing_iter::Error), + Lookup(gix_error::Error), #[error("A commit could not be decoded during traversal")] Decode(#[from] gix_object::decode::Error), #[error(transparent)] Parent(#[from] iter_parents::Error), } + + impl From for Error { + fn from(err: gix_object::find::existing_iter::Error) -> Self { + Error::Lookup(err.into_error()) + } + } } /// @@ -38,10 +45,16 @@ mod errors { #[expect(missing_docs)] pub enum Error { #[error(transparent)] - Lookup(#[from] gix_object::find::existing_iter::Error), + Lookup(gix_error::Error), #[error(transparent)] ToOwned(#[from] to_owned::Error), } + + impl From for Error { + fn from(err: gix_object::find::existing_iter::Error) -> Self { + Error::Lookup(err.into_error()) + } + } } } pub use errors::{get_or_insert_default, insert_parents}; @@ -355,8 +368,10 @@ impl<'cache, T> Graph<'_, 'cache, T> { &mut self, id: &gix_hash::oid, ) -> Result, gix_object::find::existing_iter::Error> { + use gix_error::NotFoundError; + self.try_lookup(id)? - .ok_or(gix_object::find::existing_iter::Error::NotFound { oid: id.to_owned() }) + .ok_or_else(|| NotFoundError::new(format!("An object with id {id} could not be found")).raise_erased()) } } @@ -374,18 +389,13 @@ fn try_lookup<'graph, 'cache>( backing: Either::Right((cache, pos)), })); } - Ok( - match objects - .try_find(id, buf) - .map_err(gix_object::find::existing_iter::Error::Find)? - { - Some(data) => data.kind.is_commit().then_some(LazyCommit { - object_hash: data.object_hash, - backing: Either::Left(buf), - }), - None => None, - }, - ) + Ok(match objects.try_find(id, buf)? { + Some(data) => data.kind.is_commit().then_some(LazyCommit { + object_hash: data.object_hash, + backing: Either::Left(buf), + }), + None => None, + }) } impl<'a, T> Index<&'a gix_hash::oid> for Graph<'_, '_, T> { diff --git a/gix-status/Cargo.toml b/gix-status/Cargo.toml index 9460bb11bbd..7fd6af278a6 100644 --- a/gix-status/Cargo.toml +++ b/gix-status/Cargo.toml @@ -28,6 +28,7 @@ parallel = ["gix-features/parallel"] gix-index = { version = "^0.55.0", path = "../gix-index" } gix-fs = { version = "^0.22.1", path = "../gix-fs" } gix-hash = { version = "^0.26.2", path = "../gix-hash" } +gix-error = { version = "^0.3.0", path = "../gix-error" } gix-object = { version = "^0.64.1", path = "../gix-object" } gix-path = { version = "^0.12.6", path = "../gix-path" } gix-features = { version = "^0.49.1", path = "../gix-features", features = ["progress"] } diff --git a/gix-status/src/index_as_worktree/function.rs b/gix-status/src/index_as_worktree/function.rs index 30029c58bd1..8a480d71239 100644 --- a/gix-status/src/index_as_worktree/function.rs +++ b/gix-status/src/index_as_worktree/function.rs @@ -591,11 +591,14 @@ where Find: gix_object::Find, { fn read_blob(self) -> Result<&'a [u8], Error> { - Ok(self.objects.find_blob(self.id, self.buf).map(|b| { - self.odb_reads.fetch_add(1, Ordering::Relaxed); - self.odb_bytes.fetch_add(b.data.len() as u64, Ordering::Relaxed); - b.data - })?) + self.objects + .find_blob(self.id, self.buf) + .map_err(|err| Error::Find(err.into_error())) + .map(|b| { + self.odb_reads.fetch_add(1, Ordering::Relaxed); + self.odb_bytes.fetch_add(b.data.len() as u64, Ordering::Relaxed); + b.data + }) } fn stream_worktree_file(self) -> Result, Error> { @@ -631,7 +634,7 @@ where &mut |_path, attrs| { platform.matching_attributes(attrs); }, - &mut |buf| Ok(self.objects.find_blob(self.id, buf).map(|_| Some(()))?), + &mut |buf| self.objects.find_blob(self.id, buf).map(|_| Some(())), ) .map_err(|err| Error::Io(io::Error::other(err)))?; let len = match out { diff --git a/gix-status/src/index_as_worktree/types.rs b/gix-status/src/index_as_worktree/types.rs index 273027db9e4..537bc3b1fd3 100644 --- a/gix-status/src/index_as_worktree/types.rs +++ b/gix-status/src/index_as_worktree/types.rs @@ -14,7 +14,7 @@ pub enum Error { #[error("IO error while writing blob or reading file metadata or changing filetype")] Io(#[from] std::io::Error), #[error("Failed to obtain blob from object database")] - Find(#[from] gix_object::find::existing_object::Error), + Find(#[source] gix_error::Error), #[error("Could not determine status for submodule at '{rela_path}'")] SubmoduleStatus { rela_path: BString, diff --git a/gix-tix/src/edit/rebase.rs b/gix-tix/src/edit/rebase.rs index b772dbb73a2..25b84d06e8a 100644 --- a/gix-tix/src/edit/rebase.rs +++ b/gix-tix/src/edit/rebase.rs @@ -2840,7 +2840,10 @@ fn write_commit_timed( commit = match (signature, signing) { (Signature::RedoIfNeeded, Some(options)) => { let started = Instant::now(); - let signed = commit.sign(options).context("could not sign rebased commit")?; + let signed = commit + .sign(options) + .map_err(gix::Exn::into_error) + .context("could not sign rebased commit")?; signing_time = Some(started.elapsed()); signed } diff --git a/gix-tix/src/history.rs b/gix-tix/src/history.rs index e32ac96dba6..42cb03420a3 100644 --- a/gix-tix/src/history.rs +++ b/gix-tix/src/history.rs @@ -378,6 +378,7 @@ impl HistoryGraph { return Ok(index); } let commit = gix::traverse::commit::find(cache, &repo.objects, &id, buf) + .map_err(gix::Exn::into_error) .context("could not load commit for cached history traversal")?; let (mut parents, commit_time, generation) = match commit { gix::traverse::commit::Either::CommitRefIter(iter) => { diff --git a/gix-traverse/Cargo.toml b/gix-traverse/Cargo.toml index f42a4bc49fd..e33b6c40539 100644 --- a/gix-traverse/Cargo.toml +++ b/gix-traverse/Cargo.toml @@ -22,6 +22,7 @@ sha256 = ["gix-hash/sha256"] [dependencies] gix-hash = { version = "^0.26.1", path = "../gix-hash" } +gix-error = { version = "^0.3.0", path = "../gix-error" } gix-object = { version = "^0.64.0", path = "../gix-object" } gix-date = { version = "^0.16.0", path = "../gix-date" } gix-hashtable = { version = "^0.16.0", path = "../gix-hashtable" } diff --git a/gix-traverse/src/commit/simple.rs b/gix-traverse/src/commit/simple.rs index e4997d600ee..c01b00f342f 100644 --- a/gix-traverse/src/commit/simple.rs +++ b/gix-traverse/src/commit/simple.rs @@ -73,13 +73,19 @@ pub enum Sorting { #[expect(missing_docs)] pub enum Error { #[error(transparent)] - Find(#[from] gix_object::find::existing_iter::Error), + Find(gix_error::Error), #[error(transparent)] ObjectDecode(#[from] gix_object::decode::Error), #[error(transparent)] HiddenGraph(#[from] gix_revwalk::graph::get_or_insert_default::Error), } +impl From for Error { + fn from(err: gix_object::find::existing_iter::Error) -> Self { + Error::Find(err.into_error()) + } +} + use Result as Either; type QueueKey = Either>; diff --git a/gix-traverse/src/commit/topo/mod.rs b/gix-traverse/src/commit/topo/mod.rs index fbe9812620e..e0fcc3698a2 100644 --- a/gix-traverse/src/commit/topo/mod.rs +++ b/gix-traverse/src/commit/topo/mod.rs @@ -13,7 +13,13 @@ pub enum Error { #[error(transparent)] ObjectDecode(#[from] gix_object::decode::Error), #[error(transparent)] - Find(#[from] gix_object::find::existing_iter::Error), + Find(gix_error::Error), +} + +impl From for Error { + fn from(err: gix_object::find::existing_iter::Error) -> Self { + Error::Find(err.into_error()) + } } bitflags! { diff --git a/gix-traverse/src/tree/breadthfirst.rs b/gix-traverse/src/tree/breadthfirst.rs index 36ae5afaa87..4df3bee8f27 100644 --- a/gix-traverse/src/tree/breadthfirst.rs +++ b/gix-traverse/src/tree/breadthfirst.rs @@ -8,13 +8,19 @@ use gix_hash::ObjectId; #[expect(missing_docs)] pub enum Error { #[error(transparent)] - Find(#[from] gix_object::find::existing_iter::Error), + Find(gix_error::Error), #[error("The delegate cancelled the operation")] Cancelled, #[error(transparent)] ObjectDecode(#[from] gix_object::decode::Error), } +impl From for Error { + fn from(err: gix_object::find::existing_iter::Error) -> Self { + Error::Find(err.into_error()) + } +} + /// The state used and potentially shared by multiple tree traversals. #[derive(Default, Clone)] pub struct State { diff --git a/gix-worktree-state/Cargo.toml b/gix-worktree-state/Cargo.toml index c4d091d9463..45adf15a6dc 100644 --- a/gix-worktree-state/Cargo.toml +++ b/gix-worktree-state/Cargo.toml @@ -27,6 +27,7 @@ gix-worktree = { version = "^0.56.0", path = "../gix-worktree", default-features gix-index = { version = "^0.55.0", path = "../gix-index" } gix-fs = { version = "^0.22.1", path = "../gix-fs" } gix-object = { version = "^0.64.1", path = "../gix-object" } +gix-error = { version = "^0.3.0", path = "../gix-error" } gix-path = { version = "^0.12.6", path = "../gix-path" } gix-features = { version = "^0.49.1", path = "../gix-features" } gix-filter = { version = "^0.34.0", path = "../gix-filter" } diff --git a/gix-worktree-state/src/checkout/entry.rs b/gix-worktree-state/src/checkout/entry.rs index 297b90e32cd..484ad321eec 100644 --- a/gix-worktree-state/src/checkout/entry.rs +++ b/gix-worktree-state/src/checkout/entry.rs @@ -91,7 +91,7 @@ where let obj = (*objects) .find_blob(&entry.id, buf) .map_err(|err| crate::checkout::Error::Find { - err, + err: err.into_error(), path: dest.to_path_buf(), })?; @@ -148,7 +148,7 @@ where let obj = (*objects) .find_blob(&entry.id, buf) .map_err(|err| crate::checkout::Error::Find { - err, + err: err.into_error(), path: dest.to_path_buf(), })?; if symlink { diff --git a/gix-worktree-state/src/checkout/mod.rs b/gix-worktree-state/src/checkout/mod.rs index 91d0b0f3242..30b369cf6a7 100644 --- a/gix-worktree-state/src/checkout/mod.rs +++ b/gix-worktree-state/src/checkout/mod.rs @@ -87,7 +87,7 @@ pub enum Error { #[error("object for checkout at {} could not be retrieved from object database", .path.display())] Find { #[source] - err: gix_object::find::existing_object::Error, + err: gix_error::Error, path: std::path::PathBuf, }, #[error(transparent)] diff --git a/gix-worktree-stream/tests/stream.rs b/gix-worktree-stream/tests/stream.rs index c2b1abaafa8..71d52f71fe2 100644 --- a/gix-worktree-stream/tests/stream.rs +++ b/gix-worktree-stream/tests/stream.rs @@ -16,6 +16,7 @@ mod from_tree { }; use gix_attributes::glob::pattern::Case; + use gix_error::ErrorExt; use gix_hash::oid; use gix_object::{Data, bstr::ByteSlice, tree::EntryKind}; use gix_worktree::stack::state::attributes::Source; @@ -32,7 +33,7 @@ mod from_tree { _id: &oid, _buffer: &'a mut Vec, ) -> Result>, gix_object::find::Error> { - Err(Box::new(Error::other("object retrieval failed"))) + Err(Error::other("object retrieval failed").raise_erased()) } } diff --git a/gix-worktree/src/stack/state/attributes.rs b/gix-worktree/src/stack/state/attributes.rs index 7eda457639c..d363c8fbeb9 100644 --- a/gix-worktree/src/stack/state/attributes.rs +++ b/gix-worktree/src/stack/state/attributes.rs @@ -106,7 +106,7 @@ impl Attributes { if let Ok(idx) = attr_file_in_index { let blob = objects .find_blob(&id_mappings[idx].1, buf) - .map_err(std::io::Error::other)?; + .map_err(|err| std::io::Error::other(err.into_error()))?; let attr_path = gix_path::from_bstring(attr_path_relative.into_owned()); self.stack.add_patterns_buffer( blob.data, @@ -145,7 +145,7 @@ impl Attributes { if let Some(idx) = attr_file_in_index.ok().filter(|_| !added) { let blob = objects .find_blob(&id_mappings[idx].1, buf) - .map_err(std::io::Error::other)?; + .map_err(|err| std::io::Error::other(err.into_error()))?; let attr_path = gix_path::from_bstring(attr_path_relative.into_owned()); self.stack.add_patterns_buffer( blob.data, diff --git a/gix-worktree/src/stack/state/ignore.rs b/gix-worktree/src/stack/state/ignore.rs index 2d7f8c33915..be47668dd8c 100644 --- a/gix-worktree/src/stack/state/ignore.rs +++ b/gix-worktree/src/stack/state/ignore.rs @@ -180,7 +180,7 @@ impl Ignore { Ok(idx) => { let ignore_blob = objects .find_blob(&id_mappings[idx].1, buf) - .map_err(std::io::Error::other)?; + .map_err(|err| std::io::Error::other(err.into_error()))?; let ignore_path = gix_path::from_bstring(ignore_path_relative.into_owned()); self.stack .add_patterns_buffer(ignore_blob.data, ignore_path, Some(Path::new("")), self.parse); @@ -209,7 +209,7 @@ impl Ignore { Ok(idx) => { let ignore_blob = objects .find_blob(&id_mappings[idx].1, buf) - .map_err(std::io::Error::other)?; + .map_err(|err| std::io::Error::other(err.into_error()))?; let ignore_path = gix_path::from_bstring(ignore_path_relative.into_owned()); self.stack.add_patterns_buffer( ignore_blob.data, diff --git a/gix-worktree/tests/worktree/stack/ignore.rs b/gix-worktree/tests/worktree/stack/ignore.rs index de4b8abd128..8dc7204c11b 100644 --- a/gix-worktree/tests/worktree/stack/ignore.rs +++ b/gix-worktree/tests/worktree/stack/ignore.rs @@ -58,10 +58,10 @@ fn exclude_by_dir_is_handled_just_like_git() { impl gix_object::Find for FindError { fn try_find<'a>( &self, - _id: &gix_hash::oid, - _buffer: &'a mut Vec, + id: &gix_hash::oid, + buffer: &'a mut Vec, ) -> Result>, gix_object::find::Error> { - Err(std::io::Error::other("unreachable").into()) + gix_object::FindExt::find(&gix_object::find::Never, id, buffer).map(Some) } } for (relative_entry, source_and_line) in expectations { diff --git a/gix/src/commit/sign.rs b/gix/src/commit/sign.rs index ebd1f66557f..acfa1012fbe 100644 --- a/gix/src/commit/sign.rs +++ b/gix/src/commit/sign.rs @@ -17,7 +17,7 @@ pub enum Error { #[error(transparent)] Decode(#[from] gix_object::decode::Error), #[error(transparent)] - Sign(#[from] gix_object::signature::sign::Error), + Sign(#[from] gix_error::Error), #[error(transparent)] WriteObject(#[from] crate::object::write::Error), #[error(transparent)] @@ -61,7 +61,7 @@ pub mod options { pub(crate) fn sign<'repo>(commit: &crate::Commit<'repo>) -> Result, Error> { let options = commit.repo.commit_signing_options()?; - let signed = commit.decode()?.sign(options)?; + let signed = commit.decode()?.sign(options).map_err(gix_error::Exn::into_error)?; let id = commit.repo.write_object(&signed)?; Ok(commit.repo.find_commit(id)?) } diff --git a/gix/src/commit/verify.rs b/gix/src/commit/verify.rs index cce980c7fb5..c8d2324e0be 100644 --- a/gix/src/commit/verify.rs +++ b/gix/src/commit/verify.rs @@ -18,11 +18,11 @@ pub enum Error { #[error(transparent)] InvalidTrustLevel(#[from] crate::config::key::GenericErrorWithValue), #[error("Could not interpolate a configured signature-verification path")] - ConfiguredPath(#[from] gix_error::Error), + ConfiguredPath(#[source] gix_error::Error), #[error("gpg.ssh.allowedSignersFile must be configured for SSH signature verification")] MissingAllowedSigners, #[error(transparent)] - Verify(#[from] gix_object::signature::verify::Error), + Verify(gix_error::Error), } pub(crate) fn verify(commit: &crate::Commit<'_>) -> Result, Error> { @@ -35,8 +35,12 @@ pub(crate) fn verify(commit: &crate::Commit<'_>) -> Result, Erro .map(|value| Gpg::MIN_TRUST_LEVEL.try_into_trust_level(value)) .transpose()? .unwrap_or_default(); - let format = Format::from_signature(&signature).ok_or(gix_object::signature::verify::Error::UnsupportedFormat)?; - let program = super::signature_program(&config, format)?; + let format = Format::from_signature(&signature).ok_or_else(|| { + Error::Verify(gix_error::Error::from_error(gix_error::CorruptionError::new( + "The signature format is unsupported", + ))) + })?; + let program = super::signature_program(&config, format).map_err(Error::ConfiguredPath)?; let options = match format { Format::OpenPgp => gix_object::signature::verify::Options::OpenPgp { program, @@ -52,10 +56,12 @@ pub(crate) fn verify(commit: &crate::Commit<'_>) -> Result, Erro }, Format::Ssh => { let allowed_signers = config - .trusted_path(gpg::Ssh::ALLOWED_SIGNERS_FILE)? + .trusted_path(gpg::Ssh::ALLOWED_SIGNERS_FILE) + .map_err(Error::ConfiguredPath)? .ok_or(Error::MissingAllowedSigners)?; let revocation_file = config - .trusted_path(gpg::Ssh::REVOCATION_FILE)? + .trusted_path(gpg::Ssh::REVOCATION_FILE) + .map_err(Error::ConfiguredPath)? .filter(|path| path.exists()); gix_object::signature::verify::Options::Ssh { program, @@ -68,7 +74,11 @@ pub(crate) fn verify(commit: &crate::Commit<'_>) -> Result, Erro } } }; - signed_data.verify(&signature, options).map(Some).map_err(Into::into) + let outcome = signed_data + .verify(&signature, options) + .map_err(gix_error::Exn::into_error) + .map_err(Error::Verify)?; + Ok(Some(outcome)) } fn resolve_relative_to_repository(repo: &crate::Repository, path: PathBuf) -> PathBuf { diff --git a/gix/src/mailmap.rs b/gix/src/mailmap.rs index c040cdb7bd7..0d8bec4811b 100644 --- a/gix/src/mailmap.rs +++ b/gix/src/mailmap.rs @@ -13,6 +13,6 @@ pub mod load { #[error(transparent)] PathInterpolate(#[from] gix_error::Error), #[error("Could not find object configured in `mailmap.blob`")] - FindExisting(#[from] crate::object::find::existing::Error), + FindExisting(#[source] crate::object::find::existing::Error), } } diff --git a/gix/src/object/commit.rs b/gix/src/object/commit.rs index 880345c8925..77023448924 100644 --- a/gix/src/object/commit.rs +++ b/gix/src/object/commit.rs @@ -9,9 +9,9 @@ mod error { #[error(transparent)] FindExistingObject(#[from] object::find::existing::Error), #[error("The commit could not be decoded fully or partially")] - Decode(#[from] gix_object::decode::Error), + Decode(#[source] gix_object::decode::Error), #[error("The commit date could not be parsed")] - ParseDate(#[from] gix_date::Error), + ParseDate(#[source] gix_date::Error), #[error("Expected object of type {}, but got {}", .expected, .actual)] ObjectKind { expected: gix_object::Kind, @@ -103,7 +103,8 @@ impl<'repo> Commit<'repo> { /// /// For the time at which it was authored, refer to `.author()?.time()`. pub fn time(&self) -> Result { - Ok(self.committer()?.time()?) + let committer = self.committer().map_err(Error::Decode)?; + committer.time().map_err(Error::ParseDate) } /// Decode the entire commit object and return it for accessing all commit information. @@ -174,7 +175,7 @@ impl<'repo> Commit<'repo> { /// # Ok(()) } /// ``` pub fn tree(&self) -> Result, Error> { - match self.tree_id()?.object()?.try_into_tree() { + match self.tree_id().map_err(Error::Decode)?.object()?.try_into_tree() { Ok(tree) => Ok(tree), Err(crate::object::try_into::Error { actual, expected, .. }) => Err(Error::ObjectKind { actual, expected }), } diff --git a/gix/src/object/errors.rs b/gix/src/object/errors.rs index b8f352bac5b..c1e6f284462 100644 --- a/gix/src/object/errors.rs +++ b/gix/src/object/errors.rs @@ -25,7 +25,7 @@ pub mod find { /// pub mod existing { /// An object could not be found in the database, or an error occurred when trying to obtain it. - pub type Error = gix_object::find::existing::Error; + pub type Error = gix_error::Error; /// pub mod with_conversion { /// The error returned by [Repository::find_commit()](crate::Repository::find_commit). diff --git a/gix/src/object/tree/editor.rs b/gix/src/object/tree/editor.rs index 4f4bdf042fb..5eae339a0c1 100644 --- a/gix/src/object/tree/editor.rs +++ b/gix/src/object/tree/editor.rs @@ -145,10 +145,7 @@ impl<'repo> super::Editor<'repo> { /// /// The returned cursor will then allow applying edits to the tree at `rela_path` as root. /// If `rela_path` is a single empty string, it is equivalent to using the current instance itself. - pub fn cursor_at( - &mut self, - rela_path: impl ToComponents, - ) -> Result, gix_object::tree::editor::Error> { + pub fn cursor_at(&mut self, rela_path: impl ToComponents) -> Result, gix_error::Error> { Ok(Cursor { inner: self.inner.cursor_at(rela_path.to_components())?, validate: self.validate, @@ -164,19 +161,19 @@ impl<'repo> Cursor<'_, 'repo> { rela_path: impl ToComponents, kind: EntryKind, id: impl Into, - ) -> Result<&mut Self, gix_object::tree::editor::Error> { + ) -> Result<&mut Self, gix_error::Error> { self.inner.upsert(rela_path.to_components(), kind, id.into())?; Ok(self) } /// Like [`Editor::remove()`](super::Editor::remove), but with the constraint of only editing in this cursor's tree. - pub fn remove(&mut self, rela_path: impl ToComponents) -> Result<&mut Self, gix_object::tree::editor::Error> { + pub fn remove(&mut self, rela_path: impl ToComponents) -> Result<&mut Self, gix_error::Error> { self.inner.remove(rela_path.to_components())?; Ok(self) } /// Like [`Editor::remove_leaf()`](super::Editor::remove_leaf), but with the constraint of only editing in this cursor's tree. - pub fn remove_leaf(&mut self, rela_path: impl ToComponents) -> Result<&mut Self, gix_object::tree::editor::Error> { + pub fn remove_leaf(&mut self, rela_path: impl ToComponents) -> Result<&mut Self, gix_error::Error> { self.inner.remove_leaf(rela_path.to_components())?; Ok(self) } @@ -236,14 +233,14 @@ impl<'repo> super::Editor<'repo> { rela_path: impl ToComponents, kind: EntryKind, id: impl Into, - ) -> Result<&mut Self, gix_object::tree::editor::Error> { + ) -> Result<&mut Self, gix_error::Error> { self.inner.upsert(rela_path.to_components(), kind, id.into())?; Ok(self) } /// Remove the entry at `rela_path`, loading all trees on the path accordingly. /// It's no error if the entry doesn't exist, or if `rela_path` doesn't lead to an existing entry at all. - pub fn remove(&mut self, rela_path: impl ToComponents) -> Result<&mut Self, gix_object::tree::editor::Error> { + pub fn remove(&mut self, rela_path: impl ToComponents) -> Result<&mut Self, gix_error::Error> { self.inner.remove(rela_path.to_components())?; Ok(self) } @@ -252,7 +249,7 @@ impl<'repo> super::Editor<'repo> { /// It's no error if the entry doesn't exist, or if `rela_path` doesn't lead to an existing entry at all. /// /// Return an error if the entry exists and is a tree, as that would otherwise also remove all entries below it. - pub fn remove_leaf(&mut self, rela_path: impl ToComponents) -> Result<&mut Self, gix_object::tree::editor::Error> { + pub fn remove_leaf(&mut self, rela_path: impl ToComponents) -> Result<&mut Self, gix_error::Error> { self.inner.remove_leaf(rela_path.to_components())?; Ok(self) } diff --git a/gix/src/repository/impls.rs b/gix/src/repository/impls.rs index 39aa392882c..cc37c863458 100644 --- a/gix/src/repository/impls.rs +++ b/gix/src/repository/impls.rs @@ -1,5 +1,6 @@ use std::ops::DerefMut; +use gix_error::{ErrorExt, ResultExt}; use gix_hash::ObjectId; use gix_object::Exists; @@ -109,12 +110,12 @@ impl From for crate::ThreadSafeRepository { impl gix_object::Write for crate::Repository { fn write(&self, object: &dyn gix_object::WriteTo) -> Result { let mut buf = self.empty_reusable_buffer(); - object.write_to(buf.deref_mut())?; + object.write_to(buf.deref_mut()).or_erased()?; self.write_buf(object.kind(), &buf) } fn write_buf(&self, object: gix_object::Kind, from: &[u8]) -> Result { - let oid = gix_object::compute_hash(self.object_hash(), object, from)?; + let oid = gix_object::compute_hash(self.object_hash(), object, from).or_erased()?; if self.objects.exists(&oid) { return Ok(oid); } @@ -128,9 +129,11 @@ impl gix_object::Write for crate::Repository { from: &mut dyn std::io::Read, ) -> Result { let mut buf = self.empty_reusable_buffer(); - let bytes = std::io::copy(from, buf.deref_mut())?; + let bytes = std::io::copy(from, buf.deref_mut()).or_erased()?; if size != bytes { - return Err(format!("Found {bytes} bytes in stream, but had {size} bytes declared").into()); + return Err( + gix_error::message!("Found {bytes} bytes in stream, but had {size} bytes declared").raise_erased(), + ); } self.write_buf(kind, &buf) } @@ -155,9 +158,11 @@ impl gix_object::Write for crate::Repository { id: gix_hash::ObjectId, ) -> Result { let mut buf = self.empty_reusable_buffer(); - let bytes = std::io::copy(from, buf.deref_mut())?; + let bytes = std::io::copy(from, buf.deref_mut()).or_erased()?; if size != bytes { - return Err(format!("Found {bytes} bytes in stream, but had {size} bytes declared").into()); + return Err( + gix_error::message!("Found {bytes} bytes in stream, but had {size} bytes declared").raise_erased(), + ); } self.write_buf_with_known_id(kind, &buf, id) } diff --git a/gix/src/repository/mailmap.rs b/gix/src/repository/mailmap.rs index 6ee80d2d6c9..c5ff8231759 100644 --- a/gix/src/repository/mailmap.rs +++ b/gix/src/repository/mailmap.rs @@ -59,7 +59,11 @@ impl crate::Repository { } } - if let Some(blob) = blob_id.and_then(|id| self.find_object(id).map_err(|e| err.get_or_insert(e.into())).ok()) { + if let Some(blob) = blob_id.and_then(|id| { + self.find_object(id) + .map_err(|e| err.get_or_insert(crate::mailmap::load::Error::FindExisting(e))) + .ok() + }) { target.merge(gix_mailmap::parse_ignore_errors(&blob.data)); } diff --git a/gix/src/repository/object.rs b/gix/src/repository/object.rs index ebc2ef9aea7..14ee7fd3856 100644 --- a/gix/src/repository/object.rs +++ b/gix/src/repository/object.rs @@ -1,6 +1,7 @@ #![allow(clippy::result_large_err)] use std::ops::DerefMut; +use gix_error::ResultExt; use gix_hash::ObjectId; use gix_object::{Exists, Find, FindExt, Write}; use gix_odb::{Header, HeaderExt}; @@ -143,7 +144,7 @@ impl crate::Repository { size: 0, }); } - self.objects.header(id) + self.objects.header(id).map_err(Into::into) } /// Return `true` if `id` exists in the object database. @@ -246,16 +247,13 @@ impl crate::Repository { /// we avoid writing duplicate objects using slow disks that will eventually have to be garbage collected. pub fn write_object(&self, object: impl gix_object::WriteTo) -> Result, object::write::Error> { let mut buf = self.empty_reusable_buffer(); - object - .write_to(buf.deref_mut()) - .map_err(|err| Box::new(err) as Box)?; + object.write_to(buf.deref_mut()).or_erased()?; self.write_object_inner(&buf, object.kind()) } fn write_object_inner(&self, buf: &[u8], kind: gix_object::Kind) -> Result, object::write::Error> { - let oid = gix_object::compute_hash(self.object_hash(), kind, buf) - .map_err(|err| Box::new(err) as Box)?; + let oid = gix_object::compute_hash(self.object_hash(), kind, buf).or_erased()?; if self.objects.exists(&oid) { return Ok(oid.attach(self)); } @@ -285,8 +283,7 @@ impl crate::Repository { /// ``` pub fn write_blob(&self, bytes: impl AsRef<[u8]>) -> Result, object::write::Error> { let bytes = bytes.as_ref(); - let oid = gix_object::compute_hash(self.object_hash(), gix_object::Kind::Blob, bytes) - .map_err(|err| Box::new(err) as Box)?; + let oid = gix_object::compute_hash(self.object_hash(), gix_object::Kind::Blob, bytes).or_erased()?; if self.objects.exists(&oid) { return Ok(oid.attach(self)); } @@ -304,15 +301,13 @@ impl crate::Repository { /// If that is prohibitive, use the object database directly. pub fn write_blob_stream(&self, mut bytes: impl std::io::Read) -> Result, object::write::Error> { let mut buf = self.empty_reusable_buffer(); - std::io::copy(&mut bytes, buf.deref_mut()) - .map_err(|err| Box::new(err) as Box)?; + std::io::copy(&mut bytes, buf.deref_mut()).or_erased()?; self.write_blob_stream_inner(&buf) } fn write_blob_stream_inner(&self, buf: &[u8]) -> Result, object::write::Error> { - let oid = gix_object::compute_hash(self.object_hash(), gix_object::Kind::Blob, buf) - .map_err(|err| Box::new(err) as Box)?; + let oid = gix_object::compute_hash(self.object_hash(), gix_object::Kind::Blob, buf).or_erased()?; if self.objects.exists(&oid) { return Ok(oid.attach(self)); } diff --git a/gix/src/repository/worktree.rs b/gix/src/repository/worktree.rs index 4fc627ffea8..4540e4c58a4 100644 --- a/gix/src/repository/worktree.rs +++ b/gix/src/repository/worktree.rs @@ -110,7 +110,7 @@ impl crate::Repository { ) -> Result<(gix_worktree_stream::Stream, gix_index::File), crate::repository::worktree_stream::Error> { use gix_odb::HeaderExt; let id = id.into(); - let header = self.objects.header(id)?; + let header = self.objects.header(id).map_err(gix_error::Exn::into_error)?; if !header.kind().is_tree() { return Err(crate::repository::worktree_stream::Error::NotATree { id, diff --git a/gix/tests/gix/commit.rs b/gix/tests/gix/commit.rs index 3fb06f48724..b9a4a2f73ff 100644 --- a/gix/tests/gix/commit.rs +++ b/gix/tests/gix/commit.rs @@ -232,13 +232,15 @@ mod signature { .find_commit(id)? .verify_signature() .expect_err("the configured verifier does not exist"); - let gix::commit::verify::Error::Verify(gix_object::signature::verify::Error::Spawn { program, .. }) = err - else { + let gix::commit::verify::Error::Verify(err) = err else { panic!("expected a verifier spawn failure, got {err:?}"); }; assert_eq!( - program, - home.join("bin/missing-gpg"), + err.to_string(), + format!( + "Could not execute signature verifier {:?}", + home.join("bin/missing-gpg") + ), "the configured verifier path is expanded relative to home" ); Ok(()) diff --git a/gix/tests/gix/revision/spec/from_bytes/mod.rs b/gix/tests/gix/revision/spec/from_bytes/mod.rs index a337e1b378e..99136709d8e 100644 --- a/gix/tests/gix/revision/spec/from_bytes/mod.rs +++ b/gix/tests/gix/revision/spec/from_bytes/mod.rs @@ -143,15 +143,30 @@ fn bad_objects_are_valid_until_they_are_actually_read_from_the_odb() { "we are able to return objects even though they are 'bad' when trying to decode them, like git", ); let err = parse_spec("e328^{object}", &repo).unwrap_err(); + let cause = err + .probable_cause() + .downcast_ref::() + .expect("invalid object kinds are classified as validation failures"); assert_eq!( - format!("{:?}", err.probable_cause()), - r#"InvalidObjectKind { kind: "bad" }"#, + ( + cause.message.as_ref(), + cause.input.as_ref().map(|input| input.as_slice()) + ), + ("Unknown object kind", Some(b"bad".as_slice())), "Now we enforce the object to exist and be valid, as ultimately it wants to match with a certain type" ); - insta::assert_debug_snapshot!(err, @r#" + insta::assert_snapshot!(format!("{err:#?}").replace('\\', "/"), @r#" delegate.peel_until(ValidObject) failed: "{object}" | - └─ An error occurred while obtaining an object from the loose object store + └─ Loose(Decode(The object header contained an unknown object kind., at gix-object/src/lib.rs:344 + | + └─ Unknown object kind: "bad", at gix-object/src/lib.rs:344)) + | + └─ Decode(The object header contained an unknown object kind., at gix-object/src/lib.rs:344 + | + └─ Unknown object kind: "bad", at gix-object/src/lib.rs:344) + | + └─ ValidationError { message: "Unknown object kind", input: Some("bad") } | └─ The object header contained an unknown object kind. | @@ -167,20 +182,38 @@ fn bad_objects_are_valid_until_they_are_actually_read_from_the_odb() { ); let err = parse_spec("cafea^{object}", &repo).unwrap_err(); let actual = { - let mut actual = format!("{err:#?}").replace('\\', "/").replace("windows", "unix"); + let mut actual = format!("{err:#?}") + .replace("\\\\", "/") + .replace('\\', "/") + .replace("windows", "unix"); let marker = "make_rev_spec_parse_repos/"; - if let Some(start) = actual.find(marker) { - let start = start + marker.len(); - if let Some(end) = actual[start..].find("/blob.corrupt") { - actual.replace_range(start..start + end, "$HASH/$SEED-unix"); - } + let mut search_from = 0; + while let Some(start) = actual[search_from..].find(marker) { + let start = search_from + start + marker.len(); + let Some(end) = actual[start..].find("/blob.corrupt") else { + break; + }; + actual.replace_range(start..start + end, "$HASH/$SEED-unix"); + search_from = start + "$HASH/$SEED-unix".len(); } actual }; insta::assert_snapshot!(actual, @r#" delegate.peel_until(ValidObject) failed: "{object}" | - └─ An error occurred while obtaining an object from the loose object store + └─ Loose(DecompressFile { source: Could not decode zip stream, at gix-zlib/src/inflate.rs:16 + | + └─ Invalid input data, at gix-zlib/src/decompress.rs:67, path: "tests/fixtures/generated-do-not-edit/make_rev_spec_parse_repos/$HASH/$SEED-unix/blob.corrupt/objects/ca/fea31147e840161a1860c50af999917ae1536b" }) + | + └─ DecompressFile { source: Could not decode zip stream, at gix-zlib/src/inflate.rs:16 + | + └─ Invalid input data, at gix-zlib/src/decompress.rs:67, path: "tests/fixtures/generated-do-not-edit/make_rev_spec_parse_repos/$HASH/$SEED-unix/blob.corrupt/objects/ca/fea31147e840161a1860c50af999917ae1536b" } + | + └─ Could not decode zip stream, at gix-zlib/src/inflate.rs:16 + | + └─ Invalid input data, at gix-zlib/src/decompress.rs:67 + | + └─ Invalid input data | └─ decompression of loose object at 'tests/fixtures/generated-do-not-edit/make_rev_spec_parse_repos/$HASH/$SEED-unix/blob.corrupt/objects/ca/fea31147e840161a1860c50af999917ae1536b' failed | diff --git a/tests/it/src/commands/blame_copy_royal.rs b/tests/it/src/commands/blame_copy_royal.rs index 8e3dae733fa..4c7fc91f477 100644 --- a/tests/it/src/commands/blame_copy_royal.rs +++ b/tests/it/src/commands/blame_copy_royal.rs @@ -91,7 +91,11 @@ pub(super) mod function { for blame_path_entry in &blame_infos { let dst = assets.join(format!("{}.commit", blame_path_entry.commit_id)); if !dry_run { - let blob = repo.objects.find_blob(&blame_path_entry.blob_id, &mut buf)?.data; + let blob = repo + .objects + .find_blob(&blame_path_entry.blob_id, &mut buf) + .map_err(gix::Exn::into_error)? + .data; if verbatim { std::fs::write(dst, blob)?; diff --git a/tests/it/src/commands/create_diff_cases.rs b/tests/it/src/commands/create_diff_cases.rs index ad43ae5a2e3..2372b03a1b8 100644 --- a/tests/it/src/commands/create_diff_cases.rs +++ b/tests/it/src/commands/create_diff_cases.rs @@ -79,10 +79,18 @@ mkdir -p assets let dst_old_blob = assets.join(format!("{old_blob_id}.blob")); let dst_new_blob = assets.join(format!("{new_blob_id}.blob")); if !dry_run { - let old_blob = repo.objects.find_blob(&old_blob_id, &mut buf)?.data; + let old_blob = repo + .objects + .find_blob(&old_blob_id, &mut buf) + .map_err(gix::Exn::into_error)? + .data; std::fs::write(dst_old_blob, old_blob)?; - let new_blob = repo.objects.find_blob(&new_blob_id, &mut buf)?.data; + let new_blob = repo + .objects + .find_blob(&new_blob_id, &mut buf) + .map_err(gix::Exn::into_error)? + .data; std::fs::write(dst_new_blob, new_blob)?; } diff --git a/tests/it/src/commands/git_to_sh.rs b/tests/it/src/commands/git_to_sh.rs index 435f3d69095..1a014a0e7a5 100644 --- a/tests/it/src/commands/git_to_sh.rs +++ b/tests/it/src/commands/git_to_sh.rs @@ -107,7 +107,10 @@ pub(super) mod function { unreachable!("Can't have trees in indices") } EntryKind::Blob | EntryKind::BlobExecutable => { - let obj = repo.objects.find(&entry.id, &mut blob_buf)?; + let obj = repo + .objects + .find(&entry.id, &mut blob_buf) + .map_err(gix::Exn::into_error)?; if verbatim { (entry.id, Cow::Borrowed(&blob_buf)) } else { @@ -126,7 +129,9 @@ pub(super) mod function { } } EntryKind::Link => { - repo.objects.find(&entry.id, &mut blob_buf)?; + repo.objects + .find(&entry.id, &mut blob_buf) + .map_err(gix::Exn::into_error)?; (entry.id, Cow::Borrowed(&blob_buf)) } EntryKind::Commit => continue, From 264f96159ae4a89154f00bf3371d3f9f02b68ff4 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 20 Aug 2026 17:07:43 +0200 Subject: [PATCH 04/51] change(gix-refspec)!: migrate errors to gix-error rubberstamp --- Cargo.lock | 2 +- gitoxide-core/src/pack/receive.rs | 4 +- gix-refspec/Cargo.toml | 2 +- gix-refspec/src/match_group/validate.rs | 14 ++++- gix-refspec/src/parse.rs | 66 +++++++++------------ gix-refspec/tests/refspec/matching.rs | 20 ++++++- gix-refspec/tests/refspec/parse/fetch.rs | 50 +++++++--------- gix-refspec/tests/refspec/parse/invalid.rs | 68 +++++++++------------- gix-refspec/tests/refspec/parse/mod.rs | 25 ++++++++ gix-refspec/tests/refspec/parse/push.rs | 48 ++++++--------- gix-submodule/src/access.rs | 2 +- gix-submodule/src/config.rs | 2 +- gix/src/clone/access.rs | 3 +- gix/src/clone/mod.rs | 2 +- gix/src/config/mod.rs | 2 +- gix/src/config/tree/keys.rs | 6 +- gix/src/remote/access.rs | 2 +- gix/src/remote/build.rs | 2 +- gix/src/remote/name.rs | 7 ++- 19 files changed, 171 insertions(+), 156 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 774629f36b6..25ef5f7cfb4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2399,12 +2399,12 @@ name = "gix-refspec" version = "0.45.1" dependencies = [ "bstr", + "gix-error", "gix-hash", "gix-testtools", "gix-validate", "insta", "smallvec", - "thiserror 2.0.18", ] [[package]] diff --git a/gitoxide-core/src/pack/receive.rs b/gitoxide-core/src/pack/receive.rs index 23540759a7f..a1ae86385cc 100644 --- a/gitoxide-core/src/pack/receive.rs +++ b/gitoxide-core/src/pack/receive.rs @@ -78,7 +78,9 @@ where let fetch_refspecs: Vec<_> = wanted_refs .into_iter() .map(|ref_name| { - gix::refspec::parse(ref_name.as_bstr(), gix::refspec::parse::Operation::Fetch).map(|r| r.to_owned()) + gix::refspec::parse(ref_name.as_bstr(), gix::refspec::parse::Operation::Fetch) + .map(|r| r.to_owned()) + .map_err(gix::Exn::into_error) }) .collect::>()?; let user_agent = ("agent", Some(agent.clone())); diff --git a/gix-refspec/Cargo.toml b/gix-refspec/Cargo.toml index ed56f1b2664..e44a3cce0e7 100644 --- a/gix-refspec/Cargo.toml +++ b/gix-refspec/Cargo.toml @@ -23,9 +23,9 @@ sha256 = ["gix-hash/sha256"] [dependencies] gix-validate = { version = "^0.11.4", path = "../gix-validate" } gix-hash = { version = "^0.26.2", path = "../gix-hash" } +gix-error = { version = "^0.3.0", path = "../gix-error" } bstr = { version = "1.12.0", default-features = false, features = ["std"] } -thiserror = "2.0.18" smallvec = "1.15.1" [dev-dependencies] diff --git a/gix-refspec/src/match_group/validate.rs b/gix-refspec/src/match_group/validate.rs index 39f83553b23..4d6beb508da 100644 --- a/gix-refspec/src/match_group/validate.rs +++ b/gix-refspec/src/match_group/validate.rs @@ -7,7 +7,9 @@ use crate::{ match_group::{Source, match_lhs}, }; -/// The error returned [outcome validation](match_lhs::Outcome::validated()). +/// The error returned by [outcome validation](match_lhs::Outcome::validated()). +/// +/// Its source preserves the validation classification when converted to [`gix_error::Error`]. #[derive(Debug)] pub struct Error { /// All issues discovered during validation. @@ -34,7 +36,15 @@ impl std::fmt::Display for Error { } } -impl std::error::Error for Error {} +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + static VALIDATION: gix_error::ValidationError = gix_error::ValidationError { + message: std::borrow::Cow::Borrowed("Invalid refspec mapping"), + input: None, + }; + Some(&VALIDATION) + } +} /// All possible issues found while validating matched mappings. #[derive(Debug, PartialEq, Eq)] diff --git a/gix-refspec/src/parse.rs b/gix-refspec/src/parse.rs index 767b06b1790..9d8e99e831a 100644 --- a/gix-refspec/src/parse.rs +++ b/gix-refspec/src/parse.rs @@ -1,33 +1,5 @@ /// The error returned by the [`parse()`][crate::parse()] function. -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] -pub enum Error { - #[error("Empty refspecs are invalid")] - Empty, - #[error("Negative refspecs cannot have destinations as they exclude sources")] - NegativeWithDestination, - #[error("Negative specs must not be empty")] - NegativeEmpty, - #[error("Negative specs must not be object hashes")] - NegativeObjectHash, - /// Retained for compatibility; partial negative ref names are accepted and this is no longer returned. - #[error("Negative specs must be full ref names, starting with \"refs/\"")] - NegativePartialName, - /// Retained for compatibility; negative ref patterns containing one `*` are accepted and this is no longer returned. - #[error("Negative glob patterns are not allowed")] - NegativeGlobPattern, - /// Retained for compatibility; invalid fetch destinations are reported as [`Error::ReferenceName`] instead. - #[error("Fetch destinations must be ref-names, like 'HEAD:refs/heads/branch'")] - InvalidFetchDestination, - #[error("Cannot push into an empty destination")] - PushToEmpty, - #[error("refspec patterns may only contain a single '*' character, found {pattern:?}")] - PatternUnsupported { pattern: bstr::BString }, - #[error("Both sides of a two-sided specification need a pattern, like 'a/*:b/*'")] - PatternUnbalanced, - #[error(transparent)] - ReferenceName(#[from] gix_validate::reference::name::Error), -} +pub type Error = gix_error::Exn; /// Define how the parsed refspec should be used. #[derive(PartialOrd, Ord, PartialEq, Eq, Copy, Clone, Hash, Debug)] @@ -45,6 +17,7 @@ pub(crate) mod function { types::Mode, }; use bstr::{BStr, ByteSlice}; + use gix_error::{ErrorExt, ValidationError}; /// Parse `spec` for use in `operation` and return it if it is valid. pub fn parse(mut spec: &BStr, operation: Operation) -> Result, Error> { @@ -69,7 +42,7 @@ pub(crate) mod function { Some(_) => Mode::Normal, None => { return match operation { - Operation::Push => Err(Error::Empty), + Operation::Push => Err(ValidationError::new("Empty refspecs are invalid").raise()), Operation::Fetch => Ok(fetch_head_only(Mode::Normal)), }; } @@ -81,7 +54,10 @@ pub(crate) mod function { let (mut src, dst) = match spec.rfind_byte(b':') { Some(pos) => { if mode == Mode::Negative { - return Err(Error::NegativeWithDestination); + return Err(ValidationError::new( + "Negative refspecs cannot have destinations as they exclude sources", + ) + .raise()); } let (src, dst) = spec.split_at(pos); @@ -98,7 +74,9 @@ pub(crate) mod function { Operation::Fetch => (Some("HEAD".into()), Some(dst)), }, (Some(src), None) => match operation { - Operation::Push => return Err(Error::PushToEmpty), + Operation::Push => { + return Err(ValidationError::new("Cannot push into an empty destination").raise()); + } Operation::Fetch => (Some(src), None), }, (Some(src), Some(dst)) => (Some(src), Some(dst)), @@ -125,17 +103,19 @@ pub(crate) mod function { && src_had_pattern != dst_had_pattern && !(operation == Operation::Push && dst.is_none()) { - return Err(Error::PatternUnbalanced); + return Err( + ValidationError::new("Both sides of a two-sided specification need a pattern, like 'a/*:b/*'").raise(), + ); } if mode == Mode::Negative { match src { Some(spec) => { if looks_like_object_hash(spec) { - return Err(Error::NegativeObjectHash); + return Err(ValidationError::new("Negative specs must not be object hashes").raise()); } } - None => return Err(Error::NegativeEmpty), + None => return Err(ValidationError::new("Negative specs must not be empty").raise()), } } @@ -156,7 +136,10 @@ pub(crate) mod function { buf.extend_from_slice(spec); let glob_pos = buf.find_byte(b'*').expect("glob present"); buf[glob_pos] = b'a'; - gix_validate::reference::name_partial(buf.as_bstr())?; + gix_validate::reference::name_partial(buf.as_bstr()).map_err(|source| { + let message = source.to_string(); + source.and_raise(ValidationError::new(message)) + })?; Ok(()) } @@ -168,13 +151,20 @@ pub(crate) mod function { Some(spec) => { let glob_count = spec.iter().filter(|b| **b == b'*').take(2).count(); if glob_count > 1 { - return Err(Error::PatternUnsupported { pattern: spec.into() }); + return Err(ValidationError::new_with_input( + "refspec patterns may only contain a single '*' character", + spec, + ) + .raise()); } let has_globs = glob_count > 0; if has_globs { validate_partial_name_with_single_glob(spec)?; } else if !any_name { - gix_validate::reference::name_partial(spec)?; + gix_validate::reference::name_partial(spec).map_err(|source| { + let message = source.to_string(); + source.and_raise(ValidationError::new(message)) + })?; } Ok((Some(spec), has_globs)) } diff --git a/gix-refspec/tests/refspec/matching.rs b/gix-refspec/tests/refspec/matching.rs index 7d95a5f7b51..8fa15580d62 100644 --- a/gix-refspec/tests/refspec/matching.rs +++ b/gix-refspec/tests/refspec/matching.rs @@ -132,7 +132,25 @@ pub mod baseline { let (actual, expected) = match &mode { Mode::Normal { validate_err } => match validate_err { Some(err_message) => { - assert_eq!(actual.unwrap_err().to_string(), *err_message); + use gix_error::ErrorExt; + + let err = actual.expect_err("conflicting refspecs fail validation"); + assert_eq!(err.to_string(), *err_message); + let num_issues = err.issues.len(); + let err = err.raise(); + assert!(err.is_validation(), "custom mapping errors classify as invalid input"); + let err = err.into_error(); + assert!( + err.is_validation(), + "conversion preserves the validation classification" + ); + assert_eq!( + err.downcast_any_ref::() + .expect("retain all conflicting mappings") + .issues + .len(), + num_issues + ); return; } None => { diff --git a/gix-refspec/tests/refspec/parse/fetch.rs b/gix-refspec/tests/refspec/parse/fetch.rs index a7d4ab23ce1..11b05065066 100644 --- a/gix-refspec/tests/refspec/parse/fetch.rs +++ b/gix-refspec/tests/refspec/parse/fetch.rs @@ -1,18 +1,11 @@ -use gix_refspec::{ - Instruction, - instruction::Fetch, - parse::{Error, Operation}, -}; +use gix_refspec::{Instruction, instruction::Fetch, parse::Operation}; -use crate::parse::{assert_parse, b, try_parse}; +use crate::parse::{assert_parse, assert_reference_error, assert_unsupported_pattern, assert_validation, b}; #[test] fn revspecs_are_disallowed() { for spec in ["main~1", "^@^{}", "HEAD:main~1"] { - assert!(matches!( - try_parse(spec, Operation::Fetch).unwrap_err(), - Error::ReferenceName(_) - )); + assert_reference_error(spec, Operation::Fetch); } } @@ -40,27 +33,26 @@ fn object_hash_destination_are_valid_as_they_might_be_a_strange_partial_branch_n #[test] fn negative_must_not_be_empty() { - assert!(matches!( - try_parse("^", Operation::Fetch).unwrap_err(), - Error::NegativeEmpty - )); + assert_validation("^", Operation::Fetch, "Negative specs must not be empty"); } #[test] fn negative_must_not_be_object_hash() { - assert!(matches!( - try_parse("^e69de29bb2d1d6434b8b29ae775ad8c2e48c5391", Operation::Fetch).unwrap_err(), - Error::NegativeObjectHash - )); + assert_validation( + "^e69de29bb2d1d6434b8b29ae775ad8c2e48c5391", + Operation::Fetch, + "Negative specs must not be object hashes", + ); } #[test] fn negative_with_destination() { for spec in ["^a:b", "^a:", "^:", "^:b"] { - assert!(matches!( - try_parse(spec, Operation::Fetch).unwrap_err(), - Error::NegativeWithDestination - )); + assert_validation( + spec, + Operation::Fetch, + "Negative refspecs cannot have destinations as they exclude sources", + ); } } @@ -184,10 +176,11 @@ fn empty_refspec_is_enough_for_fetching_head_into_fetchhead() { #[test] fn glob_patterns_need_a_destination() { for spec in ["refs/heads/*", "refs/heads/*:", ":refs/heads/*"] { - assert!(matches!( - try_parse(spec, Operation::Fetch).unwrap_err(), - Error::PatternUnbalanced - )); + assert_validation( + spec, + Operation::Fetch, + "Both sides of a two-sided specification need a pattern, like 'a/*:b/*'", + ); } } @@ -198,9 +191,6 @@ fn patterns_with_multiple_asterisks_are_rejected() { "refs/*/*:refs/remotes/*", "a/*/c/*:b/*", ] { - assert!(matches!( - try_parse(spec, Operation::Fetch).unwrap_err(), - Error::PatternUnsupported { .. } - )); + assert_unsupported_pattern(spec, Operation::Fetch); } } diff --git a/gix-refspec/tests/refspec/parse/invalid.rs b/gix-refspec/tests/refspec/parse/invalid.rs index 558e496e250..eaa6831bc0c 100644 --- a/gix-refspec/tests/refspec/parse/invalid.rs +++ b/gix-refspec/tests/refspec/parse/invalid.rs @@ -1,35 +1,38 @@ -use gix_refspec::parse::{Error, Operation}; +use gix_refspec::parse::Operation; -use crate::parse::try_parse; +use crate::parse::{assert_reference_error, assert_unsupported_pattern, assert_validation}; #[test] fn empty() { - assert!(matches!(try_parse("", Operation::Push).unwrap_err(), Error::Empty)); + assert_validation("", Operation::Push, "Empty refspecs are invalid"); } #[test] fn empty_component() { + let err = assert_reference_error("refs/heads/test:refs/remotes//test", Operation::Fetch); assert!(matches!( - try_parse("refs/heads/test:refs/remotes//test", Operation::Fetch).unwrap_err(), - Error::ReferenceName(gix_validate::reference::name::Error::RepeatedSlash) + err.downcast_any_ref::(), + Some(gix_validate::reference::name::Error::RepeatedSlash) )); } #[test] fn whitespace() { + let err = assert_reference_error("refs/heads/test:refs/remotes/ /test", Operation::Fetch); assert!(matches!( - try_parse("refs/heads/test:refs/remotes/ /test", Operation::Fetch).unwrap_err(), - Error::ReferenceName(gix_validate::reference::name::Error::InvalidByte { .. }) + err.downcast_any_ref::(), + Some(gix_validate::reference::name::Error::InvalidByte { .. }) )); } #[test] fn destination_cannot_be_a_lone_at_sign() { for op in [Operation::Fetch, Operation::Push] { + let err = assert_reference_error("HEAD:@", op); assert!( matches!( - try_parse("HEAD:@", op).expect_err("a lone '@' is not a valid destination"), - Error::ReferenceName(gix_validate::reference::name::Error::Reserved { name }) if name == "@" + err.downcast_any_ref::(), + Some(gix_validate::reference::name::Error::Reserved { name }) if name == "@" ), "{op:?} validates refspec destinations" ); @@ -40,33 +43,21 @@ fn destination_cannot_be_a_lone_at_sign() { fn patterns_may_contain_only_one_asterisk() { for op in [Operation::Fetch, Operation::Push] { for spec in ["a/*/c/*", "a/*/c/*:x/*/y/*", "a**:**b", "+:**/"] { - assert!(matches!( - try_parse(spec, op).unwrap_err(), - Error::PatternUnsupported { .. } - )); + assert_unsupported_pattern(spec, op); } } - assert!(matches!( - try_parse("^*/*", Operation::Fetch).unwrap_err(), - Error::PatternUnsupported { .. } - )); + assert_unsupported_pattern("^*/*", Operation::Fetch); // Negative refspec patterns follow Git's single-asterisk refspec-pattern rule. for op in [Operation::Fetch, Operation::Push] { - assert!(matches!( - try_parse("^refs/heads/qa/*/*", op).unwrap_err(), - Error::PatternUnsupported { .. } - )); + assert_unsupported_pattern("^refs/heads/qa/*/*", op); for spec in [ "^refs/heads/a*?", "^refs/heads/a[bc]*", "^refs/heads/*..bad", "^refs/heads/*/", ] { - assert!( - matches!(try_parse(spec, op).unwrap_err(), Error::ReferenceName(_)), - "{spec}" - ); + assert_reference_error(spec, op); } } } @@ -74,10 +65,7 @@ fn patterns_may_contain_only_one_asterisk() { #[test] fn one_sided_push_patterns_still_use_refspec_pattern_syntax() { for spec in ["refs/heads/[ab]*", "refs/heads/a?*", "refs/heads/*..bad"] { - assert!( - matches!(try_parse(spec, Operation::Push).unwrap_err(), Error::ReferenceName(_)), - "{spec} contains syntax Git refspec patterns do not support" - ); + assert_reference_error(spec, Operation::Push); } } @@ -86,26 +74,24 @@ fn both_sides_need_pattern_if_one_uses_it() { // For two-sided refspecs, both sides still need patterns if one uses it for op in [Operation::Fetch, Operation::Push] { for spec in ["a*:b/c", "a:b/*"] { - assert!( - matches!(try_parse(spec, op).unwrap_err(), Error::PatternUnbalanced), - "{}", - spec + assert_validation( + spec, + op, + "Both sides of a two-sided specification need a pattern, like 'a/*:b/*'", ); } } - assert!(matches!( - try_parse("refs/*/a", Operation::Fetch).unwrap_err(), - Error::PatternUnbalanced - )); + assert_validation( + "refs/*/a", + Operation::Fetch, + "Both sides of a two-sided specification need a pattern, like 'a/*:b/*'", + ); } #[test] fn push_to_empty() { - assert!(matches!( - try_parse("HEAD:", Operation::Push).unwrap_err(), - Error::PushToEmpty - )); + assert_validation("HEAD:", Operation::Push, "Cannot push into an empty destination"); } #[test] diff --git a/gix-refspec/tests/refspec/parse/mod.rs b/gix-refspec/tests/refspec/parse/mod.rs index 29a347e5f5d..bc0e792fb54 100644 --- a/gix-refspec/tests/refspec/parse/mod.rs +++ b/gix-refspec/tests/refspec/parse/mod.rs @@ -84,6 +84,31 @@ mod util { gix_refspec::parse(spec.into(), op) } + pub fn assert_validation(spec: &str, op: Operation, message: &str) -> gix_refspec::parse::Error { + let err = try_parse(spec, op).expect_err("refspec is invalid"); + assert_eq!(err.message, message); + err + } + + pub fn assert_reference_error(spec: &str, op: Operation) -> gix_refspec::parse::Error { + let err = try_parse(spec, op).expect_err("refspec contains an invalid reference name"); + let source = err + .downcast_any_ref::() + .expect("the original reference-name validation error is retained"); + assert_eq!(err.message, source.to_string()); + err + } + + pub fn assert_unsupported_pattern(spec: &str, op: Operation) { + let err = assert_validation(spec, op, "refspec patterns may only contain a single '*' character"); + let input = err.input.as_ref().expect("the unsupported pattern is retained"); + assert!( + spec.as_bytes() + .windows(input.len()) + .any(|candidate| candidate == input.as_slice()) + ); + } + pub fn assert_parse<'a>(spec: &'a str, expected: Instruction<'_>) -> RefSpecRef<'a> { let spec = try_parse(spec, expected.operation()).expect("no error"); assert_eq!(spec.instruction(), expected); diff --git a/gix-refspec/tests/refspec/parse/push.rs b/gix-refspec/tests/refspec/parse/push.rs index b0f1ea44263..22698bdae7f 100644 --- a/gix-refspec/tests/refspec/parse/push.rs +++ b/gix-refspec/tests/refspec/parse/push.rs @@ -1,33 +1,28 @@ -use crate::parse::{assert_parse, b, try_parse}; -use gix_refspec::{ - Instruction, - instruction::Push, - parse::{Error, Operation}, -}; +use crate::parse::{assert_parse, assert_reference_error, assert_validation, b}; +use gix_refspec::{Instruction, instruction::Push, parse::Operation}; #[test] fn negative_must_not_be_empty() { - assert!(matches!( - try_parse("^", Operation::Push).unwrap_err(), - Error::NegativeEmpty - )); + assert_validation("^", Operation::Push, "Negative specs must not be empty"); } #[test] fn negative_must_not_be_object_hash() { - assert!(matches!( - try_parse("^e69de29bb2d1d6434b8b29ae775ad8c2e48c5391", Operation::Push).unwrap_err(), - Error::NegativeObjectHash - )); + assert_validation( + "^e69de29bb2d1d6434b8b29ae775ad8c2e48c5391", + Operation::Push, + "Negative specs must not be object hashes", + ); } #[test] fn negative_with_destination() { for spec in ["^a:b", "^a:", "^:", "^:b"] { - assert!(matches!( - try_parse(spec, Operation::Push).unwrap_err(), - Error::NegativeWithDestination - )); + assert_validation( + spec, + Operation::Push, + "Negative refspecs cannot have destinations as they exclude sources", + ); } } @@ -81,18 +76,12 @@ fn revspecs_with_ref_name_destination() { #[test] fn destinations_must_be_ref_names() { - assert!(matches!( - try_parse("a~1:b~1", Operation::Push).unwrap_err(), - Error::ReferenceName(_) - )); + assert_reference_error("a~1:b~1", Operation::Push); } #[test] fn single_refs_must_be_refnames() { - assert!(matches!( - try_parse("a~1", Operation::Push).unwrap_err(), - Error::ReferenceName(_) - )); + assert_reference_error("a~1", Operation::Push); } #[test] @@ -174,9 +163,10 @@ fn delete() { assert_parse("+:a", Instruction::Push(Push::Delete { ref_or_pattern: b("a") })); for spec in [":refs/heads/*", "+:refs/heads/*"] { - assert!( - matches!(try_parse(spec, Operation::Push).unwrap_err(), Error::PatternUnbalanced), - "deletion destinations cannot be patterns" + assert_validation( + spec, + Operation::Push, + "Both sides of a two-sided specification need a pattern, like 'a/*:b/*'", ); } } diff --git a/gix-submodule/src/access.rs b/gix-submodule/src/access.rs index 33c591c0869..ca540d6b196 100644 --- a/gix-submodule/src/access.rs +++ b/gix-submodule/src/access.rs @@ -205,7 +205,7 @@ impl File { .map_err(|err| config::branch::Error { submodule: name.to_owned(), actual: branch, - source: err, + source: err.into_error(), }) } diff --git a/gix-submodule/src/config.rs b/gix-submodule/src/config.rs index 593d17d49fd..72b66be9604 100644 --- a/gix-submodule/src/config.rs +++ b/gix-submodule/src/config.rs @@ -164,7 +164,7 @@ pub mod branch { pub struct Error { pub submodule: BString, pub actual: BString, - pub source: gix_refspec::parse::Error, + pub source: gix_error::Error, } } diff --git a/gix/src/clone/access.rs b/gix/src/clone/access.rs index 0ba03f9e587..2c13d10a14b 100644 --- a/gix/src/clone/access.rs +++ b/gix/src/clone/access.rs @@ -81,7 +81,8 @@ impl PrepareFetch { self.revision = revision .map(|revision| { let revision = revision.into(); - let spec = gix_refspec::parse(revision.as_ref(), gix_refspec::parse::Operation::Fetch)?; + let spec = gix_refspec::parse(revision.as_ref(), gix_refspec::parse::Operation::Fetch) + .map_err(gix_error::Exn::into_error)?; let source = spec.source().expect("one-sided non-empty fetch refspec"); let is_full_ref = source.starts_with(b"refs/") && source.find_byteset(b"*?[]\\").is_none(); let is_valid = revision.as_bstr() == source diff --git a/gix/src/clone/mod.rs b/gix/src/clone/mod.rs index c0262e81158..da991dc75dd 100644 --- a/gix/src/clone/mod.rs +++ b/gix/src/clone/mod.rs @@ -56,7 +56,7 @@ pub mod with_revision { #[expect(missing_docs)] pub enum Error { #[error(transparent)] - Parse(#[from] gix_refspec::parse::Error), + Parse(#[from] gix_error::Error), #[error("A clone revision must be HEAD, a full reference name, or a full object ID, got {revision:?}")] Invalid { revision: crate::bstr::BString }, } diff --git a/gix/src/config/mod.rs b/gix/src/config/mod.rs index 066c114eecc..1e46b67c796 100644 --- a/gix/src/config/mod.rs +++ b/gix/src/config/mod.rs @@ -527,7 +527,7 @@ pub mod string { /// pub mod refspec { /// The error produced when failing to parse a refspec from the configuration. - pub type Error = super::key::Error; + pub type Error = super::key::Error; } /// diff --git a/gix/src/config/tree/keys.rs b/gix/src/config/tree/keys.rs index 91099077aba..1ae3beb7a80 100644 --- a/gix/src/config/tree/keys.rs +++ b/gix/src/config/tree/keys.rs @@ -109,7 +109,7 @@ impl Any { let value = value.as_bstr(); gix_refspec::parse(value.as_bstr(), op) .map(|spec| spec.to_owned()) - .map_err(|err| config::refspec::Error::from_value(self, value.into()).with_source(err)) + .map_err(|err| config::refspec::Error::from_value(self, value.into()).with_source(err.into_error())) } /// Try to interpret `value` as UTF-8 encoded string. @@ -697,7 +697,7 @@ pub mod validate { pub struct PushRefSpec; impl Validate for PushRefSpec { fn validate(&self, value: &BStr) -> Result<(), Box> { - gix_refspec::parse(value, gix_refspec::parse::Operation::Push)?; + gix_refspec::parse(value, gix_refspec::parse::Operation::Push).map_err(gix_error::Exn::into_error)?; Ok(()) } } @@ -707,7 +707,7 @@ pub mod validate { pub struct FetchRefSpec; impl Validate for FetchRefSpec { fn validate(&self, value: &BStr) -> Result<(), Box> { - gix_refspec::parse(value, gix_refspec::parse::Operation::Fetch)?; + gix_refspec::parse(value, gix_refspec::parse::Operation::Fetch).map_err(gix_error::Exn::into_error)?; Ok(()) } } diff --git a/gix/src/remote/access.rs b/gix/src/remote/access.rs index 1d1949f6d36..edea10377da 100644 --- a/gix/src/remote/access.rs +++ b/gix/src/remote/access.rs @@ -136,7 +136,7 @@ impl Remote<'_> { &mut self, specs: impl IntoIterator, direction: remote::Direction, - ) -> Result<(), gix_refspec::parse::Error> + ) -> Result<(), gix_error::Error> where Spec: AsRef, { diff --git a/gix/src/remote/build.rs b/gix/src/remote/build.rs index 829636ce31b..b8acbbc3108 100644 --- a/gix/src/remote/build.rs +++ b/gix/src/remote/build.rs @@ -124,7 +124,7 @@ impl Remote<'_> { mut self, specs: impl IntoIterator, direction: remote::Direction, - ) -> Result + ) -> Result where Spec: AsRef, { diff --git a/gix/src/remote/name.rs b/gix/src/remote/name.rs index 8a5fc2d0c81..bccdd3665ef 100644 --- a/gix/src/remote/name.rs +++ b/gix/src/remote/name.rs @@ -8,7 +8,7 @@ use crate::bstr::{BStr, BString, ByteSlice, ByteVec}; #[error("remote names must be valid within refspecs for fetching: {name:?}")] #[expect(missing_docs)] pub struct Error { - pub source: gix_refspec::parse::Error, + pub source: gix_error::Error, pub name: BString, } @@ -22,7 +22,10 @@ pub fn validated(name: impl Into) -> Result { gix_refspec::parse::Operation::Fetch, ) { Ok(_) => Ok(name), - Err(err) => Err(Error { source: err, name }), + Err(err) => Err(Error { + source: err.into_error(), + name, + }), } } From 2abea43f7e7d1cb1471854253f6bcff710f96474 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 20 Aug 2026 17:08:42 +0200 Subject: [PATCH 05/51] change(gix-shallow)!: migrate errors to gix-error rubberstamp --- Cargo.lock | 2 +- gix-protocol/src/fetch/error.rs | 4 +- gix-protocol/src/fetch/function.rs | 5 ++- gix-shallow/Cargo.toml | 3 +- gix-shallow/src/lib.rs | 61 ++++++++++++++++-------------- gix/src/repository/shallow.rs | 12 +++--- gix/src/revision/walk.rs | 2 +- 7 files changed, 49 insertions(+), 40 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 25ef5f7cfb4..f8d4a019be6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2467,12 +2467,12 @@ name = "gix-shallow" version = "0.13.0" dependencies = [ "bstr", + "gix-error", "gix-hash", "gix-lock", "nonempty", "serde", "tempfile", - "thiserror 2.0.18", ] [[package]] diff --git a/gix-protocol/src/fetch/error.rs b/gix-protocol/src/fetch/error.rs index 5c1416aed4e..b8116047e65 100644 --- a/gix-protocol/src/fetch/error.rs +++ b/gix-protocol/src/fetch/error.rs @@ -14,9 +14,9 @@ pub enum Error { description: &'static str, }, #[error("Could not write 'shallow' file to incorporate remote updates after fetching")] - WriteShallowFile(#[from] gix_shallow::write::Error), + WriteShallowFile(#[source] gix_error::Error), #[error("Could not read 'shallow' file to send current shallow boundary")] - ReadShallowFile(#[from] gix_shallow::read::Error), + ReadShallowFile(#[source] gix_error::Error), #[error("'shallow' file could not be locked in preparation for writing changes")] LockShallowFile(#[source] std::io::Error), #[error("Receiving objects from shallow remotes is prohibited due to the value of `clone.rejectShallow`")] diff --git a/gix-protocol/src/fetch/function.rs b/gix-protocol/src/fetch/function.rs index 70ebf7a7d75..b2fb8b33d50 100644 --- a/gix-protocol/src/fetch/function.rs +++ b/gix-protocol/src/fetch/function.rs @@ -167,7 +167,8 @@ where if let Some(shallow_lock) = shallow_lock && !previous_response.shallow_updates().is_empty() { - gix_shallow::write(shallow_lock, shallow_commits, previous_response.shallow_updates())?; + gix_shallow::write(shallow_lock, shallow_commits, previous_response.shallow_updates()) + .map_err(|err| Error::WriteShallowFile(err.into_error()))?; } Ok(Some(Outcome { last_response: previous_response, @@ -235,7 +236,7 @@ fn add_shallow_args( let expect_change = *shallow != Shallow::NoChange; let shallow_lock = expect_change.then(|| acquire_shallow_lock(shallow_file)).transpose()?; - let shallow_commits = gix_shallow::read(shallow_file)?; + let shallow_commits = gix_shallow::read(shallow_file).map_err(|err| Error::ReadShallowFile(err.into_error()))?; if (shallow_commits.is_some() || expect_change) && !args.can_use_shallow() { // NOTE: if this is an issue, we can always unshallow the repo ourselves. return Err(Error::MissingServerFeature { diff --git a/gix-shallow/Cargo.toml b/gix-shallow/Cargo.toml index 50dd8d51be7..c717a572b7f 100644 --- a/gix-shallow/Cargo.toml +++ b/gix-shallow/Cargo.toml @@ -26,8 +26,8 @@ serde = ["dep:serde", "gix-hash/serde", "nonempty/serialize"] [dependencies] gix-hash = { version = "^0.26.0", path = "../gix-hash" } gix-lock = { version = "^24.0.0", path = "../gix-lock" } +gix-error = { version = "^0.3.0", path = "../gix-error" } -thiserror = "2.0.18" bstr = { version = "1.12.0", default-features = false } nonempty = "0.12.0" serde = { version = "1.0.114", optional = true, default-features = false, features = ["std", "derive"] } @@ -37,3 +37,4 @@ features = ["sha1"] [dev-dependencies] tempfile = "3.26.0" +gix-hash = { path = "../gix-hash", features = ["sha1", "sha256"] } diff --git a/gix-shallow/src/lib.rs b/gix-shallow/src/lib.rs index 2c5d2c3b3c2..689c5a026f3 100644 --- a/gix-shallow/src/lib.rs +++ b/gix-shallow/src/lib.rs @@ -10,16 +10,23 @@ //! # let shallow_file = dir.path().join("shallow"); //! # std::fs::write(&shallow_file, format!("{first}\n"))?; //! -//! let shallow = gix_shallow::read(&shallow_file)?.expect("a shallow boundary"); +//! let shallow = gix_shallow::read(&shallow_file) +//! .map_err(gix_error::Exn::into_error)? +//! .expect("a shallow boundary"); //! let lock = gix_lock::File::acquire_to_update_resource( //! &shallow_file, //! gix_lock::acquire::Fail::Immediately, //! None, //! ) //! .map_err(|err| err.into_error())?; -//! gix_shallow::write(lock, Some(shallow), &[gix_shallow::Update::Shallow(second)])?; +//! gix_shallow::write(lock, Some(shallow), &[gix_shallow::Update::Shallow(second)]) +//! .map_err(gix_error::Exn::into_error)?; //! -//! let ids = gix_shallow::read(&shallow_file)?.unwrap().into_iter().collect::>(); +//! let ids = gix_shallow::read(&shallow_file) +//! .map_err(gix_error::Exn::into_error)? +//! .expect("a shallow boundary") +//! .into_iter() +//! .collect::>(); //! assert_eq!(ids, vec![first, second]); //! # Ok(()) } //! ``` @@ -43,16 +50,24 @@ pub enum Update { /// isn't a shallow clone. pub fn read(shallow_file: &std::path::Path) -> Result>, read::Error> { use bstr::ByteSlice; + use gix_error::{CorruptionError, ErrorExt, ResultExt, message}; let buf = match std::fs::read(shallow_file) { Ok(buf) => buf, Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None), - Err(err) => return Err(err.into()), + Err(err) => { + return Err(err + .and_raise(message("Could not open shallow file for reading")) + .erased()); + } }; let mut commits = buf .lines() .map(gix_hash::ObjectId::from_hex) - .collect::, _>>()?; + .collect::, _>>() + .or_raise_erased(|| { + CorruptionError::new("Could not decode a line in shallow file as hex-encoded object hash") + })?; commits.sort(); Ok(nonempty::NonEmpty::from_vec(commits)) @@ -63,6 +78,8 @@ pub mod write { pub(crate) mod function { use std::io::Write; + use gix_error::{ErrorExt, ResultExt, message}; + use super::Error; use crate::Update; @@ -92,7 +109,7 @@ pub mod write { if let Err(err) = std::fs::remove_file(file.resource_path()) && err.kind() != std::io::ErrorKind::NotFound { - return Err(err.into()); + return Err(err.and_raise(message("Could not remove an empty shallow file"))); } drop(file); return Ok(()); @@ -100,39 +117,27 @@ pub mod write { shallow_commits.sort(); let mut buf = Vec::::new(); for commit in shallow_commits { - commit.write_hex_to(&mut buf).map_err(Error::Io)?; + commit + .write_hex_to(&mut buf) + .or_raise(|| message("Failed to write object id to shallow file"))?; buf.push(b'\n'); } - file.write_all(&buf).map_err(Error::Io)?; - file.flush().map_err(Error::Io)?; - file.commit()?; + file.write_all(&buf) + .or_raise(|| message("Failed to write object id to shallow file"))?; + file.flush() + .or_raise(|| message("Failed to write object id to shallow file"))?; + file.commit().or_raise(|| message("Could not commit shallow file"))?; Ok(()) } } /// The error returned by [`write()`](crate::write()). - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - Commit(#[from] gix_lock::commit::Error), - #[error("Could not remove an empty shallow file")] - RemoveEmpty(#[from] std::io::Error), - #[error("Failed to write object id to shallow file")] - Io(std::io::Error), - } + pub type Error = gix_error::Exn; } pub use write::function::write; /// pub mod read { /// The error returned by [`read`](crate::read()). - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error("Could not open shallow file for reading")] - Io(#[from] std::io::Error), - #[error("Could not decode a line in shallow file as hex-encoded object hash")] - DecodeHash(#[from] gix_hash::decode::Error), - } + pub type Error = gix_error::Exn; } diff --git a/gix/src/repository/shallow.rs b/gix/src/repository/shallow.rs index 9d548fc4a4a..32ed0890902 100644 --- a/gix/src/repository/shallow.rs +++ b/gix/src/repository/shallow.rs @@ -19,11 +19,13 @@ impl Repository { /// isn't a shallow clone. /// /// The shared list is shared across all clones of this repository. - pub fn shallow_commits(&self) -> Result, crate::shallow::read::Error> { - self.shallow_commits.recent_snapshot( - || self.shallow_file().metadata().ok().and_then(|m| m.modified().ok()), - || gix_shallow::read(&self.shallow_file()), - ) + pub fn shallow_commits(&self) -> Result, crate::Error> { + self.shallow_commits + .recent_snapshot( + || self.shallow_file().metadata().ok().and_then(|m| m.modified().ok()), + || gix_shallow::read(&self.shallow_file()), + ) + .map_err(gix_error::Exn::into_error) } /// Return the path to the `shallow` file which contains hashes, one per line, that describe commits that don't have their diff --git a/gix/src/revision/walk.rs b/gix/src/revision/walk.rs index 3b0b01f6a0b..54dc8f5a5f2 100644 --- a/gix/src/revision/walk.rs +++ b/gix/src/revision/walk.rs @@ -11,7 +11,7 @@ pub enum Error { #[error(transparent)] SimpleTraversal(#[from] gix_traverse::commit::simple::Error), #[error(transparent)] - ShallowCommits(#[from] crate::shallow::read::Error), + ShallowCommits(#[from] crate::Error), #[error(transparent)] ConfigBoolean(#[from] crate::config::boolean::Error), } From 9bfec72ac1a30ee76912c9678ed70463427ffcda Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 20 Aug 2026 17:09:10 +0200 Subject: [PATCH 06/51] change(gix-pathspec)!: migrate errors to gix-error rubberstamp --- Cargo.lock | 1 - gix-pathspec/Cargo.toml | 1 - gix-pathspec/src/defaults.rs | 21 ++---- gix-pathspec/src/lib.rs | 11 +--- gix-pathspec/src/parse.rs | 86 +++++++++++++------------ gix-pathspec/src/pattern.rs | 16 +++-- gix-pathspec/tests/defaults.rs | 26 ++++++-- gix-pathspec/tests/normalize/mod.rs | 29 +++++---- gix-pathspec/tests/parse/invalid.rs | 81 ++++++++++++----------- gix-submodule/src/access.rs | 10 ++- gix-submodule/src/is_active_platform.rs | 4 +- gix/src/pathspec.rs | 10 +-- gix/src/repository/mod.rs | 2 +- gix/src/repository/pathspec.rs | 6 +- gix/src/submodule/errors.rs | 2 +- gix/src/submodule/mod.rs | 10 ++- 16 files changed, 162 insertions(+), 154 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f8d4a019be6..b4272dbe842 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2304,7 +2304,6 @@ dependencies = [ "gix-path", "gix-testtools", "serial_test", - "thiserror 2.0.18", ] [[package]] diff --git a/gix-pathspec/Cargo.toml b/gix-pathspec/Cargo.toml index 5ea44504fc3..df9cbeedaa4 100644 --- a/gix-pathspec/Cargo.toml +++ b/gix-pathspec/Cargo.toml @@ -27,7 +27,6 @@ gix-error = { version = "^0.3.0", path = "../gix-error" } bstr = { version = "1.12.0", default-features = false, features = ["std"] } bitflags = "2" -thiserror = "2.0.18" [dev-dependencies] gix-testtools = { path = "../tests/tools", features = ["sha1"] } diff --git a/gix-pathspec/src/defaults.rs b/gix-pathspec/src/defaults.rs index f479c44058f..8ce7b5c8939 100644 --- a/gix-pathspec/src/defaults.rs +++ b/gix-pathspec/src/defaults.rs @@ -1,24 +1,13 @@ use std::ffi::OsString; +use gix_error::{ErrorExt, ValidationError}; + use crate::{Defaults, MagicSignature, SearchMode}; /// pub mod from_environment { /// The error returned by [Defaults::from_environment()](super::Defaults::from_environment()). - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - ParseValue(gix_error::Error), - #[error("Glob and no-glob settings are mutually exclusive")] - MixedGlobAndNoGlob, - } - - impl From for Error { - fn from(err: gix_config_value::Error) -> Self { - Error::ParseValue(err.into_error()) - } - } + pub type Error = gix_error::Exn; } impl Defaults { @@ -35,7 +24,7 @@ impl Defaults { /// Instead of failing if `GIT_LITERAL_PATHSPECS` is used with glob globals, we ignore these. Also our implementation allows global /// `icase` settings in combination with this setting. pub fn from_environment(var: &mut dyn FnMut(&str) -> Option) -> Result { - let mut env_bool = |name: &str| -> Result, gix_config_value::Error> { + let mut env_bool = |name: &str| -> Result, from_environment::Error> { var(name) .map(|val| gix_config_value::Boolean::try_from(val).map(|b| b.0)) .transpose() @@ -59,7 +48,7 @@ impl Defaults { search_mode = env_bool("GIT_NOGLOB_PATHSPECS")? .map(|no_glob| { if glob.unwrap_or_default() && no_glob { - Err(from_environment::Error::MixedGlobAndNoGlob) + Err(ValidationError::new("Glob and no-glob settings are mutually exclusive").raise()) } else { Ok(SearchMode::Literal) } diff --git a/gix-pathspec/src/lib.rs b/gix-pathspec/src/lib.rs index d18e0a38810..5cac9d00442 100644 --- a/gix-pathspec/src/lib.rs +++ b/gix-pathspec/src/lib.rs @@ -48,17 +48,8 @@ pub use gix_attributes as attributes; /// pub mod normalize { - use std::path::PathBuf; - /// The error returned by [Pattern::normalize()](super::Pattern::normalize()). - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error("The path '{}' is not inside of the worktree '{}'", path.display(), worktree_path.display())] - AbsolutePathOutsideOfWorktree { path: PathBuf, worktree_path: PathBuf }, - #[error("The path '{}' leaves the repository", path.display())] - OutsideOfWorktree { path: PathBuf }, - } + pub type Error = gix_error::ValidationError; } mod pattern; diff --git a/gix-pathspec/src/parse.rs b/gix-pathspec/src/parse.rs index 4c590142972..ab81ddc88f7 100644 --- a/gix-pathspec/src/parse.rs +++ b/gix-pathspec/src/parse.rs @@ -1,34 +1,12 @@ use std::borrow::Cow; use bstr::{BStr, BString, ByteSlice}; +use gix_error::ValidationError; use crate::{Defaults, MagicSignature, Pattern, SearchMode}; /// The error returned by [parse()][crate::parse()]. -#[derive(thiserror::Error, Debug)] -#[expect(missing_docs)] -pub enum Error { - #[error("An empty string is not a valid pathspec")] - EmptyString, - #[error("Found {keyword:?} in signature, which is not a valid keyword")] - InvalidKeyword { keyword: BString }, - #[error("Unimplemented short keyword: {short_keyword:?}")] - Unimplemented { short_keyword: char }, - #[error("Missing ')' at the end of pathspec signature")] - MissingClosingParenthesis, - #[error(transparent)] - InvalidAttribute(#[from] gix_attributes::name::Error), - #[error("Invalid character in attribute value: {character:?}")] - InvalidAttributeValue { character: char }, - #[error(r"Escape character '\' is not allowed as the last character in an attribute value")] - TrailingEscapeCharacter, - #[error("Attribute specification cannot be empty")] - EmptyAttribute, - #[error("Only one attribute specification is allowed in the same pathspec")] - MultipleAttributeSpecifications, - #[error("'literal' and 'glob' keywords cannot be used together in the same pathspec")] - IncompatibleSearchModes, -} +pub type Error = gix_error::ValidationError; impl Pattern { /// Try to parse a path-spec pattern from the given `input` bytes. @@ -41,7 +19,10 @@ impl Pattern { }: Defaults, ) -> Result { if input.is_empty() { - return Err(Error::EmptyString); + return Err(ValidationError::new_with_input( + "An empty string is not a valid pathspec", + input, + )); } if literal { return Ok(Self::from_literal(input, signature)); @@ -104,9 +85,7 @@ fn parse_short_keywords(input: &[u8], cursor: &mut usize) -> Result MagicSignature::EXCLUDE, b':' => break, _ if unimplemented_chars.contains(&b) => { - return Err(Error::Unimplemented { - short_keyword: b.into(), - }); + return Err(ValidationError::new_with_input("Unimplemented short keyword", vec![b])); } _ => { *cursor -= 1; @@ -119,7 +98,9 @@ fn parse_short_keywords(input: &[u8], cursor: &mut usize) -> Result Result<(), Error> { - let end = input.find(")").ok_or(Error::MissingClosingParenthesis)?; + let end = input + .find(")") + .ok_or_else(|| ValidationError::new_with_input("Missing ')' at the end of pathspec signature", input))?; let input = &input[*cursor..end]; *cursor = end + 1; @@ -141,24 +122,38 @@ fn parse_long_keywords(input: &[u8], p: &mut Pattern, cursor: &mut usize) -> Res b"icase" => p.signature |= MagicSignature::ICASE, b"exclude" => p.signature |= MagicSignature::EXCLUDE, b"literal" => match p.search_mode { - SearchMode::PathAwareGlob => return Err(Error::IncompatibleSearchModes), + SearchMode::PathAwareGlob => { + return Err(ValidationError::new_with_input( + "'literal' and 'glob' keywords cannot be used together in the same pathspec", + keyword, + )); + } _ => p.search_mode = SearchMode::Literal, }, b"glob" => match p.search_mode { - SearchMode::Literal => return Err(Error::IncompatibleSearchModes), + SearchMode::Literal => { + return Err(ValidationError::new_with_input( + "'literal' and 'glob' keywords cannot be used together in the same pathspec", + keyword, + )); + } _ => p.search_mode = SearchMode::PathAwareGlob, }, _ if keyword.starts_with(attr_prefix) => { if p.attributes.is_empty() { p.attributes = parse_attributes(&keyword[attr_prefix.len()..])?; } else { - return Err(Error::MultipleAttributeSpecifications); + return Err(ValidationError::new_with_input( + "Only one attribute specification is allowed in the same pathspec", + keyword, + )); } } _ => { - return Err(Error::InvalidKeyword { - keyword: BString::from(keyword), - }); + return Err(ValidationError::new_with_input( + "Found invalid keyword in pathspec signature", + keyword, + )); } } Ok(()) @@ -192,7 +187,7 @@ fn split_on_non_escaped_char( fn parse_attributes(input: &[u8]) -> Result, Error> { if input.is_empty() { - return Err(Error::EmptyAttribute); + return Err(ValidationError::new("Attribute specification cannot be empty")); } input @@ -231,7 +226,12 @@ fn unescape_and_check_attr_value(value: &BStr) -> Result { let mut bytes = value.iter(); while let Some(mut b) = bytes.next().copied() { if b == b'\\' { - b = *bytes.next().ok_or(Error::TrailingEscapeCharacter)?; + b = *bytes.next().ok_or_else(|| { + ValidationError::new_with_input( + r"Escape character '\' is not allowed as the last character in an attribute value", + value, + ) + })?; } out.push(validated_attr_value_byte(b)?); @@ -241,7 +241,10 @@ fn unescape_and_check_attr_value(value: &BStr) -> Result { fn check_attribute_value(input: &BStr) -> Result<(), Error> { match input.iter().copied().find(|b| !is_valid_attr_value(*b)) { - Some(b) => Err(Error::InvalidAttributeValue { character: b as char }), + Some(b) => Err(ValidationError::new_with_input( + "Invalid character in attribute value", + vec![b], + )), None => Ok(()), } } @@ -254,8 +257,9 @@ fn validated_attr_value_byte(byte: u8) -> Result { if is_valid_attr_value(byte) { Ok(byte) } else { - Err(Error::InvalidAttributeValue { - character: byte as char, - }) + Err(ValidationError::new_with_input( + "Invalid character in attribute value", + vec![byte], + )) } } diff --git a/gix-pathspec/src/pattern.rs b/gix-pathspec/src/pattern.rs index 2dc1d0ed38f..fa1423f5026 100644 --- a/gix-pathspec/src/pattern.rs +++ b/gix-pathspec/src/pattern.rs @@ -1,6 +1,7 @@ use std::path::{Component, Path, PathBuf}; use bstr::{BStr, BString, ByteSlice, ByteVec}; +use gix_error::ValidationError; use crate::{MagicSignature, Pattern, SearchMode, normalize}; @@ -65,10 +66,10 @@ impl Pattern { let rela_path = match path.strip_prefix(root) { Ok(path) => path, Err(_) => { - return Err(normalize::Error::AbsolutePathOutsideOfWorktree { - path: path.into_owned(), - worktree_path: root.into(), - }); + return Err(ValidationError::new_with_input( + format!("The path is not inside of the worktree '{}'", root.display()), + gix_path::into_bstr(path.into_owned()).into_owned(), + )); } }; path = rela_path.to_owned().into(); @@ -103,9 +104,10 @@ impl Pattern { path } None => { - return Err(normalize::Error::OutsideOfWorktree { - path: path.into_owned(), - }); + return Err(ValidationError::new_with_input( + "The path leaves the repository", + gix_path::into_bstr(path.into_owned()).into_owned(), + )); } }; diff --git a/gix-pathspec/tests/defaults.rs b/gix-pathspec/tests/defaults.rs index bae81cc66fc..8ceaddecebe 100644 --- a/gix-pathspec/tests/defaults.rs +++ b/gix-pathspec/tests/defaults.rs @@ -11,7 +11,7 @@ fn literal_only_combines_with_icase() -> gix_testtools::Result { .set("GIT_ICASE_PATHSPECS", "1") .set("GIT_NOGLOB_PATHSPECS", "yes"); assert_eq!( - Defaults::from_environment(&mut |n| std::env::var_os(n))?, + Defaults::from_environment(&mut |name| std::env::var_os(name))?, Defaults { signature: MagicSignature::ICASE, search_mode: SearchMode::Literal, @@ -25,7 +25,7 @@ fn literal_only_combines_with_icase() -> gix_testtools::Result { .set("GIT_ICASE_PATHSPECS", "false") .set("GIT_GLOB_PATHSPECS", "yes"); assert_eq!( - Defaults::from_environment(&mut |n| std::env::var_os(n))?, + Defaults::from_environment(&mut |name| std::env::var_os(name))?, Defaults { signature: MagicSignature::default(), search_mode: SearchMode::Literal, @@ -40,7 +40,7 @@ fn literal_only_combines_with_icase() -> gix_testtools::Result { fn nothing_is_set_then_it_is_like_the_default_impl() -> gix_testtools::Result { let _environment = gix_testtools::isolate_git_environment()?; assert_eq!( - Defaults::from_environment(&mut |n| std::env::var_os(n))?, + Defaults::from_environment(&mut |name| std::env::var_os(name))?, Defaults::default() ); Ok(()) @@ -62,6 +62,22 @@ fn glob_and_noglob_cause_error() -> gix_testtools::Result { Ok(()) } +#[test] +#[serial] +fn invalid_values_retain_the_config_validation_error() { + let _env = gix_testtools::Env::new().set("GIT_GLOB_PATHSPECS", "invalid"); + let err = Defaults::from_environment(&mut |name| std::env::var_os(name)) + .expect_err("the environment value is not a boolean"); + assert_eq!( + err.message, + "Booleans need to be 'no', 'off', 'false', '' or 'yes', 'on', 'true' or any number" + ); + assert_eq!( + err.input.as_ref().map(|input| input.as_slice()), + Some(b"invalid".as_slice()) + ); +} + #[test] #[serial] fn noglob_works() -> gix_testtools::Result { @@ -69,7 +85,7 @@ fn noglob_works() -> gix_testtools::Result { .set("GIT_GLOB_PATHSPECS", "0") .set("GIT_NOGLOB_PATHSPECS", "true"); assert_eq!( - Defaults::from_environment(&mut |n| std::env::var_os(n))?, + Defaults::from_environment(&mut |name| std::env::var_os(name))?, Defaults { signature: MagicSignature::default(), search_mode: SearchMode::Literal, @@ -85,7 +101,7 @@ fn noglob_works() -> gix_testtools::Result { fn glob_works() -> gix_testtools::Result { let _environment = gix_testtools::isolate_git_environment()?.set("GIT_GLOB_PATHSPECS", "yes"); assert_eq!( - Defaults::from_environment(&mut |n| std::env::var_os(n))?, + Defaults::from_environment(&mut |name| std::env::var_os(name))?, Defaults { signature: MagicSignature::default(), search_mode: SearchMode::PathAwareGlob, diff --git a/gix-pathspec/tests/normalize/mod.rs b/gix-pathspec/tests/normalize/mod.rs index c8f88de992c..420402c77df 100644 --- a/gix-pathspec/tests/normalize/mod.rs +++ b/gix-pathspec/tests/normalize/mod.rs @@ -1,5 +1,7 @@ use std::path::Path; +use bstr::ByteSlice; + #[test] fn consuming_the_entire_prefix_does_not_lead_to_a_single_dot() -> crate::Result { let spec = normalized_spec("..", "a", "")?; @@ -96,31 +98,34 @@ fn absolute_top_patterns_ignore_the_prefix_but_are_made_relative() -> crate::Res #[test] fn relative_path_breaks_out_of_working_tree() { let err = normalized_spec("../a", "", "").unwrap_err(); - assert_eq!(err.to_string(), "The path '../a' leaves the repository"); + assert_eq!(err.message, "The path leaves the repository"); + assert_eq!(err.input.as_ref().expect("offending path").to_str_lossy(), "../a"); let err = normalized_spec("../../b", "a", "").unwrap_err(); + assert_eq!(err.message, "The path leaves the repository"); assert_eq!( - err.to_string(), - format!( - "The path '{}' leaves the repository", - if cfg!(windows) { r"a\../../b" } else { "a/../../b" } - ) + err.input.as_ref().expect("offending path").to_str_lossy(), + if cfg!(windows) { r"a\../../b" } else { "a/../../b" } ); } #[test] fn absolute_path_breaks_out_of_working_tree() { let err = normalized_spec("/path/to/repo/..///./a", "", "/path/to/repo").unwrap_err(); - assert_eq!(err.to_string(), "The path '..///./a' leaves the repository"); + assert_eq!(err.message, "The path leaves the repository"); + assert_eq!(err.input.as_ref().expect("offending path").to_str_lossy(), "..///./a"); let err = normalized_spec("/path/to/repo/../../../dev", "", "/path/to/repo").unwrap_err(); - assert_eq!(err.to_string(), "The path '../../../dev' leaves the repository"); + assert_eq!(err.message, "The path leaves the repository"); + assert_eq!( + err.input.as_ref().expect("offending path").to_str_lossy(), + "../../../dev" + ); } #[test] fn absolute_path_escapes_worktree() { - assert_eq!( - normalized_spec("/dev", "", "/path/to/repo").unwrap_err().to_string(), - "The path '/dev' is not inside of the worktree '/path/to/repo'" - ); + let err = normalized_spec("/dev", "", "/path/to/repo").expect_err("the path is outside of the worktree"); + assert_eq!(err.message, "The path is not inside of the worktree '/path/to/repo'"); + assert_eq!(err.input.as_ref().expect("offending path").to_str_lossy(), "/dev"); } fn normalized_spec( diff --git a/gix-pathspec/tests/parse/invalid.rs b/gix-pathspec/tests/parse/invalid.rs index b6ae3a9747d..ff30cc124ae 100644 --- a/gix-pathspec/tests/parse/invalid.rs +++ b/gix-pathspec/tests/parse/invalid.rs @@ -1,16 +1,21 @@ -use gix_pathspec::parse::Error; +use gix_error::ValidationError; use crate::parse::check_against_baseline; +fn assert_validation(input: &str, message: &str) -> ValidationError { + let err = gix_pathspec::parse(input.as_bytes(), Default::default()).expect_err("pathspec is invalid"); + assert_eq!(err.message, message); + err +} + #[test] fn empty_input() { let input = ""; assert!(!check_against_baseline(input), "This pathspec is valid in git: {input}"); - let output = gix_pathspec::parse(input.as_bytes(), Default::default()); - assert!(output.is_err()); - assert!(matches!(output.unwrap_err(), Error::EmptyString)); + let err = assert_validation(input, "An empty string is not a valid pathspec"); + assert_eq!(err.input.as_ref().map(|input| input.as_slice()), Some(b"".as_slice())); } #[test] @@ -23,9 +28,8 @@ fn invalid_short_signatures() { for input in inputs.into_iter() { assert!(!check_against_baseline(input), "This pathspec is valid in git: {input}"); - let output = gix_pathspec::parse(input.as_bytes(), Default::default()); - assert!(output.is_err()); - assert!(matches!(output.unwrap_err(), Error::Unimplemented { .. })); + let err = assert_validation(input, "Unimplemented short keyword"); + assert_eq!(err.input.as_ref().map(|input| input.len()), Some(1)); } } @@ -41,9 +45,8 @@ fn invalid_keywords() { for input in inputs.into_iter() { assert!(!check_against_baseline(input), "This pathspec is valid in git: {input}"); - let output = gix_pathspec::parse(input.as_bytes(), Default::default()); - assert!(output.is_err()); - assert!(matches!(output.unwrap_err(), Error::InvalidKeyword { .. })); + let err = assert_validation(input, "Found invalid keyword in pathspec signature"); + assert!(err.input.is_some(), "the invalid keyword is retained"); } } @@ -63,9 +66,8 @@ fn invalid_attributes() { for input in inputs { assert!(!check_against_baseline(input), "This pathspec is valid in git: {input}"); - let output = gix_pathspec::parse(input.as_bytes(), Default::default()); - assert!(output.is_err(), "This pathspec did not produce an error {input}"); - assert!(matches!(output.unwrap_err(), Error::InvalidAttribute(_))); + let err = assert_validation(input, "Attribute has non-ascii characters or starts with '-'"); + assert!(err.input.is_some(), "the invalid attribute name is retained"); } } @@ -74,13 +76,8 @@ fn attribute_values_are_not_split_on_non_space_blanks() { let input = ":(attr:a=one\tb=two)some/path"; assert!(!check_against_baseline(input), "This pathspec is valid in git: {input}"); - assert!( - matches!( - gix_pathspec::parse(input.as_bytes(), Default::default()), - Err(Error::InvalidAttributeValue { .. }) - ), - "the tab belongs to the value and makes it invalid" - ); + let err = assert_validation(input, "Invalid character in attribute value"); + assert_eq!(err.input.as_ref().map(|input| input.as_slice()), Some(b"\t".as_slice())); } #[test] @@ -99,12 +96,8 @@ fn invalid_attribute_values() { for input in inputs { assert!(!check_against_baseline(input), "This pathspec is valid in git: {input}"); - let output = gix_pathspec::parse(input.as_bytes(), Default::default()); - assert!(output.is_err(), "This pathspec did not produce an error {input}"); - assert!( - matches!(output.unwrap_err(), Error::InvalidAttributeValue { .. }), - "Errors did not match for pathspec: {input}" - ); + let err = assert_validation(input, "Invalid character in attribute value"); + assert_eq!(err.input.as_ref().map(|input| input.len()), Some(1)); } } @@ -119,9 +112,11 @@ fn escape_character_at_end_of_attribute_value() { for input in inputs { assert!(!check_against_baseline(input), "This pathspec is valid in git: {input}"); - let output = gix_pathspec::parse(input.as_bytes(), Default::default()); - assert!(output.is_err(), "This pathspec did not produce an error {input}"); - assert!(matches!(output.unwrap_err(), Error::TrailingEscapeCharacter)); + let err = assert_validation( + input, + r"Escape character '\' is not allowed as the last character in an attribute value", + ); + assert!(err.input.is_some(), "the invalid attribute value is retained"); } } @@ -131,9 +126,7 @@ fn empty_attribute_specification() { assert!(!check_against_baseline(input), "This pathspec is valid in git: {input}"); - let output = gix_pathspec::parse(input.as_bytes(), Default::default()); - assert!(output.is_err()); - assert!(matches!(output.unwrap_err(), Error::EmptyAttribute)); + assert_validation(input, "Attribute specification cannot be empty"); } #[test] @@ -142,9 +135,11 @@ fn multiple_attribute_specifications() { assert!(!check_against_baseline(input), "This pathspec is valid in git: {input}"); - let output = gix_pathspec::parse(input.as_bytes(), Default::default()); - assert!(output.is_err()); - assert!(matches!(output.unwrap_err(), Error::MultipleAttributeSpecifications)); + let err = assert_validation( + input, + "Only one attribute specification is allowed in the same pathspec", + ); + assert!(err.input.is_some(), "the duplicate attribute specification is retained"); } #[test] @@ -153,9 +148,8 @@ fn missing_parentheses() { assert!(!check_against_baseline(input), "This pathspec is valid in git: {input}"); - let output = gix_pathspec::parse(input.as_bytes(), Default::default()); - assert!(output.is_err()); - assert!(matches!(output.unwrap_err(), Error::MissingClosingParenthesis)); + let err = assert_validation(input, "Missing ')' at the end of pathspec signature"); + assert_eq!(err.input.as_ref().map(|input| input.as_slice()), Some(input.as_bytes())); } #[test] @@ -164,7 +158,12 @@ fn glob_and_literal_keywords_present() { assert!(!check_against_baseline(input), "This pathspec is valid in git: {input}"); - let output = gix_pathspec::parse(input.as_bytes(), Default::default()); - assert!(output.is_err()); - assert!(matches!(output.unwrap_err(), Error::IncompatibleSearchModes)); + let err = assert_validation( + input, + "'literal' and 'glob' keywords cannot be used together in the same pathspec", + ); + assert_eq!( + err.input.as_ref().map(|input| input.as_slice()), + Some(b"literal".as_slice()) + ); } diff --git a/gix-submodule/src/access.rs b/gix-submodule/src/access.rs index ca540d6b196..73ce00bb04a 100644 --- a/gix-submodule/src/access.rs +++ b/gix-submodule/src/access.rs @@ -84,12 +84,10 @@ impl File { let patterns = patterns .into_iter() .map(|pattern| gix_pathspec::parse(&pattern, defaults)) - .collect::, _>>()?; - Ok(gix_pathspec::Search::from_specs( - patterns, - None, - std::path::Path::new(""), - )?) + .collect::, _>>() + .map_err(crate::is_active_platform::Error::ParsePattern)?; + gix_pathspec::Search::from_specs(patterns, None, std::path::Path::new("")) + .map_err(crate::is_active_platform::Error::NormalizePattern) }) .transpose()?; Ok(IsActivePlatform { search }) diff --git a/gix-submodule/src/is_active_platform.rs b/gix-submodule/src/is_active_platform.rs index e8781a6c375..c73a31be168 100644 --- a/gix-submodule/src/is_active_platform.rs +++ b/gix-submodule/src/is_active_platform.rs @@ -7,9 +7,9 @@ use crate::IsActivePlatform; #[expect(missing_docs)] pub enum Error { #[error(transparent)] - NormalizePattern(#[from] gix_pathspec::normalize::Error), + NormalizePattern(gix_pathspec::normalize::Error), #[error(transparent)] - ParsePattern(#[from] gix_pathspec::parse::Error), + ParsePattern(gix_pathspec::parse::Error), } impl IsActivePlatform { diff --git a/gix/src/pathspec.rs b/gix/src/pathspec.rs index c76ad3d770a..20eb1ade33d 100644 --- a/gix/src/pathspec.rs +++ b/gix/src/pathspec.rs @@ -14,9 +14,9 @@ pub mod init { #[error(transparent)] Defaults(#[from] crate::repository::pathspec_defaults_ignore_case::Error), #[error(transparent)] - ParseSpec(#[from] gix_pathspec::parse::Error), + ParseSpec(gix_pathspec::parse::Error), #[error("Could not obtain the repository prefix as the relative path of the CWD as seen from the working tree")] - NormalizeSpec(#[from] gix_pathspec::normalize::Error), + NormalizeSpec(#[source] gix_pathspec::normalize::Error), #[error(transparent)] RepoPrefix(#[from] gix_path::realpath::Error), } @@ -47,7 +47,8 @@ impl<'repo> Pathspec<'repo> { let patterns = patterns .into_iter() .map(move |p| parse(p.as_ref(), defaults)) - .collect::, _>>()?; + .collect::, _>>() + .map_err(init::Error::ParseSpec)?; let needs_cache = patterns.iter().any(|p| !p.attributes.is_empty()); let prefix = if patterns.is_empty() && !empty_patterns_match_prefix { None @@ -62,7 +63,8 @@ impl<'repo> Pathspec<'repo> { repo.options.current_dir_or_empty(), gix_path::realpath::MAX_SYMLINKS, )?, - )?; + ) + .map_err(init::Error::NormalizeSpec)?; let cache = needs_cache.then(make_attributes).transpose()?; gix_trace::debug!( diff --git a/gix/src/repository/mod.rs b/gix/src/repository/mod.rs index 19f52b27aa7..be1a162ce8a 100644 --- a/gix/src/repository/mod.rs +++ b/gix/src/repository/mod.rs @@ -487,7 +487,7 @@ pub mod pathspec_defaults_ignore_case { #[error("Filesystem configuration could not be obtained to learn about case sensitivity")] FilesystemConfig(#[from] crate::config::boolean::Error), #[error(transparent)] - Defaults(#[from] gix_pathspec::defaults::from_environment::Error), + Defaults(#[from] gix_error::Error), } } diff --git a/gix/src/repository/pathspec.rs b/gix/src/repository/pathspec.rs index 60801b4cf25..a527a2e62f6 100644 --- a/gix/src/repository/pathspec.rs +++ b/gix/src/repository/pathspec.rs @@ -33,8 +33,8 @@ impl Repository { /// /// These are stemming from environment variables which have been converted to [config settings](crate::config::tree::gitoxide::Pathspec), /// which now serve as authority for configuration. - pub fn pathspec_defaults(&self) -> Result { - self.config.pathspec_defaults() + pub fn pathspec_defaults(&self) -> Result { + self.config.pathspec_defaults().map_err(gix_error::Exn::into_error) } /// Similar to [Self::pathspec_defaults()], but will automatically configure the returned defaults to match case-insensitively if the underlying @@ -43,7 +43,7 @@ impl Repository { &self, inherit_ignore_case: bool, ) -> Result { - let mut defaults = self.config.pathspec_defaults()?; + let mut defaults = self.config.pathspec_defaults().map_err(gix_error::Exn::into_error)?; if inherit_ignore_case && self .config diff --git a/gix/src/submodule/errors.rs b/gix/src/submodule/errors.rs index 362779967a6..dc4ba4ce162 100644 --- a/gix/src/submodule/errors.rs +++ b/gix/src/submodule/errors.rs @@ -47,7 +47,7 @@ pub mod is_active { #[error(transparent)] InitAttributes(#[from] crate::config::attribute_stack::Error), #[error(transparent)] - InitPathspecDefaults(#[from] gix_pathspec::defaults::from_environment::Error), + InitPathspecDefaults(gix_error::Error), #[error(transparent)] ObtainIndex(#[from] crate::repository::index_or_load_from_head::Error), } diff --git a/gix/src/submodule/mod.rs b/gix/src/submodule/mod.rs index 56016736763..07bcedfcf79 100644 --- a/gix/src/submodule/mod.rs +++ b/gix/src/submodule/mod.rs @@ -60,9 +60,13 @@ impl<'repo> SharedState<'repo> { ) -> Result<(RefMut<'_, IsActivePlatform>, RefMut<'_, gix_worktree::Stack>), is_active::Error> { let mut state = self.is_active.borrow_mut(); if state.is_none() { - let platform = self - .modules - .is_active_platform(&self.repo.config.resolved, self.repo.config.pathspec_defaults()?)?; + let platform = self.modules.is_active_platform( + &self.repo.config.resolved, + self.repo + .config + .pathspec_defaults() + .map_err(|err| is_active::Error::InitPathspecDefaults(err.into_error()))?, + )?; let index = self.index()?; let attributes = self .repo From bd01b362ddddaef397bf88ed14af8d9dc8a5409e Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 20 Aug 2026 17:09:25 +0200 Subject: [PATCH 07/51] change(gix-prompt)!: migrate errors to gix-error rubberstamp --- Cargo.lock | 2 +- gix-credentials/src/helper/cascade.rs | 4 +-- gix-credentials/src/protocol/mod.rs | 2 +- gix-prompt/Cargo.toml | 3 +- gix-prompt/examples/askpass.rs | 2 +- gix-prompt/examples/use-askpass.rs | 2 +- gix-prompt/src/lib.rs | 3 +- gix-prompt/src/types.rs | 17 +---------- gix-prompt/src/unix.rs | 37 +++++++++++++++++------- gix-prompt/tests/prompt.rs | 41 +++++++++++++++++++++++++-- 10 files changed, 76 insertions(+), 37 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b4272dbe842..2f9061ae8bf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2313,11 +2313,11 @@ dependencies = [ "expectrl", "gix-command", "gix-config-value", + "gix-error", "gix-testtools", "parking_lot", "rustix", "serial_test", - "thiserror 2.0.18", ] [[package]] diff --git a/gix-credentials/src/helper/cascade.rs b/gix-credentials/src/helper/cascade.rs index 42cb4073354..32b2379080c 100644 --- a/gix-credentials/src/helper/cascade.rs +++ b/gix-credentials/src/helper/cascade.rs @@ -174,7 +174,7 @@ impl Cascade { ctx.username = gix_prompt::ask(&message, &prompt) .map_err(|err| protocol::Error::Prompt { prompt: message, - source: err, + source: err.into_error(), })? .into(); } @@ -184,7 +184,7 @@ impl Cascade { ctx.password = gix_prompt::ask(&message, &prompt) .map_err(|err| protocol::Error::Prompt { prompt: message, - source: err, + source: err.into_error(), })? .into(); } diff --git a/gix-credentials/src/protocol/mod.rs b/gix-credentials/src/protocol/mod.rs index 9279e2c1113..12a87b10dc4 100644 --- a/gix-credentials/src/protocol/mod.rs +++ b/gix-credentials/src/protocol/mod.rs @@ -36,7 +36,7 @@ pub enum Error { #[error("The handler asked to stop trying to obtain credentials")] Quit, #[error("Couldn't obtain {prompt}")] - Prompt { prompt: String, source: gix_prompt::Error }, + Prompt { prompt: String, source: gix_error::Error }, } /// Additional context to be passed to the credentials helper. diff --git a/gix-prompt/Cargo.toml b/gix-prompt/Cargo.toml index 97729348412..da3d96b4e38 100644 --- a/gix-prompt/Cargo.toml +++ b/gix-prompt/Cargo.toml @@ -17,8 +17,7 @@ doctest = false [dependencies] gix-command = { version = "^0.10.0", path = "../gix-command" } gix-config-value = { version = "^0.19.0", path = "../gix-config-value" } - -thiserror = "2.0.18" +gix-error = { version = "^0.3.0", path = "../gix-error" } [target.'cfg(unix)'.dependencies] rustix = { version = "1.1.2", features = ["termios"] } diff --git a/gix-prompt/examples/askpass.rs b/gix-prompt/examples/askpass.rs index 72b9f0e2992..590f9047401 100644 --- a/gix-prompt/examples/askpass.rs +++ b/gix-prompt/examples/askpass.rs @@ -1,4 +1,4 @@ -fn main() -> Result<(), Box> { +fn main() -> Result<(), Box> { let prompt = std::env::args() .nth(1) .ok_or("First argument must be the prompt to display when asking for a password")?; diff --git a/gix-prompt/examples/use-askpass.rs b/gix-prompt/examples/use-askpass.rs index 49c6e92e185..a9e08155bca 100644 --- a/gix-prompt/examples/use-askpass.rs +++ b/gix-prompt/examples/use-askpass.rs @@ -1,6 +1,6 @@ use gix_prompt::{Mode, Options}; -fn main() -> Result<(), Box> { +fn main() -> Result<(), Box> { let pass = gix_prompt::ask( "Password: ", &Options { diff --git a/gix-prompt/src/lib.rs b/gix-prompt/src/lib.rs index 141c0f3f792..5e426c9cd10 100644 --- a/gix-prompt/src/lib.rs +++ b/gix-prompt/src/lib.rs @@ -18,9 +18,10 @@ use unix::imp; #[cfg(not(unix))] mod imp { use crate::{Error, Options}; + use gix_error::{ErrorExt, message}; pub(crate) fn ask(_prompt: &str, _opts: &Options) -> Result { - Err(Error::UnsupportedPlatform) + Err(message("The current platform has no implementation for prompting in the terminal").raise()) } } diff --git a/gix-prompt/src/types.rs b/gix-prompt/src/types.rs index 06e955c15f4..5a919951d92 100644 --- a/gix-prompt/src/types.rs +++ b/gix-prompt/src/types.rs @@ -1,22 +1,7 @@ use std::path::PathBuf; /// The error returned by [ask()][crate::ask()]. -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] -pub enum Error { - #[error("Terminal prompts are disabled")] - Disabled, - #[error("The current platform has no implementation for prompting in the terminal")] - UnsupportedPlatform, - #[error( - "Failed to open terminal at {:?} for writing prompt, or to write it", - crate::unix::TTY_PATH - )] - TtyIo(#[from] std::io::Error), - #[cfg(unix)] - #[error("Failed to obtain or set terminal configuration")] - TerminalConfiguration(#[from] rustix::io::Errno), -} +pub type Error = gix_error::Exn; /// The way the user is prompted. #[derive(Default, Debug, Copy, Clone, Eq, PartialEq)] diff --git a/gix-prompt/src/unix.rs b/gix-prompt/src/unix.rs index 5026f481231..cd72c6594de 100644 --- a/gix-prompt/src/unix.rs +++ b/gix-prompt/src/unix.rs @@ -9,6 +9,7 @@ pub(crate) mod imp { io::{BufRead, Read, Write}, }; + use gix_error::{ErrorExt, Message, ResultExt, message}; use parking_lot::{Mutex, RawMutex, const_mutex, lock_api::MutexGuard}; use rustix::termios::{self, Termios}; @@ -16,21 +17,33 @@ pub(crate) mod imp { static TERM_STATE: Mutex> = const_mutex(None); + fn tty_io() -> Message { + gix_error::message!("Failed to open terminal at {TTY_PATH:?} for writing prompt, or to write it") + } + + fn terminal_configuration() -> Message { + message("Failed to obtain or set terminal configuration") + } + /// Ask the user given a `prompt`, returning the result. pub(crate) fn ask(prompt: &str, Options { mode, .. }: &Options) -> Result { match mode { - Mode::Disable => Err(Error::Disabled), + Mode::Disable => Err(message("Terminal prompts are disabled").raise()), Mode::Hidden => { let state = TERM_STATE.lock(); let mut in_out = save_term_state_and_disable_echo( state, - std::fs::OpenOptions::new().write(true).read(true).open(TTY_PATH)?, + std::fs::OpenOptions::new() + .write(true) + .read(true) + .open(TTY_PATH) + .or_raise(tty_io)?, )?; - in_out.write_all(prompt.as_bytes())?; + in_out.write_all(prompt.as_bytes()).or_raise(tty_io)?; let mut buf_read = std::io::BufReader::with_capacity(64, in_out); let mut out = String::with_capacity(64); - buf_read.read_line(&mut out)?; + buf_read.read_line(&mut out).or_raise(tty_io)?; out.pop(); if out.ends_with('\r') { @@ -40,12 +53,16 @@ pub(crate) mod imp { Ok(out) } Mode::Visible => { - let mut in_out = std::fs::OpenOptions::new().write(true).read(true).open(TTY_PATH)?; - in_out.write_all(prompt.as_bytes())?; + let mut in_out = std::fs::OpenOptions::new() + .write(true) + .read(true) + .open(TTY_PATH) + .or_raise(tty_io)?; + in_out.write_all(prompt.as_bytes()).or_raise(tty_io)?; let mut buf_read = std::io::BufReader::with_capacity(64, in_out); let mut out = String::with_capacity(64); - buf_read.read_line(&mut out)?; + buf_read.read_line(&mut out).or_raise(tty_io)?; Ok(out.trim_end().to_owned()) } } @@ -88,7 +105,7 @@ pub(crate) mod imp { impl RestoreTerminalStateOnDrop<'_> { fn restore_term_state(mut self) -> Result<(), Error> { let state = self.state.take().expect("BUG: we exist only if something is saved"); - termios::tcsetattr(&self.fd, termios::OptionalActions::Flush, &state)?; + termios::tcsetattr(&self.fd, termios::OptionalActions::Flush, &state).or_raise(terminal_configuration)?; Ok(()) } } @@ -110,13 +127,13 @@ pub(crate) mod imp { "BUG: recursive calls are not possible and we restore afterwards" ); - let prev = termios::tcgetattr(&fd)?; + let prev = termios::tcgetattr(&fd).or_raise(terminal_configuration)?; let mut new = prev.clone(); *state = prev.into(); new.local_modes &= !termios::LocalModes::ECHO; new.local_modes |= termios::LocalModes::ECHONL; - termios::tcsetattr(&fd, termios::OptionalActions::Flush, &new)?; + termios::tcsetattr(&fd, termios::OptionalActions::Flush, &new).or_raise(terminal_configuration)?; Ok(RestoreTerminalStateOnDrop { fd, state }) } diff --git a/gix-prompt/tests/prompt.rs b/gix-prompt/tests/prompt.rs index 0269a18bf94..b6e4a75f95d 100644 --- a/gix-prompt/tests/prompt.rs +++ b/gix-prompt/tests/prompt.rs @@ -4,6 +4,21 @@ mod options; mod ask { use gix_testtools::bstr::ByteSlice; + #[test] + #[cfg(unix)] + fn disabled_is_a_message_without_a_source() { + let err = gix_prompt::ask( + "ignored", + &gix_prompt::Options { + mode: gix_prompt::Mode::Disable, + askpass: None, + }, + ) + .expect_err("terminal prompting is disabled"); + assert_eq!(err.error().to_string(), "Terminal prompts are disabled"); + assert_eq!(err.iter().count(), 1, "there is no underlying operation error"); + } + /// Evaluates Cargo's target directory for this project at runtime to adjust for the concrete /// execution environment. This is necessary because certain environment variables and /// configuration options can change its location (e.g. CARGO_TARGET_DIR). @@ -54,7 +69,19 @@ mod ask { #[test] fn askpass_only() -> gix_testtools::Result { let mut cmd = std::process::Command::new(env!("CARGO")); - cmd.args(["build", "--example", "use-askpass", "--example", "askpass"]); + cmd.args([ + "build", + "-p", + "gix-prompt", + "-p", + "gix-hash", + "--features", + "sha1", + "--example", + "use-askpass", + "--example", + "askpass", + ]); assert!( cmd.status().expect("Cargo can build prompt examples").success(), "prompt examples must build successfully before they run" @@ -71,7 +98,17 @@ mod ask { #[test] fn username_password() -> gix_testtools::Result { let mut cmd = std::process::Command::new(env!("CARGO")); - cmd.args(["build", "--example", "credentials"]); + cmd.args([ + "build", + "-p", + "gix-prompt", + "-p", + "gix-hash", + "--features", + "sha1", + "--example", + "credentials", + ]); assert!( cmd.status().expect("Cargo can build prompt examples").success(), "prompt examples must build successfully before they run" From a6113cb39a039afc2103460f3fa094a8d85bea18 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 20 Aug 2026 17:09:44 +0200 Subject: [PATCH 08/51] change(gix-revwalk)!: migrate errors to gix-error rubberstamp --- Cargo.lock | 1 - gix-protocol/src/fetch/negotiate.rs | 8 ++++- gix-revwalk/Cargo.toml | 1 - gix-revwalk/src/graph/commit.rs | 49 +++++++++++++---------------- gix-revwalk/src/graph/mod.rs | 48 +++++----------------------- gix-traverse/src/commit/simple.rs | 36 ++++++++++++--------- 6 files changed, 56 insertions(+), 87 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2f9061ae8bf..31bf6afa838 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2441,7 +2441,6 @@ dependencies = [ "gix-object", "gix-testtools", "smallvec", - "thiserror 2.0.18", ] [[package]] diff --git a/gix-protocol/src/fetch/negotiate.rs b/gix-protocol/src/fetch/negotiate.rs index 2d356f03d95..864bb843ce4 100644 --- a/gix-protocol/src/fetch/negotiate.rs +++ b/gix-protocol/src/fetch/negotiate.rs @@ -22,7 +22,7 @@ pub enum Error { #[error("We were unable to figure out what objects the server should send after {rounds} round(s)")] NegotiationFailed { rounds: usize }, #[error(transparent)] - LookupCommitInGraph(#[from] gix_revwalk::graph::get_or_insert_default::Error), + LookupCommitInGraph(gix_error::Error), #[error(transparent)] OpenPackedRefsBuffer(#[from] gix_ref::packed::buffer::open::Error), #[error(transparent)] @@ -35,6 +35,12 @@ pub enum Error { AlternateRefsAndObjects(Box), } +impl From for Error { + fn from(err: gix_revwalk::graph::get_or_insert_default::Error) -> Self { + Error::LookupCommitInGraph(err.into_error()) + } +} + /// Determines what should be done after [preparing the commit-graph for negotiation](mark_complete_and_common_ref). #[must_use] #[derive(Debug, Clone)] diff --git a/gix-revwalk/Cargo.toml b/gix-revwalk/Cargo.toml index 10d18eeef35..e642635e42b 100644 --- a/gix-revwalk/Cargo.toml +++ b/gix-revwalk/Cargo.toml @@ -28,7 +28,6 @@ gix-date = { version = "^0.16.0", path = "../gix-date" } gix-hashtable = { version = "^0.16.0", path = "../gix-hashtable" } gix-commitgraph = { version = "^0.39.0", path = "../gix-commitgraph" } -thiserror = "2.0.18" smallvec = "1.15.1" [dev-dependencies] diff --git a/gix-revwalk/src/graph/commit.rs b/gix-revwalk/src/graph/commit.rs index ffb9d5b64af..71c488bf2e8 100644 --- a/gix-revwalk/src/graph/commit.rs +++ b/gix-revwalk/src/graph/commit.rs @@ -1,4 +1,5 @@ use gix_date::SecondsSinceUnixEpoch; +use gix_error::{CorruptionError, ErrorExt, ResultExt}; use smallvec::SmallVec; use super::LazyCommit; @@ -68,7 +69,7 @@ impl<'graph, 'cache> LazyCommit<'graph, 'cache> { let mut parents = SmallVec::default(); let mut timestamp = None; for token in iter { - match token? { + match token.or_raise(|| CorruptionError::new("A commit could not be decoded during traversal"))? { Token::Tree { .. } => {} Token::Parent { id } => parents.push(id), Token::Author { .. } => {} @@ -94,15 +95,18 @@ impl<'graph, 'cache> LazyCommit<'graph, 'cache> { let mut parents = SmallVec::default(); let commit = cache.commit_at(*pos); for pos in commit.iter_parents() { - let pos = pos?; + let pos = pos.or_raise(|| { + CorruptionError::new("Could not find commit position in graph when traversing parents") + })?; parents.push(cache.commit_at(pos).id().to_owned()); } + let actual = commit.committer_timestamp(); Commit { parents, - commit_time: commit.committer_timestamp().try_into().map_err(|_| { - to_owned::Error::CommitGraphTime { - actual: commit.committer_timestamp(), - } + commit_time: SecondsSinceUnixEpoch::try_from(actual).or_raise(|| { + CorruptionError::new(format!( + "Commit-graph time could not be presented as signed integer: {actual}" + )) })?, generation: Some(commit.generation()), data, @@ -134,14 +138,19 @@ impl Iterator for Parents<'_, '_> { Ok(gix_object::commit::ref_iter::Token::Tree { .. }) => continue, Ok(gix_object::commit::ref_iter::Token::Parent { id }) => return Some(Ok(id)), Ok(_unused_token) => break, - Err(err) => return Some(Err(err.into())), + Err(err) => { + return Some(Err(err.and_raise(CorruptionError::new( + "An error occurred when parsing commit parents", + )))); + } } } None } - Either::Right((cache, it)) => it - .next() - .map(|r| r.map(|pos| cache.id_at(pos).to_owned()).map_err(Into::into)), + Either::Right((cache, it)) => it.next().map(|r| { + r.map(|pos| cache.id_at(pos).to_owned()) + .or_raise(|| CorruptionError::new("An error occurred when parsing parents from the commit graph")) + }), } } } @@ -149,27 +158,11 @@ impl Iterator for Parents<'_, '_> { /// pub mod iter_parents { /// The error returned by the [`Parents`][super::Parents] iterator. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error("An error occurred when parsing commit parents")] - DecodeCommit(#[from] gix_object::decode::Error), - #[error("An error occurred when parsing parents from the commit graph")] - DecodeCommitGraph(#[from] gix_error::Message), - } + pub type Error = gix_error::Exn; } /// pub mod to_owned { /// The error returned by [`to_owned()`][crate::graph::LazyCommit::to_owned()]. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error("A commit could not be decoded during traversal")] - Decode(#[from] gix_object::decode::Error), - #[error("Could not find commit position in graph when traversing parents")] - CommitGraphParent(#[from] gix_error::Message), - #[error("Commit-graph time could not be presented as signed integer: {actual}")] - CommitGraphTime { actual: u64 }, - } + pub type Error = gix_error::Exn; } diff --git a/gix-revwalk/src/graph/mod.rs b/gix-revwalk/src/graph/mod.rs index 701cb6301c3..18d0467fda8 100644 --- a/gix-revwalk/src/graph/mod.rs +++ b/gix-revwalk/src/graph/mod.rs @@ -1,6 +1,6 @@ use std::{fmt::Formatter, ops::Index}; -use gix_error::ErrorExt; +use gix_error::{ErrorExt, ResultExt}; use gix_hash::oid; use smallvec::SmallVec; @@ -15,46 +15,14 @@ pub mod commit; mod errors { /// pub mod insert_parents { - use crate::graph::commit::iter_parents; - /// The error returned by [`insert_parents()`](crate::Graph::insert_parents()). - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - Lookup(gix_error::Error), - #[error("A commit could not be decoded during traversal")] - Decode(#[from] gix_object::decode::Error), - #[error(transparent)] - Parent(#[from] iter_parents::Error), - } - - impl From for Error { - fn from(err: gix_object::find::existing_iter::Error) -> Self { - Error::Lookup(err.into_error()) - } - } + pub type Error = gix_error::Exn; } /// pub mod get_or_insert_default { - use crate::graph::commit::to_owned; - /// The error returned by [`try_lookup_or_insert_default()`](crate::Graph::try_lookup_or_insert_default()). - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - Lookup(gix_error::Error), - #[error(transparent)] - ToOwned(#[from] to_owned::Error), - } - - impl From for Error { - fn from(err: gix_object::find::existing_iter::Error) -> Self { - Error::Lookup(err.into_error()) - } - } + pub type Error = gix_error::Exn; } } pub use errors::{get_or_insert_default, insert_parents}; @@ -152,7 +120,7 @@ impl<'cache, T> Graph<'_, 'cache, T> { let commit = self.lookup(id)?; let parents: SmallVec<[_; 2]> = commit.iter_parents().collect(); for parent_id in parents { - let parent_id = parent_id?; + let parent_id = parent_id.or_erased()?; match self.map.entry(parent_id) { gix_hashtable::hash_map::Entry::Vacant(entry) => { let parent = match try_lookup(&parent_id, &*self.find, self.cache, &mut self.parent_buf)? { @@ -186,9 +154,7 @@ impl<'cache, T> Graph<'_, 'cache, T> { parent_data: &mut dyn FnMut(gix_hash::ObjectId, LazyCommit<'_, 'cache>, Option<&mut T>) -> Result, ) -> Result<(), E> where - E: From - + From - + From, + E: From + From, { let commit = self.lookup(id).map_err(E::from)?; let parents: SmallVec<[_; 2]> = commit.iter_parents().collect(); @@ -258,7 +224,7 @@ impl Graph<'_, '_, Commit> { None => return Ok(None), Some(commit) => commit, }; - let mut commit = commit.to_owned(new_data)?; + let mut commit = commit.to_owned(new_data).or_erased()?; update_data(&mut commit.data); entry.insert(commit); } @@ -309,7 +275,7 @@ impl Graph<'_, '_, Commit> { None => return Ok(None), Some(commit) => commit, }; - let mut commit = commit.to_owned(T::default)?; + let mut commit = commit.to_owned(T::default).or_erased()?; update_commit(&mut commit); entry.insert(commit); } diff --git a/gix-traverse/src/commit/simple.rs b/gix-traverse/src/commit/simple.rs index c01b00f342f..e2309d1883c 100644 --- a/gix-traverse/src/commit/simple.rs +++ b/gix-traverse/src/commit/simple.rs @@ -77,7 +77,7 @@ pub enum Error { #[error(transparent)] ObjectDecode(#[from] gix_object::decode::Error), #[error(transparent)] - HiddenGraph(#[from] gix_revwalk::graph::get_or_insert_default::Error), + HiddenGraph(gix_error::Error), } impl From for Error { @@ -191,16 +191,20 @@ fn compute_hidden_frontier( let mut queue = gix_revwalk::PriorityQueue::::new(); for &visible in visible_tips { - graph.get_or_insert_full_commit(visible, |commit| { - commit.data |= PaintFlags::VISIBLE; - queue.insert(GenThenTime::from(&*commit), visible); - })?; + graph + .get_or_insert_full_commit(visible, |commit| { + commit.data |= PaintFlags::VISIBLE; + queue.insert(GenThenTime::from(&*commit), visible); + }) + .map_err(|err| Error::HiddenGraph(err.into_error()))?; } for &hidden in hidden_tips { - graph.get_or_insert_full_commit(hidden, |commit| { - commit.data |= PaintFlags::HIDDEN; - queue.insert(GenThenTime::from(&*commit), hidden); - })?; + graph + .get_or_insert_full_commit(hidden, |commit| { + commit.data |= PaintFlags::HIDDEN; + queue.insert(GenThenTime::from(&*commit), hidden); + }) + .map_err(|err| Error::HiddenGraph(err.into_error()))?; } while queue.iter_unordered().any(|id| { @@ -219,12 +223,14 @@ fn compute_hidden_frontier( } for parent_id in commit.parents.clone() { - graph.get_or_insert_full_commit(parent_id, |parent| { - if (parent.data & flags) != flags { - parent.data |= flags; - queue.insert(GenThenTime::from(&*parent), parent_id); - } - })?; + graph + .get_or_insert_full_commit(parent_id, |parent| { + if (parent.data & flags) != flags { + parent.data |= flags; + queue.insert(GenThenTime::from(&*parent), parent_id); + } + }) + .map_err(|err| Error::HiddenGraph(err.into_error()))?; } } From ac0dca128afece256b9463d259fedde8c0361bb8 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 20 Aug 2026 17:10:32 +0200 Subject: [PATCH 09/51] change(gix-traverse)!: migrate errors to gix-error rubberstamp --- Cargo.lock | 1 - gitoxide-core/src/hours/mod.rs | 2 +- gitoxide-core/src/pack/create.rs | 10 ++- gitoxide-core/src/query/engine/update.rs | 2 +- gix-diff/src/tree_with_rewrites/function.rs | 1 + gix-index/src/init.rs | 23 ++---- gix-pack/src/data/output/count/objects/mod.rs | 4 +- .../src/data/output/count/objects/types.rs | 2 +- gix-traverse/Cargo.toml | 1 - gix-traverse/src/commit/simple.rs | 77 +++++++++---------- gix-traverse/src/commit/topo/init.rs | 6 +- gix-traverse/src/commit/topo/iter.rs | 33 ++++++-- gix-traverse/src/commit/topo/mod.rs | 19 +---- gix-traverse/src/tree/breadthfirst.rs | 29 ++----- gix-traverse/src/tree/depthfirst.rs | 4 +- .../tests/traverse/commit/simple/mod.rs | 5 +- gix/src/ext/tree.rs | 16 +--- gix/src/object/tree/traverse.rs | 8 +- gix/src/revision/walk.rs | 16 +++- 19 files changed, 123 insertions(+), 136 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 31bf6afa838..3a4c1e7f3c2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2646,7 +2646,6 @@ dependencies = [ "gix-traverse", "insta", "smallvec", - "thiserror 2.0.18", ] [[package]] diff --git a/gitoxide-core/src/hours/mod.rs b/gitoxide-core/src/hours/mod.rs index 8c5c29cc361..1cbeca2fbed 100644 --- a/gitoxide-core/src/hours/mod.rs +++ b/gitoxide-core/src/hours/mod.rs @@ -254,7 +254,7 @@ where } commit_idx += 1; } - Err(gix::traverse::commit::simple::Error::Find { .. }) => { + Err(err) if err.downcast_any_ref::().is_some() => { is_shallow = true; break; } diff --git a/gitoxide-core/src/pack/create.rs b/gitoxide-core/src/pack/create.rs index 631db5376dc..3ac60f9b84f 100644 --- a/gitoxide-core/src/pack/create.rs +++ b/gitoxide-core/src/pack/create.rs @@ -133,7 +133,7 @@ where let handle = repo.objects.into_shared_arc().to_cache_arc(); let iter = Box::new( traverse::commit::Simple::new(tips, handle.clone()) - .map(|res| res.map_err(|err| Box::new(err) as Box<_>).map(|c| c.id)) + .map(|res| res.map_err(|err| Box::new(err.into_error()) as Box<_>).map(|c| c.id)) .inspect(move |_| progress.inc()), ); (handle, iter) @@ -364,10 +364,16 @@ pub mod input_iteration { #[derive(Debug, thiserror::Error)] pub enum Error { #[error("input objects couldn't be iterated completely")] - Iteration(#[from] traverse::commit::simple::Error), + Iteration(#[source] gix::Error), #[error("An error occurred while reading hashes from standard input")] InputLinesIo(#[from] std::io::Error), #[error("Could not decode hex hash provided on standard input")] HashDecode(#[from] hash::decode::Error), } + + impl From for Error { + fn from(err: traverse::commit::simple::Error) -> Self { + Error::Iteration(err.into_error()) + } + } } diff --git a/gitoxide-core/src/query/engine/update.rs b/gitoxide-core/src/query/engine/update.rs index b345c62d9b4..8c7beee7dd5 100644 --- a/gitoxide-core/src/query/engine/update.rs +++ b/gitoxide-core/src/query/engine/update.rs @@ -441,7 +441,7 @@ pub fn update( break; } } - Err(gix::traverse::commit::simple::Error::Find { .. }) => { + Err(traverse_err) if traverse_err.downcast_any_ref::().is_some() => { writeln!(err, "shallow repository - commit history is truncated").ok(); break; } diff --git a/gix-diff/src/tree_with_rewrites/function.rs b/gix-diff/src/tree_with_rewrites/function.rs index 669a70a32b3..fceca4e9517 100644 --- a/gix-diff/src/tree_with_rewrites/function.rs +++ b/gix-diff/src/tree_with_rewrites/function.rs @@ -173,6 +173,7 @@ where let mut delegate = tree_to_changes::Delegate::new(push, self.location); let state = gix_traverse::tree::breadthfirst::State::default(); gix_traverse::tree::breadthfirst(self.src_tree, state, self.objects, &mut delegate) + .map_err(gix_error::Exn::into_error) }, )?; Ok(Some(outcome)) diff --git a/gix-index/src/init.rs b/gix-index/src/init.rs index ad52a892aac..9be2aaed5f8 100644 --- a/gix-index/src/init.rs +++ b/gix-index/src/init.rs @@ -21,7 +21,7 @@ pub mod from_tree { source: gix_validate::path::component::Error, }, #[error(transparent)] - Traversal(#[from] gix_traverse::tree::depthfirst::Error), + Traversal(#[from] gix_error::Error), } /// Initialization @@ -70,17 +70,12 @@ pub mod from_tree { { let _span = gix_features::trace::coarse!("gix_index::State::from_tree()"); let mut delegate = CollectEntries::new(validate); - match depthfirst(tree.to_owned(), depthfirst::State::default(), &objects, &mut delegate) { - Ok(()) => {} - Err(gix_traverse::tree::breadthfirst::Error::Cancelled) => { - let (path, err) = delegate - .invalid_path - .take() - .expect("cancellation only happens on validation error"); - return Err(Error::InvalidComponent { path, source: err }); - } - Err(err) => return Err(err.into()), + let traversal = depthfirst(tree.to_owned(), depthfirst::State::default(), &objects, &mut delegate); + + if let Some((path, err)) = delegate.invalid_path.take() { + return Err(Error::InvalidComponent { path, source: err }); } + traversal.map_err(gix_error::Exn::into_error)?; let CollectEntries { entries, @@ -88,13 +83,9 @@ pub mod from_tree { path: _, path_deque: _, validate: _, - invalid_path, + invalid_path: _, } = delegate; - if let Some((path, err)) = invalid_path { - return Err(Error::InvalidComponent { path, source: err }); - } - Ok(State { object_hash: tree.kind(), timestamp: filetime::FileTime::now(), diff --git a/gix-pack/src/data/output/count/objects/mod.rs b/gix-pack/src/data/output/count/objects/mod.rs index 8d91e7b151e..06230dbf3d0 100644 --- a/gix-pack/src/data/output/count/objects/mod.rs +++ b/gix-pack/src/data/output/count/objects/mod.rs @@ -216,7 +216,7 @@ mod expand { &objects, &mut traverse_delegate, ) - .map_err(Error::TreeTraverse)?; + .map_err(|err| Error::TreeTraverse(err.into_error()))?; out = objects.dissolve(stats); &traverse_delegate.non_trees } else { @@ -290,7 +290,7 @@ mod expand { &objects, &mut traverse_delegate, ) - .map_err(Error::TreeTraverse)?; + .map_err(|err| Error::TreeTraverse(err.into_error()))?; out = objects.dissolve(stats); } for id in &traverse_delegate.non_trees { diff --git a/gix-pack/src/data/output/count/objects/types.rs b/gix-pack/src/data/output/count/objects/types.rs index dba75ff9db5..c9494d3b5dd 100644 --- a/gix-pack/src/data/output/count/objects/types.rs +++ b/gix-pack/src/data/output/count/objects/types.rs @@ -88,7 +88,7 @@ pub enum Error { #[error(transparent)] InputIteration(Box), #[error(transparent)] - TreeTraverse(gix_traverse::tree::breadthfirst::Error), + TreeTraverse(gix_error::Error), #[error(transparent)] TreeChanges(gix_diff::tree::Error), #[error("Operation interrupted")] diff --git a/gix-traverse/Cargo.toml b/gix-traverse/Cargo.toml index e33b6c40539..8b5c37a5016 100644 --- a/gix-traverse/Cargo.toml +++ b/gix-traverse/Cargo.toml @@ -29,7 +29,6 @@ gix-hashtable = { version = "^0.16.0", path = "../gix-hashtable" } gix-revwalk = { version = "^0.35.0", path = "../gix-revwalk" } gix-commitgraph = { version = "^0.39.0", path = "../gix-commitgraph" } smallvec = "1.15.1" -thiserror = "2.0.18" bitflags = "2" [dev-dependencies] diff --git a/gix-traverse/src/commit/simple.rs b/gix-traverse/src/commit/simple.rs index e2309d1883c..6be98d24c7e 100644 --- a/gix-traverse/src/commit/simple.rs +++ b/gix-traverse/src/commit/simple.rs @@ -69,22 +69,7 @@ pub enum Sorting { } /// The error is part of the item returned by the [Ancestors](super::Simple) iterator. -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] -pub enum Error { - #[error(transparent)] - Find(gix_error::Error), - #[error(transparent)] - ObjectDecode(#[from] gix_object::decode::Error), - #[error(transparent)] - HiddenGraph(gix_error::Error), -} - -impl From for Error { - fn from(err: gix_object::find::existing_iter::Error) -> Self { - Error::Find(err.into_error()) - } -} +pub type Error = gix_error::Exn; use Result as Either; @@ -191,20 +176,16 @@ fn compute_hidden_frontier( let mut queue = gix_revwalk::PriorityQueue::::new(); for &visible in visible_tips { - graph - .get_or_insert_full_commit(visible, |commit| { - commit.data |= PaintFlags::VISIBLE; - queue.insert(GenThenTime::from(&*commit), visible); - }) - .map_err(|err| Error::HiddenGraph(err.into_error()))?; + graph.get_or_insert_full_commit(visible, |commit| { + commit.data |= PaintFlags::VISIBLE; + queue.insert(GenThenTime::from(&*commit), visible); + })?; } for &hidden in hidden_tips { - graph - .get_or_insert_full_commit(hidden, |commit| { - commit.data |= PaintFlags::HIDDEN; - queue.insert(GenThenTime::from(&*commit), hidden); - }) - .map_err(|err| Error::HiddenGraph(err.into_error()))?; + graph.get_or_insert_full_commit(hidden, |commit| { + commit.data |= PaintFlags::HIDDEN; + queue.insert(GenThenTime::from(&*commit), hidden); + })?; } while queue.iter_unordered().any(|id| { @@ -223,14 +204,12 @@ fn compute_hidden_frontier( } for parent_id in commit.parents.clone() { - graph - .get_or_insert_full_commit(parent_id, |parent| { - if (parent.data & flags) != flags { - parent.data |= flags; - queue.insert(GenThenTime::from(&*parent), parent_id); - } - }) - .map_err(|err| Error::HiddenGraph(err.into_error()))?; + graph.get_or_insert_full_commit(parent_id, |parent| { + if (parent.data & flags) != flags { + parent.data |= flags; + queue.insert(GenThenTime::from(&*parent), parent_id); + } + })?; } } @@ -253,6 +232,7 @@ mod init { }; use crate::commit::{Either, Info, ParentIds, Parents, Simple}; use gix_date::SecondsSinceUnixEpoch; + use gix_error::{CorruptionError, ErrorExt, ResultExt}; use gix_hash::{ObjectId, oid}; use gix_object::{CommitRefIter, FindExt}; use std::{cmp::Reverse, collections::VecDeque}; @@ -409,7 +389,10 @@ mod init { buf: &mut Vec, ) -> Result<(), Error> { let commit_iter = objects.find_commit_iter(&commit_id, buf)?; - let time = commit_iter.committer()?.seconds(); + let time = commit_iter + .committer() + .or_raise_erased(|| CorruptionError::new("A commit could not be decoded during traversal"))? + .seconds(); let key = to_queue_key(time, order); match (cutoff_time, order) { (Some(cutoff_time), _) if time >= cutoff_time => queue.insert(key, commit_id), @@ -594,11 +577,17 @@ mod init { ); } Ok(_unused_token) => break, - Err(err) => return Some(Err(err.into())), + Err(err) => { + return Some(Err(err + .and_raise(CorruptionError::new( + "A commit could not be decoded during traversal", + )) + .erased())); + } } } } - Err(err) => return Some(Err(err.into())), + Err(err) => return Some(Err(err)), } return Some(Ok(Info { @@ -659,11 +648,17 @@ mod init { } } Ok(_a_token_past_the_parents) => break, - Err(err) => return Some(Err(err.into())), + Err(err) => { + return Some(Err(err + .and_raise(CorruptionError::new( + "A commit could not be decoded during traversal", + )) + .erased())); + } } } } - Err(err) => return Some(Err(err.into())), + Err(err) => return Some(Err(err)), } return Some(Ok(Info { diff --git a/gix-traverse/src/commit/topo/init.rs b/gix-traverse/src/commit/topo/init.rs index 4da731b1726..b98331aa24d 100644 --- a/gix-traverse/src/commit/topo/init.rs +++ b/gix-traverse/src/commit/topo/init.rs @@ -1,5 +1,6 @@ use std::collections::HashSet; +use gix_error::{CorruptionError, OptionExt}; use gix_hash::{ObjectId, oid}; use gix_revwalk::{PriorityQueue, graph::IdMap}; @@ -176,7 +177,10 @@ where // the tips, but then in simplify_commit() Git is told to ignore it. For // now the tests pass. for id in self.tips.iter() { - let i = w.indegrees.get(id).ok_or(Error::MissingIndegreeUnexpected)?; + let i = w + .indegrees + .get(id) + .ok_or_raise_erased(|| CorruptionError::new("Indegree information is missing"))?; if *i != 1 { continue; diff --git a/gix-traverse/src/commit/topo/iter.rs b/gix-traverse/src/commit/topo/iter.rs index 8af085ea409..951e8c1628d 100644 --- a/gix-traverse/src/commit/topo/iter.rs +++ b/gix-traverse/src/commit/topo/iter.rs @@ -1,3 +1,4 @@ +use gix_error::{CorruptionError, ErrorExt}; use gix_hash::{ObjectId, oid}; use gix_revwalk::PriorityQueue; use smallvec::SmallVec; @@ -9,6 +10,14 @@ use crate::commit::{ pub(in crate::commit) type GenAndCommitTime = (u32, i64); +fn missing_indegree() -> Error { + CorruptionError::new("Indegree information is missing").raise_erased() +} + +fn missing_state() -> Error { + CorruptionError::new("Internal state (bitflags) not found").raise_erased() +} + // Git's priority queue works as a LIFO stack if no compare function is set, // which is the case for `--topo-order.` However, even in that case the initial // items of the queue are sorted according to the commit time before beginning @@ -72,7 +81,7 @@ where for (id, gen_time) in parents { self.indegrees.entry(id).and_modify(|e| *e += 1).or_insert(2); - let state = self.states.get_mut(&id).ok_or(Error::MissingStateUnexpected)?; + let state = self.states.get_mut(&id).ok_or_else(missing_state)?; if !state.contains(WalkFlags::InDegree) { *state |= WalkFlags::InDegree; self.indegree_queue.insert(gen_time, id); @@ -99,7 +108,7 @@ where self.process_parents(&id, &parents)?; for (id, gen_time) in parents { - let state = self.states.get_mut(&id).ok_or(Error::MissingStateUnexpected)?; + let state = self.states.get_mut(&id).ok_or_else(missing_state)?; if !state.contains(WalkFlags::Explored) { *state |= WalkFlags::Explored; @@ -115,7 +124,7 @@ where self.process_parents(id, &parents)?; for (pid, (parent_gen, parent_commit_time)) in parents { - let parent_state = self.states.get(&pid).ok_or(Error::MissingStateUnexpected)?; + let parent_state = self.states.get(&pid).ok_or_else(missing_state)?; if parent_state.contains(WalkFlags::Uninteresting) { continue; } @@ -125,7 +134,7 @@ where self.compute_indegrees_to_depth(self.min_gen)?; } - let i = self.indegrees.get_mut(&pid).ok_or(Error::MissingIndegreeUnexpected)?; + let i = self.indegrees.get_mut(&pid).ok_or_else(missing_indegree)?; *i -= 1; if *i != 1 { continue; @@ -147,7 +156,7 @@ where } fn process_parents(&mut self, id: &oid, parents: &[(ObjectId, GenAndCommitTime)]) -> Result<(), Error> { - let state = self.states.get_mut(id).ok_or(Error::MissingStateUnexpected)?; + let state = self.states.get_mut(id).ok_or_else(missing_state)?; if state.contains(WalkFlags::Added) { return Ok(()); } @@ -205,7 +214,7 @@ where let i = match self.indegrees.get_mut(&commit.id) { Some(i) => i, None => { - return Some(Err(Error::MissingIndegreeUnexpected)); + return Some(Err(missing_indegree())); } }; @@ -263,7 +272,11 @@ where } } Ok(_past_parents) => break, - Err(err) => return Err(err.into()), + Err(err) => { + return Err(err + .and_raise(CorruptionError::new("A commit could not be decoded during traversal")) + .erased()); + } } } // Need to check the cache again. That a commit is not in the cache @@ -312,7 +325,11 @@ pub(super) fn gen_and_commit_time(c: Either<'_, '_>) -> Result break, - Err(err) => return Err(err.into()), + Err(err) => { + return Err(err + .and_raise(CorruptionError::new("A commit could not be decoded during traversal")) + .erased()); + } } } Ok((gix_commitgraph::GENERATION_NUMBER_INFINITY, commit_time)) diff --git a/gix-traverse/src/commit/topo/mod.rs b/gix-traverse/src/commit/topo/mod.rs index e0fcc3698a2..7b8ec8adb10 100644 --- a/gix-traverse/src/commit/topo/mod.rs +++ b/gix-traverse/src/commit/topo/mod.rs @@ -3,24 +3,7 @@ use bitflags::bitflags; /// The errors that can occur during creation and iteration. -#[derive(thiserror::Error, Debug)] -#[expect(missing_docs)] -pub enum Error { - #[error("Indegree information is missing")] - MissingIndegreeUnexpected, - #[error("Internal state (bitflags) not found")] - MissingStateUnexpected, - #[error(transparent)] - ObjectDecode(#[from] gix_object::decode::Error), - #[error(transparent)] - Find(gix_error::Error), -} - -impl From for Error { - fn from(err: gix_object::find::existing_iter::Error) -> Self { - Error::Find(err.into_error()) - } -} +pub type Error = gix_error::Exn; bitflags! { /// Set of flags to describe the state of a particular commit while iterating. diff --git a/gix-traverse/src/tree/breadthfirst.rs b/gix-traverse/src/tree/breadthfirst.rs index 4df3bee8f27..cb7f30c65b8 100644 --- a/gix-traverse/src/tree/breadthfirst.rs +++ b/gix-traverse/src/tree/breadthfirst.rs @@ -4,22 +4,7 @@ use gix_hash::ObjectId; /// The error is part of the item returned by the [`breadthfirst()`](crate::tree::breadthfirst()) and ///[`depthfirst()`](crate::tree::depthfirst()) functions. -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] -pub enum Error { - #[error(transparent)] - Find(gix_error::Error), - #[error("The delegate cancelled the operation")] - Cancelled, - #[error(transparent)] - ObjectDecode(#[from] gix_object::decode::Error), -} - -impl From for Error { - fn from(err: gix_object::find::existing_iter::Error) -> Self { - Error::Find(err.into_error()) - } -} +pub type Error = gix_error::Exn; /// The state used and potentially shared by multiple tree traversals. #[derive(Default, Clone)] @@ -38,6 +23,7 @@ impl State { pub(super) mod function { use std::borrow::BorrowMut; + use gix_error::{CorruptionError, ErrorExt, ResultExt, message}; use gix_object::{FindExt, TreeRefIter}; use super::{Error, State}; @@ -54,8 +40,8 @@ pub(super) mod function { /// * `find` - a way to lookup new object data during traversal by their `ObjectId`, writing their data into buffer and returning /// an iterator over entries if the object is present and is a tree. Caching should be implemented within this function /// as needed. The return value is `Option` which degenerates all error information. Not finding a commit should also - /// be considered an errors as all objects in the tree DAG should be present in the database. Hence [`Error::Find`] should - /// be escalated into a more specific error if it's encountered by the caller. + /// be considered an error as all objects in the tree DAG should be present in the database. Hence lookup errors should + /// be escalated into a more specific error if encountered by the caller. /// * `delegate` - A way to observe entries and control the iteration while allowing the optimizer to let you pay only for what you use. pub fn breadthfirst( root: TreeRefIter<'_>, @@ -73,7 +59,8 @@ pub(super) mod function { let mut tree = root; loop { for entry in tree { - let entry = entry?; + let entry = + entry.or_raise_erased(|| CorruptionError::new("A tree could not be decoded during traversal"))?; if entry.mode.is_tree() { delegate.push_path_component(entry.filename); let action = delegate.visit_tree(&entry); @@ -85,13 +72,13 @@ pub(super) mod function { state.next.push_back(entry.oid.to_owned()); } std::ops::ControlFlow::Break(()) => { - return Err(Error::Cancelled); + return Err(message("The delegate cancelled the operation").raise_erased()); } } } else { delegate.push_path_component(entry.filename); if delegate.visit_nontree(&entry).is_break() { - return Err(Error::Cancelled); + return Err(message("The delegate cancelled the operation").raise_erased()); } } delegate.pop_path_component(); diff --git a/gix-traverse/src/tree/depthfirst.rs b/gix-traverse/src/tree/depthfirst.rs index 7c34f5d71ed..d111e1a5044 100644 --- a/gix-traverse/src/tree/depthfirst.rs +++ b/gix-traverse/src/tree/depthfirst.rs @@ -27,6 +27,7 @@ impl State { pub(super) mod function { use std::borrow::BorrowMut; + use gix_error::{CorruptionError, ResultExt}; use gix_hash::ObjectId; use gix_object::{FindExt, TreeRefIter}; @@ -75,7 +76,8 @@ pub(super) mod function { let mut iter = TreeRefIter::from_bytes(&buf[byte_offset_to_next_entry..], root.kind()); delegate.pop_back_tracked_path_and_set_current(); while let Some(entry) = iter.next() { - let entry = entry?; + let entry = entry + .or_raise_erased(|| CorruptionError::new("A tree could not be decoded during traversal"))?; if entry.mode.is_tree() { delegate.push_path_component(entry.filename); let res = delegate.visit_tree(&entry); diff --git a/gix-traverse/tests/traverse/commit/simple/mod.rs b/gix-traverse/tests/traverse/commit/simple/mod.rs index cf974898fe3..0d1e7eb99a2 100644 --- a/gix-traverse/tests/traverse/commit/simple/mod.rs +++ b/gix-traverse/tests/traverse/commit/simple/mod.rs @@ -18,14 +18,13 @@ fn traverse( hidden: impl IntoIterator, ) -> crate::Result> { let graph = commit_graph(odb.store_ref()); - Simple::new(tips, odb) + Ok(Simple::new(tips, odb) .sorting(sorting)? .parents(parents) .commit_graph(graph) .hide(hidden)? .map(|res| res.map(|info| info.id)) - .collect::, _>>() - .map_err(Into::into) + .collect::, _>>()?) } /// Run a traversal with both commit-graph enabled and disabled to ensure consistency. diff --git a/gix/src/ext/tree.rs b/gix/src/ext/tree.rs index dc357c2cc5b..c190b7d9057 100644 --- a/gix/src/ext/tree.rs +++ b/gix/src/ext/tree.rs @@ -12,12 +12,7 @@ pub trait TreeIterExt: Sealed { /// /// The `delegate` implements a way to store details about the traversal to allow paying only for what's actually used. /// Since it is expected to store the operation result, _unit_ is returned. - fn traverse( - &self, - state: StateMut, - objects: Find, - delegate: &mut V, - ) -> Result<(), breadthfirst::Error> + fn traverse(&self, state: StateMut, objects: Find, delegate: &mut V) -> Result<(), crate::Error> where Find: gix_object::Find, StateMut: BorrowMut, @@ -27,18 +22,13 @@ pub trait TreeIterExt: Sealed { impl Sealed for TreeRefIter<'_> {} impl TreeIterExt for TreeRefIter<'_> { - fn traverse( - &self, - state: StateMut, - objects: Find, - delegate: &mut V, - ) -> Result<(), breadthfirst::Error> + fn traverse(&self, state: StateMut, objects: Find, delegate: &mut V) -> Result<(), crate::Error> where Find: gix_object::Find, StateMut: BorrowMut, V: gix_traverse::tree::Visit, { - breadthfirst(*self, state, objects, delegate) + breadthfirst(*self, state, objects, delegate).map_err(gix_error::Exn::into_error) } } diff --git a/gix/src/object/tree/traverse.rs b/gix/src/object/tree/traverse.rs index 444bd859a2e..eb84c2d8752 100644 --- a/gix/src/object/tree/traverse.rs +++ b/gix/src/object/tree/traverse.rs @@ -28,7 +28,7 @@ pub struct BreadthFirstPresets<'a, 'repo> { impl BreadthFirstPresets<'_, '_> { /// Returns all entries and their file paths, recursively, as reachable from this tree. - pub fn files(&self) -> Result, gix_traverse::tree::breadthfirst::Error> { + pub fn files(&self) -> Result, crate::Error> { let mut recorder = gix_traverse::tree::Recorder::default(); Platform { root: self.root, @@ -46,13 +46,14 @@ impl Platform<'_, '_> { /// /// - Results are returned in sort order as per tree-sorting rules, files first, then directories, one level at a time. /// - for obtaining the direct children of the tree, use [Tree::iter()] instead. - pub fn breadthfirst(&self, delegate: &mut V) -> Result<(), gix_traverse::tree::breadthfirst::Error> + pub fn breadthfirst(&self, delegate: &mut V) -> Result<(), crate::Error> where V: gix_traverse::tree::Visit, { let root = gix_object::TreeRefIter::from_bytes(&self.root.data, self.root.id.kind()); let state = gix_traverse::tree::breadthfirst::State::default(); gix_traverse::tree::breadthfirst(root, state, &self.root.repo.objects, delegate) + .map_err(gix_error::Exn::into_error) } /// Start a depth-first, recursive traversal using `delegate`, for which a [`Recorder`](gix_traverse::tree::Recorder) can be used to get started. @@ -60,11 +61,12 @@ impl Platform<'_, '_> { /// # Note /// /// For obtaining the direct children of the tree, use [Tree::iter()] instead. - pub fn depthfirst(&self, delegate: &mut V) -> Result<(), gix_traverse::tree::breadthfirst::Error> + pub fn depthfirst(&self, delegate: &mut V) -> Result<(), crate::Error> where V: gix_traverse::tree::Visit, { let state = gix_traverse::tree::depthfirst::State::default(); gix_traverse::tree::depthfirst(self.root.id, state, &self.root.repo.objects, delegate) + .map_err(gix_error::Exn::into_error) } } diff --git a/gix/src/revision/walk.rs b/gix/src/revision/walk.rs index 54dc8f5a5f2..4d48c8041dd 100644 --- a/gix/src/revision/walk.rs +++ b/gix/src/revision/walk.rs @@ -9,13 +9,19 @@ use crate::{Repository, ext::ObjectIdExt, revision}; #[expect(missing_docs)] pub enum Error { #[error(transparent)] - SimpleTraversal(#[from] gix_traverse::commit::simple::Error), + SimpleTraversal(crate::Error), #[error(transparent)] ShallowCommits(#[from] crate::Error), #[error(transparent)] ConfigBoolean(#[from] crate::config::boolean::Error), } +impl From for Error { + fn from(err: gix_traverse::commit::simple::Error) -> Self { + Error::SimpleTraversal(err.into_error()) + } +} + /// Specify how to sort commits during a [revision::Walk] traversal. /// /// ### Sample History @@ -351,7 +357,13 @@ pub mod iter { #[expect(missing_docs)] pub enum Error { #[error(transparent)] - SimpleTraversal(#[from] gix_traverse::commit::simple::Error), + SimpleTraversal(crate::Error), + } + + impl From for Error { + fn from(err: gix_traverse::commit::simple::Error) -> Self { + Error::SimpleTraversal(err.into_error()) + } } } From 35a6e52584b90f6ad5bec319c49ea8e500cd1dc6 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 20 Aug 2026 17:10:48 +0200 Subject: [PATCH 10/51] change(gix-discover)!: migrate errors to gix-error rubberstamp --- Cargo.lock | 2 +- gitoxide-core/src/discover.rs | 2 +- gix-discover/Cargo.toml | 2 +- gix-discover/src/is.rs | 45 ++++--- gix-discover/src/lib.rs | 32 +---- gix-discover/src/parse.rs | 20 ++- gix-discover/src/path.rs | 14 +-- gix-discover/src/upwards/mod.rs | 119 +++++++++++------- gix-discover/src/upwards/types.rs | 31 +---- gix-discover/tests/discover/is_git.rs | 4 + gix-discover/tests/discover/parse.rs | 34 ++--- .../tests/discover/upwards/ceiling_dirs.rs | 19 ++- gix-discover/tests/discover/upwards/mod.rs | 5 +- gix-discover/tests/isolated.rs | 7 +- gix/src/discover.rs | 14 ++- gix/src/open/mod.rs | 5 +- gix/src/open/repository.rs | 16 ++- gix/src/remote/connect.rs | 4 +- gix/src/submodule/errors.rs | 2 +- gix/src/submodule/mod.rs | 2 +- 20 files changed, 179 insertions(+), 200 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3a4c1e7f3c2..7690d676125 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1868,6 +1868,7 @@ dependencies = [ "bstr", "defer", "dunce", + "gix-error", "gix-fs", "gix-path", "gix-ref", @@ -1876,7 +1877,6 @@ dependencies = [ "is_ci", "serial_test", "tempfile", - "thiserror 2.0.18", ] [[package]] diff --git a/gitoxide-core/src/discover.rs b/gitoxide-core/src/discover.rs index d1ab7d3d23c..aff67c84456 100644 --- a/gitoxide-core/src/discover.rs +++ b/gitoxide-core/src/discover.rs @@ -22,7 +22,7 @@ pub fn discover(repo: &Path, mut out: impl std::io::Write) -> anyhow::Result<()> writeln!(out)?; writeln!(out, "discover (plumbing) from {}:", repo.display())?; - has_err |= print_result(&mut out, gix::discover::upwards(repo))?; + has_err |= print_result(&mut out, gix::discover::upwards(repo).map_err(gix::Exn::into_error))?; if has_err { writeln!(out)?; diff --git a/gix-discover/Cargo.toml b/gix-discover/Cargo.toml index 77ea827d23d..9a356ff4a51 100644 --- a/gix-discover/Cargo.toml +++ b/gix-discover/Cargo.toml @@ -25,9 +25,9 @@ gix-sec = { version = "^0.14.2", path = "../gix-sec" } gix-path = { version = "^0.12.6", path = "../gix-path" } gix-ref = { version = "^0.67.0", path = "../gix-ref" } gix-fs = { version = "^0.22.1", path = "../gix-fs" } +gix-error = { version = "^0.3.0", path = "../gix-error" } bstr = { version = "1.12.0", default-features = false, features = ["std", "unicode"] } -thiserror = "2.0.18" [target.'cfg(windows)'.dependencies] dunce = "1.0.3" diff --git a/gix-discover/src/is.rs b/gix-discover/src/is.rs index b9d3eaf5280..f16330716ad 100644 --- a/gix-discover/src/is.rs +++ b/gix-discover/src/is.rs @@ -1,5 +1,7 @@ use std::{borrow::Cow, ffi::OsStr, path::Path}; +use gix_error::{CorruptionError, ErrorExt, NotFoundError, ResultExt, message}; + use crate::DOT_GIT_DIR; use crate::path::RepositoryKind; @@ -29,12 +31,12 @@ pub fn submodule_git_dir(git_dir: &Path) -> bool { /// /// This obtains filesystem metadata for `git_dir` before checking its repository layout. pub fn git(git_dir: &Path) -> Result { - let git_dir_metadata = git_dir.metadata().map_err(|err| crate::is_git::Error::Metadata { - source: err, - path: git_dir.into(), - })?; + let git_dir_metadata = git_dir + .metadata() + .or_raise_erased(|| gix_error::message!("Could not retrieve metadata of \"{}\"", git_dir.display()))?; // precompose-unicode can't be known here, so we just default it to false, hoping it won't matter. - let cwd = gix_fs::current_dir(false)?; + let cwd = gix_fs::current_dir(false) + .or_raise_erased(|| message("Could not obtain current directory for resolving the '.' repository path"))?; git_with_metadata(git_dir, &git_dir_metadata, &cwd) } @@ -52,7 +54,7 @@ pub(crate) fn git_with_metadata( } let dot_git = if git_dir_metadata.is_file() { - let private_git_dir = crate::path::from_gitdir_file(git_dir)?; + let private_git_dir = crate::path::from_gitdir_file(git_dir).or_erased()?; Cow::Owned(private_git_dir) } else { Cow::Borrowed(git_dir) @@ -61,7 +63,7 @@ pub(crate) fn git_with_metadata( { // Fast-path: avoid doing the complete search if HEAD is already not there. if !dot_git.join("HEAD").exists() { - return Err(crate::is_git::Error::MissingHead); + return Err(NotFoundError::new("Missing HEAD at '.git/HEAD'").raise_erased()); } // We expect to be able to parse any ref-hash, so we shouldn't have to know the repos hash here. // With ref-table, the hash is probably stored as part of the ref-db itself, so we can handle it from there. @@ -70,9 +72,11 @@ pub(crate) fn git_with_metadata( match refs.find_loose("HEAD") { Ok(head) => { if head.name.as_bstr() != "HEAD" { - return Err(crate::is_git::Error::MisplacedHead { - name: head.name.into_inner(), - }); + return Err(CorruptionError::new(format!( + "Expected HEAD at '.git/HEAD', got '.git/{}'", + head.name + )) + .raise_erased()); } } Err(gix_ref::file::find::existing::Error::Find(gix_ref::file::find::Error::ReferenceCreation { @@ -82,7 +86,7 @@ pub(crate) fn git_with_metadata( // It's fine as long as the reference is found is `HEAD`. } Err(err) => { - return Err(err.into()); + return Err(err.and_raise(message("Could not find a valid HEAD reference")).erased()); } } } @@ -91,10 +95,12 @@ pub(crate) fn git_with_metadata( let common_dir = dot_git.join("commondir"); match crate::path::from_plain_file(&common_dir) { Some(Err(err)) => { - return Err(crate::is_git::Error::MissingCommonDir { - missing: common_dir, - source: err, - }); + return Err(err + .and_raise(gix_error::message!( + "The worktree's private repo's commondir file at '{}' is missing or could not be read", + common_dir.display() + )) + .erased()); } Some(Ok(common_dir)) => { let common_dir = dot_git.join(common_dir); @@ -123,13 +129,18 @@ pub(crate) fn git_with_metadata( { let objects_path = common_dir.join("objects"); if !objects_path.is_dir() { - return Err(crate::is_git::Error::MissingObjectsDirectory { missing: objects_path }); + return Err( + NotFoundError::new(format!("Expected an objects directory at '{}'", objects_path.display())) + .raise_erased(), + ); } } { let refs_path = common_dir.join("refs"); if !refs_path.is_dir() { - return Err(crate::is_git::Error::MissingRefsDirectory { missing: refs_path }); + return Err( + NotFoundError::new(format!("Expected a refs directory at '{}'", refs_path.display())).raise_erased(), + ); } } Ok(match kind { diff --git a/gix-discover/src/lib.rs b/gix-discover/src/lib.rs index bc64e8da35a..9ece748da6d 100644 --- a/gix-discover/src/lib.rs +++ b/gix-discover/src/lib.rs @@ -17,7 +17,8 @@ //! # )?; //! # let nested = dir.path().join("src").join("module"); //! # std::fs::create_dir_all(&nested)?; -//! let (path, _trust) = gix_discover::upwards(&nested)?; +//! let (path, _trust) = +//! gix_discover::upwards(&nested).map_err(gix_discover::upwards::Error::into_error)?; //! let (repository_dir, worktree_dir) = path.into_repository_and_work_tree_directories(); //! //! assert_eq!(repository_dir, git_dir); @@ -39,35 +40,8 @@ pub mod repository; /// pub mod is_git { - use std::path::PathBuf; - /// The error returned by [`crate::is_git()`]. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error("Could not find a valid HEAD reference")] - FindHeadRef(#[from] gix_ref::file::find::existing::Error), - #[error("Missing HEAD at '.git/HEAD'")] - MissingHead, - #[error("Expected HEAD at '.git/HEAD', got '.git/{}'", .name)] - MisplacedHead { name: bstr::BString }, - #[error("Expected an objects directory at '{}'", .missing.display())] - MissingObjectsDirectory { missing: PathBuf }, - #[error("The worktree's private repo's commondir file at '{}' or it could not be read", .missing.display())] - MissingCommonDir { missing: PathBuf, source: std::io::Error }, - #[error("Expected a refs directory at '{}'", .missing.display())] - MissingRefsDirectory { missing: PathBuf }, - #[error(transparent)] - GitFile(#[from] crate::path::from_gitdir_file::Error), - #[error("Could not retrieve metadata of \"{path}\"")] - Metadata { source: std::io::Error, path: PathBuf }, - #[error( - "The repository's config file doesn't exist or didn't have a 'bare' configuration or contained core.worktree without value" - )] - Inconclusive, - #[error("Could not obtain current directory for resolving the '.' repository path")] - CurrentDir(#[from] std::io::Error), - } + pub type Error = gix_error::Exn; } mod is; diff --git a/gix-discover/src/parse.rs b/gix-discover/src/parse.rs index 38c6fd4b17d..c2f635fdb6d 100644 --- a/gix-discover/src/parse.rs +++ b/gix-discover/src/parse.rs @@ -4,30 +4,24 @@ use bstr::ByteSlice; /// pub mod gitdir { - use bstr::BString; - /// The error returned by [`parse::gitdir()`][super::gitdir()]. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error("Format should be 'gitdir: ', but got: {:?}", .input)] - InvalidFormat { input: BString }, - #[error("Couldn't decode {:?} as UTF8", .input)] - IllformedUtf8 { input: BString }, - } + pub type Error = gix_error::ValidationError; } /// Parse typical `gitdir` files as seen in worktrees and submodules. pub fn gitdir(input: &[u8]) -> Result { let path = input .strip_prefix(b"gitdir: ") - .ok_or_else(|| gitdir::Error::InvalidFormat { input: input.into() })? + .ok_or_else(|| gitdir::Error::new_with_input("Format should be 'gitdir: ', but got", input))? .as_bstr(); let path = path.trim_end().as_bstr(); if path.is_empty() { - return Err(gitdir::Error::InvalidFormat { input: input.into() }); + return Err(gitdir::Error::new_with_input( + "Format should be 'gitdir: ', but got", + input, + )); } Ok(gix_path::try_from_bstr(path) - .map_err(|_| gitdir::Error::IllformedUtf8 { input: input.into() })? + .map_err(|_| gitdir::Error::new_with_input("Couldn't decode input as UTF8", input))? .into_owned()) } diff --git a/gix-discover/src/path.rs b/gix-discover/src/path.rs index 76e5c75cb19..14eee428b5d 100644 --- a/gix-discover/src/path.rs +++ b/gix-discover/src/path.rs @@ -1,4 +1,5 @@ use crate::{DOT_GIT_DIR, MODULES}; +use gix_error::ResultExt; use std::ffi::OsStr; use std::path::Path; use std::{io::Read, path::PathBuf}; @@ -17,14 +18,7 @@ pub enum RepositoryKind { /// pub mod from_gitdir_file { /// The error returned by [`from_gitdir_file()`][crate::path::from_gitdir_file()]. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - Io(#[from] std::io::Error), - #[error(transparent)] - Parse(#[from] crate::parse::gitdir::Error), - } + pub type Error = gix_error::Exn; } fn read_regular_file_content_with_size_limit(path: &std::path::Path) -> std::io::Result> { @@ -132,8 +126,8 @@ pub fn from_plain_file_relative_to_file(path: &std::path::Path) -> Option Result { - let buf = read_regular_file_content_with_size_limit(path)?; - let mut gitdir = crate::parse::gitdir(&buf)?; + let buf = read_regular_file_content_with_size_limit(path).or_erased()?; + let mut gitdir = crate::parse::gitdir(&buf).or_erased()?; if let Some(parent) = path.parent() { gitdir = parent.join(gitdir); } diff --git a/gix-discover/src/upwards/mod.rs b/gix-discover/src/upwards/mod.rs index bc2a37a8313..1376fed11d0 100644 --- a/gix-discover/src/upwards/mod.rs +++ b/gix-discover/src/upwards/mod.rs @@ -11,6 +11,7 @@ pub(crate) mod function { path::{Path, PathBuf}, }; + use gix_error::{ErrorExt, NotFoundError, OptionExt, ResultExt, ValidationError, message}; use gix_sec::Trust; use super::{Error, Options, TrustPolicy}; @@ -90,8 +91,12 @@ pub(crate) mod function { } else { self.current.as_ref() }; - self.current_metadata = Some(path.metadata().map_err(|_| Error::InaccessibleDirectory { - path: self.current.clone(), + let inaccessible_path = self.current.clone(); + self.current_metadata = Some(path.metadata().or_raise_erased(|| { + gix_error::message!( + "Failed to access a directory, or path is not a directory: '{}'", + inaccessible_path.display() + ) })?); } Ok(self @@ -212,20 +217,25 @@ 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 that contain '..'.) - let cwd = current_dir.map_or_else( - || { - // The paths we return are relevant to the repository, but at this time it's impossible to know - // what `core.precomposeUnicode` is going to be. Hence, the one using these paths will have to - // transform the paths as needed, because we can't. `false` means to leave the obtained path as is. - gix_fs::current_dir(false).map(Cow::Owned) - }, - |cwd| Ok(Cow::Borrowed(cwd)), - )?; + let cwd = current_dir + .map_or_else( + || { + // The paths we return are relevant to the repository, but at this time it's impossible to know + // what `core.precomposeUnicode` is going to be. Hence, the one using these paths will have to + // transform the paths as needed, because we can't. `false` means to leave the obtained path as is. + gix_fs::current_dir(false).map(Cow::Owned) + }, + |cwd| Ok(Cow::Borrowed(cwd)), + ) + .or_raise_erased(|| message("Could not obtain the current working directory"))?; #[cfg(windows)] let directory = dunce::simplified(directory); let logical = gix_path::normalize(directory.into(), cwd.as_ref()) - .ok_or_else(|| Error::InvalidInput { - directory: directory.into(), + .ok_or_raise_erased(|| { + ValidationError::new(format!( + "Relative path \"{}\" tries to reach beyond root filesystem", + directory.display() + )) })? .into_owned(); let directory_to_access = if directory.is_absolute() { @@ -233,12 +243,19 @@ pub(crate) mod function { } else { Cow::Owned(cwd.join(directory)) }; - let dir_metadata = directory_to_access - .metadata() - .map_err(|_| Error::InaccessibleDirectory { path: logical.clone() })?; + let dir_metadata = directory_to_access.metadata().or_raise_erased(|| { + gix_error::message!( + "Failed to access a directory, or path is not a directory: '{}'", + logical.display() + ) + })?; if !dir_metadata.is_dir() { - return Err(Error::InaccessibleDirectory { path: logical }); + return Err(ValidationError::new(format!( + "Failed to access a directory, or path is not a directory: '{}'", + logical.display() + )) + .raise_erased()); } #[cfg(unix)] let initial_device = device_id(&dir_metadata); @@ -247,8 +264,9 @@ pub(crate) mod function { let filter_by_trust = |dir: &Path| -> Result, Error> { match trust { TrustPolicy::Required(required) => { - let trust = - Trust::from_path_ownership(dir).map_err(|err| Error::CheckTrust { path: dir.into(), err })?; + let trust = Trust::from_path_ownership(dir).or_raise_erased(|| { + gix_error::message!("Could not determine trust level for path '{}'.", dir.display()) + })?; Ok(if trust >= required { Ok(trust) } else { Err(required) }) } TrustPolicy::Assume(trust) => Ok(Ok(trust)), @@ -272,7 +290,10 @@ pub(crate) mod function { cwd.as_ref(), ); if max_height.is_none() && match_ceiling_dir_or_error { - return Err(Error::NoMatchingCeilingDir); + return Err(ValidationError::new( + "None of the passed ceiling directories prefixed the git-dir candidate, making them ineffective.", + ) + .raise_erased()); } max_height } else { @@ -282,28 +303,32 @@ pub(crate) mod function { let mut height = 0; 'outer: loop { if max_height.is_some_and(|max| height > max) { - return Err(Error::NoGitRepositoryWithinCeiling { - path: search.logical, - ceiling_height: height, - }); + return Err(NotFoundError::new(format!( + "Could not find a git repository in '{}' or in any of its parents within ceiling height of {height}", + search.logical.display() + )) + .raise_erased()); } #[cfg(unix)] if !cross_fs && device_id(search.metadata()?) != initial_device { - return Err(Error::NoGitRepositoryWithinFs { - path: search.logical, - limit: search.current, - }); + return Err(NotFoundError::new(format!( + "Could not find a git repository in '{}' or in any of its parents within device limits below '{}'", + search.logical.display(), + search.current.display() + )) + .raise_erased()); } if let Some((kind, appended_dot_git)) = search.probe_repository(cwd.as_ref(), dot_git_only) { match filter_by_trust(&search.current)? { - Err(required) => { - break 'outer Err(Error::NoTrustedGitRepository { - path: search.logical, - candidate: search.current, - required, - }); + Err(_) => { + break 'outer Err(NotFoundError::new(format!( + "Could not find a trusted git repository in '{}' or in any of its parents, candidate at '{}' discarded", + search.logical.display(), + search.current.display() + )) + .raise_erased()); } Ok(trust) => { let cursor = search.into_candidate(cwd.as_ref(), appended_dot_git); @@ -315,11 +340,14 @@ pub(crate) mod function { cursor }; break 'outer Ok(( - crate::repository::Path::from_dot_git_dir(path, kind, cwd.as_ref()).ok_or_else(|| { - Error::InvalidInput { - directory: directory.into(), - } - })?, + crate::repository::Path::from_dot_git_dir(path, kind, cwd.as_ref()).ok_or_raise_erased( + || { + ValidationError::new(format!( + "Relative path \"{}\" tries to reach beyond root filesystem", + directory.display() + )) + }, + )?, trust, )); } @@ -338,15 +366,22 @@ pub(crate) mod function { search.current.components().next(), Some(std::path::Component::RootDir | std::path::Component::Prefix(_)) ) { - break Err(Error::NoGitRepository { path: search.logical }); + break Err(NotFoundError::new(format!( + "Could not find a git repository in '{}' or in any of its parents", + search.logical.display() + )) + .raise_erased()); } else { debug_assert!( !search.current.as_os_str().is_empty(), "only a non-empty relative cursor can require normalization after ascent stalls" ); let current = gix_path::normalize(search.current.clone().into(), cwd.as_ref()) - .ok_or_else(|| Error::InvalidInput { - directory: search.current.clone(), + .ok_or_raise_erased(|| { + ValidationError::new(format!( + "Relative path \"{}\" tries to reach beyond root filesystem", + search.current.display() + )) })? .into_owned(); search.set_current(current); diff --git a/gix-discover/src/upwards/types.rs b/gix-discover/src/upwards/types.rs index d4754292aad..150d1e689c0 100644 --- a/gix-discover/src/upwards/types.rs +++ b/gix-discover/src/upwards/types.rs @@ -1,36 +1,7 @@ use std::{env, ffi::OsStr, path::PathBuf}; /// The error returned by [`gix_discover::upwards()`][crate::upwards()]. -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] -pub enum Error { - #[error("Could not obtain the current working directory")] - CurrentDir(#[from] std::io::Error), - #[error("Relative path \"{}\"tries to reach beyond root filesystem", directory.display())] - InvalidInput { directory: PathBuf }, - #[error("Failed to access a directory, or path is not a directory: '{}'", .path.display())] - InaccessibleDirectory { path: PathBuf }, - #[error("Could not find a git repository in '{}' or in any of its parents", .path.display())] - NoGitRepository { path: PathBuf }, - #[error("Could not find a git repository in '{}' or in any of its parents within ceiling height of {}", .path.display(), .ceiling_height)] - NoGitRepositoryWithinCeiling { path: PathBuf, ceiling_height: usize }, - #[error("Could not find a git repository in '{}' or in any of its parents within device limits below '{}'", .path.display(), .limit.display())] - NoGitRepositoryWithinFs { path: PathBuf, limit: PathBuf }, - #[error("None of the passed ceiling directories prefixed the git-dir candidate, making them ineffective.")] - NoMatchingCeilingDir, - #[error("Could not 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: gix_sec::Trust, - }, - #[error("Could not determine trust level for path '{}'.", .path.display())] - CheckTrust { - path: PathBuf, - #[source] - err: std::io::Error, - }, -} +pub type Error = gix_error::Exn; /// How to obtain the trust level for a discovered repository. #[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] diff --git a/gix-discover/tests/discover/is_git.rs b/gix-discover/tests/discover/is_git.rs index 52b9ba735bb..92339e38809 100644 --- a/gix-discover/tests/discover/is_git.rs +++ b/gix-discover/tests/discover/is_git.rs @@ -109,6 +109,10 @@ fn non_bare_repo_with_git_extension_is_not_a_worktree() -> crate::Result { "Missing HEAD at '.git/HEAD'", "repo.git isn't a .git directory after all" ); + assert!( + err.downcast_any_ref::().is_some(), + "a missing repository marker is classified as not found" + ); Ok(()) } diff --git a/gix-discover/tests/discover/parse.rs b/gix-discover/tests/discover/parse.rs index ec2f80f844b..b8d5fb9ef79 100644 --- a/gix-discover/tests/discover/parse.rs +++ b/gix-discover/tests/discover/parse.rs @@ -18,25 +18,17 @@ fn valid() -> crate::Result { #[test] fn invalid() { - assert!( - matches!( - parse::gitdir(b"gitdir:"), - Err(parse::gitdir::Error::InvalidFormat { .. }) - ), - "missing prefix" - ); - assert!( - matches!( - parse::gitdir(b"bogus: foo"), - Err(parse::gitdir::Error::InvalidFormat { .. }) - ), - "invalid prefix" - ); - assert!( - matches!( - parse::gitdir(b"gitdir: "), - Err(parse::gitdir::Error::InvalidFormat { .. }) - ), - "empty path" - ); + for (input, reason) in [ + (b"gitdir:".as_slice(), "missing prefix"), + (b"bogus: foo".as_slice(), "invalid prefix"), + (b"gitdir: ".as_slice(), "empty path"), + ] { + let err = parse::gitdir(input).expect_err(reason); + assert_eq!( + err.input.as_ref().map(|input| input.as_slice()), + Some(input), + "{reason}" + ); + assert_eq!(err.message, "Format should be 'gitdir: ', but got", "{reason}"); + } } diff --git a/gix-discover/tests/discover/upwards/ceiling_dirs.rs b/gix-discover/tests/discover/upwards/ceiling_dirs.rs index 426627c50f3..8c9eb9b5ac6 100644 --- a/gix-discover/tests/discover/upwards/ceiling_dirs.rs +++ b/gix-discover/tests/discover/upwards/ceiling_dirs.rs @@ -67,7 +67,7 @@ fn discovery_fails_if_we_require_a_matching_ceiling_dir_but_are_standing_on_it() .unwrap_err(); assert!( - matches!(err, gix_discover::upwards::Error::NoMatchingCeilingDir), + err.downcast_any_ref::().is_some(), "since standing on the ceiling dir doesn't match it, we get exactly the semantically correct error" ); Ok(()) @@ -86,10 +86,8 @@ fn ceiling_dir_limits_are_respected_and_prevent_discovery() -> crate::Result { }, ) .expect_err("ceiling dir prevents discovery as it ends on level too early, and they are also absolutized"); - assert!(matches!( - err, - gix_discover::upwards::Error::NoGitRepositoryWithinCeiling { ceiling_height: 5, .. } - )); + assert!(err.downcast_any_ref::().is_some()); + assert!(err.to_string().contains("ceiling height of 5")); Ok(()) } @@ -129,10 +127,8 @@ fn more_restrictive_ceiling_dirs_overrule_less_restrictive_ones() -> crate::Resu }, ) .expect_err("more restrictive ceiling dirs overrule less restrictive ones"); - assert!(matches!( - err, - gix_discover::upwards::Error::NoGitRepositoryWithinCeiling { ceiling_height: 5, .. } - )); + assert!(err.downcast_any_ref::().is_some()); + assert!(err.to_string().contains("ceiling height of 5")); Ok(()) } @@ -172,8 +168,9 @@ fn no_matching_ceiling_dirs_errors_by_default() -> crate::Result { }, ); + let err = res.expect_err("an unrelated ceiling directory cannot match"); assert!( - matches!(res, Err(gix_discover::upwards::Error::NoMatchingCeilingDir)), + err.downcast_any_ref::().is_some(), "the canonicalized ceiling dir doesn't have the same root as the git dir candidate, and can never match." ); Ok(()) @@ -223,7 +220,7 @@ fn ceiling_dirs_limit_the_physical_symlink_target() -> crate::Result { .expect_err("the physical ceiling prevents discovery of the repository above it"); assert!( - matches!(err, gix_discover::upwards::Error::NoGitRepositoryWithinCeiling { .. }), + err.downcast_any_ref::().is_some(), "the symlink target matches the ceiling before traversal reaches the repository" ); Ok(()) diff --git a/gix-discover/tests/discover/upwards/mod.rs b/gix-discover/tests/discover/upwards/mod.rs index 00846def671..615c8522abb 100644 --- a/gix-discover/tests/discover/upwards/mod.rs +++ b/gix-discover/tests/discover/upwards/mod.rs @@ -470,10 +470,7 @@ fn cross_fs() -> crate::Result { let res = gix_discover::upwards(&top_level_repo.path().join("remote")) .expect_err("the cross-fs option should prevent us from discovering the repo"); - assert!(matches!( - res, - gix_discover::upwards::Error::NoGitRepositoryWithinFs { .. } - )); + assert!(res.downcast_any_ref::().is_some()); let (repo_path, _trust) = gix_discover::upwards_opts( &top_level_repo.path().join("remote"), diff --git a/gix-discover/tests/isolated.rs b/gix-discover/tests/isolated.rs index 4841c866022..6a1bde0f1ac 100644 --- a/gix-discover/tests/isolated.rs +++ b/gix-discover/tests/isolated.rs @@ -146,12 +146,9 @@ fn upwards_with_relative_directories_and_optional_ceiling() -> gix_testtools::Re .unwrap_err(); if search_dir.parent() == Some(".".as_ref()) || search_dir.parent() == Some("".as_ref()) { - assert!(matches!(err, gix_discover::upwards::Error::NoMatchingCeilingDir)); + assert!(err.downcast_any_ref::().is_some()); } else { - assert!(matches!( - err, - gix_discover::upwards::Error::NoGitRepositoryWithinCeiling { .. } - )); + assert!(err.downcast_any_ref::().is_some()); } } diff --git a/gix/src/discover.rs b/gix/src/discover.rs index 37e28fa98ca..f529e2e1d24 100644 --- a/gix/src/discover.rs +++ b/gix/src/discover.rs @@ -1,6 +1,8 @@ #![allow(clippy::result_large_err)] use std::path::Path; +use gix_error::{ErrorExt, message}; + pub use gix_discover::*; use crate::{ThreadSafeRepository, bstr::BString}; @@ -10,7 +12,7 @@ use crate::{ThreadSafeRepository, bstr::BString}; #[expect(missing_docs)] pub enum Error { #[error(transparent)] - Discover(#[from] upwards::Error), + Discover(#[from] gix_error::Error), #[error(transparent)] Open(#[from] crate::open::Error), } @@ -36,12 +38,18 @@ impl ThreadSafeRepository { trust_map: gix_sec::trust::Mapping, ) -> Result { let _span = gix_trace::coarse!("ThreadSafeRepository::discover()"); - let (path, trust) = upwards_opts(directory.as_ref(), options)?; + let (path, trust) = + upwards_opts(directory.as_ref(), options).map_err(|err| Error::Discover(err.into_error()))?; let (git_dir, worktree_dir) = path.into_repository_and_work_tree_directories(); let mut options = trust_map.into_value_by_level(trust); options.git_dir_trust = trust.into(); // Note that we will adjust the `current_dir` later so it matches the value of `core.precomposeUnicode`. - options.current_dir = Some(gix_fs::current_dir(false).map_err(upwards::Error::CurrentDir)?); + options.current_dir = Some(gix_fs::current_dir(false).map_err(|err| { + Error::Discover( + err.and_raise(message("Could not obtain the current working directory")) + .into_error(), + ) + })?); Self::open_from_paths(git_dir, worktree_dir, options, None).map_err(Into::into) } diff --git a/gix/src/open/mod.rs b/gix/src/open/mod.rs index 9b15d26e471..4de9a4e72fb 100644 --- a/gix/src/open/mod.rs +++ b/gix/src/open/mod.rs @@ -57,10 +57,7 @@ pub enum Error { #[error("Failed to load the git configuration")] Config(#[from] config::Error), #[error("\"{path}\" does not appear to be a git repository")] - NotARepository { - source: gix_discover::is_git::Error, - path: PathBuf, - }, + NotARepository { source: gix_error::Error, path: PathBuf }, #[error(transparent)] Io(#[from] std::io::Error), #[error("The git directory at '{}' is considered unsafe as it's not owned by the current user.", .path.display())] diff --git a/gix/src/open/repository.rs b/gix/src/open/repository.rs index adbe89f70f9..86ff58dd5d1 100644 --- a/gix/src/open/repository.rs +++ b/gix/src/open/repository.rs @@ -83,13 +83,21 @@ impl ThreadSafeRepository { Ok(kind) => (candidate, kind), Err(_) => match gix_discover::is_git(&path) { Ok(kind) => (path, kind), - Err(err) => return Err(Error::NotARepository { source: err, path }), + Err(err) => { + return Err(Error::NotARepository { + source: err.into_error(), + path, + }); + } }, }, None => match gix_discover::is_git(&path) { Ok(kind) => (path, kind), Err(err) => { - return Err(Error::NotARepository { source: err, path }); + return Err(Error::NotARepository { + source: err.into_error(), + path, + }); } }, } @@ -129,7 +137,7 @@ impl ThreadSafeRepository { let (path, path_kind): (PathBuf, _) = match overrides.git_dir { Some(git_dir) => gix_discover::is_git(&git_dir) .map_err(|err| Error::NotARepository { - source: err, + source: err.into_error(), path: git_dir.clone(), }) .map(|kind| (git_dir, kind))?, @@ -137,7 +145,7 @@ impl ThreadSafeRepository { let fallback_directory = fallback_directory.into(); gix_discover::is_git(&fallback_directory) .map_err(|err| Error::NotARepository { - source: err, + source: err.into_error(), path: fallback_directory.clone(), }) .map(|kind| (fallback_directory, kind))? diff --git a/gix/src/remote/connect.rs b/gix/src/remote/connect.rs index dd970b1f7b8..8083c0a4841 100644 --- a/gix/src/remote/connect.rs +++ b/gix/src/remote/connect.rs @@ -35,7 +35,7 @@ mod error { UnknownProtocol { source: config::key::GenericErrorWithValue }, #[error("Could not verify that \"{}\" url is a valid git directory before attempting to use it", url.to_bstring())] FileUrl { - source: Box, + source: Box, url: gix_url::Url, }, } @@ -122,7 +122,7 @@ impl<'repo> Remote<'repo> { gix_discover::is_git(dir.as_ref()) }) .map_err(|err| Error::FileUrl { - source: err.into(), + source: err.into_error().into(), url: url.clone(), })?; let (git_dir, _work_dir) = gix_discover::repository::Path::from_dot_git_dir( diff --git a/gix/src/submodule/errors.rs b/gix/src/submodule/errors.rs index dc4ba4ce162..3cc92718bbc 100644 --- a/gix/src/submodule/errors.rs +++ b/gix/src/submodule/errors.rs @@ -96,7 +96,7 @@ pub mod git_dir_try_old_form { gitdir_file: std::path::PathBuf, target: Option, #[source] - source: Option, + source: Option, }, #[error(transparent)] GitDir(#[from] gix_validate::submodule::name::Error), diff --git a/gix/src/submodule/mod.rs b/gix/src/submodule/mod.rs index 07bcedfcf79..9e01aad6162 100644 --- a/gix/src/submodule/mod.rs +++ b/gix/src/submodule/mod.rs @@ -259,7 +259,7 @@ impl Submodule<'_> { git_dir_try_old_form::Error::InvalidGitDirFileTarget { gitdir_file: worktree_gitdir.clone(), target: None, - source: Some(source), + source: Some(source.into_error()), } })?; if !git_dir.is_dir() { From d03606395ab32232890de27a79e35ff90612f896 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 20 Aug 2026 17:11:03 +0200 Subject: [PATCH 11/51] change(gix-credentials)!: migrate errors to gix-error rubberstamp --- Cargo.lock | 1 - gitoxide-core/src/repository/credential.rs | 36 +++++------ gix-credentials/Cargo.toml | 1 - gix-credentials/examples/custom-helper.rs | 9 +-- .../examples/invoke-git-credential.rs | 2 +- gix-credentials/src/helper/cascade.rs | 23 +++---- gix-credentials/src/helper/invoke.rs | 15 +++-- gix-credentials/src/helper/mod.rs | 11 +--- gix-credentials/src/lib.rs | 4 -- gix-credentials/src/program/main.rs | 63 +++++++++---------- gix-credentials/src/protocol/context/mod.rs | 19 ++---- gix-credentials/src/protocol/context/serde.rs | 40 ++++++------ gix-credentials/src/protocol/mod.rs | 50 +++++---------- gix-credentials/tests/helper/cascade.rs | 12 ++-- gix-credentials/tests/helper/context.rs | 5 +- gix-credentials/tests/helper/invoke.rs | 14 ++--- gix-credentials/tests/helper/mod.rs | 27 +++++++- gix-credentials/tests/program/main.rs | 52 +++++++-------- gix-protocol/src/handshake/function.rs | 7 ++- gix-protocol/src/handshake/mod.rs | 4 +- gix-protocol/tests/protocol/fetch/mod.rs | 1 - .../src/client/blocking_io/http/curl/mod.rs | 2 +- .../client/blocking_io/http/curl/remote.rs | 6 +- gix/src/remote/connection/access.rs | 11 ++-- gix/tests/gix/remote/connect.rs | 7 ++- 25 files changed, 193 insertions(+), 229 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7690d676125..091dab7ca05 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1791,7 +1791,6 @@ dependencies = [ "gix-trace", "gix-url", "serde", - "thiserror 2.0.18", ] [[package]] diff --git a/gitoxide-core/src/repository/credential.rs b/gitoxide-core/src/repository/credential.rs index 06ce86447f2..d4229c18605 100644 --- a/gitoxide-core/src/repository/credential.rs +++ b/gitoxide-core/src/repository/credential.rs @@ -1,34 +1,29 @@ -#[derive(Debug, thiserror::Error)] -enum Error { - #[error(transparent)] - UrlParse(#[from] gix::Error), - #[error(transparent)] - Configuration(#[from] gix::config::credential_helpers::Error), - #[error(transparent)] - Protocol(#[from] gix::credentials::protocol::Error), - #[error(transparent)] - ConfigLoad(#[from] gix::config::file::init::from_paths::Error), -} - pub fn function(repo: Option, action: gix::credentials::program::main::Action) -> anyhow::Result<()> { use gix::credentials::program::main::Action::*; + use gix::error::{OptionExt, ResultExt, ValidationError, message}; gix::credentials::program::main( Some(action.as_str().into()), std::io::stdin(), std::io::stdout(), gix::credentials::protocol::ContextOptions::default(), - |action, context| -> Result<_, Error> { + |action, context| -> Result<_, gix::Exn> { let url = context .url .clone() .or_else(|| context.to_url()) - .ok_or(Error::Protocol(gix::credentials::protocol::Error::UrlMissing))?; + .ok_or_raise_erased(|| { + ValidationError::new("Either 'url' field or both 'protocol' and 'host' fields must be provided") + })?; - let url = gix::url::parse(&url).map_err(gix::Exn::into_error)?; + let url = gix::url::parse(&url).or_erased()?; let (mut cascade, _action, prompt_options) = match repo { - Some(ref repo) => repo.config_snapshot().credential_helpers(url)?, + Some(ref repo) => repo + .config_snapshot() + .credential_helpers(url) + .or_raise_erased(|| message("Could not configure credential helpers"))?, None => { - let config = gix::config::File::from_globals()?; + let config = gix::config::File::from_globals() + .or_raise_erased(|| message("Could not load global configuration"))?; let environment = gix::open::permissions::Environment::all(); gix::config::credential_helpers( url, @@ -37,7 +32,8 @@ pub fn function(repo: Option, action: gix::credentials::program |_| true, /* section filter */ environment, false, /* use http path (override, uses configuration now)*/ - )? + ) + .or_raise_erased(|| message("Could not configure credential helpers"))? } }; cascade @@ -50,8 +46,8 @@ pub fn function(repo: Option, action: gix::credentials::program prompt_options, ) .map(|outcome| outcome.and_then(|outcome| (&outcome.next).try_into().ok())) - .map_err(Into::into) }, ) - .map_err(Into::into) + .map_err(gix::Exn::into_error)?; + Ok(()) } diff --git a/gix-credentials/Cargo.toml b/gix-credentials/Cargo.toml index b001e539398..700e51662c2 100644 --- a/gix-credentials/Cargo.toml +++ b/gix-credentials/Cargo.toml @@ -30,7 +30,6 @@ gix-prompt = { version = "^0.17.0", path = "../gix-prompt" } gix-date = { version = "^0.16.0", path = "../gix-date" } gix-trace = { version = "^0.1.21", path = "../gix-trace" } -thiserror = "2.0.18" serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"] } bstr = { version = "1.12.0", default-features = false, features = ["std"] } diff --git a/gix-credentials/examples/custom-helper.rs b/gix-credentials/examples/custom-helper.rs index a678b9193c9..c148feeaacb 100644 --- a/gix-credentials/examples/custom-helper.rs +++ b/gix-credentials/examples/custom-helper.rs @@ -1,4 +1,5 @@ use gix_credentials::{program, protocol}; +use gix_error::ErrorExt; /// Run like this `echo url=https://example.com | cargo run --example custom-helper -- get` pub fn main() -> Result<(), gix_credentials::program::main::Error> { @@ -7,16 +8,16 @@ pub fn main() -> Result<(), gix_credentials::program::main::Error> { std::io::stdin(), std::io::stdout(), protocol::ContextOptions::default(), - |action, context| -> std::io::Result<_> { + |action, context| -> Result<_, gix_error::Exn> { match action { program::main::Action::Get => Ok(Some(protocol::Context { username: Some("user".into()), password: Some("pass".into()), ..context })), - program::main::Action::Erase => Err(std::io::Error::other( - "Refusing to delete credentials for demo purposes", - )), + program::main::Action::Erase => { + Err(gix_error::message("Refusing to delete credentials for demo purposes").raise_erased()) + } program::main::Action::Store => Ok(None), } }, diff --git a/gix-credentials/examples/invoke-git-credential.rs b/gix-credentials/examples/invoke-git-credential.rs index 5d4f780f7aa..cc6a14e9fed 100644 --- a/gix-credentials/examples/invoke-git-credential.rs +++ b/gix-credentials/examples/invoke-git-credential.rs @@ -1,5 +1,5 @@ /// Invokes `git credential` with the passed url as argument and prints obtained credentials. -pub fn main() -> Result<(), Box> { +pub fn main() -> Result<(), Box> { let out = gix_credentials::builtin(gix_credentials::helper::Action::get_for_url( std::env::args() .nth(1) diff --git a/gix-credentials/src/helper/cascade.rs b/gix-credentials/src/helper/cascade.rs index 32b2379080c..904a2afb164 100644 --- a/gix-credentials/src/helper/cascade.rs +++ b/gix-credentials/src/helper/cascade.rs @@ -4,6 +4,7 @@ use crate::{ protocol, protocol::{Context, ContextOptions}, }; +use gix_error::{ResultExt, RetryableError}; impl Default for Cascade { fn default() -> Self { @@ -75,14 +76,10 @@ impl Cascade { /// When _getting_ credentials, all programs are asked until the credentials are complete, stopping the cascade. /// When _storing_ or _erasing_ all programs are instructed in order. /// The input context is validated even if no helpers are available. - #[expect( - clippy::result_large_err, - reason = "will be removed once `gix-error` is used consistently" - )] pub fn invoke(&mut self, mut action: helper::Action, mut prompt: gix_prompt::Options) -> protocol::Result { if let Some(ctx) = action.context_mut() { ctx.options = self.context_options; - ctx.write_to(std::io::sink()).map_err(helper::Error::from)?; + ctx.write_to(std::io::sink()).or_erased()?; } let mut url = action .context_mut() @@ -119,7 +116,7 @@ impl Cascade { www_authenticate: _, url: ctx_url, quit, - } = Context::from_bytes(&stdout, self.context_options)?; + } = Context::from_bytes(&stdout, self.context_options).or_erased()?; if let Some(dst_ctx) = action.context_mut() { if let Some(src) = path { dst_ctx.path = Some(src); @@ -158,8 +155,8 @@ impl Cascade { } } } - Err(helper::Error::CredentialsHelperFailed { .. }) => continue, // ignore helpers that we can't call - Err(err) if action.context().is_some() => return Err(err.into()), // communication errors are fatal when getting credentials + Err(err) if err.downcast_any_ref::().is_some() => continue, + Err(err) if action.context().is_some() => return Err(err), // communication errors are fatal when getting credentials Err(_) => {} // for other actions, ignore everything, try the operation } } @@ -172,20 +169,14 @@ impl Cascade { let message = ctx.to_prompt("Username"); prompt.mode = gix_prompt::Mode::Visible; ctx.username = gix_prompt::ask(&message, &prompt) - .map_err(|err| protocol::Error::Prompt { - prompt: message, - source: err.into_error(), - })? + .or_raise_erased(|| gix_error::message!("Couldn't obtain {message}"))? .into(); } if ctx.password.is_none() { let message = ctx.to_prompt("Password"); prompt.mode = gix_prompt::Mode::Hidden; ctx.password = gix_prompt::ask(&message, &prompt) - .map_err(|err| protocol::Error::Prompt { - prompt: message, - source: err.into_error(), - })? + .or_raise_erased(|| gix_error::message!("Couldn't obtain {message}"))? .into(); } } diff --git a/gix-credentials/src/helper/invoke.rs b/gix-credentials/src/helper/invoke.rs index e922af0ad68..f8d3790e016 100644 --- a/gix-credentials/src/helper/invoke.rs +++ b/gix-credentials/src/helper/invoke.rs @@ -1,5 +1,7 @@ use std::io::Read; +use gix_error::{ErrorExt, ResultExt, RetryableError, message}; + use crate::helper::{Action, Context, Error, NextAction, Outcome, Result}; impl Action { @@ -27,7 +29,7 @@ pub fn invoke(helper: &mut crate::Program, action: &Action) -> Result { match raw(helper, action)? { None => Ok(None), Some(stdout) => { - let ctx = Context::from_bytes(stdout.as_slice(), options)?; + let ctx = Context::from_bytes(stdout.as_slice(), options).or_erased()?; Ok(Some(Outcome { username: ctx.username, password: ctx.password, @@ -43,11 +45,12 @@ pub fn invoke(helper: &mut crate::Program, action: &Action) -> Result { } pub(crate) fn raw(helper: &mut crate::Program, action: &Action) -> std::result::Result>, Error> { - let (mut stdin, stdout) = helper.start(action)?; + let communication_error = || message("An IO error occurred while communicating to the credentials helper"); + let (mut stdin, stdout) = helper.start(action).or_raise_erased(communication_error)?; if let (Action::Get(_), None) = (&action, &stdout) { panic!("BUG: `Helper` impls must return an output handle to read output from if Action::Get is provided") } - action.send(&mut stdin)?; + action.send(&mut stdin).or_raise_erased(communication_error)?; drop(stdin); let stdout = stdout .map(|mut stdout| { @@ -55,12 +58,12 @@ pub(crate) fn raw(helper: &mut crate::Program, action: &Action) -> std::result:: stdout.read_to_end(&mut buf).map(|_| buf) }) .transpose() - .map_err(|err| Error::CredentialsHelperFailed { source: err })?; + .map_err(|err| RetryableError::new(err).raise_erased())?; helper.finish().map_err(|err| { if err.kind() == std::io::ErrorKind::Other { - Error::CredentialsHelperFailed { source: err } + RetryableError::new(err).raise_erased() } else { - err.into() + err.and_raise(communication_error()).erased() } })?; diff --git a/gix-credentials/src/helper/mod.rs b/gix-credentials/src/helper/mod.rs index 49641470606..f14a29df239 100644 --- a/gix-credentials/src/helper/mod.rs +++ b/gix-credentials/src/helper/mod.rs @@ -57,16 +57,7 @@ impl Outcome { pub type Result = std::result::Result, Error>; /// The error used in the [credentials helper invocation][crate::helper::invoke()]. -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] -pub enum Error { - #[error(transparent)] - ContextDecode(#[from] protocol::context::decode::Error), - #[error("An IO error occurred while communicating to the credentials helper")] - Io(#[from] std::io::Error), - #[error(transparent)] - CredentialsHelperFailed { source: std::io::Error }, -} +pub type Error = gix_error::Exn; /// The action to perform by the credentials [helper][`crate::helper::invoke()`]. #[derive(Clone, Debug)] diff --git a/gix-credentials/src/lib.rs b/gix-credentials/src/lib.rs index 039bf978189..9500800004b 100644 --- a/gix-credentials/src/lib.rs +++ b/gix-credentials/src/lib.rs @@ -33,10 +33,6 @@ pub mod protocol; /// and does everything `git` typically does. The `action` should have been created with [`helper::Action::get_for_url()`] to /// contain only the URL to kick off the process, or should be created by [`helper::NextAction`]. /// If more control is required, use the [`Cascade`][helper::Cascade] type. -#[expect( - clippy::result_large_err, - reason = "will be removed once `gix-error` is used consistently" -)] pub fn builtin(action: helper::Action) -> protocol::Result { protocol::helper_outcome_to_result( helper::invoke(&mut Program::from_kind(program::Kind::Builtin), &action)?, diff --git a/gix-credentials/src/program/main.rs b/gix-credentials/src/program/main.rs index e7ef34cfc0a..359f77e9d87 100644 --- a/gix-credentials/src/program/main.rs +++ b/gix-credentials/src/program/main.rs @@ -1,6 +1,6 @@ use std::ffi::OsString; -use bstr::BString; +use gix_error::ValidationError; /// The action passed to the credential helper implementation in [`main()`][crate::program::main()]. #[derive(Debug, Copy, Clone)] @@ -14,14 +14,19 @@ pub enum Action { } impl TryFrom for Action { - type Error = Error; + type Error = ValidationError; fn try_from(value: OsString) -> Result { Ok(match value.to_str() { Some("fill" | "get") => Action::Get, Some("approve" | "store") => Action::Store, Some("reject" | "erase") => Action::Erase, - _ => return Err(Error::ActionInvalid { name: value }), + _ => { + return Err(ValidationError::new_with_input( + "Action is invalid, need 'get', 'store', 'erase' or 'fill', 'approve', 'reject'", + value.as_encoded_bytes().to_owned(), + )); + } }) } } @@ -38,30 +43,13 @@ impl Action { } /// The error of [`main()`][crate::program::main()]. -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] -pub enum Error { - #[error("Action named {name:?} is invalid, need 'get', 'store', 'erase' or 'fill', 'approve', 'reject'")] - ActionInvalid { name: OsString }, - #[error("The first argument must be the action to perform")] - ActionMissing, - #[error(transparent)] - Helper { - source: Box, - }, - #[error(transparent)] - Io(#[from] std::io::Error), - #[error(transparent)] - Context(#[from] crate::protocol::context::decode::Error), - #[error("Credentials for {url:?} could not be obtained")] - CredentialsMissing { url: BString }, - #[error("Either 'url' field or both 'protocol' and 'host' fields must be provided")] - UrlMissing, -} +pub type Error = gix_error::Exn; pub(crate) mod function { use std::ffi::OsString; + use gix_error::{ErrorExt, ResultExt, ValidationError}; + use crate::{ program::main::{Action, Error}, protocol::{Context, ContextOptions}, @@ -75,7 +63,7 @@ pub(crate) mod function { /// /// Call this function from a programs `main`, passing `std::env::args_os()`, `stdin()` and `stdout` accordingly, along with /// the context encoding `options` and your own helper implementation. - pub fn main( + pub fn main( args: impl IntoIterator, mut stdin: impl std::io::Read, stdout: impl std::io::Write, @@ -83,17 +71,23 @@ pub(crate) mod function { credentials: CredentialsFn, ) -> Result<(), Error> where - CredentialsFn: FnOnce(Action, Context) -> Result, E>, - E: std::error::Error + Send + Sync + 'static, + CredentialsFn: FnOnce(Action, Context) -> Result, gix_error::Exn>, { - let action: Action = args.into_iter().next().ok_or(Error::ActionMissing)?.try_into()?; + let action = args + .into_iter() + .next() + .ok_or_else(|| ValidationError::new("The first argument must be the action to perform").raise_erased())?; + let action = Action::try_from(action).or_erased()?; let mut buf = Vec::::with_capacity(512); - stdin.read_to_end(&mut buf)?; - let ctx = Context::from_bytes(&buf, options)?; + stdin.read_to_end(&mut buf).or_erased()?; + let ctx = Context::from_bytes(&buf, options).or_erased()?; if ctx.url.is_none() && (ctx.protocol.is_none() || ctx.host.is_none()) { - return Err(Error::UrlMissing); + return Err(ValidationError::new( + "Either 'url' field or both 'protocol' and 'host' fields must be provided", + ) + .raise_erased()); } - let res = credentials(action, ctx.clone()).map_err(|err| Error::Helper { source: Box::new(err) })?; + let res = credentials(action, ctx.clone())?; match (action, res) { (Action::Get, None) => { let ctx_for_error = ctx; @@ -102,11 +96,14 @@ pub(crate) mod function { .clone() .or_else(|| ctx_for_error.to_url()) .expect("URL is available either directly or via protocol+host which we checked for"); - return Err(Error::CredentialsMissing { url }); + return Err( + gix_error::NotFoundError::new(format!("Credentials for {url:?} could not be obtained")) + .raise_erased(), + ); } (Action::Get, Some(mut ctx)) => { ctx.options = options; - ctx.write_to(stdout)?; + ctx.write_to(stdout).or_erased()?; } (Action::Erase | Action::Store, None) => {} (Action::Erase | Action::Store, Some(_)) => { diff --git a/gix-credentials/src/protocol/context/mod.rs b/gix-credentials/src/protocol/context/mod.rs index f71d124aa95..72651a0a44f 100644 --- a/gix-credentials/src/protocol/context/mod.rs +++ b/gix-credentials/src/protocol/context/mod.rs @@ -3,12 +3,7 @@ use bstr::BString; use crate::protocol::{Context, ContextOptions}; /// Indicates key or values contain errors that can't be encoded. -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] -pub enum Error { - #[error("{key:?}={value:?} must not contain null bytes or newlines neither in key nor in value.")] - Encoding { key: String, value: BString }, -} +pub type Error = gix_error::ValidationError; impl Context { /// Create a context containing `url`, encoded and decoded according to `options`. @@ -99,6 +94,7 @@ mod access { mod mutate { use bstr::ByteSlice; + use gix_error::{OptionExt, ResultExt, ValidationError}; use crate::{protocol, protocol::Context}; @@ -107,17 +103,14 @@ mod mutate { /// Destructure the url at our `url` field into parts like protocol, host, username and path and store /// them in our respective fields. If `use_http_path` is set, http paths are significant even though /// normally this isn't the case. - #[expect( - clippy::result_large_err, - reason = "will be removed once `gix-error` is used consistently" - )] pub fn destructure_url_in_place(&mut self, use_http_path: bool) -> Result<&mut Self, protocol::Error> { if self.url.is_none() { - self.url = Some(self.to_url().ok_or(protocol::Error::UrlMissing)?); + self.url = Some(self.to_url().ok_or_raise_erased(|| { + ValidationError::new("Either 'url' field or both 'protocol' and 'host' fields must be provided") + })?); } - let url = gix_url::parse(self.url.as_ref().expect("URL is present after check above")) - .map_err(gix_error::Exn::into_error)?; + let url = gix_url::parse(self.url.as_ref().expect("URL is present after check above")).or_erased()?; self.protocol = Some(url.scheme.as_str().into()); self.username = url.user().map(ToOwned::to_owned); self.password = url.password().map(ToOwned::to_owned); diff --git a/gix-credentials/src/protocol/context/serde.rs b/gix-credentials/src/protocol/context/serde.rs index b66bdf0062f..d42e7b794ce 100644 --- a/gix-credentials/src/protocol/context/serde.rs +++ b/gix-credentials/src/protocol/context/serde.rs @@ -75,21 +75,13 @@ mod write { /// pub mod decode { - use bstr::{BString, ByteSlice}; + use bstr::ByteSlice; + use gix_error::ValidationError; - use crate::protocol::{Context, ContextOptions, context, context::serde::validate}; + use crate::protocol::{Context, ContextOptions, context::serde::validate}; /// The error returned by [`from_bytes()`][Context::from_bytes()]. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error("Illformed UTF-8 in value of key {key:?}: {value:?}")] - IllformedUtf8InValue { key: String, value: BString }, - #[error(transparent)] - Encoding(#[from] context::Error), - #[error("Invalid format in line {line:?}, expecting key=value")] - Syntax { line: BString }, - } + pub type Error = ValidationError; impl Context { /// Decode ourselves from `input` which is the format written by [`write_to()`][Self::write_to()]. @@ -118,17 +110,23 @@ pub mod decode { it.next().and_then(|k| k.to_str().ok()), it.next().map(ByteSlice::as_bstr), ) { - (Some(key), Some(value)) => validate(key, value, options.protect_protocol) - .map(|_| (key, value.to_owned())) - .map_err(Into::into), - _ => Err(Error::Syntax { line: line.into() }), + (Some(key), Some(value)) => { + validate(key, value, options.protect_protocol).map(|_| (key, value.to_owned())) + } + _ => Err(ValidationError::new_with_input( + "Invalid format, expecting key=value", + line, + )), } }) { let (key, value) = res?; match key { "protocol" | "host" | "username" | "password" | "oauth_refresh_token" => { if !value.is_utf8() { - return Err(Error::IllformedUtf8InValue { key: key.into(), value }); + return Err(ValidationError::new_with_input( + format!("Illformed UTF-8 in value of key {key:?}"), + value, + )); } let value = value.to_string(); *match key { @@ -167,10 +165,10 @@ fn validate(key: &str, value: &BStr, protect_protocol: bool) -> Result<(), Error || value.contains(&b'\n') || (protect_protocol && value.contains(&b'\r')) { - return Err(Error::Encoding { - key: key.to_owned(), - value: value.to_owned(), - }); + return Err(gix_error::ValidationError::new_with_input( + format!("{key:?}={value:?} must not contain null bytes or newlines neither in key nor in value."), + value, + )); } Ok(()) } diff --git a/gix-credentials/src/protocol/mod.rs b/gix-credentials/src/protocol/mod.rs index 12a87b10dc4..9fecf0e4a9c 100644 --- a/gix-credentials/src/protocol/mod.rs +++ b/gix-credentials/src/protocol/mod.rs @@ -1,4 +1,5 @@ use bstr::BString; +use gix_error::ErrorExt; use crate::helper; @@ -15,29 +16,7 @@ pub struct Outcome { pub type Result = std::result::Result, Error>; /// The error returned top-level credential functions. -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] -pub enum Error { - #[error(transparent)] - UrlParse(#[from] gix_error::Error), - #[error("Either 'url' field or both 'protocol' and 'host' fields must be provided")] - UrlMissing, - #[error(transparent)] - ContextDecode(#[from] context::decode::Error), - #[error(transparent)] - InvokeHelper(#[from] helper::Error), - #[error("Could not configure credential helpers")] - ConfigureCredentialHelpers { - #[source] - source: Box, - }, - #[error("Could not obtain identity for context: {}", { let mut buf = Vec::::new(); context.write_to(&mut buf).ok(); String::from_utf8_lossy(&buf).into_owned() })] - IdentityMissing { context: Context }, - #[error("The handler asked to stop trying to obtain credentials")] - Quit, - #[error("Couldn't obtain {prompt}")] - Prompt { prompt: String, source: gix_error::Error }, -} +pub type Error = gix_error::Exn; /// Additional context to be passed to the credentials helper. #[derive(Debug, Default, Clone, Eq, PartialEq)] @@ -88,31 +67,34 @@ impl Default for ContextOptions { } /// Convert the outcome of a helper invocation to a helper result, assuring that the identity is complete in the process. -#[expect( - clippy::result_large_err, - reason = "will be removed once `gix-error` is used consistently" -)] pub fn helper_outcome_to_result(outcome: Option, action: helper::Action) -> Result { match (action, outcome) { - (helper::Action::Get(ctx), None) => Err(Error::IdentityMissing { - context: ctx.redacted(), - }), + (helper::Action::Get(ctx), None) => Err(identity_missing(ctx)), (helper::Action::Get(ctx), Some(mut outcome)) => match outcome.consume_identity() { Some(identity) => Ok(Some(Outcome { identity, next: outcome.next, })), None => Err(if outcome.quit { - Error::Quit + gix_error::message("The handler asked to stop trying to obtain credentials").raise_erased() } else { - Error::IdentityMissing { - context: ctx.redacted(), - } + identity_missing(ctx) }), }, (helper::Action::Store(_) | helper::Action::Erase(_), _ignore) => Ok(None), } } +fn identity_missing(context: Context) -> Error { + let mut buf = Vec::new(); + // Invalid protocol values must not prevent reporting the missing identity. + context.redacted().write_to(&mut buf).ok(); + gix_error::NotFoundError::new(format!( + "Could not obtain identity for context: {}", + String::from_utf8_lossy(&buf) + )) + .raise_erased() +} + /// pub mod context; diff --git a/gix-credentials/tests/helper/cascade.rs b/gix-credentials/tests/helper/cascade.rs index 26dc02d084e..09c272af347 100644 --- a/gix-credentials/tests/helper/cascade.rs +++ b/gix-credentials/tests/helper/cascade.rs @@ -25,10 +25,7 @@ mod invoke { ) .expect_err("malformed authentication challenges must fail without panicking"); assert!( - matches!( - err, - protocol::Error::InvokeHelper(gix_credentials::helper::Error::Io(_)) - ), + err.downcast_any_ref::().is_some(), "protocol validation must run even when no helper is configured and prompting is disabled" ); } @@ -54,7 +51,8 @@ mod invoke { mode: gix_prompt::Mode::Disable, askpass: None, }, - )? + ) + .map_err(gix_error::Exn::into_error)? .expect("the fallback helper supplies a complete credential"); assert_eq!( outcome.identity, @@ -89,7 +87,8 @@ mod invoke { mode: gix_prompt::Mode::Disable, askpass: None, }, - )? + ) + .map_err(gix_error::Exn::into_error)? .expect("both helpers contribute to the credential"); assert_eq!( outcome.identity, @@ -297,7 +296,6 @@ mod invoke { } } - #[expect(clippy::result_large_err)] fn invoke_cascade<'a>(names: impl IntoIterator, action: Action) -> protocol::Result { Cascade::default().use_http_path(true).extend(fixtures(names)).invoke( action, diff --git a/gix-credentials/tests/helper/context.rs b/gix-credentials/tests/helper/context.rs index da8dc96238b..70d6b1ce789 100644 --- a/gix-credentials/tests/helper/context.rs +++ b/gix-credentials/tests/helper/context.rs @@ -173,10 +173,7 @@ username=bob"; #[test] fn null_bytes_when_decoding() { let err = Context::from_bytes(b"url=https://foo\0", ContextOptions::default()).unwrap_err(); - assert!(matches!( - err, - gix_credentials::protocol::context::decode::Error::Encoding(_) - )); + assert!(err.message.contains("must not contain null bytes or newlines")); } #[test] diff --git a/gix-credentials/tests/helper/invoke.rs b/gix-credentials/tests/helper/invoke.rs index b4176e70442..9b71cd3d34a 100644 --- a/gix-credentials/tests/helper/invoke.rs +++ b/gix-credentials/tests/helper/invoke.rs @@ -79,15 +79,13 @@ mod program { } let temp = gix_testtools::tempfile::tempdir()?; let _cwd = gix_testtools::set_current_dir(temp.path())?; + let err = gix_credentials::helper::invoke( + &mut Program::from_kind(Kind::Builtin).suppress_stderr(), + &helper::Action::get_for_url("/path/without/scheme/fails/with/error"), + ) + .expect_err("the builtin helper rejects a URL without a scheme"); assert!( - matches!( - gix_credentials::helper::invoke( - &mut Program::from_kind(Kind::Builtin).suppress_stderr(), - &helper::Action::get_for_url("/path/without/scheme/fails/with/error"), - ) - .unwrap_err(), - helper::Error::CredentialsHelperFailed { .. } - ), + err.downcast_any_ref::().is_some(), "this failure indicates we could launch the helper, even though it wasn't happy which is fine. It doesn't like the URL" ); Ok(()) diff --git a/gix-credentials/tests/helper/mod.rs b/gix-credentials/tests/helper/mod.rs index 867675c05fd..9593558abd1 100644 --- a/gix-credentials/tests/helper/mod.rs +++ b/gix-credentials/tests/helper/mod.rs @@ -19,7 +19,27 @@ mod invoke_outcome_to_helper_result { action, ) .unwrap_err(); - assert!(matches!(err, protocol::Error::IdentityMissing { .. })); + assert!(err.downcast_any_ref::().is_some()); + } + + #[test] + fn invalid_context_still_reports_missing_identity() { + for value in ["invalid\nvalue", "invalid\0value", "invalid\rvalue"] { + for context in [ + protocol::Context::from_url(value, Default::default()), + protocol::Context { + path: Some(value.into()), + ..Default::default() + }, + ] { + let err = helper_outcome_to_result(None, helper::Action::Get(context)) + .expect_err("Missing credentials must return an error even when the context is invalid"); + assert!( + err.downcast_any_ref::().is_some(), + "Invalid context must not replace the missing-credentials classification" + ); + } + } } #[test] @@ -36,7 +56,10 @@ mod invoke_outcome_to_helper_result { action, ) .unwrap_err(); - assert!(matches!(err, protocol::Error::Quit)); + assert_eq!( + err.to_string(), + "The handler asked to stop trying to obtain credentials" + ); } } diff --git a/gix-credentials/tests/program/main.rs b/gix-credentials/tests/program/main.rs index 3b45dc8edfc..0b1f5cc4176 100644 --- a/gix-credentials/tests/program/main.rs +++ b/gix-credentials/tests/program/main.rs @@ -1,9 +1,14 @@ use gix_credentials::program::main; use std::io::Cursor; -#[derive(Debug, thiserror::Error)] -#[error("Test error")] -struct TestError; +#[test] +#[cfg(unix)] +fn invalid_non_utf8_action_is_preserved() { + use std::{ffi::OsString, os::unix::ffi::OsStringExt}; + + let err = main::Action::try_from(OsString::from_vec(vec![0xff])).expect_err("the action is invalid"); + assert_eq!(err.input.expect("the invalid action is retained").as_slice(), &[0xff]); +} #[test] fn context_options_apply_to_input_and_output() { @@ -18,7 +23,7 @@ fn context_options_apply_to_input_and_output() { Cursor::new(input), &mut output, options, - |_action, context| -> Result, TestError> { + |_action, context| -> Result, gix_error::Exn> { assert_eq!( context.url.as_ref().map(|url| url.as_slice()), Some(&input[4..input.len() - 1]) @@ -45,7 +50,7 @@ fn protocol_and_host_without_url_is_valid() { Cursor::new(input), &mut output, gix_credentials::protocol::ContextOptions::default(), - |_action, context| -> Result, TestError> { + |_action, context| -> Result, gix_error::Exn> { assert_eq!(context.protocol.as_deref(), Some("https")); assert_eq!(context.host.as_deref(), Some("github.com")); assert_eq!(context.url, None, "the URL isn't automatically populated"); @@ -57,15 +62,12 @@ fn protocol_and_host_without_url_is_valid() { // This should fail because our mock helper returned None (no credentials found) // but it should NOT fail because of missing URL - match result { - Err(gix_credentials::program::main::Error::CredentialsMissing { .. }) => { - assert!( - called, - "The helper gets called, but as nothing is provided in the function it ulimately fails" - ); - } - other => panic!("Expected CredentialsMissing error, got: {other:?}"), - } + let err = result.expect_err("missing credentials must fail"); + assert!(err.downcast_any_ref::().is_some()); + assert!( + called, + "The helper gets called, but as nothing is provided in the function it ultimately fails" + ); } #[test] @@ -79,18 +81,15 @@ fn missing_protocol_with_only_host_or_protocol_fails() { Cursor::new(input), &mut output, gix_credentials::protocol::ContextOptions::default(), - |_action, _context| -> Result, TestError> { + |_action, _context| -> Result, gix_error::Exn> { called = true; Ok(None) }, ); - match result { - Err(gix_credentials::program::main::Error::UrlMissing) => { - assert!(!called, "the context is lacking, hence nothing gets called"); - } - other => panic!("Expected UrlMissing error, got: {other:?}"), - } + let err = result.expect_err("incomplete URL must fail validation"); + assert!(err.downcast_any_ref::().is_some()); + assert!(!called, "the context is lacking, hence nothing gets called"); } } @@ -105,7 +104,7 @@ fn url_alone_is_valid() { Cursor::new(input), &mut output, gix_credentials::protocol::ContextOptions::default(), - |_action, context| -> Result, TestError> { + |_action, context| -> Result, gix_error::Exn> { called = true; assert_eq!(context.url.unwrap(), "https://github.com"); assert_eq!(context.host, None, "not auto-populated"); @@ -117,10 +116,7 @@ fn url_alone_is_valid() { // This should fail because our mock helper returned None (no credentials found) // but it should NOT fail because of missing URL - match result { - Err(gix_credentials::program::main::Error::CredentialsMissing { .. }) => { - assert!(called); - } - other => panic!("Expected CredentialsMissing error, got: {other:?}"), - } + let err = result.expect_err("missing credentials must fail"); + assert!(err.downcast_any_ref::().is_some()); + assert!(called); } diff --git a/gix-protocol/src/handshake/function.rs b/gix-protocol/src/handshake/function.rs index 96f9a34f350..511246e99ad 100644 --- a/gix-protocol/src/handshake/function.rs +++ b/gix-protocol/src/handshake/function.rs @@ -60,19 +60,20 @@ where url: Some(url.clone()), www_authenticate, ..Default::default() - }))? + })) + .map_err(|err| Error::Credentials(err.into_error()))? .ok_or(Error::EmptyCredentials)?; transport.set_identity(identity)?; progress.step(); progress.set_name("handshake (authenticated)".into()); match transport.handshake(service, &extra_parameters).await { Ok(v) => { - authenticate(next.store())?; + authenticate(next.store()).map_err(|err| Error::Credentials(err.into_error()))?; Ok(v) } // Still no permission? Reject the credentials. Err(client::Error::Io(err)) if err.kind() == std::io::ErrorKind::PermissionDenied => { - authenticate(next.erase())?; + authenticate(next.erase()).map_err(|err| Error::Credentials(err.into_error()))?; return Err(Error::InvalidCredentials { url, source: err }); } // Otherwise, do nothing, as we don't know if it actually got to try the credentials. diff --git a/gix-protocol/src/handshake/mod.rs b/gix-protocol/src/handshake/mod.rs index 9fd6ce403f4..b578e34d198 100644 --- a/gix-protocol/src/handshake/mod.rs +++ b/gix-protocol/src/handshake/mod.rs @@ -164,14 +164,14 @@ mod error { use bstr::BString; use gix_transport::client; - use crate::{credentials, handshake::refs}; + use crate::handshake::refs; /// The error returned by [`handshake()`][crate::handshake()]. #[derive(Debug, thiserror::Error)] #[expect(missing_docs)] pub enum Error { #[error("Failed to obtain credentials")] - Credentials(#[from] credentials::protocol::Error), + Credentials(#[source] gix_error::Error), #[error("No credentials were returned at all as if the credential helper isn't functioning unknowingly")] EmptyCredentials, #[error("Credentials provided for \"{url}\" were not accepted by the remote")] diff --git a/gix-protocol/tests/protocol/fetch/mod.rs b/gix-protocol/tests/protocol/fetch/mod.rs index 259a355e9bc..476cab84c13 100644 --- a/gix-protocol/tests/protocol/fetch/mod.rs +++ b/gix-protocol/tests/protocol/fetch/mod.rs @@ -46,7 +46,6 @@ type Cursor = std::io::Cursor>; #[cfg(all(feature = "async-client", not(feature = "blocking-client")))] type Cursor = futures_lite::io::Cursor>; -#[expect(clippy::result_large_err)] fn helper_unused(_action: gix_credentials::helper::Action) -> gix_credentials::protocol::Result { panic!("Call to credentials helper is unexpected") } diff --git a/gix-transport/src/client/blocking_io/http/curl/mod.rs b/gix-transport/src/client/blocking_io/http/curl/mod.rs index cb2733c74bb..45f19739ab8 100644 --- a/gix-transport/src/client/blocking_io/http/curl/mod.rs +++ b/gix-transport/src/client/blocking_io/http/curl/mod.rs @@ -35,7 +35,7 @@ pub enum Error { #[error("Could not finish reading all data to post to the remote")] ReadPostBody(#[from] std::io::Error), #[error(transparent)] - Authenticate(#[from] gix_credentials::protocol::Error), + Authenticate(gix_error::Error), } impl crate::IsSpuriousError for Error { diff --git a/gix-transport/src/client/blocking_io/http/curl/remote.rs b/gix-transport/src/client/blocking_io/http/curl/remote.rs index 832992af286..34a32e11854 100644 --- a/gix-transport/src/client/blocking_io/http/curl/remote.rs +++ b/gix-transport/src/client/blocking_io/http/curl/remote.rs @@ -466,7 +466,8 @@ pub fn new() -> Worker { handle.proxy_type(proxy_type)?; if let Some((obtain_creds_action, authenticate)) = proxy_authenticate { - let creds = authenticate.lock().expect("no panics in other threads")(obtain_creds_action)? + let creds = authenticate.lock().expect("no panics in other threads")(obtain_creds_action) + .map_err(|err| Error::Authenticate(err.into_error()))? .expect("action to fetch credentials"); handle.proxy_username(&creds.identity.username)?; handle.proxy_password(&creds.identity.password)?; @@ -603,7 +604,8 @@ pub fn new() -> Worker { action.store() } else { action.erase() - })?; + }) + .map_err(|err| Error::Authenticate(err.into_error()))?; } handler.reset(); handler.receive_body.take(); diff --git a/gix/src/remote/connection/access.rs b/gix/src/remote/connection/access.rs index 8d1e3f0d768..0c1e4e6dd4c 100644 --- a/gix/src/remote/connection/access.rs +++ b/gix/src/remote/connection/access.rs @@ -2,6 +2,7 @@ use crate::{ Remote, remote::{Connection, connection::AuthenticateFn, connection::ConnectionDetached}, }; +use gix_error::{OptionExt, ResultExt, ValidationError, message}; #[cfg(feature = "async-network-client")] use gix_transport::client::async_io::Transport; #[cfg(feature = "blocking-network-client")] @@ -146,13 +147,13 @@ fn configured_credentials_for_current_url(repo: crate::Repository) -> Authentica let url = action .context() .and_then(|ctx| ctx.url.clone().or_else(|| ctx.to_url())) - .ok_or(gix_credentials::protocol::Error::UrlMissing)?; + .ok_or_raise_erased(|| { + ValidationError::new("Either 'url' field or both 'protocol' and 'host' fields must be provided") + })?; let (mut cascade, _action_with_normalized_url, prompt_opts) = repo .config_snapshot() - .credential_helpers(gix_url::parse(&url).map_err(gix_error::Exn::into_error)?) - .map_err(|source| gix_credentials::protocol::Error::ConfigureCredentialHelpers { - source: Box::new(source), - })?; + .credential_helpers(gix_url::parse(&url).or_erased()?) + .or_raise_erased(|| message("Could not configure credential helpers"))?; let outcome = cascade.invoke(action, prompt_opts.clone()); previous_cascade_and_prompt = Some((cascade, prompt_opts)); outcome diff --git a/gix/tests/gix/remote/connect.rs b/gix/tests/gix/remote/connect.rs index d5de8bb00a7..1d0fd64f236 100644 --- a/gix/tests/gix/remote/connect.rs +++ b/gix/tests/gix/remote/connect.rs @@ -5,6 +5,8 @@ mod http_authentication { use std::io::{BufRead, Write}; + use gix_error::ErrorExt; + #[test] fn cached_credentials_are_selected_without_prompting() -> crate::Result { if gix_testtools::run_in_isolated_process()? { @@ -65,7 +67,7 @@ mod http_authentication { .with_credentials(|action| { obtained = Some(authenticate(action)); // Stop after credential lookup, before the transport sends these dummy credentials. - Err(gix_credentials::protocol::Error::Quit) + Err(gix_error::message("stop after credential lookup").raise_erased()) }) .ref_map(gix::progress::Discard, Default::default()); server.join().expect("the HTTP fixture thread does not panic")?; @@ -74,7 +76,8 @@ mod http_authentication { "the callback stops the handshake after credential lookup" ); let outcome = obtained - .expect("the 401 response invokes the credential callback")? + .expect("the 401 response invokes the credential callback") + .map_err(gix_error::Exn::into_error)? .expect("the cached credential is complete"); assert_eq!( outcome.identity.username, "cached-user", From 2c08f901b60e7f573d7a6ac082988f4be2ab4f96 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 20 Aug 2026 17:11:21 +0200 Subject: [PATCH 12/51] change(gix-dir)!: migrate errors to gix-error rubberstamp --- Cargo.lock | 2 +- gix-dir/Cargo.toml | 2 +- gix-dir/src/walk/classify.rs | 5 +-- gix-dir/src/walk/function.rs | 32 +++++++++++------- gix-dir/src/walk/mod.rs | 33 ++----------------- gix-dir/src/walk/readdir.rs | 14 ++++---- gix-dir/tests/dir/walk.rs | 12 +++---- gix-dir/tests/walk_utils/mod.rs | 11 ++++--- .../src/index_as_worktree_with_renames/mod.rs | 2 +- .../index_as_worktree_with_renames/types.rs | 2 +- gix/src/dirwalk/iter.rs | 14 +++++--- gix/src/dirwalk/mod.rs | 2 +- gix/src/repository/dirwalk.rs | 3 +- 13 files changed, 59 insertions(+), 75 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 091dab7ca05..11dab7446bf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1846,6 +1846,7 @@ dependencies = [ "bstr", "gix-dir", "gix-discover", + "gix-error", "gix-fs", "gix-ignore", "gix-index", @@ -1857,7 +1858,6 @@ dependencies = [ "gix-utils", "gix-worktree", "pretty_assertions", - "thiserror 2.0.18", ] [[package]] diff --git a/gix-dir/Cargo.toml b/gix-dir/Cargo.toml index 5d75af230c9..95c40c78704 100644 --- a/gix-dir/Cargo.toml +++ b/gix-dir/Cargo.toml @@ -22,6 +22,7 @@ sha1 = ["gix-index/sha1", "gix-worktree/sha1"] sha256 = ["gix-index/sha256", "gix-worktree/sha256"] [dependencies] +gix-error = { version = "^0.3.0", path = "../gix-error" } gix-trace = { version = "^0.1.21", path = "../gix-trace" } gix-index = { version = "^0.55.0", path = "../gix-index" } gix-discover = { version = "^0.55.0", path = "../gix-discover" } @@ -34,7 +35,6 @@ gix-ignore = { version = "^0.22.1", path = "../gix-ignore" } gix-utils = { version = "^0.3.6", path = "../gix-utils", features = ["bstr"] } bstr = { version = "1.12.0", default-features = false } -thiserror = "2.0.18" [dev-dependencies] gix-dir = { path = ".", features = ["sha1", "sha256"] } diff --git a/gix-dir/src/walk/classify.rs b/gix-dir/src/walk/classify.rs index 6bf9d0b6961..e25c9fb145f 100644 --- a/gix-dir/src/walk/classify.rs +++ b/gix-dir/src/walk/classify.rs @@ -4,6 +4,7 @@ use std::{ }; use bstr::{BStr, BString, ByteSlice}; +use gix_error::{ResultExt, message}; use crate::{ Entry, EntryRef, entry, @@ -176,7 +177,7 @@ pub fn path( ) .map(|platform| platform.excluded_kind()) }) - .map_err(Error::ExcludesAccess)? + .or_raise_erased(|| message("Failed to update the excludes stack to see if a path is excluded"))? .filter(|_| filename_start_idx > 0) { out.status = entry::Status::Ignored(excluded); @@ -253,7 +254,7 @@ pub fn path( .at_entry(rela_path.as_bstr(), is_dir, ctx.objects) .map(|platform| platform.excluded_kind()) }) - .map_err(Error::ExcludesAccess)? + .or_raise_erased(|| message("Failed to update the excludes stack to see if a path is excluded"))? { if emit_ignored.is_some() { if matches!( diff --git a/gix-dir/src/walk/function.rs b/gix-dir/src/walk/function.rs index 674737ff20b..6a9d1290c09 100644 --- a/gix-dir/src/walk/function.rs +++ b/gix-dir/src/walk/function.rs @@ -4,6 +4,7 @@ use std::{ }; use bstr::{BStr, BString, ByteSlice}; +use gix_error::{ErrorExt, OptionExt, ResultExt, ValidationError}; use crate::{ EntryRef, entry, @@ -88,7 +89,10 @@ pub fn walk( ); if !can_recurse { if buf.is_empty() && !root_info.disk_kind.is_some_and(|kind| kind.is_dir()) { - return Err(Error::WorktreeRootIsFile { root: root.to_owned() }); + return Err( + ValidationError::new(format!("Worktree root at '{}' is not a directory", root.display())) + .raise_erased(), + ); } if options.precompose_unicode { buf = gix_utils::str::precompose_bstr(buf.into()).into_owned(); @@ -136,21 +140,25 @@ fn assure_no_symlink_in_root<'root>( let worktree_relative = root .strip_prefix(worktree_root) .expect("BUG: root was created from worktree_root + prefix"); - let worktree_relative = gix_path::normalize(worktree_relative.into(), Path::new("")) - .ok_or(Error::NormalizeRoot { root: root.to_owned() })?; + let worktree_relative = gix_path::normalize(worktree_relative.into(), Path::new("")).ok_or_raise_erased(|| { + ValidationError::new(format!( + "Traversal root '{}' contains relative path components and could not be normalized", + root.display() + )) + })?; for (idx, component) in worktree_relative.components().enumerate() { current.push(component); - let meta = current.symlink_metadata().map_err(|err| Error::SymlinkMetadata { - source: err, - path: current.to_owned(), - })?; + let meta = current + .symlink_metadata() + .or_raise_erased(|| gix_error::message!("Could not obtain symlink metadata on '{}'", current.display()))?; if meta.is_symlink() { - return Err(Error::SymlinkInRoot { - root: root.to_owned(), - worktree_root: worktree_root.to_owned(), - component_index: idx, - }); + return Err(ValidationError::new(format!( + "A symlink was found at component {idx} of traversal root '{}' as seen from worktree root '{}'", + root.display(), + worktree_root.display() + )) + .raise_erased()); } } Ok((current, worktree_relative)) diff --git a/gix-dir/src/walk/mod.rs b/gix-dir/src/walk/mod.rs index 0529f097cca..044df22d59c 100644 --- a/gix-dir/src/walk/mod.rs +++ b/gix-dir/src/walk/mod.rs @@ -1,4 +1,4 @@ -use std::{collections::BTreeSet, path::PathBuf, sync::atomic::AtomicBool}; +use std::{collections::BTreeSet, sync::atomic::AtomicBool}; use bstr::{BStr, BString}; @@ -274,36 +274,7 @@ pub struct Outcome { } /// The error returned by [`walk()`](function::walk()). -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] -pub enum Error { - #[error("Interrupted")] - Interrupted, - #[error("Worktree root at '{}' is not a directory", root.display())] - WorktreeRootIsFile { root: PathBuf }, - #[error("Traversal root '{}' contains relative path components and could not be normalized", root.display())] - NormalizeRoot { root: PathBuf }, - #[error("A symlink was found at component {component_index} of traversal root '{}' as seen from worktree root '{}'", root.display(), worktree_root.display())] - SymlinkInRoot { - root: PathBuf, - worktree_root: PathBuf, - /// This index starts at 0, with 0 being the first component. - component_index: usize, - }, - #[error("Failed to update the excludes stack to see if a path is excluded")] - ExcludesAccess(std::io::Error), - #[error("Failed to read the directory at '{}'", path.display())] - ReadDir { path: PathBuf, source: std::io::Error }, - #[error("Could not obtain directory entry in root of '{}'", parent_directory.display())] - DirEntry { - parent_directory: PathBuf, - source: std::io::Error, - }, - #[error("Could not obtain filetype of directory entry '{}'", path.display())] - DirEntryFileType { path: PathBuf, source: std::io::Error }, - #[error("Could not obtain symlink metadata on '{}'", path.display())] - SymlinkMetadata { path: PathBuf, source: std::io::Error }, -} +pub type Error = gix_error::Exn; mod classify; pub(crate) mod function; diff --git a/gix-dir/src/walk/readdir.rs b/gix-dir/src/walk/readdir.rs index 56374cb2f86..7fa828f05f3 100644 --- a/gix-dir/src/walk/readdir.rs +++ b/gix-dir/src/walk/readdir.rs @@ -5,6 +5,7 @@ use std::{ }; use bstr::{BStr, BString, ByteSlice}; +use gix_error::{ErrorExt, ResultExt, message}; use crate::{ Entry, EntryRef, entry, @@ -34,21 +35,18 @@ pub(super) fn recursive( state: &mut State, ) -> Result<(Action, bool), Error> { if ctx.should_interrupt.is_some_and(|flag| flag.load(Ordering::Relaxed)) { - return Err(Error::Interrupted); + return Err(message("Interrupted").raise_erased()); } out.read_dir_calls += 1; - let entries = gix_fs::read_dir(current, opts.precompose_unicode).map_err(|err| Error::ReadDir { - path: current.to_owned(), - source: err, - })?; + let entries = gix_fs::read_dir(current, opts.precompose_unicode) + .or_raise_erased(|| gix_error::message!("Failed to read the directory at '{}'", current.display()))?; let mut num_entries = 0; let mark = state.mark(may_collapse); let mut prevent_collapse = false; for entry in entries { - let entry = entry.map_err(|err| Error::DirEntry { - parent_directory: current.to_owned(), - source: err, + let entry = entry.or_raise_erased(|| { + gix_error::message!("Could not obtain directory entry in root of '{}'", current.display()) })?; // Important to count right away, otherwise the directory could be seen as empty even though it's not. // That is, this should be independent of the kind. diff --git a/gix-dir/tests/dir/walk.rs b/gix-dir/tests/dir/walk.rs index 44a1795f805..db4892bad70 100644 --- a/gix-dir/tests/dir/walk.rs +++ b/gix-dir/tests/dir/walk.rs @@ -36,10 +36,7 @@ fn root_is_fifo() { ) }) .unwrap_err(); - assert!( - matches!(err, gix_dir::walk::Error::WorktreeRootIsFile { .. }), - "roots simply need to be directories to work" - ); + assert!(err.is_validation(), "roots simply need to be directories to work"); } #[test] @@ -204,8 +201,9 @@ fn root_may_not_lead_through_symlinks() -> crate::Result { Default::default(), ) .unwrap_err(); + assert!(err.is_validation(), "symlinks in the traversal root are invalid"); assert!( - matches!(err, walk::Error::SymlinkInRoot { component_index, .. } if component_index == expected), + err.to_string().contains(&format!("component {expected}")), "{name} should have component {expected}" ); } @@ -320,7 +318,7 @@ fn should_interrupt_works_even_in_empty_directories() { }, ) .unwrap_err(); - assert!(matches!(err, gix_dir::walk::Error::Interrupted)); + assert_eq!(err.to_string(), "Interrupted"); } #[test] @@ -3354,7 +3352,7 @@ fn root_that_is_untracked_file_is_returned() -> crate::Result { fn top_level_root_that_is_a_file() { let root = fixture("just-a-file"); let err = try_collect(&root, None, |keep, ctx| walk(&root, ctx, options(), keep)).unwrap_err(); - assert!(matches!(err, walk::Error::WorktreeRootIsFile { .. })); + assert!(err.is_validation()); } #[test] diff --git a/gix-dir/tests/walk_utils/mod.rs b/gix-dir/tests/walk_utils/mod.rs index 0abee1d026e..54f8b92b92f 100644 --- a/gix-dir/tests/walk_utils/mod.rs +++ b/gix-dir/tests/walk_utils/mod.rs @@ -201,7 +201,7 @@ pub fn try_collect( worktree_root: &Path, root: Option<&Path>, cb: impl FnOnce(&mut dyn walk::Delegate, walk::Context) -> Result<(walk::Outcome, PathBuf), walk::Error>, -) -> Result<((walk::Outcome, PathBuf), Entries), walk::Error> { +) -> Result<((walk::Outcome, PathBuf), Entries), gix_error::Error> { try_collect_filtered(worktree_root, root, cb, None::<&str>) } @@ -210,7 +210,7 @@ pub fn try_collect_filtered( root: Option<&Path>, cb: impl FnOnce(&mut dyn walk::Delegate, walk::Context) -> Result<(walk::Outcome, PathBuf), walk::Error>, patterns: impl IntoIterator>, -) -> Result<((walk::Outcome, PathBuf), Entries), walk::Error> { +) -> Result<((walk::Outcome, PathBuf), Entries), gix_error::Error> { try_collect_filtered_opts_collect(worktree_root, root, cb, patterns, Default::default()) } @@ -220,7 +220,7 @@ pub fn try_collect_filtered_opts_collect( cb: impl FnOnce(&mut dyn walk::Delegate, walk::Context) -> Result<(walk::Outcome, PathBuf), walk::Error>, patterns: impl IntoIterator>, options: Options<'_>, -) -> Result<((walk::Outcome, PathBuf), Entries), walk::Error> { +) -> Result<((walk::Outcome, PathBuf), Entries), gix_error::Error> { let mut dlg = gix_dir::walk::delegate::Collect::default(); let outcome = try_collect_filtered_opts(worktree_root, root, None, None, cb, patterns, &mut dlg, options)?; Ok((outcome, dlg.into_entries_by_path())) @@ -233,7 +233,7 @@ pub fn try_collect_filtered_opts_collect_with_root( cb: impl FnOnce(&mut dyn walk::Delegate, walk::Context) -> Result<(walk::Outcome, PathBuf), walk::Error>, patterns: impl IntoIterator>, options: Options<'_>, -) -> Result<((walk::Outcome, PathBuf), Entries), walk::Error> { +) -> Result<((walk::Outcome, PathBuf), Entries), gix_error::Error> { let mut dlg = gix_dir::walk::delegate::Collect::default(); let outcome = try_collect_filtered_opts( worktree_root, @@ -284,7 +284,7 @@ pub fn try_collect_filtered_opts( git_dir, should_interrupt, }: Options<'_>, -) -> Result<(walk::Outcome, PathBuf), walk::Error> { +) -> Result<(walk::Outcome, PathBuf), gix_error::Error> { let git_dir = worktree_root.join(git_dir.unwrap_or(".git")); let mut index = std::fs::read(git_dir.join("index")).ok().map_or_else( || gix_index::State::new(gix_testtools::object_hash()), @@ -361,6 +361,7 @@ pub fn try_collect_filtered_opts( should_interrupt, }, ) + .map_err(Into::into) } pub struct Options<'a> { diff --git a/gix-status/src/index_as_worktree_with_renames/mod.rs b/gix-status/src/index_as_worktree_with_renames/mod.rs index e037616d27e..b103b227095 100644 --- a/gix-status/src/index_as_worktree_with_renames/mod.rs +++ b/gix-status/src/index_as_worktree_with_renames/mod.rs @@ -117,7 +117,7 @@ pub(super) mod function { options, &mut collect, ) - .map_err(Error::DirWalk) + .map_err(|err| Error::DirWalk(err.into_error())) } }) .map_err(Error::SpawnThread) 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 c4ad76bac9e..95ebb166d45 100644 --- a/gix-status/src/index_as_worktree_with_renames/types.rs +++ b/gix-status/src/index_as_worktree_with_renames/types.rs @@ -11,7 +11,7 @@ pub enum Error { #[error(transparent)] TrackedFileModifications(#[from] crate::index_as_worktree::Error), #[error(transparent)] - DirWalk(gix_dir::walk::Error), + DirWalk(gix_error::Error), #[error(transparent)] SpawnThread(std::io::Error), #[error("Failed to change the context for querying gitattributes to the respective path")] diff --git a/gix/src/dirwalk/iter.rs b/gix/src/dirwalk/iter.rs index 7cd369c9b72..92e54241cc5 100644 --- a/gix/src/dirwalk/iter.rs +++ b/gix/src/dirwalk/iter.rs @@ -1,5 +1,8 @@ use std::path::PathBuf; +#[cfg(feature = "parallel")] +use gix_error::ErrorExt; + use super::Iter; use crate::{ PathspecDetached, Repository, bstr::BString, dirwalk, util::OwnedOrStaticAtomicBool, @@ -81,10 +84,13 @@ impl Iter { index, excludes: out.excludes.detach(), pathspec: out.pathspec.detach().map_err(|err| { - dirwalk::Error::Walk(gix_dir::walk::Error::ReadDir { - path: repo.git_dir().to_owned(), - source: err, - }) + dirwalk::Error::Walk( + err.and_raise(gix_error::message!( + "Could not detach the pathspec at '{}'", + repo.git_dir().display() + )) + .into_error(), + ) })?, traversal_root: out.traversal_root, dirwalk: out.dirwalk, diff --git a/gix/src/dirwalk/mod.rs b/gix/src/dirwalk/mod.rs index 63d427e8f5b..bd3c83dccbd 100644 --- a/gix/src/dirwalk/mod.rs +++ b/gix/src/dirwalk/mod.rs @@ -43,7 +43,7 @@ pub struct Iter { #[expect(missing_docs)] pub enum Error { #[error(transparent)] - Walk(#[from] gix_dir::walk::Error), + Walk(gix_error::Error), #[error("A working tree is required to perform a directory walk")] MissingWorkDir, #[error(transparent)] diff --git a/gix/src/repository/dirwalk.rs b/gix/src/repository/dirwalk.rs index eb8e4b6e5e7..2a992182807 100644 --- a/gix/src/repository/dirwalk.rs +++ b/gix/src/repository/dirwalk.rs @@ -104,7 +104,8 @@ impl Repository { }, opts, delegate, - )?; + ) + .map_err(|err| dirwalk::Error::Walk(err.into_error()))?; Ok(dirwalk::Outcome { dirwalk: outcome, From 15dd98fc5c76e6cbe03c428872d8913891234126 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 20 Aug 2026 17:12:01 +0200 Subject: [PATCH 13/51] change(gix-status)!: migrate errors to gix-error rubberstamp --- Cargo.lock | 1 - gix-status/Cargo.toml | 1 - gix-status/src/index_as_worktree/function.rs | 50 +++-- gix-status/src/index_as_worktree/traits.rs | 4 +- gix-status/src/index_as_worktree/types.rs | 29 +-- .../src/index_as_worktree_with_renames/mod.rs | 211 ++++++++++-------- .../index_as_worktree_with_renames/types.rs | 23 +- gix-status/src/stack.rs | 11 +- gix-status/tests/status/index_as_worktree.rs | 23 +- gix/src/status/index_worktree.rs | 37 ++- 10 files changed, 184 insertions(+), 206 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 11dab7446bf..0a69ada787c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2497,7 +2497,6 @@ dependencies = [ "hashbrown 0.16.1", "portable-atomic", "pretty_assertions", - "thiserror 2.0.18", "windows-sys 0.61.2", ] diff --git a/gix-status/Cargo.toml b/gix-status/Cargo.toml index 7fd6af278a6..5a9224c8472 100644 --- a/gix-status/Cargo.toml +++ b/gix-status/Cargo.toml @@ -39,7 +39,6 @@ gix-pathspec = { version = "^0.20.0", path = "../gix-pathspec" } gix-dir = { version = "^0.29.1", path = "../gix-dir", optional = true } gix-diff = { version = "^0.67.1", path = "../gix-diff", default-features = false, features = ["blob"], optional = true } -thiserror = "2.0.18" filetime = "0.2.29" bstr = { version = "1.12.0", default-features = false } diff --git a/gix-status/src/index_as_worktree/function.rs b/gix-status/src/index_as_worktree/function.rs index 8a480d71239..40643539430 100644 --- a/gix-status/src/index_as_worktree/function.rs +++ b/gix-status/src/index_as_worktree/function.rs @@ -1,5 +1,4 @@ use std::{ - io, path::Path, slice::Chunks, sync::atomic::{AtomicUsize, Ordering}, @@ -7,6 +6,7 @@ use std::{ use bstr::BStr; use filetime::FileTime; +use gix_error::{ErrorExt, ResultExt, message}; use gix_features::parallel::{Reduce, in_parallel_if}; use gix_filter::pipeline::convert::ToGitOutcome; use gix_object::FindExt; @@ -52,12 +52,12 @@ use crate::{ /// Thus, some care has to be taken to do the right thing when letting the index match the worktree by evaluating the changes observed /// by the `collector`. #[expect(clippy::too_many_arguments)] -pub fn index_as_worktree<'index, T, U, Find, E>( +pub fn index_as_worktree<'index, T, U, Find>( index: &'index gix_index::State, worktree: &Path, collector: &mut impl VisitEntry<'index, ContentChange = T, SubmoduleStatus = U>, compare: impl CompareBlobs + Send + Clone, - submodule: impl SubmoduleStatus + Send + Clone, + submodule: impl SubmoduleStatus + Send + Clone, objects: Find, progress: &mut dyn gix_features::progress::Progress, Context { @@ -71,7 +71,6 @@ pub fn index_as_worktree<'index, T, U, Find, E>( where T: Send, U: Send, - E: std::error::Error + Send + Sync + 'static, Find: gix_object::Find + Send + Clone, { // the order is absolutely critical here we use the old timestamp to detect racy index entries @@ -260,19 +259,18 @@ type StatusResult<'index, T, U> = Result<(&'index gix_index::Entry, usize, &'ind impl<'index> State<'_, 'index> { #[expect(clippy::too_many_arguments)] - fn process( + fn process( &mut self, entries: &'index [gix_index::Entry], entry: &'index gix_index::Entry, entry_index: usize, pathspec: &mut gix_pathspec::Search, diff: &mut impl CompareBlobs, - submodule: &mut impl SubmoduleStatus, + submodule: &mut impl SubmoduleStatus, objects: &Find, outer_entry_index: &mut usize, ) -> Option> where - E: std::error::Error + Send + Sync + 'static, Find: gix_object::Find, { if entry.flags.intersects( @@ -370,16 +368,15 @@ impl<'index> State<'_, 'index> { /// which is a constant. /// /// Adapted from [here](https://github.com/GitoxideLabs/gitoxide/pull/805#discussion_r1164676777). - fn compute_status( + fn compute_status( &mut self, entry: &gix_index::Entry, rela_path: &BStr, diff: &mut impl CompareBlobs, - submodule: &mut impl SubmoduleStatus, + submodule: &mut impl SubmoduleStatus, objects: &Find, ) -> Result>, Error> where - E: std::error::Error + Send + Sync + 'static, Find: gix_object::Find, { let worktree_path = match self.path_stack.verified_path(gix_path::from_bstr(rela_path).as_ref()) { @@ -388,7 +385,11 @@ impl<'index> State<'_, 'index> { Err(err) if gix_fs::io_err::is_not_found(err.kind(), err.raw_os_error()) => { return Ok(Some(Change::Removed.into())); } - Err(err) => return Err(Error::Io(err)), + Err(err) => { + return Err(err + .and_raise(gix_error::message!("Could not access worktree path {rela_path:?}")) + .erased()); + } }; // Acquire metadata. On Windows we consult the precomputed stats first and @@ -418,12 +419,9 @@ impl<'index> State<'_, 'index> { // The only exception here are submodules which are part of the index despite being directories. if metadata.is_dir() { if entry.mode.is_submodule() { - let status = submodule - .status(entry, rela_path) - .map_err(|err| Error::SubmoduleStatus { - rela_path: rela_path.into(), - source: Box::new(err), - })?; + let status = submodule.status(entry, rela_path).or_raise_erased(|| { + gix_error::message!("Could not determine status for submodule at '{rela_path}'") + })?; return Ok(status.map(|status| Change::SubmoduleModification(status).into())); } else { return Ok(Some(Change::Removed.into())); @@ -435,9 +433,12 @@ impl<'index> State<'_, 'index> { } #[cfg(windows)] - let new_stat = metadata.to_stat()?; + let new_stat = metadata + .to_stat() + .or_raise_erased(|| message("The clock was off when reading file metadata"))?; #[cfg(not(windows))] - let new_stat = gix_index::entry::Stat::from_fs(&metadata)?; + let new_stat = gix_index::entry::Stat::from_fs(&metadata) + .or_raise_erased(|| message("The clock was off when reading file metadata"))?; #[cfg(windows)] let mode_change = metadata.mode_change(entry.mode, self.options.fs.symlink, self.options.fs.executable_bit); @@ -593,7 +594,7 @@ where fn read_blob(self) -> Result<&'a [u8], Error> { self.objects .find_blob(self.id, self.buf) - .map_err(|err| Error::Find(err.into_error())) + .or_raise_erased(|| message("Failed to obtain blob from object database")) .map(|b| { self.odb_reads.fetch_add(1, Ordering::Relaxed); self.odb_bytes.fetch_add(b.data.len() as u64, Ordering::Relaxed); @@ -636,7 +637,7 @@ where }, &mut |buf| self.objects.find_blob(self.id, buf).map(|_| Some(())), ) - .map_err(|err| Error::Io(io::Error::other(err)))?; + .or_raise_erased(|| message("Could not convert worktree file to Git format"))?; let len = match out { ToGitOutcome::Unchanged(_) => Some(self.file_len), ToGitOutcome::Process(_) | ToGitOutcome::Buffer(_) => None, @@ -735,6 +736,11 @@ fn live_metadata(worktree_path: &Path) -> Result match gix_index::fs::Metadata::from_path_no_follow(worktree_path) { Ok(md) => Ok(Some(md)), Err(err) if gix_fs::io_err::is_not_found(err.kind(), err.raw_os_error()) => Ok(None), - Err(err) => Err(Error::Io(err)), + Err(err) => Err(err + .and_raise(gix_error::message!( + "Could not read metadata for worktree path '{}'", + worktree_path.display() + )) + .erased()), } } diff --git a/gix-status/src/index_as_worktree/traits.rs b/gix-status/src/index_as_worktree/traits.rs index fb1d62f7de8..ca5678a55bd 100644 --- a/gix-status/src/index_as_worktree/traits.rs +++ b/gix-status/src/index_as_worktree/traits.rs @@ -30,11 +30,9 @@ pub trait CompareBlobs { pub trait SubmoduleStatus { /// The status result, describing in which way the submodule changed. type Output; - /// A custom error that may occur while computing the submodule status. - type Error: std::error::Error + Send + Sync + 'static; /// Compute the status of the submodule at `entry` and `rela_path`, or return `None` if no change was detected. - fn status(&mut self, entry: &gix_index::Entry, rela_path: &BStr) -> Result, Self::Error>; + fn status(&mut self, entry: &gix_index::Entry, rela_path: &BStr) -> Result, gix_error::Exn>; } /// Lazy borrowed access to worktree or blob data, with streaming support for worktree files. diff --git a/gix-status/src/index_as_worktree/types.rs b/gix-status/src/index_as_worktree/types.rs index 537bc3b1fd3..07177307e0f 100644 --- a/gix-status/src/index_as_worktree/types.rs +++ b/gix-status/src/index_as_worktree/types.rs @@ -1,35 +1,10 @@ use std::sync::atomic::AtomicBool; -use bstr::{BStr, BString}; +use bstr::BStr; use gix_index::entry; /// The error returned by [index_as_worktree()`](crate::index_as_worktree()). -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] -pub enum Error { - #[error("Could not convert path to UTF8")] - IllformedUtf8, - #[error("The clock was off when reading file related metadata after updating a file on disk")] - Time(#[from] std::time::SystemTimeError), - #[error("IO error while writing blob or reading file metadata or changing filetype")] - Io(#[from] std::io::Error), - #[error("Failed to obtain blob from object database")] - Find(#[source] gix_error::Error), - #[error("Could not determine status for submodule at '{rela_path}'")] - SubmoduleStatus { - rela_path: BString, - source: Box, - }, -} - -impl From for Error { - fn from(err: gix_hash::io::Error) -> Self { - let kind = err - .downcast_any_ref::() - .map_or(std::io::ErrorKind::Other, std::io::Error::kind); - Error::Io(std::io::Error::new(kind, err.into_error())) - } -} +pub type Error = gix_error::Exn; /// Options that control how the index status with a worktree is computed. #[derive(Clone, Default, Debug, PartialEq, Eq, Hash)] diff --git a/gix-status/src/index_as_worktree_with_renames/mod.rs b/gix-status/src/index_as_worktree_with_renames/mod.rs index b103b227095..9563d15a387 100644 --- a/gix-status/src/index_as_worktree_with_renames/mod.rs +++ b/gix-status/src/index_as_worktree_with_renames/mod.rs @@ -9,6 +9,7 @@ pub(super) mod function { use std::{borrow::Cow, path::Path}; use bstr::ByteSlice; + use gix_error::{ResultExt, message}; use gix_worktree::stack::State; use crate::{ @@ -45,12 +46,12 @@ pub(super) mod function { /// * `options` /// - a way to configure both paths of the operation. #[expect(clippy::too_many_arguments)] - pub fn index_as_worktree_with_renames<'index, T, U, Find, E>( + pub fn index_as_worktree_with_renames<'index, T, U, Find>( index: &'index gix_index::State, worktree: &Path, collector: &mut impl VisitEntry<'index, ContentChange = T, SubmoduleStatus = U>, compare: impl CompareBlobs + Send + Clone, - submodule: impl SubmoduleStatus + Send + Clone, + submodule: impl SubmoduleStatus + Send + Clone, objects: Find, progress: &mut dyn gix_features::progress::Progress, mut ctx: Context<'_>, @@ -59,7 +60,6 @@ pub(super) mod function { where T: Send + Clone, U: Send + Clone, - E: std::error::Error + Send + Sync + 'static, Find: gix_object::Find + gix_object::FindHeader + Send + Clone, { let mut tracked_file_modifications = options.tracked_file_modifications; @@ -117,10 +117,9 @@ pub(super) mod function { options, &mut collect, ) - .map_err(|err| Error::DirWalk(err.into_error())) } }) - .map_err(Error::SpawnThread) + .or_raise_erased(|| message("Failed to spawn directory-walk thread")) }) .transpose()?; @@ -158,10 +157,9 @@ pub(super) mod function { }, tracked_file_modifications, ) - .map_err(Error::TrackedFileModifications) } }) - .map_err(Error::SpawnThread)?; + .or_raise_erased(|| message("Failed to spawn index-worktree status thread"))?; let tracker = options .rewrites @@ -217,75 +215,79 @@ pub(super) mod function { remaining }); - let outcome = tracker.emit( - |dest, src| { - match src { - None => { - let entry = rewrite::change_to_entry(dest.change, entries); - if let Some(v) = entries_for_sorting.as_mut() { - v.push(entry); - } else { - collector.visit_entry(entry); - } - } - Some(src) => { - let ModificationOrDirwalkEntry::DirwalkEntry { - id, - entry, - collapsed_directory_status, - } = dest.change - else { - unreachable!("BUG: only possible destinations are dirwalk entries (additions)"); - }; - let source = match src.change { - ModificationOrDirwalkEntry::Modification(record) => { - RewriteSource::RewriteFromIndex { - index_entries: entries, - source_entry: record.entry, - source_entry_index: record.entry_index, - source_rela_path: record.relative_path, - source_status: record.status.clone(), - } + let outcome = tracker + .emit( + |dest, src| { + match src { + None => { + let entry = rewrite::change_to_entry(dest.change, entries); + if let Some(v) = entries_for_sorting.as_mut() { + v.push(entry); + } else { + collector.visit_entry(entry); } - ModificationOrDirwalkEntry::DirwalkEntry { + } + Some(src) => { + let ModificationOrDirwalkEntry::DirwalkEntry { id, entry, collapsed_directory_status, - } => RewriteSource::CopyFromDirectoryEntry { - source_dirwalk_entry: entry.clone(), - source_dirwalk_entry_collapsed_directory_status: - *collapsed_directory_status, - source_dirwalk_entry_id: *id, - }, - }; - - let entry = Entry::Rewrite { - source, - dirwalk_entry: entry, - dirwalk_entry_collapsed_directory_status: collapsed_directory_status, - dirwalk_entry_id: id, - diff: src.diff, - copy: src.kind == gix_diff::rewrites::tracker::visit::SourceKind::Copy, - }; - if let Some(v) = entries_for_sorting.as_mut() { - v.push(entry); - } else { - collector.visit_entry(entry); + } = dest.change + else { + unreachable!( + "BUG: only possible destinations are dirwalk entries (additions)" + ); + }; + let source = match src.change { + ModificationOrDirwalkEntry::Modification(record) => { + RewriteSource::RewriteFromIndex { + index_entries: entries, + source_entry: record.entry, + source_entry_index: record.entry_index, + source_rela_path: record.relative_path, + source_status: record.status.clone(), + } + } + ModificationOrDirwalkEntry::DirwalkEntry { + id, + entry, + collapsed_directory_status, + } => RewriteSource::CopyFromDirectoryEntry { + source_dirwalk_entry: entry.clone(), + source_dirwalk_entry_collapsed_directory_status: + *collapsed_directory_status, + source_dirwalk_entry_id: *id, + }, + }; + + let entry = Entry::Rewrite { + source, + dirwalk_entry: entry, + dirwalk_entry_collapsed_directory_status: collapsed_directory_status, + dirwalk_entry_id: id, + diff: src.diff, + copy: src.kind == gix_diff::rewrites::tracker::visit::SourceKind::Copy, + }; + if let Some(v) = entries_for_sorting.as_mut() { + v.push(entry); + } else { + collector.visit_entry(entry); + } } } - } - std::ops::ControlFlow::Continue(()) - }, - &mut ctx.resource_cache, - &objects, - |_cb| { - // NOTE: to make this work, we'd want to wait the index modification check to complete. - // Then it's possible to efficiently emit the tracked files along with what we already sent, - // i.e. untracked and ignored files. - gix_features::trace::debug!("full-tree copy tracking isn't currently supported"); - Ok::<_, std::io::Error>(()) - }, - )?; + std::ops::ControlFlow::Continue(()) + }, + &mut ctx.resource_cache, + &objects, + |_cb| { + // NOTE: to make this work, we'd want to wait the index modification check to complete. + // Then it's possible to efficiently emit the tracked files along with what we already sent, + // i.e. untracked and ignored files. + gix_features::trace::debug!("full-tree copy tracking isn't currently supported"); + Ok::<_, std::io::Error>(()) + }, + ) + .or_raise_erased(|| message("Could not track worktree rewrites"))?; if let Some(mut v) = entries_for_sorting { v.sort_by(|a, b| a.destination_rela_path().cmp(b.destination_rela_path())); @@ -413,6 +415,8 @@ pub(super) mod function { } mod rewrite { + use gix_error::{ErrorExt, ResultExt}; + use crate::{ index_as_worktree::{Change, EntryStatus}, index_as_worktree_with_renames::{Entry, Error}, @@ -523,9 +527,9 @@ pub(super) mod function { return Ok(object_hash.null()); } Kind::File => { - let platform = attrs - .at_entry(rela_path, None, objects) - .map_err(Error::SetAttributeContext)?; + let platform = attrs.at_entry(rela_path, None, objects).or_raise_erased(|| { + gix_error::message!("Failed to change the attribute context for worktree path {rela_path:?}") + })?; let rela_path = gix_path::from_bstr(rela_path); let file_path = worktree_root.join(rela_path.as_ref()); let file = match std::fs::File::open(&file_path) { @@ -543,44 +547,73 @@ pub(super) mod function { ); return Ok(object_hash.null()); } - Err(err) => return Err(Error::OpenWorktreeFile(err)), + Err(err) => { + return Err(err + .and_raise(gix_error::message!( + "Could not open worktree file '{}' for reading", + file_path.display() + )) + .erased()); + } }; - let out = filter.convert_to_git( - file, - rela_path.as_ref(), - &mut |_path, attrs| { - platform.matching_attributes(attrs); - }, - &mut |_buf| Ok(None), - )?; + let out = filter + .convert_to_git( + file, + rela_path.as_ref(), + &mut |_path, attrs| { + platform.matching_attributes(attrs); + }, + &mut |_buf| Ok(None), + ) + .or_raise_erased(|| { + gix_error::message!("Could not convert worktree file {rela_path:?} to Git format") + })?; match out { ToGitOutcome::Unchanged(mut file) => gix_object::compute_stream_hash( object_hash, gix_object::Kind::Blob, &mut file, - file_path.metadata().map_err(Error::OpenWorktreeFile)?.len(), + file_path + .metadata() + .or_raise_erased(|| { + gix_error::message!( + "Could not read metadata for worktree file '{}'", + file_path.display() + ) + })? + .len(), &mut gix_features::progress::Discard, should_interrupt, ) - .map_err(|err| Error::HashFile(std::io::Error::other(err.into_error())))?, + .or_raise_erased(|| { + gix_error::message!("Could not hash worktree file '{}'", file_path.display()) + })?, ToGitOutcome::Buffer(buf) => gix_object::compute_hash(object_hash, gix_object::Kind::Blob, buf) .map_err(gix_hash::io::from_hasher) - .map_err(|err| Error::HashFile(std::io::Error::other(err.into_error())))?, + .or_raise_erased(|| { + gix_error::message!("Could not hash worktree file '{}'", file_path.display()) + })?, ToGitOutcome::Process(mut stream) => { buf.clear(); - stream.read_to_end(buf).map_err(Error::HashFile)?; + stream.read_to_end(buf).or_raise_erased(|| { + gix_error::message!("Could not read filtered worktree file '{}'", file_path.display()) + })?; gix_object::compute_hash(object_hash, gix_object::Kind::Blob, buf) .map_err(gix_hash::io::from_hasher) - .map_err(|err| Error::HashFile(std::io::Error::other(err.into_error())))? + .or_raise_erased(|| { + gix_error::message!("Could not hash worktree file '{}'", file_path.display()) + })? } } } Kind::Symlink => { let path = worktree_root.join(gix_path::from_bstr(rela_path)); - let target = gix_path::into_bstr(std::fs::read_link(path).map_err(Error::ReadLink)?); + let target = gix_path::into_bstr(std::fs::read_link(&path).or_raise_erased(|| { + gix_error::message!("Could not read worktree link '{}'", path.display()) + })?); gix_object::compute_hash(object_hash, gix_object::Kind::Blob, &target) .map_err(gix_hash::io::from_hasher) - .map_err(|err| Error::HashFile(std::io::Error::other(err.into_error())))? + .or_raise_erased(|| gix_error::message!("Could not hash worktree link '{}'", path.display()))? } Kind::Directory | Kind::Repository => object_hash.null(), }) 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 95ebb166d45..1bc6d984472 100644 --- a/gix-status/src/index_as_worktree_with_renames/types.rs +++ b/gix-status/src/index_as_worktree_with_renames/types.rs @@ -5,28 +5,7 @@ use bstr::{BStr, ByteSlice}; use crate::index_as_worktree::{Change, EntryStatus}; /// The error returned by [index_as_worktree_with_renames()`](crate::index_as_worktree_with_renames()). -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] -pub enum Error { - #[error(transparent)] - TrackedFileModifications(#[from] crate::index_as_worktree::Error), - #[error(transparent)] - DirWalk(gix_error::Error), - #[error(transparent)] - SpawnThread(std::io::Error), - #[error("Failed to change the context for querying gitattributes to the respective path")] - SetAttributeContext(std::io::Error), - #[error("Could not open worktree file for reading")] - OpenWorktreeFile(std::io::Error), - #[error(transparent)] - HashFile(std::io::Error), - #[error("Could not read worktree link content")] - ReadLink(std::io::Error), - #[error(transparent)] - ConvertToGit(#[from] gix_filter::pipeline::convert::to_git::Error), - #[error(transparent)] - RewriteTracker(#[from] gix_diff::rewrites::tracker::emit::Error), -} +pub type Error = gix_error::Exn; /// The way all output should be sorted. #[derive(Clone, Copy, Default, Debug, Eq, PartialEq, PartialOrd, Ord, Hash)] diff --git a/gix-status/src/stack.rs b/gix-status/src/stack.rs index 19541ac7bf7..7c69e3223ff 100644 --- a/gix-status/src/stack.rs +++ b/gix-status/src/stack.rs @@ -4,18 +4,17 @@ use std::{ }; use bstr::BStr; +use gix_error::ValidationError; use gix_fs::{Stack, stack::ToNormalPathComponents}; use crate::SymlinkCheck; -#[derive(Debug, thiserror::Error)] -#[error("Cannot step through symlink to perform an lstat")] -struct CannotStepThroughSymlink; +const CANNOT_STEP_THROUGH_SYMLINK: &str = "Cannot step through symlink to perform an lstat"; pub(crate) fn is_symlink_step_error(err: &std::io::Error) -> bool { err.get_ref() - .and_then(|source| source.downcast_ref::()) - .is_some() + .and_then(|source| source.downcast_ref::()) + .is_some_and(|err| err.message == CANNOT_STEP_THROUGH_SYMLINK) } impl SymlinkCheck { @@ -79,7 +78,7 @@ impl gix_fs::stack::Delegate for Delegate { } if stack.current().symlink_metadata()?.is_symlink() { - return Err(std::io::Error::other(CannotStepThroughSymlink)); + return Err(std::io::Error::other(ValidationError::new(CANNOT_STEP_THROUGH_SYMLINK))); } Ok(()) } diff --git a/gix-status/tests/status/index_as_worktree.rs b/gix-status/tests/status/index_as_worktree.rs index 9cfbbab5832..2e8dcf2b083 100644 --- a/gix-status/tests/status/index_as_worktree.rs +++ b/gix-status/tests/status/index_as_worktree.rs @@ -267,9 +267,8 @@ pub(super) struct SubmoduleStatusMock { impl SubmoduleStatus for SubmoduleStatusMock { type Output = (); - type Error = std::convert::Infallible; - fn status(&mut self, _entry: &Entry, _rela_path: &BStr) -> Result, Self::Error> { + fn status(&mut self, _entry: &Entry, _rela_path: &BStr) -> Result, gix_error::Exn> { Ok(self.dirty.then_some(())) } } @@ -301,27 +300,23 @@ fn hash_errors_preserve_io_kinds() { &AtomicBool::new(interrupted), ) .expect_err("a short stream or requested interruption prevents hashing"); - let index_as_worktree::Error::Io(err) = err.into() else { - panic!("hashing I/O failures must remain I/O errors"); - }; + let err = err + .downcast_any_ref::() + .expect("hashing I/O failures must remain I/O errors"); assert_eq!(err.kind(), expected_kind, "callers must retain the native I/O kind"); } } #[test] -fn hash_errors_without_io_causes_use_other() { +fn hash_errors_without_io_causes_preserve_hashing_failure() { let err = gix_hash::io::from_hasher(gix_hash::hasher::Error::new("hash collision")); - let index_as_worktree::Error::Io(err) = err.into() else { - panic!("hashing failures must be represented by the I/O error variant"); - }; - assert_eq!( - err.kind(), - std::io::ErrorKind::Other, + assert!( + err.downcast_any_ref::().is_none(), "a hashing failure without an I/O cause has no native kind" ); assert!( - std::iter::successors(std::error::Error::source(&err), |source| source.source()) - .any(|source| source.to_string().contains("hash collision")), + err.downcast_any_ref::() + .is_some_and(|source| source.to_string().contains("hash collision")), "the original hashing failure remains available for diagnostics" ); } diff --git a/gix/src/status/index_worktree.rs b/gix/src/status/index_worktree.rs index 2b72ddd2bd6..f2901b41cf1 100644 --- a/gix/src/status/index_worktree.rs +++ b/gix/src/status/index_worktree.rs @@ -23,7 +23,7 @@ pub enum Error { #[error(transparent)] FilesystemOptions(#[from] config::boolean::Error), #[error(transparent)] - IndexAsWorktreeWithRenames(#[from] gix_status::index_as_worktree_with_renames::Error), + IndexAsWorktreeWithRenames(gix_error::Error), #[error(transparent)] StatOptions(#[from] config::stat_options::Error), #[error(transparent)] @@ -87,7 +87,7 @@ impl Repository { /// /// This is a lower-level method, prefer the [`status`](Repository::status()) method for greater ease of use. #[expect(clippy::too_many_arguments)] - pub fn index_worktree_status<'index, T, U, E>( + pub fn index_worktree_status<'index, T, U>( &self, index: &'index gix_index::State, patterns: impl IntoIterator>, @@ -97,7 +97,7 @@ impl Repository { SubmoduleStatus = U, >, compare: impl CompareBlobs + Send + Clone, - submodule: impl SubmoduleStatus + Send + Clone, + submodule: impl SubmoduleStatus + Send + Clone, progress: &mut dyn gix_features::progress::Progress, should_interrupt: &AtomicBool, options: Options, @@ -105,7 +105,6 @@ impl Repository { where T: Send + Clone, U: Send + Clone, - E: std::error::Error + Send + Sync + 'static, { let _span = gix_trace::coarse!("gix::index_worktree_status"); let workdir = self.workdir().ok_or(Error::MissingWorkDir)?; @@ -168,7 +167,8 @@ impl Repository { dirwalk: options.dirwalk_options.map(Into::into), rewrites: options.rewrites, }, - )?; + ) + .map_err(|err| Error::IndexAsWorktreeWithRenames(err.into_error()))?; Ok(out) } @@ -212,6 +212,8 @@ pub struct BuiltinSubmoduleStatus { /// mod submodule_status { + use gix_error::ResultExt; + use crate::config::cache::util::ApplyLeniency; use crate::{ bstr, @@ -247,22 +249,14 @@ mod submodule_status { } } - /// The error returned submodule status checks. - #[derive(Debug, thiserror::Error)] - pub enum Error { - #[error(transparent)] - SubmoduleStatus(#[from] crate::submodule::status::Error), - #[error(transparent)] - IgnoreConfig(#[from] crate::submodule::config::Error), - #[error(transparent)] - DiffSubmoduleIgnoreConfig(#[from] config::key::GenericErrorWithValue), - } - impl gix_status::index_as_worktree::traits::SubmoduleStatus for BuiltinSubmoduleStatus { type Output = crate::submodule::Status; - type Error = Error; - fn status(&mut self, _entry: &gix_index::Entry, rela_path: &BStr) -> Result, Self::Error> { + fn status( + &mut self, + _entry: &gix_index::Entry, + rela_path: &BStr, + ) -> Result, gix_error::Exn> { use bstr::ByteSlice; if self .submodule_paths @@ -291,18 +285,19 @@ mod submodule_status { .string(config::tree::Diff::IGNORE_SUBMODULES) .map(|value| config::tree::Diff::IGNORE_SUBMODULES.try_into_ignore(value)) .transpose() - .with_leniency(repo.config.lenient_config)?; + .with_leniency(repo.config.lenient_config) + .or_erased()?; if let Some(ignore) = global_ignore { (ignore, check_dirty) } else { // If no global ignore is set, use the submodule's ignore setting. - let ignore = sm.ignore()?.unwrap_or_default(); + let ignore = sm.ignore().or_erased()?.unwrap_or_default(); (ignore, check_dirty) } } Submodule::Given { ignore, check_dirty } => (ignore, check_dirty), }; - let status = sm.status(ignore, check_dirty)?; + let status = sm.status(ignore, check_dirty).or_erased()?; Ok(status.is_dirty().and_then(|dirty| dirty.then_some(status))) } } From cbe793bff7b1a2f4223e25c8cb1ece4de680f800 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 20 Aug 2026 17:12:17 +0200 Subject: [PATCH 14/51] change(gix-blame)!: migrate errors to gix-error --- Cargo.lock | 1 - gix-blame/Cargo.toml | 1 - gix-blame/src/error.rs | 50 +--------- gix-blame/src/file/function.rs | 112 +++++++++++++--------- gix-blame/src/file/tests.rs | 7 +- gix-blame/src/types.rs | 14 +-- gix/src/repository/blame.rs | 3 +- gix/src/repository/mod.rs | 2 +- tests/it/src/commands/blame_copy_royal.rs | 3 +- 9 files changed, 87 insertions(+), 106 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0a69ada787c..1072bd84cd3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1696,7 +1696,6 @@ dependencies = [ "gix-worktree", "pretty_assertions", "smallvec", - "thiserror 2.0.18", ] [[package]] diff --git a/gix-blame/Cargo.toml b/gix-blame/Cargo.toml index 82deb566de5..e91253a329a 100644 --- a/gix-blame/Cargo.toml +++ b/gix-blame/Cargo.toml @@ -30,7 +30,6 @@ gix-worktree = { version = "^0.56.0", path = "../gix-worktree", default-features gix-traverse = { version = "^0.61.0", path = "../gix-traverse" } smallvec = "1.15.1" -thiserror = "2.0.18" [dev-dependencies] gix-hash = { path = "../gix-hash", features = ["sha1", "sha256"] } diff --git a/gix-blame/src/error.rs b/gix-blame/src/error.rs index c5584247562..3950deb2d43 100644 --- a/gix-blame/src/error.rs +++ b/gix-blame/src/error.rs @@ -1,50 +1,2 @@ -use gix_object::bstr::BString; - /// The error returned by [file()](crate::file()). -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] -pub enum Error { - #[error("No commit was given")] - EmptyTraversal, - #[error(transparent)] - BlobDiffSetResource(#[from] gix_diff::blob::platform::set_resource::Error), - #[error(transparent)] - BlobDiffPrepare(#[from] gix_diff::blob::platform::prepare_diff::Error), - #[error("The file to blame at '{file_path}' wasn't found in the first commit at {commit_id}")] - FileMissing { - /// The file-path to the object to blame. - file_path: BString, - /// The commit whose tree didn't contain `file_path`. - commit_id: gix_hash::ObjectId, - }, - #[error("Couldn't find commit or tree in the object database")] - FindObject(#[from] gix_object::find::Error), - #[error("Could not find existing blob or commit")] - FindExistingObject(#[source] gix_error::Error), - #[error("Could not find existing iterator over a tree")] - FindExistingIter(#[source] gix_error::Error), - #[error("Failed to obtain the next commit in the commit-graph traversal")] - Traverse(#[source] Box), - #[error(transparent)] - DiffTree(#[from] gix_diff::tree::Error), - #[error(transparent)] - DiffTreeWithRewrites(#[from] gix_diff::tree_with_rewrites::Error), - #[error( - "Invalid line range was given, line range is expected to be a 1-based inclusive range in the format ','" - )] - InvalidOneBasedLineRange, - #[error("Failure to decode commit during traversal")] - DecodeCommit(#[from] gix_object::decode::Error), - #[error("Failed to get parent from commitgraph during traversal")] - GetParentFromCommitGraph(#[from] gix_error::Message), -} - -impl Error { - pub(crate) fn find_existing_object(err: gix_object::find::existing_object::Error) -> Self { - Error::FindExistingObject(err.into_error()) - } - - pub(crate) fn find_existing_iter(err: gix_object::find::existing_iter::Error) -> Self { - Error::FindExistingIter(err.into_error()) - } -} +pub type Error = gix_error::Exn; diff --git a/gix-blame/src/file/function.rs b/gix-blame/src/file/function.rs index 64f19bfad57..bbadb7c63c6 100644 --- a/gix-blame/src/file/function.rs +++ b/gix-blame/src/file/function.rs @@ -1,6 +1,7 @@ use std::num::NonZeroU32; use gix_diff::{blob::TokenSource, tree::Visit}; +use gix_error::{ErrorExt, NotFoundError, OptionExt, ResultExt, message}; use gix_hash::ObjectId; use gix_object::{ FindExt, @@ -107,8 +108,14 @@ pub fn file( gix_revwalk::PriorityQueue::new(); if let Some(first_suspect) = first_suspect { - let commit = find_commit(cache.as_ref(), &odb, &first_suspect, &mut buf).map_err(Error::find_existing_iter)?; - queue.insert(commit.commit_time()?, first_suspect); + let commit = find_commit(cache.as_ref(), &odb, &first_suspect, &mut buf) + .or_raise_erased(|| message("Could not find existing iterator over a tree"))?; + queue.insert( + commit + .commit_time() + .or_raise_erased(|| message("Failure to decode commit during traversal"))?, + first_suspect, + ); } let mut diff_state = gix_diff::tree::State::default(); @@ -137,8 +144,11 @@ pub fn file( .clone() .unwrap_or_else(|| file_path.to_owned()); - let commit = find_commit(cache.as_ref(), &odb, &suspect, &mut buf).map_err(Error::find_existing_iter)?; - let commit_time = commit.commit_time()?; + let commit = find_commit(cache.as_ref(), &odb, &suspect, &mut buf) + .or_raise_erased(|| message("Could not find existing iterator over a tree"))?; + let commit_time = commit + .commit_time() + .or_raise_erased(|| message("Failure to decode commit during traversal"))?; if let Some(since) = options.since && commit_time < since.seconds @@ -210,7 +220,7 @@ pub fn file( { let source_blob = odb .find_blob(&entry_id, &mut buf) - .map_err(Error::find_existing_object)? + .or_raise_erased(|| message("Could not find existing blob or commit"))? .data .to_vec(); let mut source_interner = gix_diff::blob::Interner::new(source_blob.len() / 100); @@ -572,21 +582,23 @@ fn tree_diff_at_file_path( rewrites: Option, ) -> Result, Error> { let parent_tree_id = find_commit(cache, &odb, &parent_id, commit_buf) - .map_err(Error::find_existing_iter)? - .tree_id()?; + .or_raise_erased(|| message("Could not find existing iterator over a tree"))? + .tree_id() + .or_raise_erased(|| message("Failure to decode commit during traversal"))?; let parent_tree_iter = odb .find_tree_iter(&parent_tree_id, lhs_tree_buf) - .map_err(Error::find_existing_iter)?; + .or_raise_erased(|| message("Could not find existing iterator over a tree"))?; stats.trees_decoded += 1; let tree_id = find_commit(cache, &odb, &id, commit_buf) - .map_err(Error::find_existing_iter)? - .tree_id()?; + .or_raise_erased(|| message("Could not find existing iterator over a tree"))? + .tree_id() + .or_raise_erased(|| message("Failure to decode commit during traversal"))?; let tree_iter = odb .find_tree_iter(&tree_id, rhs_tree_buf) - .map_err(Error::find_existing_iter)?; + .or_raise_erased(|| message("Could not find existing iterator over a tree"))?; stats.trees_decoded += 1; let result = tree_diff_without_rewrites_at_file_path(&odb, file_path, stats, state, parent_tree_iter, tree_iter)?; @@ -714,7 +726,7 @@ fn tree_diff_without_rewrites_at_file_path( match result { Ok(_) | Err(gix_diff::tree::Error::Cancelled) => Ok(recorder.change.map(Into::into)), - Err(error) => Err(Error::DiffTree(error)), + Err(error) => Err(error.raise_erased()), } } @@ -757,7 +769,7 @@ fn tree_diff_with_rewrites_at_file_path( Ok(_) | Err(gix_diff::tree_with_rewrites::Error::Diff(gix_diff::tree::Error::Cancelled)) => { Ok(change.map(Into::into)) } - Err(error) => Err(Error::DiffTreeWithRewrites(error)), + Err(error) => Err(error.raise_erased()), } } @@ -772,23 +784,27 @@ fn blob_changes( diff_algorithm: gix_diff::blob::Algorithm, stats: &mut Statistics, ) -> Result, Error> { - resource_cache.set_resource( - previous_oid, - // TODO(blame): add a test to show of symlink blaming works. - gix_object::tree::EntryKind::Blob, - previous_file_path, - gix_diff::blob::ResourceKind::OldOrSource, - &odb, - )?; - resource_cache.set_resource( - oid, - gix_object::tree::EntryKind::Blob, - file_path, - gix_diff::blob::ResourceKind::NewOrDestination, - &odb, - )?; - - let outcome = resource_cache.prepare_diff()?; + resource_cache + .set_resource( + previous_oid, + // TODO(blame): add a test to show of symlink blaming works. + gix_object::tree::EntryKind::Blob, + previous_file_path, + gix_diff::blob::ResourceKind::OldOrSource, + &odb, + ) + .or_erased()?; + resource_cache + .set_resource( + oid, + gix_object::tree::EntryKind::Blob, + file_path, + gix_diff::blob::ResourceKind::NewOrDestination, + &odb, + ) + .or_erased()?; + + let outcome = resource_cache.prepare_diff().or_erased()?; Ok(blob_changes_from_data( outcome.old.data.as_slice().unwrap_or_default(), @@ -857,16 +873,21 @@ fn find_path_entry_in_commit( stats: &mut Statistics, ) -> Result, Error> { let tree_id = find_commit(cache, odb, commit, buf) - .map_err(Error::find_existing_iter)? - .tree_id()?; - let tree_iter = odb.find_tree_iter(&tree_id, buf).map_err(Error::find_existing_iter)?; + .or_raise_erased(|| message("Could not find existing iterator over a tree"))? + .tree_id() + .or_raise_erased(|| message("Failure to decode commit during traversal"))?; + let tree_iter = odb + .find_tree_iter(&tree_id, buf) + .or_raise_erased(|| message("Could not find existing iterator over a tree"))?; stats.trees_decoded += 1; - let res = tree_iter.lookup_entry( - odb, - buf2, - file_path.split(|b| *b == b'/').inspect(|_| stats.trees_decoded += 1), - )?; + let res = tree_iter + .lookup_entry( + odb, + buf2, + file_path.split(|b| *b == b'/').inspect(|_| stats.trees_decoded += 1), + ) + .or_raise_erased(|| message("Couldn't find commit or tree in the object database"))?; stats.trees_decoded -= 1; Ok(res.map(|e| e.oid)) } @@ -886,7 +907,9 @@ fn collect_parents( .as_ref() .expect("find returned a cached commit, so we expect cache to be present"); for parent_pos in commit.iter_parents() { - let parent = cache.commit_at(parent_pos?); + let parent = cache.commit_at( + parent_pos.or_raise_erased(|| message("Failed to get parent from commitgraph during traversal"))?, + ); parent_ids.push((parent.id().to_owned(), parent.committer_timestamp() as i64)); } } @@ -935,13 +958,14 @@ fn initial_state( match start { Start::Commit(suspect) => { let blamed_file_entry_id = find_path_entry_in_commit(&odb, &suspect, file_path, cache, buf, buf2, stats)? - .ok_or_else(|| Error::FileMissing { - file_path: file_path.to_owned(), - commit_id: suspect, + .ok_or_raise_erased(|| { + NotFoundError::new(format!( + "The file to blame at '{file_path}' wasn't found in the first commit at {suspect}" + )) })?; let blamed_file_blob = odb .find_blob(&blamed_file_entry_id, buf) - .map_err(Error::find_existing_object)? + .or_raise_erased(|| message("Could not find existing blob or commit"))? .data .to_vec(); let num_lines_in_blamed = tokens_for_diffing(&blamed_file_blob).tokenize().count() as u32; @@ -1015,7 +1039,7 @@ fn initial_state( let first_suspect_blob = odb .find_blob(&first_suspect_entry_id, buf) - .map_err(Error::find_existing_object)? + .or_raise_erased(|| message("Could not find existing blob or commit"))? .data .to_vec(); diff --git a/gix-blame/src/file/tests.rs b/gix-blame/src/file/tests.rs index 2b60af1d4a4..1e308e4a611 100644 --- a/gix-blame/src/file/tests.rs +++ b/gix-blame/src/file/tests.rs @@ -986,13 +986,16 @@ mod process_changes { } mod blame_ranges { - use crate::{BlameRanges, Error}; + use crate::BlameRanges; #[test] fn create_with_invalid_range() { let ranges = BlameRanges::from_one_based_inclusive_range(0..=10); - assert!(matches!(ranges, Err(Error::InvalidOneBasedLineRange))); + assert_eq!( + ranges.expect_err("zero isn't a valid one-based line number").message, + "Invalid line range was given, line range is expected to be a 1-based inclusive range in the format ','" + ); } #[test] diff --git a/gix-blame/src/types.rs b/gix-blame/src/types.rs index c82cf6d103c..635fb50a937 100644 --- a/gix-blame/src/types.rs +++ b/gix-blame/src/types.rs @@ -1,3 +1,4 @@ +use gix_error::ValidationError; use gix_hash::ObjectId; use gix_object::bstr::BString; use smallvec::SmallVec; @@ -7,7 +8,6 @@ use std::{ ops::{AddAssign, Range, SubAssign}, }; -use crate::Error; use crate::file::function::tokens_for_diffing; /// A type to represent one or more line ranges to blame in a file. @@ -54,7 +54,7 @@ impl BlameRanges { /// /// Note that the input range is 1-based inclusive, as used by git, and /// the output is a zero-based `BlameRanges` instance. - pub fn from_one_based_inclusive_range(range: RangeInclusive) -> Result { + pub fn from_one_based_inclusive_range(range: RangeInclusive) -> Result { let zero_based_range = Self::inclusive_to_zero_based_exclusive(range)?; Ok(Self::PartialFile(vec![zero_based_range])) } @@ -65,7 +65,7 @@ impl BlameRanges { /// the output is a zero-based `BlameRanges` instance. /// /// If the input vector is empty, the result will be `WholeFile`. - pub fn from_one_based_inclusive_ranges(ranges: Vec>) -> Result { + pub fn from_one_based_inclusive_ranges(ranges: Vec>) -> Result { if ranges.is_empty() { return Ok(Self::WholeFile); } @@ -82,9 +82,11 @@ impl BlameRanges { } /// Convert a 1-based inclusive range to a 0-based exclusive range. - fn inclusive_to_zero_based_exclusive(range: RangeInclusive) -> Result, Error> { + fn inclusive_to_zero_based_exclusive(range: RangeInclusive) -> Result, ValidationError> { if range.start() == &0 { - return Err(Error::InvalidOneBasedLineRange); + return Err(ValidationError::new( + "Invalid line range was given, line range is expected to be a 1-based inclusive range in the format ','", + )); } let start = range.start() - 1; let end = *range.end(); @@ -96,7 +98,7 @@ impl BlameRanges { /// Add a single range to blame. /// /// The new range will be merged with any overlapping existing ranges. - pub fn add_one_based_inclusive_range(&mut self, new_range: RangeInclusive) -> Result<(), Error> { + pub fn add_one_based_inclusive_range(&mut self, new_range: RangeInclusive) -> Result<(), ValidationError> { let zero_based_range = Self::inclusive_to_zero_based_exclusive(new_range)?; self.merge_zero_based_exclusive_range(zero_based_range); diff --git a/gix/src/repository/blame.rs b/gix/src/repository/blame.rs index 4f3177f4fb0..f0125cfdeac 100644 --- a/gix/src/repository/blame.rs +++ b/gix/src/repository/blame.rs @@ -45,7 +45,8 @@ impl Repository { &mut resource_cache, file_path, options, - )?; + ) + .map_err(|err| blame_file::Error::Blame(err.into_error()))?; Ok(outcome) } diff --git a/gix/src/repository/mod.rs b/gix/src/repository/mod.rs index be1a162ce8a..ea31ff98ac3 100644 --- a/gix/src/repository/mod.rs +++ b/gix/src/repository/mod.rs @@ -119,7 +119,7 @@ pub mod blame_file { #[error(transparent)] DiffResourceCache(#[from] super::diff_resource_cache::Error), #[error(transparent)] - Blame(#[from] gix_blame::Error), + Blame(gix_error::Error), } } diff --git a/tests/it/src/commands/blame_copy_royal.rs b/tests/it/src/commands/blame_copy_royal.rs index 4c7fc91f477..5e49eb1de3c 100644 --- a/tests/it/src/commands/blame_copy_royal.rs +++ b/tests/it/src/commands/blame_copy_royal.rs @@ -71,7 +71,8 @@ pub(super) mod function { &mut resource_cache, file.as_bstr(), options, - )?; + ) + .map_err(gix::Exn::into_error)?; let blame_infos = outcome .blame_path From cf7803b827caa19b37ebd0fa2b30712e40fa3bd5 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 20 Aug 2026 17:12:33 +0200 Subject: [PATCH 15/51] change(gix-worktree-state)!: migrate errors to gix-error --- Cargo.lock | 1 - gitoxide-core/src/index/checkout.rs | 3 +- gix-filter/tests/helpers/arrow.rs | 3 +- gix-worktree-state/Cargo.toml | 1 - gix-worktree-state/src/checkout/chunk.rs | 65 +++++++----- gix-worktree-state/src/checkout/entry.rs | 100 ++++++++++-------- gix-worktree-state/src/checkout/function.rs | 7 +- gix-worktree-state/src/checkout/mod.rs | 35 +----- .../tests/worktree-state/checkout.rs | 84 +++++++++++++-- gix/src/clone/checkout.rs | 5 +- 10 files changed, 184 insertions(+), 120 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1072bd84cd3..b01d6f9fc58 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2724,7 +2724,6 @@ dependencies = [ "gix-worktree", "gix-worktree-state", "io-close", - "thiserror 2.0.18", "walkdir", ] diff --git a/gitoxide-core/src/index/checkout.rs b/gitoxide-core/src/index/checkout.rs index 8c71380e184..c2428d7fe7a 100644 --- a/gitoxide-core/src/index/checkout.rs +++ b/gitoxide-core/src/index/checkout.rs @@ -107,7 +107,8 @@ pub fn checkout_exclusive( should_interrupt, opts, ), - }?; + } + .map_err(gix::Exn::into_error)?; files.show_throughput(start); bytes.show_throughput(start); diff --git a/gix-filter/tests/helpers/arrow.rs b/gix-filter/tests/helpers/arrow.rs index 37522e9c45c..98a8e859538 100644 --- a/gix-filter/tests/helpers/arrow.rs +++ b/gix-filter/tests/helpers/arrow.rs @@ -22,6 +22,7 @@ fn main() -> Result<(), Box> { "process" => { let disallow_delay = next_arg.as_deref() == Some("disallow-delay"); let fail_on_shutdown = next_arg.as_deref() == Some("fail-on-shutdown"); + let forget_delayed = next_arg.as_deref() == Some("forget-delayed"); let mut srv = gix_filter::driver::process::Server::handshake( stdin(), stdout(), @@ -145,7 +146,7 @@ fn main() -> Result<(), Box> { let mut out = request.as_write(); let mut last_cmd = None; let mut buf = Vec::::new(); - for (cmd, path, _) in &delayed { + for (cmd, path, _) in delayed.iter().skip(usize::from(forget_delayed)) { if last_cmd.get_or_insert(*cmd) != cmd { panic!("the API doesn't support mixing cmds as paths might not be unique anymore") } diff --git a/gix-worktree-state/Cargo.toml b/gix-worktree-state/Cargo.toml index 45adf15a6dc..d9aea6bc6ff 100644 --- a/gix-worktree-state/Cargo.toml +++ b/gix-worktree-state/Cargo.toml @@ -33,7 +33,6 @@ gix-features = { version = "^0.49.1", path = "../gix-features" } gix-filter = { version = "^0.34.0", path = "../gix-filter" } io-close = "0.3.7" -thiserror = "2.0.18" bstr = { version = "1.12.0", default-features = false } [dev-dependencies] diff --git a/gix-worktree-state/src/checkout/chunk.rs b/gix-worktree-state/src/checkout/chunk.rs index 3588bdfbd49..08e959249e2 100644 --- a/gix-worktree-state/src/checkout/chunk.rs +++ b/gix-worktree-state/src/checkout/chunk.rs @@ -4,6 +4,7 @@ use std::{ }; use bstr::{BStr, BString}; +use gix_error::{CorruptionError, ErrorExt, ResultExt}; use gix_worktree::Stack; use crate::{checkout, checkout::entry}; @@ -179,7 +180,7 @@ where let mut unknown_paths = Vec::new(); for key in keys { loop { - let rela_paths = ctx.filters.driver_state_mut().list_delayed_paths(&key)?; + let rela_paths = ctx.filters.driver_state_mut().list_delayed_paths(&key).or_erased()?; if rela_paths.is_empty() { break; } @@ -192,17 +193,19 @@ where unknown_paths.push(rela_path); continue; } else { - return Err(checkout::Error::FilterPathUnknown { rela_path }); + return Err(CorruptionError::new(format!( + "The entry at path '{rela_path}' was listed as delayed by the filter process, but we never passed it" + )) + .raise_erased()); } } }; let mut read = std::io::BufReader::with_capacity( 512 * 1024, - ctx.filters.driver_state_mut().fetch_delayed( - &key, - rela_path.as_ref(), - gix_filter::driver::Operation::Smudge, - )?, + ctx.filters + .driver_state_mut() + .fetch_delayed(&key, rela_path.as_ref(), gix_filter::driver::Operation::Smudge) + .or_erased()?, ); let (file, executable_bit_change) = match entry::open_file( &std::mem::take(&mut delayed.validated_file_path), // mark it as seen, relevant for `unprocessed_paths` @@ -214,9 +217,15 @@ where Ok(res) => res, Err(err) => { if !is_collision(&err, delayed.entry_path, &mut out.collisions, files) { - handle_error(err, delayed.entry_path, files, &mut out.errors, ctx.options.keep_going)?; + handle_error( + err.raise_erased(), + delayed.entry_path, + files, + &mut out.errors, + ctx.options.keep_going, + )?; } - std::io::copy(&mut read, &mut std::io::sink())?; + std::io::copy(&mut read, &mut std::io::sink()).or_erased()?; continue; } }; @@ -224,11 +233,15 @@ where inner: std::io::BufWriter::with_capacity(512 * 1024, file), progress: bytes, }; - let actual_bytes = std::io::copy(&mut read, &mut write)?; + let actual_bytes = std::io::copy(&mut read, &mut write).or_erased()?; bytes_written += actual_bytes; entry::finalize_entry( delayed.entry, - write.inner.into_inner().map_err(std::io::IntoInnerError::into_error)?, + write + .inner + .into_inner() + .map_err(std::io::IntoInnerError::into_error) + .or_erased()?, actual_bytes, executable_bit_change, )?; @@ -238,15 +251,16 @@ where } } - let unprocessed_paths = delayed_filter_results + let unprocessed_paths: Vec = delayed_filter_results .into_iter() .filter_map(|d| (!d.validated_file_path.as_os_str().is_empty()).then(|| d.entry_path.to_owned())) .collect(); - if !keep_going && !unknown_paths.is_empty() { - return Err(checkout::Error::FilterPathsUnprocessed { - rela_paths: unprocessed_paths, - }); + if !keep_going && !unprocessed_paths.is_empty() { + return Err(CorruptionError::new(format!( + "The following paths were delayed and apparently forgotten to be processed by the filter driver: {unprocessed_paths:?}" + )) + .raise_erased()); } out.delayed_paths_unknown = unknown_paths; @@ -314,7 +328,13 @@ where } Ok(out) } - Err(checkout::Error::Io(err)) if is_collision(&err, entry_path, collisions, files) => { + Err(err) + if err + .frame() + .error() + .downcast_ref::() + .is_some_and(|err| is_collision(err, entry_path, collisions, files)) => + { Ok(entry::Outcome::Written { bytes: 0 }) } Err(err) => handle_error(err, entry_path, files, errors, options.keep_going) @@ -322,20 +342,17 @@ where } } -fn handle_error( - err: E, +fn handle_error( + err: checkout::Error, entry_path: &BStr, files: &AtomicUsize, errors: &mut Vec, keep_going: bool, -) -> Result<(), E> -where - E: std::error::Error + Send + Sync + 'static, -{ +) -> Result<(), checkout::Error> { if keep_going { errors.push(checkout::ErrorRecord { path: entry_path.into(), - error: Box::new(err), + error: err.into_error(), }); files.fetch_add(1, Ordering::Relaxed); Ok(()) diff --git a/gix-worktree-state/src/checkout/entry.rs b/gix-worktree-state/src/checkout/entry.rs index 484ad321eec..815c2cf0009 100644 --- a/gix-worktree-state/src/checkout/entry.rs +++ b/gix-worktree-state/src/checkout/entry.rs @@ -5,6 +5,7 @@ use std::{ }; use bstr::BStr; +use gix_error::{ResultExt, ValidationError, message}; use gix_filter::{ driver::apply::MaybeDelayed, pipeline::convert::{ToWorktreeOutcome, to_worktree}, @@ -80,32 +81,35 @@ pub fn checkout<'entry, Find>( where Find: gix_object::Find, { - let dest_relative = gix_path::try_from_bstr(entry_path).map_err(|_| crate::checkout::Error::IllformedUtf8 { - path: entry_path.to_owned(), - })?; - let path_cache = path_cache.at_path(dest_relative.as_ref(), Some(entry.mode), &*objects)?; + let dest_relative = gix_path::try_from_bstr(entry_path) + .or_raise_erased(|| ValidationError::new_with_input("Could not convert path to UTF8", entry_path))?; + let path_cache = path_cache + .at_path(dest_relative.as_ref(), Some(entry.mode), &*objects) + .or_erased()?; let dest = path_cache.path(); let object_size = match entry.mode { gix_index::entry::Mode::FILE | gix_index::entry::Mode::FILE_EXECUTABLE => { - let obj = (*objects) - .find_blob(&entry.id, buf) - .map_err(|err| crate::checkout::Error::Find { - err: err.into_error(), - path: dest.to_path_buf(), - })?; + let obj = (*objects).find_blob(&entry.id, buf).or_raise_erased(|| { + message!( + "object for checkout at {} could not be retrieved from object database", + dest.display() + ) + })?; - let filtered = filters.convert_to_worktree( - obj.data, - entry_path, - &mut |_, attrs| { - path_cache.matching_attributes(attrs); - }, - to_worktree::Options { - can_delay: filter_process_delay, - unknown_encoding: to_worktree::UnknownEncoding::Ignore, - }, - )?; + let filtered = filters + .convert_to_worktree( + obj.data, + entry_path, + &mut |_, attrs| { + path_cache.matching_attributes(attrs); + }, + to_worktree::Options { + can_delay: filter_process_delay, + unknown_encoding: to_worktree::UnknownEncoding::Ignore, + }, + ) + .or_erased()?; let (num_bytes, file, executable_bit_change) = match filtered { ToWorktreeOutcome::Unchanged(buf) | ToWorktreeOutcome::Buffer(buf) => { let (mut file, flag) = open_file( @@ -114,8 +118,9 @@ where overwrite_existing, executable_bit, entry.mode, - )?; - file.write_all(buf)?; + ) + .or_erased()?; + file.write_all(buf).or_erased()?; (buf.len(), file, flag) } ToWorktreeOutcome::Process(MaybeDelayed::Immediate(mut filtered)) => { @@ -125,8 +130,9 @@ where overwrite_existing, executable_bit, entry.mode, - )?; - let num_bytes = std::io::copy(&mut filtered, &mut file)? as usize; + ) + .or_erased()?; + let num_bytes = std::io::copy(&mut filtered, &mut file).or_erased()? as usize; (num_bytes, file, flag) } ToWorktreeOutcome::Process(MaybeDelayed::Delayed(key)) => { @@ -145,18 +151,18 @@ where num_bytes } gix_index::entry::Mode::SYMLINK => { - let obj = (*objects) - .find_blob(&entry.id, buf) - .map_err(|err| crate::checkout::Error::Find { - err: err.into_error(), - path: dest.to_path_buf(), - })?; + let obj = (*objects).find_blob(&entry.id, buf).or_raise_erased(|| { + message!( + "object for checkout at {} could not be retrieved from object database", + dest.display() + ) + })?; if symlink { #[cfg_attr(not(windows), allow(unused_mut))] - let mut symlink_destination = Cow::Borrowed( - gix_path::try_from_byte_slice(obj.data) - .map_err(|_| crate::checkout::Error::IllformedUtf8 { path: obj.data.into() })?, - ); + let mut symlink_destination = + Cow::Borrowed(gix_path::try_from_byte_slice(obj.data).or_raise_erased(|| { + ValidationError::new_with_input("Could not convert path to UTF8", obj.data) + })?); #[cfg(windows)] { symlink_destination = gix_path::to_native_path_on_windows(gix_path::into_bstr(symlink_destination)) @@ -164,16 +170,18 @@ where try_op_or_unlink(dest, overwrite_existing, |p| { gix_fs::symlink::create(symlink_destination.as_ref(), p) - })?; + }) + .or_erased()?; } else { let mut file = try_op_or_unlink(dest, overwrite_existing, |p| { open_options(destination_is_initially_empty, overwrite_existing).open(p) - })?; - file.write_all(obj.data)?; - file.close()?; + }) + .or_erased()?; + file.write_all(obj.data).or_erased()?; + file.close().or_erased()?; } - entry.stat = Stat::from_fs(&gix_index::fs::Metadata::from_path_no_follow(dest)?)?; + entry.stat = Stat::from_fs(&gix_index::fs::Metadata::from_path_no_follow(dest).or_erased()?).or_erased()?; obj.data.len() } gix_index::entry::Mode::DIR => { @@ -297,22 +305,22 @@ pub(crate) fn finalize_entry( #[cfg(unix)] match executable_bit_change { ExecutableBitChange::NoChange => {} - ExecutableBitChange::Set => adjust_executable_bits(&file, true)?, - ExecutableBitChange::Remove => adjust_executable_bits(&file, false)?, + ExecutableBitChange::Set => adjust_executable_bits(&file, true).or_erased()?, + ExecutableBitChange::Remove => adjust_executable_bits(&file, false).or_erased()?, } - let md = &gix_index::fs::Metadata::from_file(&file)?; + let md = &gix_index::fs::Metadata::from_file(&file).or_erased()?; // A last sanity check: if the file wasn't truncated upon opening, which is good in case something // goes wrong during writing, not everything is lost, then after writing the file is smaller than it was // before, it needs truncation. We do that here. let needs_truncation = md.len() > desired_bytes; if needs_truncation { - file.set_len(desired_bytes)?; + file.set_len(desired_bytes).or_erased()?; } // NOTE: we don't call `file.sync_all()` here knowing that some filesystems don't handle this well. // revisit this once there is a bug to fix. - entry.stat = Stat::from_fs(md)?; - file.close()?; + entry.stat = Stat::from_fs(md).or_erased()?; + file.close().or_erased()?; Ok(()) } diff --git a/gix-worktree-state/src/checkout/function.rs b/gix-worktree-state/src/checkout/function.rs index 1675629b8a5..3c24be2d841 100644 --- a/gix-worktree-state/src/checkout/function.rs +++ b/gix-worktree-state/src/checkout/function.rs @@ -1,5 +1,6 @@ use std::sync::atomic::AtomicBool; +use gix_error::{ResultExt, message}; use gix_features::{interrupt, parallel::in_parallel_with_finalize}; use gix_worktree::{Stack, stack}; @@ -126,7 +127,8 @@ where ctx.filters .driver_state_mut() .shutdown(gix_filter::driver::shutdown::Mode::WaitForProcesses) - .map_err(crate::checkout::Error::FilterShutdownIo)?; + .or_raise(|| message("Could not shut down filter processes")) + .or_erased()?; Ok(out) }, chunk::Reduce { @@ -153,7 +155,8 @@ where ctx.filters .driver_state_mut() .shutdown(gix_filter::driver::shutdown::Mode::WaitForProcesses) - .map_err(crate::checkout::Error::FilterShutdownIo)?; + .or_raise(|| message("Could not shut down filter processes")) + .or_erased()?; Ok(crate::checkout::Outcome { files_updated, diff --git a/gix-worktree-state/src/checkout/mod.rs b/gix-worktree-state/src/checkout/mod.rs index 30b369cf6a7..ede16160a0d 100644 --- a/gix-worktree-state/src/checkout/mod.rs +++ b/gix-worktree-state/src/checkout/mod.rs @@ -10,13 +10,13 @@ pub struct Collision { pub error_kind: std::io::ErrorKind, } -/// A path that encountered an IO error. +/// A path that encountered an error. #[derive(Debug)] pub struct ErrorRecord { /// the path that encountered the error. pub path: BString, - /// The error - pub error: Box, + /// The error. + pub error: gix_error::Error, } /// The outcome of checking out an entire index. @@ -75,34 +75,7 @@ pub struct Options { } /// The error returned by the [checkout()][crate::checkout()] function. -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] -pub enum Error { - #[error("Could not convert path to UTF8: {}", .path)] - IllformedUtf8 { path: BString }, - #[error("The clock was off when reading file related metadata after updating a file on disk")] - Time(#[from] std::time::SystemTimeError), - #[error("IO error while writing blob or reading file metadata or changing filetype")] - Io(#[from] std::io::Error), - #[error("object for checkout at {} could not be retrieved from object database", .path.display())] - Find { - #[source] - err: gix_error::Error, - path: std::path::PathBuf, - }, - #[error(transparent)] - Filter(#[from] gix_filter::pipeline::convert::to_worktree::Error), - #[error(transparent)] - FilterListDelayed(#[from] gix_filter::driver::delayed::list::Error), - #[error(transparent)] - FilterFetchDelayed(#[from] gix_filter::driver::delayed::fetch::Error), - #[error("Could not shut down filter processes")] - FilterShutdownIo(#[source] std::io::Error), - #[error("The entry at path '{rela_path}' was listed as delayed by the filter process, but we never passed it")] - FilterPathUnknown { rela_path: BString }, - #[error("The following paths were delayed and apparently forgotten to be processed by the filter driver: ")] - FilterPathsUnprocessed { rela_paths: Vec }, -} +pub type Error = gix_error::Exn; mod chunk; mod entry; diff --git a/gix-worktree-state/tests/worktree-state/checkout.rs b/gix-worktree-state/tests/worktree-state/checkout.rs index f61e9dfd139..087a3a64690 100644 --- a/gix-worktree-state/tests/worktree-state/checkout.rs +++ b/gix-worktree-state/tests/worktree-state/checkout.rs @@ -292,6 +292,52 @@ fn delayed_symlinks_do_not_reuse_replaced_directory_prefixes() -> gix_testtools: Ok(()) } +#[test] +fn filter_spawn_errors_are_not_collisions() -> crate::Result { + // A relative path with forward slashes makes spawning fail directly, without a shell. + let dir = gix_testtools::tempfile::tempdir_in(".")?; + let program = Path::new(dir.path().file_name().expect("temporary directories have a name")).join("smudge.exe"); + // Both failures look like destination collisions, but occur while spawning the filter. + #[cfg(unix)] + std::os::unix::fs::symlink("smudge.exe", &program)?; + #[cfg(windows)] + std::fs::create_dir(&program)?; + + for keep_going in [false, true] { + let mut opts = opts_from_probe(); + opts.keep_going = keep_going; + opts.filters.options_mut().drivers = vec![gix_filter::Driver { + name: "arrow".into(), + clean: None, + smudge: Some(gix_path::to_unix_separators_on_windows(gix_path::into_bstr(&program)).into_owned()), + process: None, + required: true, + }]; + let result = checkout_index_in_tmp_dir(opts, "make_mixed_without_submodules_and_symlinks", None); + let err = if keep_going { + let (_, _, _, outcome) = result?; + assert!(outcome.collisions.is_empty(), "filter failures are not collisions"); + outcome + .errors + .into_iter() + .next() + .expect("keep-going records the required filter failure") + .error + } else { + *result + .expect_err("a required filter failure must abort checkout") + .downcast::() + .expect("checkout retains the error chain") + }; + assert!( + err.downcast_any_ref::() + .is_some_and(gix_fs::symlink::is_collision_error), + "the filter failure retains its collision-like I/O cause" + ); + } + Ok(()) +} + #[test] fn delayed_driver_process() -> crate::Result { let mut opts = opts_from_probe(); @@ -354,6 +400,27 @@ fn filter_process_failure_during_shutdown_is_ignored() -> crate::Result { Ok(()) } +#[test] +fn forgotten_delayed_path_is_a_corruption_error() { + let mut opts = opts_from_probe(); + opts.filter_process_delay = gix_filter::driver::apply::Delay::Allow; + setup_filter_pipeline(opts.filters.options_mut()); + opts.filters.options_mut().drivers[0].process = Some((driver_exe() + " process forget-delayed").into()); + + let err = checkout_index_in_tmp_dir_opts( + opts, + "make_mixed_without_submodules_and_symlinks", + None, + |_| true, + |_| Ok(()), + ) + .expect_err("a required filter must not silently forget a delayed path"); + let err = err + .downcast_ref::() + .expect("checkout errors retain their classification"); + assert!(err.is_corrupted()); +} + #[cfg(unix)] #[test] fn delayed_driver_process_removes_obsolete_executable_bits() -> crate::Result { @@ -710,17 +777,14 @@ fn safety_checks_dotdot_trees() { let err = checkout_index_in_tmp_dir(opts.clone(), "make_traverse_trees", Some("traverse_dotdot_trees")).unwrap_err(); let expected_err_msg = "Input path \"../outside\" contains relative or absolute components"; - assert_eq!(err.source().expect("inner").to_string(), expected_err_msg); + assert_eq!(err.to_string(), expected_err_msg); opts.keep_going = true; let (_source_tree, _destination, _index, outcome) = checkout_index_in_tmp_dir(opts, "make_traverse_trees", Some("traverse_dotdot_trees")) .expect("keep-going checks out as much as possible"); assert_eq!(outcome.errors.len(), 1, "one path could not be checked out"); - assert_eq!( - outcome.errors[0].error.source().expect("inner").to_string(), - expected_err_msg - ); + assert_eq!(outcome.errors[0].error.to_string(), expected_err_msg); } #[test] @@ -728,10 +792,7 @@ fn safety_checks_dotgit_trees() { let opts = opts_from_probe(); let err = checkout_index_in_tmp_dir(opts.clone(), "make_traverse_trees", Some("traverse_dotgit_trees")).unwrap_err(); - assert_eq!( - err.source().expect("inner").to_string(), - "The .git name may never be used" - ); + assert_eq!(err.to_string(), "The .git name may never be used"); } #[test] @@ -740,7 +801,7 @@ fn safety_checks_dotgit_ntfs_stream() { let err = checkout_index_in_tmp_dir(opts.clone(), "make_traverse_trees", Some("traverse_dotgit_stream")).unwrap_err(); assert_eq!( - err.source().expect("inner").to_string(), + err.to_string(), "The .git name may never be used", "note how it is still discovered even though the path is `.git::$INDEX_ALLOCATION`" ); @@ -935,7 +996,8 @@ fn checkout_index_in_tmp_dir_opts( &progress::Discard, &AtomicBool::default(), opts, - )?; + ) + .map_err(gix_error::Exn::into_error)?; Ok((source_tree, destination, index, outcome)) } diff --git a/gix/src/clone/checkout.rs b/gix/src/clone/checkout.rs index d2296203cac..cc741a4d821 100644 --- a/gix/src/clone/checkout.rs +++ b/gix/src/clone/checkout.rs @@ -26,7 +26,7 @@ pub mod main_worktree { #[error(transparent)] CheckoutOptions(#[from] crate::config::checkout_options::Error), #[error(transparent)] - IndexCheckout(#[from] gix_worktree_state::checkout::Error), + IndexCheckout(#[from] gix_error::Error), #[error(transparent)] Peel(#[from] crate::reference::peel::Error), #[error("Failed to reopen object database as Arc (only if thread-safety wasn't compiled in)")] @@ -136,7 +136,8 @@ pub mod main_worktree { &bytes, should_interrupt, opts, - )?; + ) + .map_err(gix_error::Exn::into_error)?; files.show_throughput(start); bytes.show_throughput(start); From 2ab0173ef95998f55672feb883f1d87abadb58d9 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 20 Aug 2026 17:12:51 +0200 Subject: [PATCH 16/51] change(gix-submodule)!: migrate errors to gix-error rubberstamp --- Cargo.lock | 1 - gix-submodule/Cargo.toml | 1 - gix-submodule/src/access.rs | 106 ++++++++++++------------ gix-submodule/src/config.rs | 60 ++------------ gix-submodule/src/is_active_platform.rs | 9 +- gix-submodule/src/lib.rs | 21 ++--- gix-submodule/tests/file/mod.rs | 78 ++++++++++------- gix/src/repository/submodule.rs | 10 +-- gix/src/submodule/errors.rs | 4 +- gix/src/submodule/mod.rs | 25 +++--- gix/tests/gix/submodule.rs | 17 ++-- 11 files changed, 139 insertions(+), 193 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b01d6f9fc58..e2931f400da 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2512,7 +2512,6 @@ dependencies = [ "gix-refspec", "gix-testtools", "gix-url", - "thiserror 2.0.18", ] [[package]] diff --git a/gix-submodule/Cargo.toml b/gix-submodule/Cargo.toml index 4b0f5e6e910..83cb0fcf058 100644 --- a/gix-submodule/Cargo.toml +++ b/gix-submodule/Cargo.toml @@ -29,7 +29,6 @@ gix-path = { version = "^0.12.6", path = "../gix-path" } gix-url = { version = "^0.38.0", path = "../gix-url" } bstr = { version = "1.12.0", default-features = false } -thiserror = "2.0.18" [dev-dependencies] gix-testtools = { path = "../tests/tools" } diff --git a/gix-submodule/src/access.rs b/gix-submodule/src/access.rs index 73ce00bb04a..5264c3aa252 100644 --- a/gix-submodule/src/access.rs +++ b/gix-submodule/src/access.rs @@ -1,6 +1,7 @@ use std::{collections::HashSet, path::Path}; use bstr::{BStr, BString, ByteSlice}; +use gix_error::{ErrorExt, ResultExt, ValidationError}; use crate::{ File, IsActivePlatform, config, @@ -84,10 +85,8 @@ impl File { let patterns = patterns .into_iter() .map(|pattern| gix_pathspec::parse(&pattern, defaults)) - .collect::, _>>() - .map_err(crate::is_active_platform::Error::ParsePattern)?; + .collect::, _>>()?; gix_pathspec::Search::from_specs(patterns, None, std::path::Path::new("")) - .map_err(crate::is_active_platform::Error::NormalizePattern) }) .transpose()?; Ok(IsActivePlatform { search }) @@ -116,50 +115,49 @@ impl File { /// Git currently allows absolute paths to be used when adding submodules, but fails later as it can't find the submodule by /// relative path anymore. Let's play it safe here. pub fn path(&self, name: &BStr) -> Result { - let path_bstr = - self.config - .string(&format!("submodule.{name}.path")) - .ok_or_else(|| config::path::Error::Missing { - submodule: name.to_owned(), - })?; + let path_bstr = self.config.string(&format!("submodule.{name}.path")).ok_or_else(|| { + ValidationError::new(format!( + "The submodule '{name}' was missing its 'path' field or it was empty" + )) + })?; if path_bstr.is_empty() { - return Err(config::path::Error::Missing { - submodule: name.to_owned(), - }); + return Err(ValidationError::new(format!( + "The submodule '{name}' was missing its 'path' field or it was empty" + ))); } let path = gix_path::from_bstr(path_bstr.as_bstr()); if path.is_absolute() { - return Err(config::path::Error::Absolute { - submodule: name.to_owned(), - actual: path_bstr, - }); + return Err(ValidationError::new_with_input( + format!("The path of submodule '{name}' needs to be relative"), + path_bstr, + )); } if gix_path::normalize(path, "".as_ref()).is_none() { - return Err(config::path::Error::OutsideOfWorktree { - submodule: name.to_owned(), - actual: path_bstr, - }); + return Err(ValidationError::new_with_input( + "The path would lead outside of the repository worktree", + path_bstr, + )); } Ok(path_bstr) } /// Retrieve the `url` field of the submodule named `name`. It's an error if it doesn't exist or is empty. pub fn url(&self, name: &BStr) -> Result { - let url = self - .config - .string(&format!("submodule.{name}.url")) - .ok_or_else(|| config::url::Error::Missing { - submodule: name.to_owned(), - })?; + let url = self.config.string(&format!("submodule.{name}.url")).ok_or_else(|| { + ValidationError::new(format!( + "The submodule '{name}' was missing its 'url' field or it was empty" + )) + .raise() + })?; if url.is_empty() { - return Err(config::url::Error::Missing { - submodule: name.to_owned(), - }); + return Err(ValidationError::new(format!( + "The submodule '{name}' was missing its 'url' field or it was empty" + )) + .raise()); } - gix_url::Url::from_bytes(url.as_ref()).map_err(|err| config::url::Error::Parse { - submodule: name.to_owned(), - source: err.into_error(), + gix_url::Url::from_bytes(url.as_ref()).or_raise(|| { + ValidationError::new_with_input(format!("The url of submodule '{name}' could not be parsed"), url) }) } @@ -171,9 +169,8 @@ impl File { value_is_from_modules_file = Some(std::ptr::eq(meta, our_meta)); true }) { - Some(v) => v.as_bstr().try_into().map_err(|()| config::update::Error::Invalid { - submodule: name.to_owned(), - actual: v, + Some(v) => v.as_bstr().try_into().map_err(|()| { + ValidationError::new_with_input(format!("The 'update' field of submodule '{name}' was invalid"), v) })?, None => return Ok(None), }; @@ -181,10 +178,10 @@ impl File { if let Update::Command(cmd) = &value && value_is_from_modules_file.unwrap_or_default() { - return Err(config::update::Error::CommandForbiddenInModulesConfiguration { - submodule: name.to_owned(), - actual: cmd.to_owned(), - }); + return Err(ValidationError::new_with_input( + format!("The 'update' field of submodule '{name}' tried to set a command to be shared"), + cmd.to_owned(), + )); } Ok(Some(value)) } @@ -198,13 +195,12 @@ impl File { None => return Ok(None), }; - Branch::try_from(branch.as_ref()) - .map(Some) - .map_err(|err| config::branch::Error { - submodule: name.to_owned(), - actual: branch, - source: err.into_error(), - }) + Branch::try_from(branch.as_ref()).map(Some).or_raise(|| { + ValidationError::new_with_input( + format!("The 'branch' field of submodule '{name}' couldn't be turned into a valid fetch refspec"), + branch, + ) + }) } /// Retrieve the `fetchRecurseSubmodules` field of the submodule named `name`, or `None` if unset. @@ -212,11 +208,10 @@ impl File { /// Note that if it's unset, it should be retrieved from `fetch.recurseSubmodules` in the configuration. pub fn fetch_recurse(&self, name: &BStr) -> Result, config::Error> { FetchRecurse::new(self.config.boolean(&format!("submodule.{name}.fetchRecurseSubmodules"))).map_err(|value| { - config::Error { - field: "fetchRecurseSubmodules", - submodule: name.to_owned(), - actual: value, - } + ValidationError::new_with_input( + format!("The 'fetchRecurseSubmodules' field of submodule '{name}' was invalid"), + value, + ) }) } @@ -225,10 +220,11 @@ impl File { self.config .string(&format!("submodule.{name}.ignore")) .map(|value| { - Ignore::try_from(value.as_ref()).map_err(|()| config::Error { - field: "ignore", - submodule: name.to_owned(), - actual: value, + Ignore::try_from(value.as_ref()).map_err(|()| { + ValidationError::new_with_input( + format!("The 'ignore' field of submodule '{name}' was invalid"), + value, + ) }) }) .transpose() diff --git a/gix-submodule/src/config.rs b/gix-submodule/src/config.rs index 72b66be9604..48f67959e09 100644 --- a/gix-submodule/src/config.rs +++ b/gix-submodule/src/config.rs @@ -142,78 +142,28 @@ impl TryFrom<&BStr> for Update { } /// The error returned by [File::fetch_recurse()](crate::File::fetch_recurse) and [File::ignore()](crate::File::ignore). -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] -#[error("The '{field}' field of submodule '{submodule}' was invalid: '{actual}'")] -pub struct Error { - pub field: &'static str, - pub submodule: BString, - pub actual: BString, -} +pub type Error = gix_error::ValidationError; /// pub mod branch { - use bstr::BString; - /// The error returned by [File::branch()](crate::File::branch). - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - #[error( - "The value '{actual}' of the 'branch' field of submodule '{submodule}' couldn't be turned into a valid fetch refspec" - )] - pub struct Error { - pub submodule: BString, - pub actual: BString, - pub source: gix_error::Error, - } + pub type Error = gix_error::Exn; } /// pub mod update { - use bstr::BString; - /// The error returned by [File::update()](crate::File::update). - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error("The 'update' field of submodule '{submodule}' tried to set command '{actual}' to be shared")] - CommandForbiddenInModulesConfiguration { submodule: BString, actual: BString }, - #[error("The 'update' field of submodule '{submodule}' was invalid: '{actual}'")] - Invalid { submodule: BString, actual: BString }, - } + pub type Error = gix_error::ValidationError; } /// pub mod url { - use bstr::BString; - /// The error returned by [File::url()](crate::File::url). - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error("The url of submodule '{submodule}' could not be parsed")] - Parse { - submodule: BString, - source: gix_error::Error, - }, - #[error("The submodule '{submodule}' was missing its 'url' field or it was empty")] - Missing { submodule: BString }, - } + pub type Error = gix_error::Exn; } /// pub mod path { - use bstr::BString; - /// The error returned by [File::path()](crate::File::path). - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error("The path '{actual}' of submodule '{submodule}' needs to be relative")] - Absolute { actual: BString, submodule: BString }, - #[error("The submodule '{submodule}' was missing its 'path' field or it was empty")] - Missing { submodule: BString }, - #[error("The path '{actual}' would lead outside of the repository worktree")] - OutsideOfWorktree { actual: BString, submodule: BString }, - } + pub type Error = gix_error::ValidationError; } diff --git a/gix-submodule/src/is_active_platform.rs b/gix-submodule/src/is_active_platform.rs index c73a31be168..62bcabc3cd6 100644 --- a/gix-submodule/src/is_active_platform.rs +++ b/gix-submodule/src/is_active_platform.rs @@ -3,14 +3,7 @@ use bstr::BStr; use crate::IsActivePlatform; /// The error returned by [File::names_and_active_state](crate::File::names_and_active_state()). -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] -pub enum Error { - #[error(transparent)] - NormalizePattern(gix_pathspec::normalize::Error), - #[error(transparent)] - ParsePattern(gix_pathspec::parse::Error), -} +pub type Error = gix_error::ValidationError; impl IsActivePlatform { /// Returns `true` if the submodule named `name` is active or `false` otherwise. diff --git a/gix-submodule/src/lib.rs b/gix-submodule/src/lib.rs index cb1f1b5e551..45292e1e098 100644 --- a/gix-submodule/src/lib.rs +++ b/gix-submodule/src/lib.rs @@ -96,6 +96,8 @@ impl File { pub mod init { use std::path::PathBuf; + use gix_error::{ResultExt, ValidationError}; + use crate::File; impl std::fmt::Debug for File { @@ -112,18 +114,7 @@ pub mod init { /// Lifecycle /// The error returned when parsing a submodule configuration file. - #[derive(Debug, thiserror::Error)] - pub enum Error { - /// The configuration could not be parsed. - #[error(transparent)] - Parse(#[from] gix_config::parse::Error), - /// Applying configuration overrides exceeded the supported span size. - #[error(transparent)] - Span(#[from] gix_config::parse::span::Error), - /// Applying configuration overrides failed. - #[error(transparent)] - SectionValue(#[from] gix_config::file::section::value::Error), - } + pub type Error = gix_error::Exn; impl File { /// Parse `bytes` as git configuration, typically from `.gitmodules`, without doing any further validation. @@ -149,12 +140,14 @@ pub mod init { meta }; let modules = gix_config::File::from_parse_events_no_includes( - gix_config::parse::Events::from_bytes(bytes, None)?, + gix_config::parse::Events::from_bytes(bytes, None) + .or_raise(|| ValidationError::new("Could not parse submodule configuration"))?, metadata, ); let mut res = Self { config: modules }; - res.append_submodule_overrides(config)?; + res.append_submodule_overrides(config) + .or_raise(|| ValidationError::new("Could not apply submodule configuration overrides"))?; Ok(res) } diff --git a/gix-submodule/tests/file/mod.rs b/gix-submodule/tests/file/mod.rs index 841999479ee..20b152c07d7 100644 --- a/gix-submodule/tests/file/mod.rs +++ b/gix-submodule/tests/file/mod.rs @@ -181,25 +181,35 @@ mod path { #[test] fn validate_upon_retrieval() { - assert!(matches!( - submodule_path(if cfg!(windows) { - r"c:\\hello" - } else { - r"/definitely/absolute\\" - }), - Error::Absolute { .. } - )); - assert!(matches!(submodule_path(""), Error::Missing { .. })); - assert!(matches!(submodule_path("../attack"), Error::OutsideOfWorktree { .. })); + let absolute = submodule_path(if cfg!(windows) { + r"c:\\hello" + } else { + r"/definitely/absolute\\" + }); + assert!(absolute.message.contains("needs to be relative")); + assert!(submodule_path("").message.contains("missing its 'path'")); + assert!(submodule_path("../attack").message.contains("outside")); { let module = submodule("[submodule.a]\n path"); - assert!(matches!(module.path("a".into()).unwrap_err(), Error::Missing { .. })); + assert!( + module + .path("a".into()) + .unwrap_err() + .message + .contains("missing its 'path'") + ); } { let module = submodule("[submodule.a]\n"); - assert!(matches!(module.path("a".into()).unwrap_err(), Error::Missing { .. })); + assert!( + module + .path("a".into()) + .unwrap_err() + .message + .contains("missing its 'path'") + ); } } } @@ -223,18 +233,32 @@ mod url { #[test] fn validate_upon_retrieval() { - assert!(matches!(submodule_url(""), Error::Missing { .. })); + assert!(submodule_url("").error().message.contains("missing its 'url'")); { let module = submodule("[submodule.a]\n url"); - assert!(matches!(module.url("a".into()).unwrap_err(), Error::Missing { .. })); + assert!( + module + .url("a".into()) + .unwrap_err() + .error() + .message + .contains("missing its 'url'") + ); } { let module = submodule("[submodule.a]\n"); - assert!(matches!(module.url("a".into()).unwrap_err(), Error::Missing { .. })); + assert!( + module + .url("a".into()) + .unwrap_err() + .error() + .message + .contains("missing its 'url'") + ); } - assert!(matches!(submodule_url("file://"), Error::Parse { .. })); + assert!(submodule_url("file://").error().message.contains("could not be parsed")); } } @@ -293,13 +317,10 @@ mod update { #[test] fn validate_upon_retrieval() { - assert!(matches!(submodule_update(""), Error::Invalid { .. })); - assert!(matches!(submodule_update("bogus"), Error::Invalid { .. })); + assert!(submodule_update("").message.contains("was invalid")); + assert!(submodule_update("bogus").message.contains("was invalid")); assert!( - matches!( - submodule_update("!dangerous"), - Error::CommandForbiddenInModulesConfiguration { .. } - ), + submodule_update("!dangerous").message.contains("command to be shared"), "forbidden unless it's an override" ); } @@ -316,14 +337,13 @@ mod update { .append_submodule_overrides(&repo_config) .expect("the fixture fits into the backing buffer"); + let err = module.update("a".into()).unwrap_err(); + assert_eq!( + err.input.as_ref().map(|input| input.as_slice()), + Some(b"dangerous".as_slice()) + ); assert!( - matches!( - module.update("a".into()), - Err(Error::CommandForbiddenInModulesConfiguration { - actual, - .. - }) if actual == "dangerous" - ), + err.message.contains("command to be shared"), "a same-named local section must not authorize a command that still originates from .gitmodules" ); Ok(()) diff --git a/gix/src/repository/submodule.rs b/gix/src/repository/submodule.rs index a70cc4df1a2..b9183d4fc8a 100644 --- a/gix/src/repository/submodule.rs +++ b/gix/src/repository/submodule.rs @@ -27,11 +27,9 @@ impl Repository { return Ok(None); } let buf = std::fs::read(&path)?; - Ok(Some(gix_submodule::File::from_bytes( - &buf, - path, - &self.config.resolved, - )?)) + Ok(Some( + gix_submodule::File::from_bytes(&buf, path, &self.config.resolved).map_err(gix_error::Exn::into_error)?, + )) } /// Return a shared [`.gitmodules` file](submodule::File) which is updated automatically if the in-memory snapshot @@ -81,7 +79,7 @@ impl Repository { }; Ok(Some(gix_features::threading::OwnShared::new( gix_submodule::File::from_bytes(&self.find_object(id)?.data, None, &self.config.resolved) - .map_err(submodule::open_modules_file::Error::from)? + .map_err(|err| submodule::open_modules_file::Error::from(err.into_error()))? .into(), ))) } diff --git a/gix/src/submodule/errors.rs b/gix/src/submodule/errors.rs index 3cc92718bbc..3e9656cfe7d 100644 --- a/gix/src/submodule/errors.rs +++ b/gix/src/submodule/errors.rs @@ -5,7 +5,7 @@ pub mod open_modules_file { #[expect(missing_docs)] pub enum Error { #[error(transparent)] - Configuration(#[from] gix_submodule::init::Error), + Configuration(#[from] gix_error::Error), #[error("Could not read '.gitmodules' file")] Io(#[from] std::io::Error), } @@ -41,7 +41,7 @@ pub mod is_active { #[expect(missing_docs)] pub enum Error { #[error(transparent)] - InitIsActivePlatform(#[from] gix_submodule::is_active_platform::Error), + InitIsActivePlatform(gix_error::Error), #[error(transparent)] QueryIsActive(#[from] gix_error::Error), #[error(transparent)] diff --git a/gix/src/submodule/mod.rs b/gix/src/submodule/mod.rs index 9e01aad6162..1679659c62f 100644 --- a/gix/src/submodule/mod.rs +++ b/gix/src/submodule/mod.rs @@ -60,13 +60,16 @@ impl<'repo> SharedState<'repo> { ) -> Result<(RefMut<'_, IsActivePlatform>, RefMut<'_, gix_worktree::Stack>), is_active::Error> { let mut state = self.is_active.borrow_mut(); if state.is_none() { - let platform = self.modules.is_active_platform( - &self.repo.config.resolved, - self.repo - .config - .pathspec_defaults() - .map_err(|err| is_active::Error::InitPathspecDefaults(err.into_error()))?, - )?; + let platform = self + .modules + .is_active_platform( + &self.repo.config.resolved, + self.repo + .config + .pathspec_defaults() + .map_err(|err| is_active::Error::InitPathspecDefaults(err.into_error()))?, + ) + .map_err(|err| is_active::Error::InitIsActivePlatform(gix_error::Error::from_error(err)))?; let index = self.index()?; let attributes = self .repo @@ -116,8 +119,8 @@ impl Submodule<'_> { /// Return the url from which to clone or update the submodule. /// /// This method takes into consideration submodule configuration overrides. - pub fn url(&self) -> Result { - self.state.modules.url(self.name()) + pub fn url(&self) -> Result { + self.state.modules.url(self.name()).map_err(Into::into) } /// Return the `update` field from this submodule's configuration, if present, or `None`. @@ -130,8 +133,8 @@ impl Submodule<'_> { /// Return the `branch` field from this submodule's configuration, if present, or `None`. /// /// This method takes into consideration submodule configuration overrides. - pub fn branch(&self) -> Result, config::branch::Error> { - self.state.modules.branch(self.name()) + pub fn branch(&self) -> Result, gix_error::Error> { + self.state.modules.branch(self.name()).map_err(Into::into) } /// Return the `fetchRecurseSubmodules` field from this submodule's configuration, or retrieve the value from `fetch.recurseSubmodules` if unset. diff --git a/gix/tests/gix/submodule.rs b/gix/tests/gix/submodule.rs index 7ee0d26fd8a..91c2a279b82 100644 --- a/gix/tests/gix/submodule.rs +++ b/gix/tests/gix/submodule.rs @@ -766,7 +766,6 @@ mod open { #[cfg(unix)] mod advisory { - use gix::bstr::BString; /// Reproducer for GHSA-p3hw-mv63-rf9w and GHSA-fr8x-3vfx-f45h: a crafted submodule name with /// traversal components is reused to derive `.git/modules/`, so `Submodule::state()` and @@ -835,16 +834,12 @@ mod advisory { .expect("submodules present") .next() .expect("one submodule"); - match sm.update() { - Err(gix::submodule::config::update::Error::CommandForbiddenInModulesConfiguration { - submodule, - actual, - }) => { - assert_eq!(submodule, BString::from("sub")); - assert_eq!(actual, BString::from("touch pwned")); - } - other => panic!("expected forbidden command from `.gitmodules`, got {other:?}"), - } + let err = sm.update().expect_err("commands from `.gitmodules` are forbidden"); + assert!(err.message.contains("command to be shared")); + assert_eq!( + err.input.as_ref().map(|input| input.as_slice()), + Some(b"touch pwned".as_slice()) + ); Ok(()) } } From 64865b12ef1c5223d31e7f2293cb36405f19eb87 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 20 Aug 2026 17:13:16 +0200 Subject: [PATCH 17/51] change(gitoxide-core)!: migrate errors to gix-error rubberstamp --- Cargo.lock | 1 - gitoxide-core/Cargo.toml | 1 - gitoxide-core/src/pack/create.rs | 19 --------- gitoxide-core/src/pack/explode.rs | 67 ++++++++++++------------------- 4 files changed, 26 insertions(+), 62 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e2931f400da..6b4f92a8611 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1526,7 +1526,6 @@ dependencies = [ "smallvec", "sysinfo", "tempfile", - "thiserror 2.0.18", "tracing", "tracing-forest 0.2.0", "tracing-subscriber", diff --git a/gitoxide-core/Cargo.toml b/gitoxide-core/Cargo.toml index e4329b29ff2..baddde6b516 100644 --- a/gitoxide-core/Cargo.toml +++ b/gitoxide-core/Cargo.toml @@ -58,7 +58,6 @@ gix-fsck = { version = "^0.25.1", path = "../gix-fsck" } gix-error-for-configuration-only = { package = "gix-error", version = "^0.3.2", path = "../gix-error", features = ["anyhow"] } serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"] } anyhow = "1.0.102" -thiserror = "2.0.18" bytesize = "2.3.1" tempfile = "3.26.0" unicode-width = "0.2.2" diff --git a/gitoxide-core/src/pack/create.rs b/gitoxide-core/src/pack/create.rs index 3ac60f9b84f..ef4e1144f04 100644 --- a/gitoxide-core/src/pack/create.rs +++ b/gitoxide-core/src/pack/create.rs @@ -358,22 +358,3 @@ struct Statistics { counts: pack::data::output::count::objects::Outcome, entries: pack::data::output::entry::iter_from_counts::Outcome, } - -pub mod input_iteration { - use gix::{hash, traverse}; - #[derive(Debug, thiserror::Error)] - pub enum Error { - #[error("input objects couldn't be iterated completely")] - Iteration(#[source] gix::Error), - #[error("An error occurred while reading hashes from standard input")] - InputLinesIo(#[from] std::io::Error), - #[error("Could not decode hex hash provided on standard input")] - HashDecode(#[from] hash::decode::Error), - } - - impl From for Error { - fn from(err: traverse::commit::simple::Error) -> Self { - Error::Iteration(err.into_error()) - } - } -} diff --git a/gitoxide-core/src/pack/explode.rs b/gitoxide-core/src/pack/explode.rs index 19766213ff0..bf938fc1af6 100644 --- a/gitoxide-core/src/pack/explode.rs +++ b/gitoxide-core/src/pack/explode.rs @@ -6,6 +6,7 @@ use std::{ }; use anyhow::{Result, anyhow}; +use gix::error::{ErrorExt, NotFoundError, message}; use gix::{ NestedProgress, hash::ObjectId, @@ -64,32 +65,6 @@ impl From for pack::index::traverse::SafetyCheck { } } -#[derive(Debug, thiserror::Error)] -enum Error { - #[error("An IO error occurred while writing an object")] - Io(#[from] std::io::Error), - #[error("An object could not be written to the database")] - OdbWrite(#[from] loose::write::Error), - #[error("Failed to write {kind} object {id}")] - Write { - source: Box, - kind: object::Kind, - id: ObjectId, - }, - #[error("Object didn't verify after right after writing it")] - Verify(#[source] std::io::Error), - #[error("{kind} object wasn't re-encoded without change")] - ObjectEncodeMismatch { - #[source] - source: gix::hash::verify::Error, - kind: object::Kind, - }, - #[error("The recently written file for loose object {id} could not be found")] - WrittenFileMissing { id: ObjectId }, - #[error("The recently written file for loose object {id} cold not be read")] - WrittenFileCorrupt { source: loose::find::Error, id: ObjectId }, -} - #[expect( clippy::large_enum_variant, reason = "will be removed once `gix-error` is used consistently" @@ -228,11 +203,15 @@ pub fn pack_or_pack_index( .flatten(); let mut read_buf = Vec::new(); move |object_kind, buf, index_entry, progress| { - let written_id = out.write_buf(object_kind, buf).map_err(|err| Error::Write { - source: err.into(), - kind: object_kind, - id: index_entry.oid, - })?; + let written_id = out + .write_buf(object_kind, buf) + .map_err(|err| { + err.raise(message!( + "Failed to write {object_kind} object {}", + index_entry.oid + )) + .into_error() + })?; if let Err(err) = written_id.verify(&index_entry.oid) { if let object::Kind::Tree = object_kind { progress.info(format!( @@ -240,22 +219,28 @@ pub fn pack_or_pack_index( index_entry.oid, written_id )); } else { - return Err(Error::ObjectEncodeMismatch { - source: err, - kind: object_kind, - }); + return Err(err + .and_raise(message!("{object_kind} object wasn't re-encoded without change")) + .into_error()); } } if let Some(verifier) = loose_odb.as_ref() { let obj = verifier .try_find(&written_id, &mut read_buf) - .map_err(|err| Error::WrittenFileCorrupt { - source: err, - id: written_id, + .map_err(|err| { + err.and_raise(message!( + "The recently written file for loose object {written_id} could not be read" + )) + .into_error() })? - .ok_or(Error::WrittenFileMissing { id: written_id })?; - obj.verify_checksum(&written_id) - .map_err(|err| Error::Verify(std::io::Error::other(err.into_error())))?; + .ok_or_else(|| { + NotFoundError::new(format!( + "The recently written file for loose object {written_id} could not be found" + )) + .raise() + .into_error() + })?; + obj.verify_checksum(&written_id).map_err(gix::Exn::into_error)?; } Ok(()) } From 7670cefb7809ceaf2bc63b538fb4b9dd569395a2 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 20 Aug 2026 17:13:46 +0200 Subject: [PATCH 18/51] change(gix-config)!: migrate errors to gix-error rubberstamp --- Cargo.lock | 1 - gix-config/Cargo.toml | 1 - gix-config/fuzz/fuzz_targets/fuzz_file.rs | 16 +-- gix-config/src/file/access/mutate.rs | 13 +-- gix-config/src/file/access/raw.rs | 97 ++++++++++--------- gix-config/src/file/access/read_only.rs | 22 ++--- gix-config/src/file/includes/mod.rs | 56 ++++++----- gix-config/src/file/includes/types.rs | 27 +----- gix-config/src/file/init/comfort.rs | 52 +++++----- gix-config/src/file/init/from_env.rs | 68 +++++-------- gix-config/src/file/init/from_paths.rs | 71 ++++++-------- gix-config/src/file/init/mod.rs | 11 ++- gix-config/src/file/init/types.rs | 21 +--- gix-config/src/file/mod.rs | 22 +---- gix-config/src/file/mutable/multi_value.rs | 2 +- gix-config/src/file/mutable/section.rs | 4 +- gix-config/src/file/section/mod.rs | 9 +- gix-config/src/file/util.rs | 6 +- gix-config/src/lookup.rs | 68 ++++++++++--- gix-config/src/parse/error.rs | 10 +- gix-config/src/parse/from_bytes/tests.rs | 4 + gix-config/src/parse/mod.rs | 20 ++-- gix-config/src/parse/section/header.rs | 15 +-- gix-config/src/parse/section/mod.rs | 8 +- gix-config/src/parse/tests.rs | 8 +- gix-config/tests/config/file/access/mutate.rs | 62 ++++++------ .../config/file/access/raw/raw_multi_value.rs | 23 ++--- .../tests/config/file/access/raw/raw_value.rs | 43 ++++---- .../config/file/access/raw/set_raw_value.rs | 10 +- .../tests/config/file/access/read_only.rs | 55 ++++++++--- gix-config/tests/config/file/init/from_env.rs | 20 ++-- .../includes/conditional/gitdir/mod.rs | 28 +++--- .../init/from_paths/includes/unconditional.rs | 33 +++---- .../tests/config/file/init/from_paths/mod.rs | 7 +- .../tests/config/file/mutable/section.rs | 31 +++--- gix-config/tests/config/parse/error.rs | 17 +++- gix-config/tests/config/parse/section.rs | 17 +++- gix/examples/init-repo-and-commit.rs | 8 +- gix/src/clone/fetch/mod.rs | 4 +- gix/src/config/cache/init.rs | 5 +- gix/src/config/mod.rs | 30 ++++-- gix/src/config/overrides.rs | 6 +- gix/src/create.rs | 4 +- gix/src/lib.rs | 2 +- gix/src/remote/save.rs | 4 +- gix/src/repository/branch.rs | 7 +- tests/tools/src/repository/gix.rs | 6 +- 47 files changed, 547 insertions(+), 507 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6b4f92a8611..ce0e3d4f61a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1754,7 +1754,6 @@ dependencies = [ "serde", "serial_test", "smallvec", - "thiserror 2.0.18", "unicode-bom", ] diff --git a/gix-config/Cargo.toml b/gix-config/Cargo.toml index f280ee1e86d..39792670ffd 100644 --- a/gix-config/Cargo.toml +++ b/gix-config/Cargo.toml @@ -31,7 +31,6 @@ gix-ref = { version = "^0.67.0", path = "../gix-ref" } gix-glob = { version = "^0.27.1", path = "../gix-glob" } gix-utils = { version = "^0.3.6", path = "../gix-utils", features = ["bstr"] } -thiserror = "2.0.18" unicode-bom = { version = "2.0.3" } bstr = { version = "1.12.0", default-features = false, features = ["std"] } serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"] } diff --git a/gix-config/fuzz/fuzz_targets/fuzz_file.rs b/gix-config/fuzz/fuzz_targets/fuzz_file.rs index 8ab93ddbfe5..8fb23b31550 100644 --- a/gix-config/fuzz/fuzz_targets/fuzz_file.rs +++ b/gix-config/fuzz/fuzz_targets/fuzz_file.rs @@ -34,7 +34,9 @@ fn fuzz_mutable_section( // Mutate section. let section_id = { - let mut section = file.section_mut(section_name, subsection_name)?; + let mut section = file + .section_mut(section_name, subsection_name) + .map_err(|err| err.into_error())?; let key = section.value_names().next(); if let Some(key) = key { @@ -94,7 +96,8 @@ fn fuzz_mutable_section( fn fuzz(input: &[u8]) -> Result<()> { let meta = Metadata::default(); let options = Options::default(); - let file = gix_config::File::from_bytes_no_includes(input, meta.clone(), options)?; + let file = + gix_config::File::from_bytes_no_includes(input, meta.clone(), options).map_err(|err| err.into_error())?; // Sections and frontmatter. _ = black_box(file.sections_and_ids().count()); @@ -120,11 +123,10 @@ fn fuzz(input: &[u8]) -> Result<()> { } _ = black_box(mutated_file.append(file)); - _ = black_box(gix_config::File::from_bytes_no_includes( - &mutated_file.to_bstring(), - meta, - options, - )?); + _ = black_box( + gix_config::File::from_bytes_no_includes(&mutated_file.to_bstring(), meta, options) + .map_err(|err| err.into_error())?, + ); Ok(()) } diff --git a/gix-config/src/file/access/mutate.rs b/gix-config/src/file/access/mutate.rs index 95c8fb20a8e..54908f9ad22 100644 --- a/gix-config/src/file/access/mutate.rs +++ b/gix-config/src/file/access/mutate.rs @@ -72,7 +72,7 @@ impl File { /// Returns the last found mutable section with a given `key`, identifying the name and subsection name like `core` or `remote.origin`. pub fn section_mut_by_key(&mut self, key: impl crate::AsBStr) -> Result, lookup::existing::Error> { - let key = section::unvalidated::KeyRef::parse(&key).ok_or(lookup::existing::Error::KeyMissing)?; + let key = section::unvalidated::KeyRef::parse(&key).ok_or_else(lookup::existing::key_missing)?; self.section_mut_inner(key.section_name, key.subsection_name) } @@ -172,7 +172,7 @@ impl File { key: impl crate::AsBStr, filter: impl FnMut(&Metadata) -> bool, ) -> Result>, lookup::existing::Error> { - let key = section::unvalidated::KeyRef::parse(&key).ok_or(lookup::existing::Error::KeyMissing)?; + let key = section::unvalidated::KeyRef::parse(&key).ok_or_else(lookup::existing::key_missing)?; self.section_mut_filter_inner(key.section_name, key.subsection_name, filter) } @@ -358,8 +358,7 @@ impl File { /// /// Existing sections with the target name are preserved. /// - /// Note that the otherwise unused [`lookup::existing::Error::KeyMissing`] variant is used to indicate - /// that the `filter` rejected all candidates, leading to no section being renamed after all. + /// A not-found error indicates that the `filter` rejected all candidates, leading to no section being renamed. pub fn rename_section_filter( &mut self, name: impl AsRef, @@ -373,9 +372,11 @@ impl File { .filter(|id| filter(&self.sections.get(id).expect("each id has a section").meta)) .collect(); if ids.is_empty() { - return Err(rename_section::Error::Lookup(lookup::existing::Error::KeyMissing)); + return Err(lookup::existing::key_missing()); } - let header = section::HeaderData::new_in(new_name, new_subsection_name.into_bstring_opt(), &mut self.backing)?; + use gix_error::ResultExt; + let header = section::HeaderData::new_in(new_name, new_subsection_name.into_bstring_opt(), &mut self.backing) + .or_erased()?; for id in ids { file::util::set_section_header( self.sections diff --git a/gix-config/src/file/access/raw.rs b/gix-config/src/file/access/raw.rs index 2155da7f69a..d5bf64e7683 100644 --- a/gix-config/src/file/access/raw.rs +++ b/gix-config/src/file/access/raw.rs @@ -1,6 +1,7 @@ use std::collections::HashMap; use bstr::{BStr, BString}; +use gix_error::ResultExt; use smallvec::ToSmallVec; use crate::{ @@ -154,7 +155,7 @@ impl File { } } - Err(lookup::existing::Error::KeyMissing) + Err(lookup::existing::key_missing()) } /// Returns a mutable reference to an uninterpreted value given a `key`. @@ -222,7 +223,7 @@ impl File { let mut section_ids = self .section_ids_by_name_and_subname(section_name, subsection_name)? .rev(); - let key = section::ValueName::try_from(value_name)?; + let key = section::ValueName::try_from(value_name).or_erased()?; while let Some(section_id) = section_ids.next() { let mut index = 0; @@ -271,7 +272,7 @@ impl File { }); } - Err(lookup::existing::Error::KeyMissing) + Err(lookup::existing::key_missing()) } /// Returns all uninterpreted values given a `key`. @@ -476,7 +477,7 @@ impl File { } if values.is_empty() { - Err(lookup::existing::Error::KeyMissing) + Err(lookup::existing::key_missing()) } else { Ok(values) } @@ -503,7 +504,7 @@ impl File { /// # use std::convert::TryFrom; /// # let mut git_config = gix_config::File::try_from("[core]a=b\n[core]\na=c\na=d").unwrap(); /// assert_eq!( - /// git_config.raw_values("core.a")?, + /// git_config.raw_values("core.a").expect("values exist"), /// vec![ /// bstr::BString::from("b"), /// bstr::BString::from("c"), @@ -511,10 +512,10 @@ impl File { /// ] /// ); /// - /// git_config.raw_values_mut("core.a")?.set_all("g"); + /// git_config.raw_values_mut("core.a").expect("values exist").set_all("g"); /// /// assert_eq!( - /// git_config.raw_values("core.a")?, + /// git_config.raw_values("core.a").expect("values exist"), /// vec![ /// bstr::BString::from("g"), /// bstr::BString::from("g"), @@ -556,7 +557,7 @@ impl File { /// # use std::convert::TryFrom; /// # let mut git_config = gix_config::File::try_from("[core]a=b\n[core]\na=c\na=d").unwrap(); /// assert_eq!( - /// git_config.raw_values("core.a")?, + /// git_config.raw_values("core.a").expect("values exist"), /// vec![ /// bstr::BString::from("b"), /// bstr::BString::from("c"), @@ -564,10 +565,10 @@ impl File { /// ] /// ); /// - /// git_config.raw_values_mut_by("core", None, "a")?.set_all("g"); + /// git_config.raw_values_mut_by("core", None, "a").expect("values exist").set_all("g"); /// /// assert_eq!( - /// git_config.raw_values("core.a")?, + /// git_config.raw_values("core.a").expect("values exist"), /// vec![ /// bstr::BString::from("g"), /// bstr::BString::from("g"), @@ -627,7 +628,7 @@ impl File { mut filter: impl FnMut(&Metadata) -> bool, ) -> Result, lookup::existing::Error> { let section_ids = self.section_ids_by_name_and_subname(section_name, subsection_name)?; - let key = section::ValueName::try_from(value_name)?; + let key = section::ValueName::try_from(value_name).or_erased()?; let mut offsets = HashMap::new(); let mut entries = Vec::new(); @@ -671,7 +672,7 @@ impl File { entries.sort(); if entries.is_empty() { - Err(lookup::existing::Error::KeyMissing) + Err(lookup::existing::key_missing()) } else { Ok(MultiValueMut { section: &mut self.sections, @@ -705,10 +706,10 @@ impl File { /// # use gix_config::File; /// # use std::convert::TryFrom; /// # let mut git_config = gix_config::File::try_from("[core]a=b\n[core]\na=c\na=d").unwrap(); - /// git_config.set_existing_raw_value("core.a", "e")?; - /// assert_eq!(git_config.raw_value("core.a")?, "e"); + /// git_config.set_existing_raw_value("core.a", "e").expect("value exists"); + /// assert_eq!(git_config.raw_value("core.a").expect("value exists"), "e"); /// assert_eq!( - /// git_config.raw_values("core.a")?, + /// git_config.raw_values("core.a").expect("values exist"), /// vec![ /// bstr::BString::from("b"), /// bstr::BString::from("c"), @@ -724,7 +725,8 @@ impl File { ) -> Result<(), crate::file::set_raw_value::Error> { let key = key.as_key(); self.raw_value_mut_filter_inner(key.section_name, key.subsection_name, key.value_name, |_| true)? - .set(new_value)?; + .set(new_value) + .or_erased()?; Ok(()) } @@ -750,10 +752,10 @@ impl File { /// # use gix_config::File; /// # use std::convert::TryFrom; /// # let mut git_config = gix_config::File::try_from("[core]a=b\n[core]\na=c\na=d").unwrap(); - /// git_config.set_existing_raw_value_by("core", None, "a", "e")?; - /// assert_eq!(git_config.raw_value("core.a")?, "e"); + /// git_config.set_existing_raw_value_by("core", None, "a", "e").expect("value exists"); + /// assert_eq!(git_config.raw_value("core.a").expect("value exists"), "e"); /// assert_eq!( - /// git_config.raw_values("core.a")?, + /// git_config.raw_values("core.a").expect("values exist"), /// vec![ /// bstr::BString::from("b"), /// bstr::BString::from("c"), @@ -770,7 +772,8 @@ impl File { new_value: impl crate::AsBStr, ) -> Result<(), crate::file::set_raw_value::Error> { self.raw_value_mut_by(section_name, subsection_name, value_name)? - .set(new_value)?; + .set(new_value) + .or_erased()?; Ok(()) } @@ -791,11 +794,11 @@ impl File { /// ``` /// # use gix_config::File; /// # let mut git_config = gix_config::File::try_from("[core]a=b").unwrap(); - /// let prev = git_config.set_raw_value(&"core.a", "e")?; - /// git_config.set_raw_value(&"core.b", "f")?; + /// let prev = git_config.set_raw_value(&"core.a", "e").expect("valid value"); + /// git_config.set_raw_value(&"core.b", "f").expect("valid value"); /// assert_eq!(prev.expect("present"), "b"); - /// assert_eq!(git_config.raw_value("core.a")?, "e"); - /// assert_eq!(git_config.raw_value("core.b")?, "f"); + /// assert_eq!(git_config.raw_value("core.a").expect("value exists"), "e"); + /// assert_eq!(git_config.raw_value("core.b").expect("value exists"), "f"); /// # Ok::<(), Box>(()) /// ``` pub fn set_raw_value( @@ -823,11 +826,11 @@ impl File { /// ``` /// # use gix_config::File; /// # let mut git_config = gix_config::File::try_from("[core]a=b").unwrap(); - /// let prev = git_config.set_raw_value_by("core", None, "a", "e")?; - /// git_config.set_raw_value_by("core", None, "b", "f")?; + /// let prev = git_config.set_raw_value_by("core", None, "a", "e").expect("valid value"); + /// git_config.set_raw_value_by("core", None, "b", "f").expect("valid value"); /// assert_eq!(prev.expect("present"), "b"); - /// assert_eq!(git_config.raw_value("core.a")?, "e"); - /// assert_eq!(git_config.raw_value("core.b")?, "f"); + /// assert_eq!(git_config.raw_value("core.a").expect("value exists"), "e"); + /// assert_eq!(git_config.raw_value("core.b").expect("value exists"), "f"); /// # Ok::<(), Box>(()) /// ``` pub fn set_raw_value_by( @@ -879,9 +882,11 @@ impl File { new_value: impl crate::AsBStr, filter: impl FnMut(&Metadata) -> bool, ) -> Result, crate::file::set_raw_value::Error> { - let key = section::ValueName::try_from(value_name)?; - let mut section = self.section_mut_or_create_new_filter_inner(section_name, subsection_name, filter)?; - section.set_inner(key, new_value.as_bstr()).map_err(Into::into) + let key = section::ValueName::try_from(value_name).or_erased()?; + let mut section = self + .section_mut_or_create_new_filter_inner(section_name, subsection_name, filter) + .or_erased()?; + section.set_inner(key, new_value.as_bstr()).or_erased() } /// Sets a multivar in a given `key`. @@ -921,8 +926,8 @@ impl File { /// "y", /// "z", /// ]; - /// git_config.set_existing_raw_multi_value("core.a", new_values.into_iter())?; - /// let fetched_config = git_config.raw_values("core.a")?; + /// git_config.set_existing_raw_multi_value("core.a", new_values.into_iter()).expect("values exist"); + /// let fetched_config = git_config.raw_values("core.a").expect("values exist"); /// assert!(fetched_config.iter().any(|v| v == "x")); /// assert!(fetched_config.iter().any(|v| v == "y")); /// assert!(fetched_config.iter().any(|v| v == "z")); @@ -939,8 +944,8 @@ impl File { /// "x", /// "y", /// ]; - /// git_config.set_existing_raw_multi_value("core.a", new_values.into_iter())?; - /// let fetched_config = git_config.raw_values("core.a")?; + /// git_config.set_existing_raw_multi_value("core.a", new_values.into_iter()).expect("values exist"); + /// let fetched_config = git_config.raw_values("core.a").expect("values exist"); /// assert!(fetched_config.iter().any(|v| v == "x")); /// assert!(fetched_config.iter().any(|v| v == "y")); /// # Ok::<(), Box>(()) @@ -958,8 +963,8 @@ impl File { /// "z", /// "discarded", /// ]; - /// git_config.set_existing_raw_multi_value("core.a", new_values)?; - /// assert!(!git_config.raw_values("core.a")?.iter().any(|v| v == "discarded")); + /// git_config.set_existing_raw_multi_value("core.a", new_values).expect("values exist"); + /// assert!(!git_config.raw_values("core.a").expect("values exist").iter().any(|v| v == "discarded")); /// # Ok::<(), Box>(()) /// ``` pub fn set_existing_raw_multi_value( @@ -973,7 +978,8 @@ impl File { { let key = key.as_key(); self.raw_values_mut_filter_inner(key.section_name, key.subsection_name, key.value_name, |_| true)? - .set_values(new_values)?; + .set_values(new_values) + .or_erased()?; Ok(()) } @@ -1014,8 +1020,8 @@ impl File { /// "y", /// "z", /// ]; - /// git_config.set_existing_raw_multi_value_by("core", None, "a", new_values.into_iter())?; - /// let fetched_config = git_config.raw_values("core.a")?; + /// git_config.set_existing_raw_multi_value_by("core", None, "a", new_values.into_iter()).expect("values exist"); + /// let fetched_config = git_config.raw_values("core.a").expect("values exist"); /// assert!(fetched_config.iter().any(|v| v == "x")); /// assert!(fetched_config.iter().any(|v| v == "y")); /// assert!(fetched_config.iter().any(|v| v == "z")); @@ -1032,8 +1038,8 @@ impl File { /// "x", /// "y", /// ]; - /// git_config.set_existing_raw_multi_value_by("core", None, "a", new_values.into_iter())?; - /// let fetched_config = git_config.raw_values("core.a")?; + /// git_config.set_existing_raw_multi_value_by("core", None, "a", new_values.into_iter()).expect("values exist"); + /// let fetched_config = git_config.raw_values("core.a").expect("values exist"); /// assert!(fetched_config.iter().any(|v| v == "x")); /// assert!(fetched_config.iter().any(|v| v == "y")); /// # Ok::<(), Box>(()) @@ -1051,8 +1057,8 @@ impl File { /// "z", /// "discarded", /// ]; - /// git_config.set_existing_raw_multi_value_by("core", None, "a", new_values)?; - /// assert!(!git_config.raw_values("core.a")?.iter().any(|v| v == "discarded")); + /// git_config.set_existing_raw_multi_value_by("core", None, "a", new_values).expect("values exist"); + /// assert!(!git_config.raw_values("core.a").expect("values exist").iter().any(|v| v == "discarded")); /// # Ok::<(), Box>(()) /// ``` pub fn set_existing_raw_multi_value_by( @@ -1067,7 +1073,8 @@ impl File { Item: crate::AsBStr, { self.raw_values_mut_by(section_name, subsection_name, value_name)? - .set_values(new_values)?; + .set_values(new_values) + .or_erased()?; Ok(()) } } diff --git a/gix-config/src/file/access/read_only.rs b/gix-config/src/file/access/read_only.rs index 17f6d263e4a..f6a1085ad14 100644 --- a/gix-config/src/file/access/read_only.rs +++ b/gix-config/src/file/access/read_only.rs @@ -39,10 +39,10 @@ impl File { /// "#; /// let git_config = gix_config::File::try_from(config).expect("valid config"); /// // You can either use the turbofish to determine the type... - /// let a_value = git_config.value::("core.a")?; + /// let a_value = git_config.value::("core.a").expect("valid value"); /// // ... or explicitly declare the type to avoid the turbofish - /// let c_value: Boolean = git_config.value("core.c")?; - /// # Ok::<(), gix_config::lookup::Error>(()) + /// let c_value: Boolean = git_config.value("core.c").expect("valid value"); + /// # Ok::<(), Box>(()) /// ``` pub fn value>(&self, key: impl AsKey) -> Result> { let key = key.as_key(); @@ -73,10 +73,10 @@ impl File { /// "#; /// let git_config = gix_config::File::try_from(config).expect("valid config"); /// // You can either use the turbofish to determine the type... - /// let a_value = git_config.value_by::("core", None, "a")?; + /// let a_value = git_config.value_by::("core", None, "a").expect("valid value"); /// // ... or explicitly declare the type to avoid the turbofish - /// let c_value: Boolean = git_config.value_by("core", None, "c")?; - /// # Ok::<(), gix_config::lookup::Error>(()) + /// let c_value: Boolean = git_config.value_by("core", None, "c").expect("valid value"); + /// # Ok::<(), Box>(()) /// ``` pub fn value_by>( &self, @@ -164,7 +164,7 @@ impl File { /// "#; /// let git_config = gix_config::File::try_from(config).expect("valid config"); /// // You can either use the turbofish to determine the type... - /// let a_value = git_config.values::("core.a")?; + /// let a_value = git_config.values::("core.a").expect("valid values"); /// assert_eq!( /// a_value, /// vec![ @@ -218,7 +218,7 @@ impl File { /// "#; /// let git_config = gix_config::File::try_from(config).expect("valid config"); /// // You can either use the turbofish to determine the type... - /// let a_value = git_config.values_by::("core", None, "a")?; + /// let a_value = git_config.values_by::("core", None, "a").expect("valid values"); /// assert_eq!( /// a_value, /// vec![ @@ -279,7 +279,7 @@ impl File { subsection_name: impl AsBStrOpt, ) -> Result, lookup::existing::Error> { self.section_filter(name, subsection_name, |_| true)? - .ok_or(lookup::existing::Error::SectionMissing) + .ok_or_else(lookup::existing::section_missing) } /// Returns the last found immutable section with a given `section_key`, identifying the name and subsection name like `core` @@ -289,7 +289,7 @@ impl File { section_key: impl crate::AsBStr, ) -> Result, lookup::existing::Error> { let key = crate::parse::section::unvalidated::KeyRef::parse(section_key.as_bstr()) - .ok_or(lookup::existing::Error::KeyMissing)?; + .ok_or_else(lookup::existing::key_missing)?; self.section(key.section_name, key.subsection_name) } @@ -322,7 +322,7 @@ impl File { filter: impl FnMut(&Metadata) -> bool, ) -> Result>, lookup::existing::Error> { let key = crate::parse::section::unvalidated::KeyRef::parse(section_key.as_bstr()) - .ok_or(lookup::existing::Error::KeyMissing)?; + .ok_or_else(lookup::existing::key_missing)?; self.section_filter(key.section_name, key.subsection_name, filter) } diff --git a/gix-config/src/file/includes/mod.rs b/gix-config/src/file/includes/mod.rs index aec8f06eef3..1af131bb943 100644 --- a/gix-config/src/file/includes/mod.rs +++ b/gix-config/src/file/includes/mod.rs @@ -1,7 +1,7 @@ use std::path::{Path, PathBuf}; use bstr::{BStr, BString, ByteSlice, ByteVec}; -use gix_error::ValidationError; +use gix_error::{ErrorExt, NotFoundError, OptionExt, ResultExt, ValidationError, message}; use gix_features::threading::OwnShared; use gix_ref::Category; @@ -53,9 +53,11 @@ fn resolve_includes_recursive( ) -> Result<(), Error> { if depth == options.includes.max_depth { return if options.includes.err_on_max_depth_exceeded { - Err(Error::IncludeDepthExceeded { - max_depth: options.includes.max_depth, - }) + Err(ValidationError::new(format!( + "The maximum allowed length {} of the file include chain built by following nested resolve_includes is exceeded", + options.includes.max_depth + )) + .raise_erased()) } else { Ok(()) }; @@ -109,13 +111,15 @@ fn insert_includes_recursively( buf.clear(); std::io::copy( - &mut std::fs::File::open(&config_path).map_err(|err| Error::Io { - source: err, - path: config_path.to_owned(), + &mut std::fs::File::open(&config_path).or_raise_erased(|| { + message!( + "Could not read included configuration file at '{}'", + config_path.display() + ) })?, buf, ) - .map_err(Error::CopyBuffer)?; + .or_raise_erased(|| message("Failed to copy configuration file into buffer"))?; let config_meta = Metadata { path: Some(config_path), trust: meta.trust, @@ -127,16 +131,13 @@ fn insert_includes_recursively( ..options }; - let mut include_config = - File::from_bytes_owned(buf, config_meta, no_follow_options).map_err(|err| match err { - init::Error::Parse(err) => Error::Parse(err), - init::Error::Interpolate(err) => Error::Interpolate(err), - init::Error::Span(err) => Error::Span(err), - init::Error::Includes(_) => unreachable!("BUG: {:?} not possible due to no-follow options", err), - })?; + let mut include_config = File::from_bytes_owned(buf, config_meta, no_follow_options) + .or_raise_erased(|| message("Could not parse included configuration file"))?; resolve_includes_recursive(Some(target_config), &mut include_config, depth + 1, buf, options)?; - target_config.append_or_insert(include_config, Some(section_id))?; + target_config + .append_or_insert(include_config, Some(section_id)) + .or_raise_erased(|| message("Could not append included configuration"))?; } Ok(()) } @@ -246,13 +247,15 @@ fn gitdir_matches( if !err_on_interpolation_failure && git_dir.is_none() { return Ok(false); } - let git_dir = gix_path::to_unix_separators_on_windows(gix_path::into_bstr(git_dir.ok_or(Error::MissingGitDir)?)); + let git_dir = gix_path::to_unix_separators_on_windows(gix_path::into_bstr(git_dir.ok_or_raise_erased(|| { + NotFoundError::new("The git directory must be provided to support `gitdir:` conditional includes") + })?)); let mut pattern_path = match check_interpolation_result( err_on_interpolation_failure, crate::Path::from(condition_path.to_owned()).interpolate(context), ) - .map_err(|err| Error::Interpolate(err.into_error()))? + .or_raise_erased(|| message("Could not interpolate conditional include path"))? { Some(path) => gix_path::into_bstr(path).into_owned(), // Git keeps the original condition pattern when interpolation fails. @@ -268,7 +271,11 @@ fn gitdir_matches( return Ok(false); } let parent_dir = target_config_path - .ok_or(Error::MissingConfigPath)? + .ok_or_raise_erased(|| { + NotFoundError::new( + "Include paths from environment variables must not be relative as no config file path exists as root", + ) + })? .parent() .expect("config path can never be /"); let mut joined_path = gix_path::to_unix_separators_on_windows(gix_path::into_bstr(parent_dir)).into_owned(); @@ -294,7 +301,8 @@ fn gitdir_matches( } let expanded_git_dir = gix_path::to_unix_separators_on_windows(gix_path::into_bstr( - gix_path::realpath(gix_path::from_byte_slice(&git_dir)).map_err(|err| Error::Realpath(err.into_error()))?, + gix_path::realpath(gix_path::from_byte_slice(&git_dir)) + .or_raise_erased(|| message("Could not resolve the git directory to its real path"))?, )); Ok(gix_glob::wildmatch( pattern_path.as_bstr(), @@ -328,7 +336,7 @@ fn resolve_path( }: includes::Options<'_>, ) -> Result, Error> { let path = match check_interpolation_result(err_on_interpolation_failure, path.interpolate(context)) - .map_err(|err| Error::Interpolate(err.into_error()))? + .or_raise_erased(|| message("Could not interpolate include path"))? { Some(p) => p, None => return Ok(None), @@ -338,7 +346,11 @@ fn resolve_path( return Ok(None); } target_config_path - .ok_or(Error::MissingConfigPath)? + .ok_or_raise_erased(|| { + NotFoundError::new( + "Include paths from environment variables must not be relative as no config file path exists as root", + ) + })? .parent() .expect("path is a config file which naturally lives in a directory") .join(path) diff --git a/gix-config/src/file/includes/types.rs b/gix-config/src/file/includes/types.rs index 16496ad9d29..9b9530ea4d7 100644 --- a/gix-config/src/file/includes/types.rs +++ b/gix-config/src/file/includes/types.rs @@ -1,30 +1,7 @@ -use std::path::PathBuf; - -use crate::{parse, path::interpolate}; +use crate::path::interpolate; /// The error returned when following includes. -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] -pub enum Error { - #[error("Failed to copy configuration file into buffer")] - CopyBuffer(#[source] std::io::Error), - #[error("Could not read included configuration file at '{}'", path.display())] - Io { path: PathBuf, source: std::io::Error }, - #[error(transparent)] - Parse(#[from] parse::Error), - #[error(transparent)] - Span(#[from] parse::span::Error), - #[error(transparent)] - Interpolate(gix_error::Error), - #[error("The maximum allowed length {} of the file include chain built by following nested resolve_includes is exceeded", .max_depth)] - IncludeDepthExceeded { max_depth: u8 }, - #[error("Include paths from environment variables must not be relative as no config file paths exists as root")] - MissingConfigPath, - #[error("The git directory must be provided to support `gitdir:` conditional includes")] - MissingGitDir, - #[error(transparent)] - Realpath(gix_error::Error), -} +pub type Error = gix_error::Exn; /// Options to handle includes, like `include.path` or `includeIf..path`, #[derive(Clone, Copy)] diff --git a/gix-config/src/file/init/comfort.rs b/gix-config/src/file/init/comfort.rs index 275a7b0e53c..808a1f76f9c 100644 --- a/gix-config/src/file/init/comfort.rs +++ b/gix-config/src/file/init/comfort.rs @@ -85,6 +85,8 @@ impl File { /// Includes will be resolved within limits as some information like the git installation directory is missing to interpolate /// paths with as well as git repository information like the branch name. pub fn from_git_dir(dir: std::path::PathBuf) -> Result { + use gix_error::{ResultExt, message}; + let (mut local, git_dir) = { let source = Source::Local; let mut path = dir; @@ -93,7 +95,8 @@ impl File { .storage_location(&mut gix_path::env::var) .expect("location available for local"), ); - let local = Self::from_path_no_includes(path.clone(), source)?; + let local = Self::from_path_no_includes(path.clone(), source) + .or_raise(|| message("Could not read repository-local configuration"))?; path.pop(); (local, path) }; @@ -110,7 +113,8 @@ impl File { }), _ => None, } - .transpose()?; + .transpose() + .or_raise(|| message("Could not read worktree configuration"))?; let home = gix_path::env::home_dir(); let options = init::Options { @@ -127,16 +131,30 @@ impl File { ..Default::default() }; - let mut globals = Self::from_globals()?; - globals.resolve_includes(options)?; - local.resolve_includes(options)?; + let mut globals = Self::from_globals().or_raise(|| message("Could not read global configuration"))?; + globals + .resolve_includes(options) + .or_raise(|| message("Could not resolve includes in global configuration"))?; + local + .resolve_includes(options) + .or_raise(|| message("Could not resolve includes in repository-local configuration"))?; - globals.append(local)?; + globals + .append(local) + .or_raise(|| message("Could not append repository-local configuration"))?; if let Some(mut worktree) = worktree { - worktree.resolve_includes(options)?; - globals.append(worktree)?; + worktree + .resolve_includes(options) + .or_raise(|| message("Could not resolve includes in worktree configuration"))?; + globals + .append(worktree) + .or_raise(|| message("Could not append worktree configuration"))?; } - globals.append(Self::from_environment_overrides()?)?; + let environment = + Self::from_environment_overrides().or_raise(|| message("Could not read environment configuration"))?; + globals + .append(environment) + .or_raise(|| message("Could not append environment configuration"))?; Ok(globals) } @@ -144,20 +162,6 @@ impl File { /// pub mod from_git_dir { - use crate::file::init; - /// The error returned by [`File::from_git_dir()`][crate::File::from_git_dir()]. - #[derive(Debug, thiserror::Error)] - pub enum Error { - #[error(transparent)] - FromPaths(#[from] init::from_paths::Error), - #[error(transparent)] - FromEnv(#[from] init::from_env::Error), - #[error(transparent)] - Init(#[from] init::Error), - #[error(transparent)] - Includes(#[from] init::includes::Error), - #[error(transparent)] - Span(#[from] crate::parse::span::Error), - } + pub type Error = gix_error::Exn; } diff --git a/gix-config/src/file/init/from_env.rs b/gix-config/src/file/init/from_env.rs index d8f16ff662a..6b123c1e2b0 100644 --- a/gix-config/src/file/init/from_env.rs +++ b/gix-config/src/file/init/from_env.rs @@ -1,36 +1,9 @@ use bstr::ByteSlice; -use crate::{File, KeyRef, file, file::init, parse::section, path::interpolate}; +use crate::{File, KeyRef, file, file::init}; /// Represents the errors that may occur when calling [`File::from_env()`]. -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] -pub enum Error { - #[error("Configuration {kind} at index {index} contained illformed UTF-8")] - IllformedUtf8 { index: usize, kind: &'static str }, - #[error("GIT_CONFIG_COUNT was not a positive integer: {}", .input)] - InvalidConfigCount { 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(gix_error::Error), - #[error(transparent)] - Includes(#[from] init::includes::Error), - #[error(transparent)] - Section(#[from] section::header::Error), - #[error(transparent)] - SectionValue(#[from] file::section::value::Error), -} - -impl From for Error { - fn from(err: interpolate::Error) -> Self { - Error::PathInterpolationError(err.into_error()) - } -} +pub type Error = gix_error::Exn; /// Instantiation from environment variables impl File { @@ -41,9 +14,12 @@ impl File { /// /// [`git-config`'s documentation]: https://git-scm.com/docs/git-config#Documentation/git-config.txt-GITCONFIGCOUNT pub fn from_env(options: init::Options<'_>) -> Result, Error> { + use gix_error::{ErrorExt, NotFoundError, OptionExt, ResultExt, ValidationError, message}; use std::env; let count: usize = match env::var("GIT_CONFIG_COUNT") { - Ok(v) => v.parse().map_err(|_| Error::InvalidConfigCount { input: v })?, + Ok(v) => v.parse::().or_raise_erased(|| { + ValidationError::new_with_input("GIT_CONFIG_COUNT was not a positive integer", v) + })?, Err(_) => return Ok(None), }; @@ -60,33 +36,41 @@ impl File { let mut config = File::new(meta); for i in 0..count { let key = gix_path::os_string_into_bstring( - env::var_os(format!("GIT_CONFIG_KEY_{i}")).ok_or(Error::InvalidKeyId { key_id: i })?, + env::var_os(format!("GIT_CONFIG_KEY_{i}")) + .ok_or_raise_erased(|| NotFoundError::new(format!("GIT_CONFIG_KEY_{i} was not set")))?, ) - .map_err(|_| Error::IllformedUtf8 { index: i, kind: "key" })?; - let value = env::var_os(format!("GIT_CONFIG_VALUE_{i}")).ok_or(Error::InvalidValueId { value_id: i })?; - let key = KeyRef::parse_unvalidated(key.as_ref()).ok_or_else(|| Error::InvalidKeyValue { - key_id: i, - key_val: key.to_string(), + .or_raise_erased(|| { + ValidationError::new(format!("Configuration key at index {i} contained illformed UTF-8")) + })?; + let value = env::var_os(format!("GIT_CONFIG_VALUE_{i}")) + .ok_or_raise_erased(|| NotFoundError::new(format!("GIT_CONFIG_VALUE_{i} was not set")))?; + let key = KeyRef::parse_unvalidated(key.as_ref()).ok_or_else(|| { + ValidationError::new_with_input(format!("GIT_CONFIG_KEY_{i} was set to an invalid value"), key.clone()) + .raise_erased() })?; config - .section_mut_or_create_new_inner(key.section_name, key.subsection_name)? + .section_mut_or_create_new_inner(key.section_name, key.subsection_name) + .or_erased()? .push( key.value_name, Some( gix_path::os_str_into_bstr(&value) - .map_err(|_| Error::IllformedUtf8 { - index: i, - kind: "value", + .or_raise_erased(|| { + ValidationError::new(format!( + "Configuration value at index {i} contained illformed UTF-8" + )) })? .as_bytes() .into(), ), - )?; + ) + .or_erased()?; } let mut buf = Vec::new(); - init::includes::resolve(&mut config, &mut buf, options)?; + init::includes::resolve(&mut config, &mut buf, options) + .or_raise_erased(|| message("Could not resolve includes in environment configuration"))?; Ok(Some(config)) } } diff --git a/gix-config/src/file/init/from_paths.rs b/gix-config/src/file/init/from_paths.rs index 63227112aa9..4f3088df7c0 100644 --- a/gix-config/src/file/init/from_paths.rs +++ b/gix-config/src/file/init/from_paths.rs @@ -2,21 +2,11 @@ use std::collections::BTreeSet; use crate::{ File, - file::{Metadata, init, init::Options}, + file::{Metadata, init::Options}, }; /// The error returned by [`File::from_paths_metadata()`] and [`File::from_path_no_includes()`]. -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] -pub enum Error { - #[error("The configuration file at \"{}\" could not be read", path.display())] - Io { - source: std::io::Error, - path: std::path::PathBuf, - }, - #[error(transparent)] - Init(#[from] init::Error), -} +pub type Error = gix_error::Exn; /// Instantiation from one or more paths impl File { @@ -24,28 +14,26 @@ impl File { /// /// Note that the path will be checked for ownership to derive trust. pub fn from_path_no_includes(path: std::path::PathBuf, source: crate::Source) -> Result { - let trust = match gix_sec::Trust::from_path_ownership(&path) { - Ok(t) => t, - Err(err) => return Err(Error::Io { source: err, path }), - }; + use gix_error::{ResultExt, message}; + let trust = gix_sec::Trust::from_path_ownership(&path).or_raise(|| { + message!( + "The configuration file at \"{}\" could not be inspected", + path.display() + ) + })?; let mut buf = Vec::new(); - match std::io::copy( - &mut match std::fs::File::open(&path) { - Ok(f) => f, - Err(err) => return Err(Error::Io { source: err, path }), - }, - &mut buf, - ) { - Ok(_) => {} - Err(err) => return Err(Error::Io { source: err, path }), - } + let mut file = std::fs::File::open(&path) + .or_raise(|| message!("The configuration file at \"{}\" could not be read", path.display()))?; + std::io::copy(&mut file, &mut buf) + .or_raise(|| message!("The configuration file at \"{}\" could not be read", path.display()))?; - Ok(File::from_bytes_owned( + File::from_bytes_owned( &mut buf, Metadata::from(source).at(path).with(trust), Default::default(), - )?) + ) + .or_raise(|| message("Could not initialize configuration from a path")) } /// Constructs a `git-config` file from the provided metadata, which must include a path to read from or be ignored. @@ -77,6 +65,7 @@ impl File { err_on_non_existing_paths: bool, options: Options<'_>, ) -> Result, Error> { + use gix_error::{ErrorExt, ResultExt, message}; let mut target = None; let mut seen = BTreeSet::default(); for (path, mut meta) in path_meta.filter_map(|mut meta| meta.path.take().map(|p| (p, meta))) { @@ -90,7 +79,10 @@ impl File { Ok(f) => f, Err(err) if !err_on_non_existing_paths && err.kind() == std::io::ErrorKind::NotFound => continue, Err(err) => { - let err = Error::Io { source: err, path }; + let err = err.and_raise(message!( + "The configuration file at \"{}\" could not be read", + path.display() + )); if options.ignore_io_errors { gix_features::trace::warn!("ignoring: {err:#?}"); continue; @@ -103,29 +95,30 @@ impl File { ) { Ok(_) => {} Err(err) => { + let err = err.and_raise(message!( + "The configuration file at \"{}\" could not be read", + path.display() + )); if options.ignore_io_errors { - gix_features::trace::warn!( - "ignoring: {:#?}", - Error::Io { - source: err, - path: path.clone() - } - ); + gix_features::trace::warn!("ignoring: {err:#?}"); buf.clear(); } else { - return Err(Error::Io { source: err, path }); + return Err(err); } } } meta.path = Some(path); - let config = Self::from_bytes_owned(buf, meta, options)?; + let config = Self::from_bytes_owned(buf, meta, options) + .or_raise(|| message("Could not initialize configuration from a path"))?; match &mut target { None => { target = Some(config); } Some(target) => { - target.append(config).map_err(init::Error::from)?; + target + .append(config) + .or_raise(|| message("Could not append configuration from a path"))?; } } } diff --git a/gix-config/src/file/init/mod.rs b/gix-config/src/file/init/mod.rs index 356b3a9c5d4..5f9b70c25c7 100644 --- a/gix-config/src/file/init/mod.rs +++ b/gix-config/src/file/init/mod.rs @@ -37,9 +37,11 @@ impl File { meta: impl Into>, options: Options<'_>, ) -> Result { + use gix_error::{ResultExt, message}; let meta = meta.into(); Ok(Self::from_parse_events_no_includes( - parse::Events::from_bytes(input, options.to_event_filter())?, + parse::Events::from_bytes(input, options.to_event_filter()) + .or_raise_erased(|| message("Could not parse configuration"))?, meta, )) } @@ -83,12 +85,15 @@ impl File { meta: impl Into>, options: Options<'_>, ) -> Result { + use gix_error::{ResultExt, message}; let mut config = Self::from_parse_events_no_includes( - parse::Events::from_bytes(input_and_buf, options.to_event_filter()).map_err(Error::from)?, + parse::Events::from_bytes(input_and_buf, options.to_event_filter()) + .or_raise_erased(|| message("Could not parse configuration"))?, meta, ); - includes::resolve(&mut config, input_and_buf, options).map_err(Error::from)?; + includes::resolve(&mut config, input_and_buf, options) + .or_raise_erased(|| message("Could not resolve configuration includes"))?; Ok(config) } } diff --git a/gix-config/src/file/init/types.rs b/gix-config/src/file/init/types.rs index 2fdb38f5a51..36186a1899f 100644 --- a/gix-config/src/file/init/types.rs +++ b/gix-config/src/file/init/types.rs @@ -1,24 +1,7 @@ -use crate::{file::init, parse, parse::EventRef, path::interpolate}; +use crate::{file::init, parse::EventRef}; /// The error returned by [`File::from_bytes_no_includes()`][crate::File::from_bytes_no_includes()]. -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] -pub enum Error { - #[error(transparent)] - Parse(#[from] parse::Error), - #[error(transparent)] - Interpolate(gix_error::Error), - #[error(transparent)] - Includes(#[from] init::includes::Error), - #[error(transparent)] - Span(#[from] parse::span::Error), -} - -impl From for Error { - fn from(err: interpolate::Error) -> Self { - Error::Interpolate(err.into_error()) - } -} +pub type Error = gix_error::Exn; /// Options when loading git config using [`File::from_paths_metadata()`][crate::File::from_paths_metadata()]. #[derive(Clone, Copy, Default)] diff --git a/gix-config/src/file/mod.rs b/gix-config/src/file/mod.rs index 5f4c4350906..68f916c1960 100644 --- a/gix-config/src/file/mod.rs +++ b/gix-config/src/file/mod.rs @@ -27,31 +27,13 @@ pub mod section; /// pub mod rename_section { /// The error returned by [`File::rename_section(…)`][crate::File::rename_section()]. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - Lookup(#[from] crate::lookup::existing::Error), - #[error(transparent)] - Section(#[from] crate::parse::section::header::Error), - } + pub type Error = gix_error::Exn; } /// pub mod set_raw_value { /// The error returned by [`File::set_raw_value(…)`][crate::File::set_raw_value()]. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - Lookup(#[from] crate::lookup::existing::Error), - #[error(transparent)] - Header(#[from] crate::parse::section::header::Error), - #[error(transparent)] - ValueName(#[from] crate::parse::section::value_name::Error), - #[error(transparent)] - Span(#[from] crate::parse::span::Error), - } + pub type Error = gix_error::Exn; } /// Convert ergonomic subsection inputs into an optional owned name. diff --git a/gix-config/src/file/mutable/multi_value.rs b/gix-config/src/file/mutable/multi_value.rs index 19711e20bfb..91eb99cb726 100644 --- a/gix-config/src/file/mutable/multi_value.rs +++ b/gix-config/src/file/mutable/multi_value.rs @@ -74,7 +74,7 @@ impl MultiValueMut<'_> { } if values.is_empty() { - return Err(lookup::existing::Error::KeyMissing); + return Err(lookup::existing::key_missing()); } Ok(values) diff --git a/gix-config/src/file/mutable/section.rs b/gix-config/src/file/mutable/section.rs index 3f8ba026f0f..e3c8a4c6123 100644 --- a/gix-config/src/file/mutable/section.rs +++ b/gix-config/src/file/mutable/section.rs @@ -175,7 +175,7 @@ impl SectionMut<'_> { value: impl crate::AsBStr, ) -> Result, file::section::value::Error> { let value_name = ValueName::try_from(value_name.as_ref())?; - self.set_inner(value_name, value.as_bstr()).map_err(Into::into) + self.set_inner(value_name, value.as_bstr()) } pub(crate) fn set_inner( @@ -419,7 +419,7 @@ impl<'a> SectionMut<'a> { } } - Err(lookup::existing::Error::KeyMissing) + Err(lookup::existing::key_missing()) } pub(crate) fn delete(&mut self, start: Index, end: Index) { diff --git a/gix-config/src/file/section/mod.rs b/gix-config/src/file/section/mod.rs index a7770c8a3d7..21cb46eab9f 100644 --- a/gix-config/src/file/section/mod.rs +++ b/gix-config/src/file/section/mod.rs @@ -18,14 +18,7 @@ use crate::file::{SectionId, write::platform_newline}; /// Errors related to changing values in a section. pub mod value { /// The error returned when adding or changing a value in a section. - #[derive(Debug, thiserror::Error)] - #[allow(missing_docs)] - pub enum Error { - #[error(transparent)] - ValueName(#[from] crate::parse::section::value_name::Error), - #[error(transparent)] - Span(#[from] crate::parse::span::Error), - } + pub type Error = gix_error::ValidationError; } impl std::ops::Deref for SectionData { diff --git a/gix-config/src/file/util.rs b/gix-config/src/file/util.rs index 4fa0a516250..912edb091b1 100644 --- a/gix-config/src/file/util.rs +++ b/gix-config/src/file/util.rs @@ -67,12 +67,12 @@ impl File { let lookup = self .section_lookup_tree .get(§ion_name) - .ok_or(lookup::existing::Error::SectionMissing)?; + .ok_or_else(lookup::existing::section_missing)?; match subsection_name { Some(name) => lookup.by_subsection.get(name), None => (!lookup.without_subsection.is_empty()).then_some(&lookup.without_subsection), } - .ok_or(lookup::existing::Error::SubSectionMissing) + .ok_or_else(lookup::existing::subsection_missing) .map(|ids| ids.iter().copied()) } @@ -84,7 +84,7 @@ impl File { let lookup = self .section_lookup_tree .get(&lookup_name) - .ok_or(lookup::existing::Error::SectionMissing)?; + .ok_or_else(lookup::existing::section_missing)?; let mut ids = Vec::with_capacity(self.section_order.len()); ids.extend_from_slice(&lookup.without_subsection); ids.extend(lookup.by_subsection.values().flatten().copied()); diff --git a/gix-config/src/lookup.rs b/gix-config/src/lookup.rs index f16d073a39e..e54ac502696 100644 --- a/gix-config/src/lookup.rs +++ b/gix-config/src/lookup.rs @@ -1,26 +1,64 @@ /// The error when looking up a value, for example via [`File::try_value()`][crate::File::try_value()]. -#[derive(Debug, thiserror::Error)] +#[derive(Debug)] #[expect(missing_docs)] pub enum Error { - #[error(transparent)] - ValueMissing(#[from] existing::Error), - #[error(transparent)] + ValueMissing(gix_error::Error), FailedConversion(E), } +impl> Error { + /// Convert this lookup error into a standard error, retaining the inner exception's context. + pub fn into_error(self) -> gix_error::Error { + match self { + Error::ValueMissing(err) => err, + Error::FailedConversion(err) => err.into(), + } + } +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::ValueMissing(err) => std::fmt::Display::fmt(err, f), + Error::FailedConversion(err) => std::fmt::Display::fmt(err, f), + } + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::ValueMissing(err) => Some(err), + Error::FailedConversion(err) => Some(err), + } + } +} + +impl From for Error { + fn from(err: existing::Error) -> Self { + Error::ValueMissing(err.into_error()) + } +} + /// pub mod existing { /// The error when looking up a value that doesn't exist, for example via [`File::value()`][crate::File::value()]. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - 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, - #[error(transparent)] - ValueName(#[from] crate::parse::section::value_name::Error), + pub type Error = gix_error::Exn; + + pub(crate) fn section_missing() -> Error { + not_found("The requested section does not exist") + } + + pub(crate) fn subsection_missing() -> Error { + not_found("The requested subsection does not exist") + } + + pub(crate) fn key_missing() -> Error { + not_found("The key does not exist in the requested section") + } + + fn not_found(message: &'static str) -> Error { + use gix_error::ErrorExt; + gix_error::NotFoundError::new(message).raise_erased() } } diff --git a/gix-config/src/parse/error.rs b/gix-config/src/parse/error.rs index 611568c39e5..cc1a5ba5e82 100644 --- a/gix-config/src/parse/error.rs +++ b/gix-config/src/parse/error.rs @@ -109,4 +109,12 @@ impl Display for Error { } } -impl std::error::Error for Error {} +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + static VALIDATION: gix_error::ValidationError = gix_error::ValidationError { + message: std::borrow::Cow::Borrowed("Invalid configuration input"), + input: None, + }; + Some(&VALIDATION) + } +} diff --git a/gix-config/src/parse/from_bytes/tests.rs b/gix-config/src/parse/from_bytes/tests.rs index 8bf40c48ddc..7f230db2349 100644 --- a/gix-config/src/parse/from_bytes/tests.rs +++ b/gix-config/src/parse/from_bytes/tests.rs @@ -15,6 +15,10 @@ fn input_size_is_limited_by_span_representation() { u32::MAX ) ); + assert!( + gix_error::Error::from_error(err).is_validation(), + "input outside the parser's supported size range is invalid input" + ); } /// Extend basic types to get their owned versions, without leaking this into production code. diff --git a/gix-config/src/parse/mod.rs b/gix-config/src/parse/mod.rs index c6717261de0..46a2187a44e 100644 --- a/gix-config/src/parse/mod.rs +++ b/gix-config/src/parse/mod.rs @@ -37,9 +37,14 @@ pub(crate) struct Span { /// Errors produced when a span cannot be represented. pub mod span { /// A span offset or length exceeded the supported 32-bit representation. - #[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, thiserror::Error)] - #[error("configuration data exceeds the supported span size of {} bytes", u32::MAX)] - pub struct Error; + pub type Error = gix_error::ValidationError; + + pub(crate) fn error() -> Error { + Error::new(format!( + "configuration data exceeds the supported span size of {} bytes", + u32::MAX + )) + } } /// A raw span whose semantic value may have required decoding while parsing. @@ -90,15 +95,15 @@ impl Span { pub(crate) fn append(backing: &mut Vec, bytes: &[u8]) -> Result { let start = backing.len(); let span = Self::range(start, bytes.len())?; - backing.len().checked_add(bytes.len()).ok_or(span::Error)?; + backing.len().checked_add(bytes.len()).ok_or_else(span::error)?; backing.extend_from_slice(bytes); Ok(span) } pub(crate) fn range(start: usize, len: usize) -> Result { Ok(Span { - start: start.try_into().map_err(|_| span::Error)?, - len: len.try_into().map_err(|_| span::Error)?, + start: start.try_into().map_err(|_| span::error())?, + len: len.try_into().map_err(|_| span::error())?, }) } @@ -140,7 +145,7 @@ impl Span { self.start = (self.start as usize) .checked_add(offset) .and_then(|start| start.try_into().ok()) - .ok_or(span::Error)?; + .ok_or_else(span::error)?; Ok(()) } } @@ -255,6 +260,7 @@ pub(crate) struct Comment { /// A parser error reports the one-indexed line number where the parsing error /// occurred, as well as the last parser node and the remaining data to be /// parsed. +/// Its source preserves the validation classification when converted to [`gix_error::Error`]. #[derive(PartialEq, Debug)] pub struct Error { kind: error::Kind, diff --git a/gix-config/src/parse/section/header.rs b/gix-config/src/parse/section/header.rs index e6629898500..5b2e53dcbd5 100644 --- a/gix-config/src/parse/section/header.rs +++ b/gix-config/src/parse/section/header.rs @@ -3,16 +3,7 @@ use bstr::{BStr, BString, ByteSlice}; use crate::parse::{Span, section::HeaderData}; /// The error returned when creating a section header. -#[derive(Debug, PartialOrd, PartialEq, Eq, thiserror::Error)] -#[expect(missing_docs)] -pub enum Error { - #[error("section names can only be ascii, '-'")] - InvalidName, - #[error("sub-section names must not contain newlines or null bytes")] - InvalidSubSection, - #[error(transparent)] - Span(#[from] crate::parse::span::Error), -} +pub type Error = gix_error::ValidationError; impl HeaderData { pub(crate) fn new_in( @@ -53,14 +44,14 @@ pub fn is_valid_subsection(name: impl crate::AsBStr) -> bool { fn validated_subsection(name: &BStr) -> Result { is_valid_subsection(name) .then(|| name.into()) - .ok_or(Error::InvalidSubSection) + .ok_or_else(|| Error::new_with_input("sub-section names must not contain newlines or null bytes", name)) } fn validated_name(name: &BStr) -> Result { name.iter() .all(|b| b.is_ascii_alphanumeric() || *b == b'-') .then(|| name.into()) - .ok_or(Error::InvalidName) + .ok_or_else(|| Error::new_with_input("section names can only be ascii, '-'", name)) } impl HeaderData { diff --git a/gix-config/src/parse/section/mod.rs b/gix-config/src/parse/section/mod.rs index 0e1d0f68d2f..a6c1e5e91e0 100644 --- a/gix-config/src/parse/section/mod.rs +++ b/gix-config/src/parse/section/mod.rs @@ -27,9 +27,7 @@ mod types { /// pub mod $module { /// The error returned when `TryFrom` is invoked to create an instance. - #[derive(Debug, thiserror::Error, Copy, Clone)] - #[error($err_doc)] - pub struct Error; + pub type Error = gix_error::ValidationError; } #[doc = $comment] @@ -104,7 +102,7 @@ mod types { if $validate(s.as_slice().as_bstr()) { Ok(Self(s.into())) } else { - Err($module::Error) + Err(gix_error::ValidationError::new_with_input($err_doc, s)) } } } @@ -116,7 +114,7 @@ mod types { if $validate(s) { Ok(Self(s.into())) } else { - Err($module::Error) + Err(gix_error::ValidationError::new_with_input($err_doc, s)) } } } diff --git a/gix-config/src/parse/tests.rs b/gix-config/src/parse/tests.rs index da8b6702afe..c946c76dbe2 100644 --- a/gix-config/src/parse/tests.rs +++ b/gix-config/src/parse/tests.rs @@ -67,7 +67,13 @@ mod span { assert!(Span::range(0, u32::MAX as usize + 1).is_err()); let mut span = Span::range(u32::MAX as usize, 0).expect("the maximum offset is representable"); - assert_eq!(span.rebase(1), Err(crate::parse::span::Error)); + assert_eq!( + span.rebase(1).expect_err("rebasing beyond u32 must fail").message, + format!( + "configuration data exceeds the supported span size of {} bytes", + u32::MAX + ) + ); } } diff --git a/gix-config/tests/config/file/access/mutate.rs b/gix-config/tests/config/file/access/mutate.rs index 2c4b320001c..83bee8b256a 100644 --- a/gix-config/tests/config/file/access/mutate.rs +++ b/gix-config/tests/config/file/access/mutate.rs @@ -79,11 +79,11 @@ mod remove_section { )?; file.remove_section("core", None).expect("plain section exists"); - assert!( - matches!( - file.section("core", None), - Err(gix_config::lookup::existing::Error::SubSectionMissing) - ), + let err = file.section("core", None).unwrap_err(); + assert!(err.downcast_any_ref::().is_some()); + assert_eq!( + err.to_string(), + "The requested subsection does not exist", "the `core` section name still exists through its siblings, but its no-subsection bucket was removed" ); assert_eq!(file.section("core", "a")?.value("key"), Some("a".into())); @@ -92,10 +92,9 @@ mod remove_section { assert_eq!(file.section("core", "b")?.value("key"), Some("b".into())); file.remove_section("core", "b").expect("final subsection exists"); - assert!(matches!( - file.section("core", "b"), - Err(gix_config::lookup::existing::Error::SectionMissing) - )); + let err = file.section("core", "b").unwrap_err(); + assert!(err.downcast_any_ref::().is_some()); + assert_eq!(err.to_string(), "The requested section does not exist"); Ok(()) } @@ -145,22 +144,20 @@ mod remove_section_filter { } mod rename_section { - use gix_config::{file::rename_section, parse::section}; #[test] fn section_renaming_validates_new_name() { let mut file = gix_config::File::try_from("[core] a = b").unwrap(); - assert!(matches!( - file.rename_section("core", None, "new_core", None), - Err(rename_section::Error::Section(section::header::Error::InvalidName)) - )); - - assert!(matches!( - file.rename_section("core", None, "new-core", "a\nb"), - Err(rename_section::Error::Section( - section::header::Error::InvalidSubSection - )) - )); + let err = file.rename_section("core", None, "new_core", None).unwrap_err(); + assert!(err.downcast_any_ref::().is_some()); + assert_eq!(err.to_string(), "section names can only be ascii, '-': \"new_core\""); + + let err = file.rename_section("core", None, "new-core", "a\nb").unwrap_err(); + assert!(err.downcast_any_ref::().is_some()); + assert_eq!( + err.to_string(), + "sub-section names must not contain newlines or null bytes: \"a\\nb\"" + ); } #[test] @@ -238,13 +235,13 @@ mod rename_section { "#); let prev = file.to_string(); - assert!( - matches!( - file.rename_section_filter("branch", "source", "branch", "other", |_| false), - Err(rename_section::Error::Lookup( - gix_config::lookup::existing::Error::KeyMissing - )), - ), + let err = file + .rename_section_filter("branch", "source", "branch", "other", |_| false) + .unwrap_err(); + assert!(err.downcast_any_ref::().is_some()); + assert_eq!( + err.to_string(), + "The key does not exist in the requested section", "matching nothing causes an error" ); assert_eq!( @@ -275,12 +272,9 @@ mod rename_section { fn an_empty_lookup_bucket_is_reported_as_missing() -> crate::Result { let mut file = gix_config::File::try_from("[core] key = value\n")?; file.remove_section("core", None).expect("section exists"); - assert!(matches!( - file.rename_section("core", None, "other", None), - Err(rename_section::Error::Lookup( - gix_config::lookup::existing::Error::SectionMissing - )) - )); + let err = file.rename_section("core", None, "other", None).unwrap_err(); + assert!(err.downcast_any_ref::().is_some()); + assert_eq!(err.to_string(), "The requested section does not exist"); Ok(()) } } diff --git a/gix-config/tests/config/file/access/raw/raw_multi_value.rs b/gix-config/tests/config/file/access/raw/raw_multi_value.rs index b8c1fcaf2ab..0044d1c8b70 100644 --- a/gix-config/tests/config/file/access/raw/raw_multi_value.rs +++ b/gix-config/tests/config/file/access/raw/raw_multi_value.rs @@ -1,4 +1,4 @@ -use gix_config::{File, lookup}; +use gix_config::File; use crate::file::bstring; @@ -94,30 +94,27 @@ fn values_with_sections_filter_returns_values_from_accepted_sections() -> crate: #[test] fn section_not_found() -> crate::Result { let config = File::try_from("[core]\na=b\nc=d")?; - assert!(matches!( - config.raw_values("foo.a"), - Err(lookup::existing::Error::SectionMissing) - )); + let err = config.raw_values("foo.a").unwrap_err(); + assert!(err.downcast_any_ref::().is_some()); + assert_eq!(err.to_string(), "The requested section does not exist"); Ok(()) } #[test] fn subsection_not_found() -> crate::Result { let config = File::try_from("[core]\na=b\nc=d")?; - assert!(matches!( - config.raw_values("core.a.a"), - Err(lookup::existing::Error::SubSectionMissing) - )); + let err = config.raw_values("core.a.a").unwrap_err(); + assert!(err.downcast_any_ref::().is_some()); + assert_eq!(err.to_string(), "The requested subsection does not exist"); Ok(()) } #[test] fn key_not_found() -> crate::Result { let config = File::try_from("[core]\na=b\nc=d")?; - assert!(matches!( - config.raw_values("core.aaaaaa"), - Err(lookup::existing::Error::KeyMissing) - )); + let err = config.raw_values("core.aaaaaa").unwrap_err(); + assert!(err.downcast_any_ref::().is_some()); + assert_eq!(err.to_string(), "The key does not exist in the requested section"); Ok(()) } diff --git a/gix-config/tests/config/file/access/raw/raw_value.rs b/gix-config/tests/config/file/access/raw/raw_value.rs index df76bd10fe4..67831d539a5 100644 --- a/gix-config/tests/config/file/access/raw/raw_value.rs +++ b/gix-config/tests/config/file/access/raw/raw_value.rs @@ -1,4 +1,4 @@ -use gix_config::{File, lookup}; +use gix_config::File; #[test] fn single_section() -> crate::Result { @@ -115,44 +115,45 @@ fn mutable_value_filters_have_key_and_component_variants() -> crate::Result { #[test] fn section_not_found() -> crate::Result { let config = File::try_from("[core]\na=b\nc=d")?; - assert!(matches!( - config.raw_value("foo.a"), - Err(lookup::existing::Error::SectionMissing) - )); + let err = config.raw_value("foo.a").unwrap_err(); + assert!(err.downcast_any_ref::().is_some()); + assert_eq!(err.to_string(), "The requested section does not exist"); Ok(()) } #[test] fn subsection_not_found() -> crate::Result { let config = File::try_from("[core]\na=b\nc=d")?; - assert!(matches!( - config.raw_value("core.a.a"), - Err(lookup::existing::Error::SubSectionMissing) - )); + let err = config.raw_value("core.a.a").unwrap_err(); + assert!(err.downcast_any_ref::().is_some()); + assert_eq!(err.to_string(), "The requested subsection does not exist"); Ok(()) } #[test] fn key_not_found() -> crate::Result { let config = File::try_from("[core]\na=b\nc=d")?; - assert!(matches!( - config.raw_value("core.aaaaaa"), - Err(lookup::existing::Error::KeyMissing) - )); + let err = config.raw_value("core.aaaaaa").unwrap_err(); + assert!(err.downcast_any_ref::().is_some()); + assert_eq!(err.to_string(), "The key does not exist in the requested section"); Ok(()) } #[test] fn invalid_value_names_are_reported_by_mutable_lookups() -> crate::Result { let mut config = File::try_from("[core]\na=b")?; - assert!(matches!( - config.raw_value_mut_by("core", None, "1invalid"), - Err(lookup::existing::Error::ValueName(_)) - )); - assert!(matches!( - config.raw_values_mut_by("core", None, "contains.dot"), - Err(lookup::existing::Error::ValueName(_)) - )); + let err = config.raw_value_mut_by("core", None, "1invalid").unwrap_err(); + assert!(err.downcast_any_ref::().is_some()); + assert_eq!( + err.to_string(), + "Valid value names consist of alphanumeric characters or dashes, starting with an alphabetic character.: \"1invalid\"" + ); + let err = config.raw_values_mut_by("core", None, "contains.dot").unwrap_err(); + assert!(err.downcast_any_ref::().is_some()); + assert_eq!( + err.to_string(), + "Valid value names consist of alphanumeric characters or dashes, starting with an alphabetic character.: \"contains.dot\"" + ); Ok(()) } diff --git a/gix-config/tests/config/file/access/raw/set_raw_value.rs b/gix-config/tests/config/file/access/raw/set_raw_value.rs index 7bcf9820ca0..80d201c5947 100644 --- a/gix-config/tests/config/file/access/raw/set_raw_value.rs +++ b/gix-config/tests/config/file/access/raw/set_raw_value.rs @@ -79,9 +79,11 @@ fn accepts_short_lived_keys() -> crate::Result { #[test] fn invalid_value_names_fail_without_creating_a_section() { let mut file = gix_config::File::default(); - assert!(matches!( - file.set_raw_value_by("new", None, "not.valid", "value"), - Err(gix_config::file::set_raw_value::Error::ValueName(_)) - )); + let err = file.set_raw_value_by("new", None, "not.valid", "value").unwrap_err(); + assert!(err.downcast_any_ref::().is_some()); + assert_eq!( + err.to_string(), + "Valid value names consist of alphanumeric characters or dashes, starting with an alphabetic character.: \"not.valid\"" + ); assert_eq!(file.sections().count(), 0, "validation precedes section creation"); } diff --git a/gix-config/tests/config/file/access/read_only.rs b/gix-config/tests/config/file/access/read_only.rs index 84d37c16e22..a2a4f1abe42 100644 --- a/gix-config/tests/config/file/access/read_only.rs +++ b/gix-config/tests/config/file/access/read_only.rs @@ -10,10 +10,38 @@ use gix_config::{ use crate::file::bstring; fn lookup_error(err: gix_config::lookup::Error>) -> gix_error::Error { - match err { - gix_config::lookup::Error::ValueMissing(err) => gix_error::Error::from_error(err), - gix_config::lookup::Error::FailedConversion(err) => err.into_error(), + err.into_error() +} + +#[test] +fn typed_lookup_errors_can_be_erased() -> crate::Result { + use gix_error::ResultExt; + + let config = File::try_from("[core]\nvalue = invalid\n")?; + for result in [ + config.value::("core.value").map(|_| ()), + config.value::("core.value").map(|_| ()), + config.value::("core.value").map(|_| ()), + config.values::("core.value").map(|_| ()), + config.values::("core.value").map(|_| ()), + config.values::("core.value").map(|_| ()), + ] { + let err = result + .map_err(gix_config::lookup::Error::into_error) + .or_erased() + .expect_err("invalid typed values must fail conversion") + .into_error(); + assert!( + err.downcast_any_ref::().is_some(), + "erasure retains the conversion error" + ); } + let err = config + .value::("core.missing") + .expect_err("the key does not exist") + .into_error(); + assert!(err.is_not_found(), "erasure retains missing-value classification"); + Ok(()) } #[test] @@ -368,20 +396,18 @@ fn sections_by_name_ignores_subsections_and_preserves_file_order() -> crate::Res #[test] fn unknown_section() -> crate::Result { let config = File::default(); - assert!(matches!( - config.section("missing", None).unwrap_err(), - gix_config::lookup::existing::Error::SectionMissing - )); + let err = config.section("missing", None).unwrap_err(); + assert!(err.downcast_any_ref::().is_some()); + assert_eq!(err.to_string(), "The requested section does not exist"); let config = r#" [present] key = false "#; let mut config = File::try_from(config)?; - assert!(matches!( - config.section("present", Some("subsection".into())).unwrap_err(), - gix_config::lookup::existing::Error::SubSectionMissing - )); + let err = config.section("present", Some("subsection".into())).unwrap_err(); + assert!(err.downcast_any_ref::().is_some()); + assert_eq!(err.to_string(), "The requested subsection does not exist"); config.set_raw_value_by("present", "subsection", "key", "value")?; assert!(config.section("present", Some("subsection".into())).is_ok()); @@ -392,10 +418,9 @@ fn unknown_section() -> crate::Result { for id in config.sections_and_ids().map(|(_, id)| id).collect::>() { assert!(config.remove_section_by_id(id).is_some()); } - assert!(matches!( - config.section("present", None).unwrap_err(), - gix_config::lookup::existing::Error::SectionMissing - )); + let err = config.section("present", None).unwrap_err(); + assert!(err.downcast_any_ref::().is_some()); + assert_eq!(err.to_string(), "The requested section does not exist"); Ok(()) } diff --git a/gix-config/tests/config/file/init/from_env.rs b/gix-config/tests/config/file/init/from_env.rs index d00eb43cb07..fa8f5566c0a 100644 --- a/gix-config/tests/config/file/init/from_env.rs +++ b/gix-config/tests/config/file/init/from_env.rs @@ -2,7 +2,7 @@ use std::fs; use gix_config::{ File, - file::{includes, init, init::from_env}, + file::{includes, init}, }; use gix_testtools::tempfile::tempdir; use serial_test::serial; @@ -31,8 +31,11 @@ fn empty_with_zero_count() -> crate::Result { #[serial] fn parse_error_with_invalid_count() -> crate::Result { let _environment = gix_testtools::isolate_git_environment()?.set("GIT_CONFIG_COUNT", "invalid"); - let err = File::from_env(Default::default()).unwrap_err(); - assert!(matches!(err, from_env::Error::InvalidConfigCount { .. })); + let err = File::from_env(Default::default()).expect_err("the configuration count is not an integer"); + let err = err + .downcast_any_ref::() + .expect("invalid counts are validation errors"); + assert_eq!(err.message, "GIT_CONFIG_COUNT was not a positive integer"); Ok(()) } @@ -92,10 +95,13 @@ fn error_on_relative_paths_in_include_paths() -> crate::Result { .strict(), ..Default::default() }); - assert!(matches!( - res, - Err(from_env::Error::Includes(includes::Error::MissingConfigPath)) - )); + let err = res.expect_err("relative includes without a configuration path must fail"); + assert_eq!( + err.downcast_any_ref::() + .expect("the missing configuration path is retained") + .message, + "Include paths from environment variables must not be relative as no config file path exists as root" + ); Ok(()) } diff --git a/gix-config/tests/config/file/init/from_paths/includes/conditional/gitdir/mod.rs b/gix-config/tests/config/file/init/from_paths/includes/conditional/gitdir/mod.rs index ca99033df1a..4896ad8d13b 100644 --- a/gix-config/tests/config/file/init/from_paths/includes/conditional/gitdir/mod.rs +++ b/gix-config/tests/config/file/init/from_paths/includes/conditional/gitdir/mod.rs @@ -155,14 +155,12 @@ fn dot_slash_from_environment_causes_error() -> crate::Result { .set("GIT_CONFIG_VALUE_0", "./include.path"); let res = gix_config::File::from_env(env.to_init_options()); - assert!( - matches!( - res, - Err(gix_config::file::init::from_env::Error::Includes( - gix_config::file::includes::Error::MissingConfigPath - )) - ), - "this is a failure of resolving the include path, after trying to include it" + assert_eq!( + res.expect_err("resolving the relative include path must fail") + .downcast_any_ref::() + .expect("the missing configuration path is retained") + .message, + "Include paths from environment variables must not be relative as no config file path exists as root" ); } @@ -174,14 +172,12 @@ fn dot_slash_from_environment_causes_error() -> crate::Result { .set("GIT_CONFIG_VALUE_0", &absolute_path); let res = gix_config::File::from_env(env.to_init_options()); - assert!( - matches!( - res, - Err(gix_config::file::init::from_env::Error::Includes( - gix_config::file::includes::Error::MissingConfigPath - )) - ), - "here the pattern path tries to be resolved and fails as target config isn't set" + assert_eq!( + res.expect_err("resolving the relative pattern must fail") + .downcast_any_ref::() + .expect("the missing configuration path is retained") + .message, + "Include paths from environment variables must not be relative as no config file path exists as root" ); } diff --git a/gix-config/tests/config/file/init/from_paths/includes/unconditional.rs b/gix-config/tests/config/file/init/from_paths/includes/unconditional.rs index 94f8c5a2b93..7b909674024 100644 --- a/gix-config/tests/config/file/init/from_paths/includes/unconditional.rs +++ b/gix-config/tests/config/file/init/from_paths/includes/unconditional.rs @@ -18,6 +18,18 @@ fn follow_options() -> init::Options<'static> { } } +fn assert_include_depth(err: from_paths::Error, max_depth: u8) { + let err = err + .downcast_any_ref::() + .expect("exceeding the include depth is a validation error"); + assert_eq!( + err.to_string(), + format!( + "The maximum allowed length {max_depth} of the file include chain built by following nested resolve_includes is exceeded" + ) + ); +} + #[test] fn multiple() -> crate::Result { let dir = tempdir()?; @@ -158,12 +170,7 @@ fn respect_max_depth() -> crate::Result { // with max_allowed_depth of 2 and 4 levels of includes, max_allowed_depth is exceeded and error is returned let options = make_options(2, true); let config = File::from_paths_metadata(into_meta(vec![dir.path().join("0")]), options); - assert!(matches!( - config.unwrap_err(), - from_paths::Error::Init(init::Error::Includes(includes::Error::IncludeDepthExceeded { - max_depth: 2 - })) - )); + assert_include_depth(config.expect_err("the configured include depth must be enforced"), 2); // with max_allowed_depth of 2 and 4 levels of includes and error_on_max_depth_exceeded: false , max_allowed_depth is exceeded and the value of level 2 is returned let options = make_options(2, false); @@ -173,12 +180,7 @@ fn respect_max_depth() -> crate::Result { // with max_allowed_depth of 0 and 4 levels of includes, max_allowed_depth is exceeded and error is returned let options = make_options(0, true); let config = File::from_paths_metadata(into_meta(vec![dir.path().join("0")]), options); - assert!(matches!( - config.unwrap_err(), - from_paths::Error::Init(init::Error::Includes(includes::Error::IncludeDepthExceeded { - max_depth: 0 - })) - )); + assert_include_depth(config.expect_err("the configured include depth must be enforced"), 0); Ok(()) } @@ -258,12 +260,7 @@ fn cycle_detection() -> crate::Result { ..Default::default() }; let config = File::from_paths_metadata(into_meta(vec![a_path.clone()]), options); - assert!(matches!( - config.unwrap_err(), - from_paths::Error::Init(init::Error::Includes(includes::Error::IncludeDepthExceeded { - max_depth: 4 - })) - )); + assert_include_depth(config.expect_err("the configured include depth must be enforced"), 4); let options = init::Options { includes: includes::Options { diff --git a/gix-config/tests/config/file/init/from_paths/mod.rs b/gix-config/tests/config/file/init/from_paths/mod.rs index 6aa735c31c2..74c16cc55e2 100644 --- a/gix-config/tests/config/file/init/from_paths/mod.rs +++ b/gix-config/tests/config/file/init/from_paths/mod.rs @@ -17,8 +17,11 @@ mod from_path_no_includes { let config_path = dir.path().join("config"); let err = gix_config::File::from_path_no_includes(config_path, gix_config::Source::Local).unwrap_err(); - assert!( - matches!(err, gix_config::file::init::from_paths::Error::Io{source: io_error, ..} if io_error.kind() == std::io::ErrorKind::NotFound) + assert_eq!( + err.downcast_any_ref::() + .expect("the I/O source is retained") + .kind(), + std::io::ErrorKind::NotFound ); } diff --git a/gix-config/tests/config/file/mutable/section.rs b/gix-config/tests/config/file/mutable/section.rs index 9d0b971e4e0..bd7fe8ae673 100644 --- a/gix-config/tests/config/file/mutable/section.rs +++ b/gix-config/tests/config/file/mutable/section.rs @@ -189,25 +189,28 @@ mod set { } mod value_name_validation { - use gix_config::file::section::value; - #[test] fn mutations_validate_names_and_leave_the_section_unchanged_on_error() -> crate::Result { let mut config = gix_config::File::default(); let mut section = config.new_section("core", None)?; - assert!(matches!( - section.push("not.valid", Some("value".into())), - Err(value::Error::ValueName(_)) - )); - assert!(matches!( - section.push_with_comment("1invalid", Some("value".into()), "comment"), - Err(value::Error::ValueName(_)) - )); - assert!(matches!( - section.set("also invalid", "value"), - Err(value::Error::ValueName(_)) - )); + let err: gix_error::ValidationError = section.push("not.valid", Some("value".into())).unwrap_err(); + assert_eq!( + err.to_string(), + "Valid value names consist of alphanumeric characters or dashes, starting with an alphabetic character.: \"not.valid\"" + ); + let err: gix_error::ValidationError = section + .push_with_comment("1invalid", Some("value".into()), "comment") + .unwrap_err(); + assert_eq!( + err.to_string(), + "Valid value names consist of alphanumeric characters or dashes, starting with an alphabetic character.: \"1invalid\"" + ); + let err: gix_error::ValidationError = section.set("also invalid", "value").unwrap_err(); + assert_eq!( + err.to_string(), + "Valid value names consist of alphanumeric characters or dashes, starting with an alphabetic character.: \"also invalid\"" + ); assert_eq!(section.num_values(), 0, "validation happens before mutation"); Ok(()) } diff --git a/gix-config/tests/config/parse/error.rs b/gix-config/tests/config/parse/error.rs index b9ece239e4e..f51fee88eca 100644 --- a/gix-config/tests/config/parse/error.rs +++ b/gix-config/tests/config/parse/error.rs @@ -6,8 +6,21 @@ fn line_no_is_one_indexed() { } #[test] -fn remaining_data_contains_bad_tokens() { - assert_eq!(Events::from_str("[hello").unwrap_err().remaining_data(), b"[hello"); +fn conversion_retains_validation_and_bad_tokens() { + use gix_error::ErrorExt; + + let err = Events::from_str("[hello") + .expect_err("the section header is unterminated") + .raise(); + assert!(err.is_validation(), "malformed configuration is invalid input"); + let err = err.into_error(); + assert!(err.is_validation(), "conversion preserves the classification"); + assert_eq!( + err.downcast_any_ref::() + .expect("retain the original parse error") + .remaining_data(), + b"[hello" + ); } #[test] diff --git a/gix-config/tests/config/parse/section.rs b/gix-config/tests/config/parse/section.rs index f61d71b97f8..844b2ee5ccc 100644 --- a/gix-config/tests/config/parse/section.rs +++ b/gix-config/tests/config/parse/section.rs @@ -38,21 +38,28 @@ mod header { } } mod new { - use gix_config::parse::section; - use crate::parse::section::header::serialized; #[test] fn names_must_be_mostly_ascii() { for name in ["🤗", "x.y", "x y", "x\ny"] { - assert_eq!(serialized(name, None), Err(section::header::Error::InvalidName)); + assert_eq!( + serialized(name, None).expect_err("name must be rejected").message, + "section names can only be ascii, '-'" + ); } } #[test] fn subsections_with_newlines_and_null_bytes_are_rejected() { - assert_eq!(serialized("a", "a\nb"), Err(section::header::Error::InvalidSubSection)); - assert_eq!(serialized("a", "a\0b"), Err(section::header::Error::InvalidSubSection)); + for subsection in ["a\nb", "a\0b"] { + assert_eq!( + serialized("a", subsection) + .expect_err("subsection must be rejected") + .message, + "sub-section names must not contain newlines or null bytes" + ); + } } } } diff --git a/gix/examples/init-repo-and-commit.rs b/gix/examples/init-repo-and-commit.rs index 79129ebbe4f..5e44e6be320 100644 --- a/gix/examples/init-repo-and-commit.rs +++ b/gix/examples/init-repo-and-commit.rs @@ -20,8 +20,12 @@ fn main() -> anyhow::Result<()> { let empty_tree_id = repo.write_object(&tree)?.detach(); let mut config = repo.config_snapshot_mut(); - config.set_raw_value(Author::NAME, "Maria Sanchez")?; - config.set_raw_value(Author::EMAIL, "maria@example.com")?; + config + .set_raw_value(Author::NAME, "Maria Sanchez") + .map_err(gix::Exn::into_error)?; + config + .set_raw_value(Author::EMAIL, "maria@example.com") + .map_err(gix::Exn::into_error)?; { let repo = config.commit_auto_rollback()?; let initial_commit_id = repo.commit("HEAD", "initial commit", empty_tree_id, gix::commit::NO_PARENT_IDS)?; diff --git a/gix/src/clone/fetch/mod.rs b/gix/src/clone/fetch/mod.rs index a989f292cde..6591b0e8e6f 100644 --- a/gix/src/clone/fetch/mod.rs +++ b/gix/src/clone/fetch/mod.rs @@ -29,9 +29,7 @@ pub enum Error { #[error(transparent)] ApplyConfig(#[from] crate::config::Error), #[error(transparent)] - Span(#[from] gix_config::parse::span::Error), - #[error(transparent)] - ConfigValue(#[from] gix_config::file::section::value::Error), + Config(#[from] gix_error::ValidationError), #[error("Failed to load repo-local git configuration before writing")] LoadConfig(#[from] gix_config::file::init::from_paths::Error), #[error("Failed to store configured remote in memory")] diff --git a/gix/src/config/cache/init.rs b/gix/src/config/cache/init.rs index c5788cfc632..056c7f198dc 100644 --- a/gix/src/config/cache/init.rs +++ b/gix/src/config/cache/init.rs @@ -321,10 +321,7 @@ pub(crate) fn load( ..options }, ) - .map_err(|err| match err { - gix_config::file::init::from_paths::Error::Init(err) => Error::from(err), - gix_config::file::init::from_paths::Error::Io { source, path } => Error::Io { source, path }, - })? + .map_err(Error::from)? .unwrap_or_default(); let local_meta = git_dir_config.as_ref().map(gix_config::File::meta_owned); diff --git a/gix/src/config/mod.rs b/gix/src/config/mod.rs index 1e46b67c796..158ae53ead4 100644 --- a/gix/src/config/mod.rs +++ b/gix/src/config/mod.rs @@ -70,7 +70,7 @@ pub mod set_value { #[expect(missing_docs)] pub enum Error { #[error(transparent)] - SetRaw(#[from] gix_config::file::set_raw_value::Error), + SetRaw(gix_error::Error), #[error(transparent)] Validate(#[from] crate::config::tree::key::validate::Error), #[error("The key needs a subsection parameter to be valid.")] @@ -78,6 +78,12 @@ pub mod set_value { #[error("The key must not be used with a subsection")] SubSectionForbidden, } + + impl From for Error { + fn from(err: gix_config::file::set_raw_value::Error) -> Self { + Error::SetRaw(err.into_error()) + } + } } /// The error returned when failing to initialize the repository configuration. @@ -113,15 +119,9 @@ pub enum Error { path: std::path::PathBuf, }, #[error(transparent)] - Init(#[from] gix_config::file::init::Error), - #[error(transparent)] - ResolveIncludes(#[from] gix_config::file::includes::Error), - #[error(transparent)] - Span(#[from] gix_config::parse::span::Error), - #[error(transparent)] - ConfigValue(#[from] gix_config::file::section::value::Error), + Config(gix_error::Error), #[error(transparent)] - FromEnv(#[from] gix_config::file::init::from_env::Error), + ConfigValidation(#[from] gix_error::ValidationError), #[error("The path {path:?} at the 'core.worktree' configuration could not be interpolated")] PathInterpolation { path: BString, source: gix_error::Error }, #[error("{source:?} configuration overrides at open or init time could not be applied.")] @@ -132,6 +132,18 @@ pub enum Error { }, } +impl From for Error { + fn from(err: gix_error::Exn) -> Self { + Error::Config(err.into_error()) + } +} + +impl From> for Error { + fn from(err: gix_error::Exn) -> Self { + Error::Config(err.into_error()) + } +} + /// pub mod merge { /// diff --git a/gix/src/config/overrides.rs b/gix/src/config/overrides.rs index 2249450c8bf..2030156e892 100644 --- a/gix/src/config/overrides.rs +++ b/gix/src/config/overrides.rs @@ -7,11 +7,7 @@ pub enum Error { #[error("{input:?} is not a valid configuration key. Examples are 'core.abbrev' or 'remote.origin.url'")] InvalidKey { input: BString }, #[error(transparent)] - SectionHeader(#[from] gix_config::parse::section::header::Error), - #[error(transparent)] - Span(#[from] gix_config::parse::span::Error), - #[error(transparent)] - ConfigValue(#[from] gix_config::file::section::value::Error), + Config(#[from] gix_error::ValidationError), } pub(crate) fn append( diff --git a/gix/src/create.rs b/gix/src/create.rs index 5eff998329c..b7e9b049f00 100644 --- a/gix/src/create.rs +++ b/gix/src/create.rs @@ -23,9 +23,7 @@ pub enum Error { #[error("Could not create directory at '{}'", .path.display())] CreateDirectory { source: std::io::Error, path: PathBuf }, #[error(transparent)] - Span(#[from] gix_config::parse::span::Error), - #[error(transparent)] - ConfigValue(#[from] gix_config::file::section::value::Error), + Config(#[from] gix_error::ValidationError), } /// The kind of repository to create. diff --git a/gix/src/lib.rs b/gix/src/lib.rs index 4c652620275..e0c759c4869 100644 --- a/gix/src/lib.rs +++ b/gix/src/lib.rs @@ -481,7 +481,7 @@ pub fn config_path( /// ```no_run /// # fn main() -> Result<(), Box> { /// let mut file = gix::config_mut(gix::config::Source::User, &gix::open::Options::default())?; -/// file.set_raw_value("user.name", "Ada Lovelace")?; +/// file.set_raw_value("user.name", "Ada Lovelace").map_err(|err| err.into_error())?; /// file.commit()?; /// # Ok(()) } /// ``` diff --git a/gix/src/remote/save.rs b/gix/src/remote/save.rs index 90a56c35b29..23d28038e61 100644 --- a/gix/src/remote/save.rs +++ b/gix/src/remote/save.rs @@ -8,9 +8,7 @@ pub enum Error { #[error("The remote pointing to {} is anonymous and can't be saved.", url.to_bstring())] NameMissing { url: gix_url::Url }, #[error(transparent)] - Span(#[from] gix_config::parse::span::Error), - #[error(transparent)] - ConfigValue(#[from] gix_config::file::section::value::Error), + Config(#[from] gix_error::ValidationError), } /// The error returned by [`Remote::save_as_to()`]. diff --git a/gix/src/repository/branch.rs b/gix/src/repository/branch.rs index 8f291f9b93d..a60b1cab41d 100644 --- a/gix/src/repository/branch.rs +++ b/gix/src/repository/branch.rs @@ -121,9 +121,10 @@ impl crate::Repository { .map_err(|err| delete::Error::ConfigLock(err.into_error()))?; let mut config = match gix_config::File::from_path_no_includes(config_path.clone(), gix_config::Source::Local) { Ok(config) => Some(config), - // TODO(gix-error): this is what should just be `err.not_found()` in future, anywhere. - Err(gix_config::file::init::from_paths::Error::Io { source, .. }) - if source.kind() == std::io::ErrorKind::NotFound => + Err(err) + if err + .downcast_any_ref::() + .is_some_and(|source| source.kind() == std::io::ErrorKind::NotFound) => { None } diff --git a/tests/tools/src/repository/gix.rs b/tests/tools/src/repository/gix.rs index f0094e1ff1e..ab2f6e8f615 100644 --- a/tests/tools/src/repository/gix.rs +++ b/tests/tools/src/repository/gix.rs @@ -73,10 +73,10 @@ fn worktree_config(git_dir: &Path, config: &gix_config::File) -> Result Date: Thu, 20 Aug 2026 17:15:54 +0200 Subject: [PATCH 19/51] change(gix-diff)!: migrate errors to gix-error rubberstamp --- Cargo.lock | 1 - gitoxide-core/src/hours/core.rs | 10 +- gitoxide-core/src/query/engine/update.rs | 2 +- gix-blame/src/file/function.rs | 6 +- gix-diff/Cargo.toml | 1 - gix-diff/src/blob/pipeline.rs | 235 ++++++++------------ gix-diff/src/blob/platform.rs | 163 ++++++-------- gix-diff/src/index/function.rs | 42 ++-- gix-diff/src/index/mod.rs | 13 +- gix-diff/src/rewrites/tracker.rs | 30 +-- gix-diff/src/tree/function.rs | 4 +- gix-diff/src/tree/mod.rs | 41 +++- gix-diff/src/tree_with_rewrites/function.rs | 119 +++++----- gix-diff/src/tree_with_rewrites/mod.rs | 12 - gix-diff/tests/diff/blob/platform.rs | 16 +- gix-diff/tests/diff/index.rs | 18 +- gix-diff/tests/diff/tree_with_rewrites.rs | 10 +- gix-merge/src/tree/function/change.rs | 4 +- gix-merge/src/tree/mod.rs | 2 +- gix-tix/src/lib.rs | 5 +- gix/src/object/blob.rs | 2 +- gix/src/object/tree/diff/for_each.rs | 35 +-- gix/src/object/tree/diff/mod.rs | 2 +- gix/src/repository/diff.rs | 2 +- gix/src/repository/mod.rs | 2 +- gix/src/status/iter/mod.rs | 4 +- gix/src/status/mod.rs | 4 +- gix/src/status/tree_index.rs | 17 +- gix/tests/gix/object/tree/diff.rs | 6 +- 29 files changed, 354 insertions(+), 454 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ce0e3d4f61a..98439985bcf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1833,7 +1833,6 @@ dependencies = [ "insta", "pretty_assertions", "serde", - "thiserror 2.0.18", ] [[package]] diff --git a/gitoxide-core/src/hours/core.rs b/gitoxide-core/src/hours/core.rs index e4ad7f7c54e..268a90b5260 100644 --- a/gitoxide-core/src/hours/core.rs +++ b/gitoxide-core/src/hours/core.rs @@ -6,7 +6,7 @@ use std::{ }, }; -use gix::bstr::BStr; +use gix::{bstr::BStr, error::ResultExt}; use crate::hours::{ CommitIdx, FileStats, LineStats, WorkByEmail, WorkByPerson, @@ -168,11 +168,9 @@ pub fn spawn_tree_delta_threads<'scope>( (true, true) => { files.modified += 1; if let Some(cache) = cache.as_mut() { - let mut diff = change.diff(cache).map_err(std::io::Error::other)?; + let mut diff = change.diff(cache).or_erased()?; let mut nl = 0; - if let Some(counts) = - diff.line_counts().map_err(std::io::Error::other)? - { + if let Some(counts) = diff.line_counts().or_erased()? { nl += counts.insertions as usize + counts.removals as usize; lines.added += counts.insertions as usize; lines.removed += counts.removals as usize; @@ -182,7 +180,7 @@ pub fn spawn_tree_delta_threads<'scope>( } }, } - Ok::<_, std::io::Error>(std::ops::ControlFlow::Continue(())) + Ok::<_, gix::Exn>(std::ops::ControlFlow::Continue(())) })?; out.push((commit_idx, files, lines)); } diff --git a/gitoxide-core/src/query/engine/update.rs b/gitoxide-core/src/query/engine/update.rs index 8c7beee7dd5..8cdff3906ac 100644 --- a/gitoxide-core/src/query/engine/update.rs +++ b/gitoxide-core/src/query/engine/update.rs @@ -317,7 +317,7 @@ pub fn update( }); } } - Ok::<_, Infallible>(std::ops::ControlFlow::Continue(())) + Ok::<_, gix::Exn>(std::ops::ControlFlow::Continue(())) })?; out_chunk.push(CommitDiffStats { id: commit, diff --git a/gix-blame/src/file/function.rs b/gix-blame/src/file/function.rs index bbadb7c63c6..34cd1436ab1 100644 --- a/gix-blame/src/file/function.rs +++ b/gix-blame/src/file/function.rs @@ -753,7 +753,7 @@ fn tree_diff_with_rewrites_at_file_path( resource_cache, state, &odb, - |change_ref| -> Result<_, std::convert::Infallible> { + |change_ref| -> Result<_, gix_error::Exn> { if change_ref.location() == file_path { change = Some(change_ref.into_owned()); Ok(std::ops::ControlFlow::Break(())) @@ -766,9 +766,7 @@ fn tree_diff_with_rewrites_at_file_path( stats.trees_diffed_with_rewrites += 1; match result { - Ok(_) | Err(gix_diff::tree_with_rewrites::Error::Diff(gix_diff::tree::Error::Cancelled)) => { - Ok(change.map(Into::into)) - } + Ok(_) | Err(gix_diff::tree::Error::Cancelled) => Ok(change.map(Into::into)), Err(error) => Err(error.raise_erased()), } } diff --git a/gix-diff/Cargo.toml b/gix-diff/Cargo.toml index 957a2ebd52b..c8841c953bc 100644 --- a/gix-diff/Cargo.toml +++ b/gix-diff/Cargo.toml @@ -68,7 +68,6 @@ gix-trace = { version = "^0.1.21", path = "../gix-trace", optional = true } gix-traverse = { version = "^0.61.0", path = "../gix-traverse", optional = true } imara-diff = { package = "gix-imara-diff", version = "^0.2.5", optional = true, path = "../gix-imara-diff" } -thiserror = "2.0.18" serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"] } getrandom = { version = "0.4", optional = true, default-features = false, features = ["wasm_js"] } bstr = { version = "1.12.0", default-features = false } diff --git a/gix-diff/src/blob/pipeline.rs b/gix-diff/src/blob/pipeline.rs index 00717d78b21..135d8b9602d 100644 --- a/gix-diff/src/blob/pipeline.rs +++ b/gix-diff/src/blob/pipeline.rs @@ -5,7 +5,7 @@ use std::{ }; use bstr::{BStr, ByteSlice}; -use gix_error::{ErrorExt, NotFoundError}; +use gix_error::{ErrorExt, NotFoundError, ResultExt, message}; use gix_filter::{ driver::apply::{Delay, MaybeDelayed}, pipeline::convert::{ToGitOutcome, ToWorktreeOutcome, to_worktree}, @@ -125,54 +125,8 @@ impl Mode { /// pub mod convert_to_diffable { - use std::collections::TryReserveError; - - use bstr::BString; - use gix_object::tree::EntryKind; - /// The error returned by [Pipeline::convert_to_diffable()](super::Pipeline::convert_to_diffable()). - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error("Entry at '{rela_path}' must be regular file or symlink, but was {actual:?}")] - InvalidEntryKind { rela_path: BString, actual: EntryKind }, - #[error("Entry at '{rela_path}' is declared as symlink but symlinks are disabled via core.symlinks")] - SymlinkDisabled { rela_path: BString }, - #[error("Entry at '{rela_path}' could not be read as symbolic link")] - ReadLink { rela_path: BString, source: std::io::Error }, - #[error("Entry at '{rela_path}' could not be opened for reading or read from")] - OpenOrRead { rela_path: BString, source: std::io::Error }, - #[error("Entry at '{rela_path}' could not be copied from a filter process to a memory buffer")] - StreamCopy { rela_path: BString, source: std::io::Error }, - #[error("Failed to run '{cmd}' for binary-to-text conversion of entry at {rela_path}")] - RunTextConvFilter { - rela_path: BString, - cmd: String, - source: std::io::Error, - }, - #[error("Tempfile for binary-to-text conversion for entry at {rela_path} could not be created")] - CreateTempfile { rela_path: BString, source: std::io::Error }, - #[error("Binary-to-text conversion '{cmd}' for entry at {rela_path} failed with: {stderr}")] - TextConvFilterFailed { - rela_path: BString, - cmd: String, - stderr: BString, - }, - #[error(transparent)] - FindObject(gix_error::Error), - #[error(transparent)] - ConvertToWorktree(#[from] gix_filter::pipeline::convert::to_worktree::Error), - #[error(transparent)] - ConvertToGit(#[from] gix_filter::pipeline::convert::to_git::Error), - #[error("Memory allocation failed")] - OutOfMemory(#[from] TryReserveError), - } - - impl From for Error { - fn from(err: gix_object::find::existing_object::Error) -> Self { - Error::FindObject(err.into_error()) - } - } + pub type Error = gix_error::Exn; } /// Lifecycle @@ -254,10 +208,9 @@ impl Pipeline { EntryKind::Link => true, EntryKind::Blob | EntryKind::BlobExecutable => false, _ => { - return Err(convert_to_diffable::Error::InvalidEntryKind { - rela_path: rela_path.to_owned(), - actual: mode, - }); + return Err( + message!("Entry at '{rela_path}' must be regular file or symlink, but was {mode:?}").raise(), + ); } }; @@ -284,16 +237,13 @@ impl Pipeline { self.path.push(gix_path::from_bstr(rela_path)); let data = if is_symlink { if !self.options.fs.symlink { - return Err(convert_to_diffable::Error::SymlinkDisabled { - rela_path: rela_path.to_owned(), - }); + return Err(message!( + "Entry at '{rela_path}' is declared as symlink but symlinks are disabled via core.symlinks" + ) + .raise()); } - let target = none_if_missing(std::fs::read_link(&self.path)).map_err(|err| { - convert_to_diffable::Error::ReadLink { - rela_path: rela_path.to_owned(), - source: err, - } - })?; + let target = none_if_missing(std::fs::read_link(&self.path)) + .or_raise(|| message!("Entry at '{rela_path}' could not be read as symbolic link"))?; target.map(|target| { out.extend_from_slice(gix_path::into_bstr(target).as_ref()); Data::Buffer { is_derived: false } @@ -303,11 +253,8 @@ impl Pipeline { let size_in_bytes = (need_size_only || (is_binary != Some(false) && self.options.large_file_threshold_bytes > 0)) .then(|| { - none_if_missing(self.path.metadata().map(|md| md.len())).map_err(|err| { - convert_to_diffable::Error::OpenOrRead { - rela_path: rela_path.to_owned(), - source: err, - } + none_if_missing(self.path.metadata().map(|md| md.len())).or_raise(|| { + message!("Entry at '{rela_path}' could not be opened for reading or read from") }) }) .transpose()?; @@ -325,9 +272,10 @@ impl Pipeline { // Avoid letting the driver program fail if it doesn't exist. if self.options.large_file_threshold_bytes == 0 && none_if_missing(std::fs::symlink_metadata(&self.path)) - .map_err(|err| convert_to_diffable::Error::OpenOrRead { - rela_path: rela_path.to_owned(), - source: err, + .or_raise(|| { + message!( + "Entry at '{rela_path}' could not be opened for reading or read from" + ) })? .is_none() { @@ -338,57 +286,59 @@ impl Pipeline { } } None => { - let file = none_if_missing(std::fs::File::open(&self.path)).map_err(|err| { - convert_to_diffable::Error::OpenOrRead { - rela_path: rela_path.to_owned(), - source: err, - } + let file = none_if_missing(std::fs::File::open(&self.path)).or_raise(|| { + message!("Entry at '{rela_path}' could not be opened for reading or read from") })?; match file { Some(mut file) => { if convert.to_git() { - let res = self.worktree_filter.convert_to_git( - file, - gix_path::from_bstr(rela_path).as_ref(), - attributes, - &mut |buf| { - if id.is_null() { - return Ok(None); - } - objects.try_find(id, buf).map(|obj| obj.map(|_| ())) - }, - )?; + let res = self + .worktree_filter + .convert_to_git( + file, + gix_path::from_bstr(rela_path).as_ref(), + attributes, + &mut |buf| { + if id.is_null() { + return Ok(None); + } + objects.try_find(id, buf).map(|obj| obj.map(|_| ())) + }, + ) + .or_raise(|| { + message!( + "Entry at '{rela_path}' could not be converted to Git form" + ) + })?; match res { ToGitOutcome::Unchanged(mut file) => { - file.read_to_end(out).map_err(|err| { - convert_to_diffable::Error::OpenOrRead { - rela_path: rela_path.to_owned(), - source: err, - } + file.read_to_end(out).or_raise(|| { + message!( + "Entry at '{rela_path}' could not be opened for reading or read from" + ) })?; } ToGitOutcome::Process(mut stream) => { - stream.read_to_end(out).map_err(|err| { - convert_to_diffable::Error::OpenOrRead { - rela_path: rela_path.to_owned(), - source: err, - } + stream.read_to_end(out).or_raise(|| { + message!( + "Entry at '{rela_path}' could not be opened for reading or read from" + ) })?; } ToGitOutcome::Buffer(buf) => { out.clear(); - out.try_reserve(buf.len())?; + out.try_reserve(buf.len()) + .or_raise(|| message("Memory allocation failed"))?; out.extend_from_slice(buf); } } } else { - file.read_to_end(out).map_err(|err| { - convert_to_diffable::Error::OpenOrRead { - rela_path: rela_path.to_owned(), - source: err, - } + file.read_to_end(out).or_raise(|| { + message!( + "Entry at '{rela_path}' could not be opened for reading or read from" + ) })?; } @@ -413,9 +363,11 @@ impl Pipeline { let data = if id.is_null() { None } else { - let header = objects.try_header(id)?.ok_or_else(|| { - NotFoundError::new(format!("An object with id {id} could not be found")).raise_erased() - })?; + let header = objects + .try_header(id) + .or_raise(|| message!("Could not find object {id}"))? + .ok_or_else(|| NotFoundError::new(format!("An object with id {id} could not be found"))) + .or_raise(|| message!("Could not find object {id}"))?; if is_binary.is_none() && self.options.large_file_threshold_bytes > 0 && header.size > self.options.large_file_threshold_bytes @@ -425,24 +377,31 @@ impl Pipeline { let data = if is_binary == Some(true) { Data::Binary { size: header.size } } else { - objects.try_find(id, out)?.ok_or_else(|| { - NotFoundError::new(format!("An object with id {id} could not be found")).raise_erased() - })?; + objects + .try_find(id, out) + .or_raise(|| message!("Could not find object {id}"))? + .ok_or_else(|| NotFoundError::new(format!("An object with id {id} could not be found"))) + .or_raise(|| message!("Could not find object {id}"))?; let mut is_derived = false; if matches!(mode, EntryKind::Blob | EntryKind::BlobExecutable) && convert == Mode::ToWorktreeAndBinaryToText || (convert == Mode::ToGitUnlessBinaryToTextIsPresent && driver.is_some_and(|d| d.binary_to_text_command.is_some())) { - let res = self.worktree_filter.convert_to_worktree( - out, - rela_path, - attributes, - to_worktree::Options { - can_delay: Delay::Forbid, - unknown_encoding: to_worktree::UnknownEncoding::Fail, - }, - )?; + let res = self + .worktree_filter + .convert_to_worktree( + out, + rela_path, + attributes, + to_worktree::Options { + can_delay: Delay::Forbid, + unknown_encoding: to_worktree::UnknownEncoding::Fail, + }, + ) + .or_raise(|| { + message!("Entry at '{rela_path}' could not be converted to worktree form") + })?; let cmd_and_file = driver .and_then(|d| { @@ -467,9 +426,10 @@ impl Pipeline { }) }) .transpose() - .map_err(|err| convert_to_diffable::Error::CreateTempfile { - source: err, - rela_path: rela_path.to_owned(), + .or_raise(|| { + message!( + "Tempfile for binary-to-text conversion for entry at {rela_path} could not be created" + ) })?; match cmd_and_file { Some((cmd, mut tmp_file)) => { @@ -484,11 +444,10 @@ impl Pipeline { unreachable!("we prohibit this") } } - .map_err(|err| { - convert_to_diffable::Error::StreamCopy { - source: err, - rela_path: rela_path.to_owned(), - } + .or_raise(|| { + message!( + "Entry at '{rela_path}' could not be copied from a filter process to a memory buffer" + ) })?; out.clear(); run_cmd(rela_path, cmd, out)?; @@ -498,15 +457,15 @@ impl Pipeline { ToWorktreeOutcome::Unchanged(_) => {} ToWorktreeOutcome::Buffer(src) => { out.clear(); - out.try_reserve(src.len())?; + out.try_reserve(src.len()) + .or_raise(|| message("Memory allocation failed"))?; out.extend_from_slice(src); } ToWorktreeOutcome::Process(MaybeDelayed::Immediate(mut stream)) => { - std::io::copy(&mut stream, out).map_err(|err| { - convert_to_diffable::Error::StreamCopy { - rela_path: rela_path.to_owned(), - source: err, - } + std::io::copy(&mut stream, out).or_raise(|| { + message!( + "Entry at '{rela_path}' could not be copied from a filter process to a memory buffer" + ) })?; } ToWorktreeOutcome::Process(MaybeDelayed::Delayed(_)) => { @@ -551,17 +510,13 @@ fn run_cmd(rela_path: &BStr, mut cmd: Command, out: &mut Vec) -> Result<(), gix_trace::debug!(cmd = ?cmd, "Running binary-to-text command"); let mut res = cmd .output() - .map_err(|err| convert_to_diffable::Error::RunTextConvFilter { - rela_path: rela_path.to_owned(), - cmd: format!("{cmd:?}"), - source: err, - })?; + .or_raise(|| message!("Failed to run '{cmd:?}' for binary-to-text conversion of entry at {rela_path}"))?; if !res.status.success() { - return Err(convert_to_diffable::Error::TextConvFilterFailed { - rela_path: rela_path.to_owned(), - cmd: format!("{cmd:?}"), - stderr: res.stderr.into(), - }); + return Err(message!( + "Binary-to-text conversion '{cmd:?}' for entry at {rela_path} failed with: {}", + BStr::new(&res.stderr) + ) + .raise()); } out.append(&mut res.stdout); Ok(()) diff --git a/gix-diff/src/blob/platform.rs b/gix-diff/src/blob/platform.rs index 3f26d212560..4fb67f87305 100644 --- a/gix-diff/src/blob/platform.rs +++ b/gix-diff/src/blob/platform.rs @@ -1,6 +1,7 @@ use std::{cmp::Ordering, io::Write, process::Stdio}; use bstr::{BStr, BString, ByteSlice}; +use gix_error::{ErrorExt, OptionExt, ResultExt, ValidationError, message}; use super::Algorithm; use crate::blob::{Pipeline, Platform, ResourceKind, pipeline}; @@ -240,31 +241,8 @@ pub mod resource { /// pub mod set_resource { - use bstr::BString; - - use crate::blob::{ResourceKind, pipeline}; - /// The error returned by [Platform::set_resource](super::Platform::set_resource). - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error("Can only diff blobs and links, not {mode:?}")] - InvalidMode { mode: gix_object::tree::EntryKind }, - #[error("Failed to read {kind} worktree data from '{rela_path}'")] - Io { - rela_path: BString, - kind: ResourceKind, - source: std::io::Error, - }, - #[error("Failed to obtain attributes for {kind} resource at '{rela_path}'")] - Attributes { - rela_path: BString, - kind: ResourceKind, - source: std::io::Error, - }, - #[error(transparent)] - ConvertToDiffable(#[from] pipeline::convert_to_diffable::Error), - } + pub type Error = gix_error::Exn; } /// @@ -335,35 +313,15 @@ pub mod prepare_diff { } /// The error returned by [Platform::prepare_diff()](super::Platform::prepare_diff()). - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error("Either the source or the destination of the diff operation were not set")] - SourceOrDestinationUnset, - #[error("Tried to diff resources that are both considered removed")] - SourceAndDestinationRemoved, - } + pub type Error = gix_error::ValidationError; } /// pub mod prepare_diff_command { use std::ops::{Deref, DerefMut}; - use bstr::BString; - /// The error returned by [Platform::prepare_diff_command()](super::Platform::prepare_diff_command()). - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error("Either the source or the destination of the diff operation were not set")] - SourceOrDestinationUnset, - #[error("Binary resources can't be diffed with an external command (as we don't have the data anymore)")] - SourceOrDestinationBinary, - #[error("Tempfile to store content of '{rela_path}' for passing to external diff command could not be created")] - CreateTempfile { rela_path: BString, source: std::io::Error }, - #[error("Could not write content of '{rela_path}' to tempfile for passing to external diff command")] - WriteTempfile { rela_path: BString, source: std::io::Error }, - } + pub type Error = gix_error::Exn; /// The outcome of a [`prepare_diff_command`](super::Platform::prepare_diff_command()) operation. /// @@ -505,37 +463,49 @@ impl Platform { gix_tempfile::ContainingDirectory::Exists, gix_tempfile::AutoRemove::Tempfile, ) - .map_err(|err| prepare_diff_command::Error::CreateTempfile { - rela_path: res.rela_path.to_owned(), - source: err, + .or_raise(|| { + message!( + "Tempfile to store content of '{}' for passing to external diff command could not be created", + res.rela_path + ) + })?; + tmp.write_all(buf).or_raise(|| { + message!( + "Could not write content of '{}' to tempfile for passing to external diff command", + res.rela_path + ) })?; - tmp.write_all(buf) - .map_err(|err| prepare_diff_command::Error::WriteTempfile { - rela_path: res.rela_path.to_owned(), - source: err, - })?; tmp.with_mut(|f| { cmd.arg(f.path()); }) - .map_err(|err| prepare_diff_command::Error::WriteTempfile { - rela_path: res.rela_path.to_owned(), - source: err, + .or_raise(|| { + message!( + "Could not access tempfile for '{}' while preparing external diff command", + res.rela_path + ) })?; cmd.arg(res.id.to_string()).arg(res.mode.as_octal_str().to_string()); - let tmp = tmp.close().map_err(|err| prepare_diff_command::Error::WriteTempfile { - rela_path: res.rela_path.to_owned(), - source: err, + let tmp = tmp.close().or_raise(|| { + message!( + "Could not close tempfile for '{}' while preparing external diff command", + res.rela_path + ) })?; Some(tmp) } - resource::Data::Binary { .. } => return Err(prepare_diff_command::Error::SourceOrDestinationBinary), + resource::Data::Binary { .. } => { + return Err(message( + "Binary resources can't be diffed with an external command (as we don't have the data anymore)", + ) + .raise()); + } }; Ok(tmpfile) } let (old, new) = self .resources() - .ok_or(prepare_diff_command::Error::SourceOrDestinationUnset)?; + .ok_or_raise(|| message("Either the source or the destination of the diff operation were not set"))?; let mut cmd: std::process::Command = gix_command::prepare(gix_path::from_bstring(diff_command)) .command_may_be_shell_script_disallow_manual_argument_splitting() .with_context(context) @@ -593,16 +563,18 @@ impl Platform { /// The returned outcome allows to easily perform diff operations, based on the [`prepare_diff::Outcome::operation`] field, /// which hints at what should be done. pub fn prepare_diff(&mut self) -> Result, prepare_diff::Error> { - let old_key = &self.old.as_ref().ok_or(prepare_diff::Error::SourceOrDestinationUnset)?; - let old = self - .diff_cache - .get(old_key) - .ok_or(prepare_diff::Error::SourceOrDestinationUnset)?; - let new_key = &self.new.as_ref().ok_or(prepare_diff::Error::SourceOrDestinationUnset)?; - let new = self - .diff_cache - .get(new_key) - .ok_or(prepare_diff::Error::SourceOrDestinationUnset)?; + let old_key = &self.old.as_ref().ok_or_else(|| { + ValidationError::new("Either the source or the destination of the diff operation were not set") + })?; + let old = self.diff_cache.get(old_key).ok_or_else(|| { + ValidationError::new("Either the source or the destination of the diff operation were not set") + })?; + let new_key = &self.new.as_ref().ok_or_else(|| { + ValidationError::new("Either the source or the destination of the diff operation were not set") + })?; + let new = self.diff_cache.get(new_key).ok_or_else(|| { + ValidationError::new("Either the source or the destination of the diff operation were not set") + })?; let mut out = { let old = Resource::new(old_key, old); let new = Resource::new(new_key, new); @@ -615,7 +587,11 @@ impl Platform { }; match (old.conversion.data, new.conversion.data) { - (None, None) => return Err(prepare_diff::Error::SourceAndDestinationRemoved), + (None, None) => { + return Err(ValidationError::new( + "Tried to diff resources that are both considered removed", + )); + } (Some(pipeline::Data::Binary { .. }), _) | (_, Some(pipeline::Data::Binary { .. })) => return Ok(out), _either_missing_or_non_binary => { if let Some(command) = old @@ -701,7 +677,7 @@ impl Platform { mode, gix_object::tree::EntryKind::Commit | gix_object::tree::EntryKind::Tree ) { - return Err(set_resource::Error::InvalidMode { mode }); + return Err(message!("Can only diff blobs and links, not {mode:?}").raise()); } let storage = match kind { ResourceKind::OldOrSource => &mut self.old, @@ -717,27 +693,26 @@ impl Platform { if self.diff_cache.contains_key(storage) { return Ok(()); } - let entry = - self.attr_stack - .at_entry(rela_path, None, objects) - .map_err(|err| set_resource::Error::Attributes { - source: err, - kind, - rela_path: rela_path.to_owned(), - })?; + let entry = self + .attr_stack + .at_entry(rela_path, None, objects) + .or_raise(|| message!("Failed to obtain attributes for {kind} resource at '{rela_path}'"))?; let mut buf = self.free_list.pop().unwrap_or_default(); - let out = self.filter.convert_to_diffable( - &id, - mode, - rela_path, - kind, - &mut |_, out| { - let _ = entry.matching_attributes(out); - }, - objects, - self.filter_mode, - &mut buf, - )?; + let out = self + .filter + .convert_to_diffable( + &id, + mode, + rela_path, + kind, + &mut |_, out| { + let _ = entry.matching_attributes(out); + }, + objects, + self.filter_mode, + &mut buf, + ) + .or_raise(|| message!("Failed to convert {kind} resource at '{rela_path}' to diffable data"))?; let key = storage.clone(); assert!( self.diff_cache diff --git a/gix-diff/src/index/function.rs b/gix-diff/src/index/function.rs index baccdcb6e50..df364eb3225 100644 --- a/gix-diff/src/index/function.rs +++ b/gix-diff/src/index/function.rs @@ -1,6 +1,7 @@ use std::{borrow::Cow, cell::RefCell, cmp::Ordering}; use bstr::BStr; +use gix_error::{ErrorExt, message}; use gix_filter::attributes::glob::pattern::Case; use super::{Action, ChangeRef, Error, RewriteOptions}; @@ -26,27 +27,28 @@ use crate::rewrites; /// The entries in `lhs` and `rhs` are both expected to be sorted like index entries are typically sorted. /// /// Note that sparse indices aren't supported, they must be "unsparsed" before. -pub fn diff<'rhs, 'lhs: 'rhs, E, Find>( +pub fn diff<'rhs, 'lhs: 'rhs, Find>( lhs: &'lhs gix_index::State, rhs: &'rhs gix_index::State, - mut cb: impl FnMut(ChangeRef<'lhs, 'rhs>) -> Result, + mut cb: impl FnMut(ChangeRef<'lhs, 'rhs>) -> Result, rewrite_options: Option>, pathspec: &mut gix_pathspec::Search, pathspec_attributes: &mut dyn FnMut(&BStr, Case, bool, &mut gix_attributes::search::Outcome) -> bool, ) -> Result, Error> where - E: Into>, Find: gix_object::FindObjectOrHeader, { if lhs.is_sparse() || rhs.is_sparse() { - return Err(Error::IsSparse); + return Err(message("Cannot diff indices that contain sparse entries").raise()); } if lhs .entries() .iter() .any(|e| e.stage() != gix_index::entry::Stage::Unconflicted) { - return Err(Error::LhsHasUnmerged); + return Err( + message("Unmerged entries aren't allowed in the left-hand index, only in the right-hand index").raise(), + ); } let lhs_range = lhs @@ -122,7 +124,7 @@ where Some(tracker) => tracker.try_push_change(change, rhs_path), }; if let Some(change) = change { - match cb(change).map_err(|err| Error::Callback(err.into()))? { + match cb(change).map_err(callback_error)? { std::ops::ControlFlow::Continue(()) => {} std::ops::ControlFlow::Break(()) => return Ok(None), } @@ -183,7 +185,7 @@ where Ok(std::ops::ControlFlow::Continue(())) => std::ops::ControlFlow::Continue(()), Ok(std::ops::ControlFlow::Break(())) => std::ops::ControlFlow::Break(()), Err(err) => { - cb_err = Some(Error::Callback(err.into())); + cb_err = Some(callback_error(err)); std::ops::ControlFlow::Break(()) } } @@ -220,14 +222,11 @@ where } } -fn emit_deletion<'rhs, 'lhs: 'rhs, E>( +fn emit_deletion<'rhs, 'lhs: 'rhs>( (idx, path, entry): (usize, &'lhs BStr, &'lhs gix_index::Entry), - mut cb: impl FnMut(ChangeRef<'lhs, 'rhs>) -> Result, + mut cb: impl FnMut(ChangeRef<'lhs, 'rhs>) -> Result, tracker: Option<&mut rewrites::Tracker>>, -) -> Result -where - E: Into>, -{ +) -> Result { let change = ChangeRef::Deletion { location: Cow::Borrowed(path), index: idx, @@ -243,17 +242,14 @@ where }, }; - cb(change).map_err(|err| Error::Callback(err.into())) + cb(change).map_err(callback_error) } -fn emit_addition<'rhs, 'lhs: 'rhs, E>( +fn emit_addition<'rhs, 'lhs: 'rhs>( (idx, path, entry): (usize, &'rhs BStr, &'rhs gix_index::Entry), - mut cb: impl FnMut(ChangeRef<'lhs, 'rhs>) -> Result, + mut cb: impl FnMut(ChangeRef<'lhs, 'rhs>) -> Result, tracker: Option<&mut rewrites::Tracker>>, -) -> Result -where - E: Into>, -{ +) -> Result { if ignore_unmerged_and_intent_to_add((idx, path, entry)) { return Ok(std::ops::ControlFlow::Continue(())); } @@ -273,7 +269,11 @@ where }, }; - cb(change).map_err(|err| Error::Callback(err.into())) + cb(change).map_err(callback_error) +} + +fn callback_error(err: gix_error::Exn) -> Error { + err.raise(message("The callback indicated failure")) } fn ignore_unmerged_and_intent_to_add<'rhs, 'lhs: 'rhs>( diff --git a/gix-diff/src/index/mod.rs b/gix-diff/src/index/mod.rs index 1a380ac8dee..2028175787b 100644 --- a/gix-diff/src/index/mod.rs +++ b/gix-diff/src/index/mod.rs @@ -3,18 +3,7 @@ use std::borrow::Cow; use bstr::BStr; /// The error returned by [`index()`](crate::index()). -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] -pub enum Error { - #[error("Cannot diff indices that contain sparse entries")] - IsSparse, - #[error("Unmerged entries aren't allowed in the left-hand index, only in the right-hand index")] - LhsHasUnmerged, - #[error("The callback indicated failure")] - Callback(#[source] Box), - #[error("Failure during rename tracking")] - RenameTracking(#[from] crate::rewrites::tracker::emit::Error), -} +pub type Error = gix_error::Exn; /// What to do after a [ChangeRef] was passed ot the callback of [`index()`](crate::index()). /// diff --git a/gix-diff/src/rewrites/tracker.rs b/gix-diff/src/rewrites/tracker.rs index 72849f8d36c..e7091bd36e5 100644 --- a/gix-diff/src/rewrites/tracker.rs +++ b/gix-diff/src/rewrites/tracker.rs @@ -11,6 +11,7 @@ use std::ops::Range; use bstr::{BStr, ByteSlice}; +use gix_error::ResultExt; use gix_object::tree::{EntryKind, EntryMode}; use crate::{ @@ -136,24 +137,7 @@ pub mod visit { /// pub mod emit { /// The error returned by [Tracker::emit()](super::Tracker::emit()). - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error("Could not find blob for similarity checking")] - FindExistingBlob(#[source] gix_error::Error), - #[error("Could not obtain exhaustive item set to use as possible sources for copy detection")] - GetItemsForExhaustiveCopyDetection(#[source] Box), - #[error(transparent)] - SetResource(#[from] crate::blob::platform::set_resource::Error), - #[error(transparent)] - PrepareDiff(#[from] crate::blob::platform::prepare_diff::Error), - } - - impl From for Error { - fn from(err: gix_object::find::existing_object::Error) -> Self { - Error::FindExistingBlob(err.into_error()) - } - } + pub type Error = gix_error::Exn; } /// Lifecycle @@ -313,7 +297,11 @@ impl Tracker { self.items.last_mut().expect("just pushed").emitted = true; } }) - .map_err(|err| emit::Error::GetItemsForExhaustiveCopyDetection(Box::new(err)))?; + .or_raise(|| { + gix_error::message( + "Could not obtain exhaustive item set to use as possible sources for copy detection", + ) + })?; self.sort_items_by_id_and_location(); self.match_pairs_of_kind( @@ -778,7 +766,9 @@ fn find_match<'a, T: Change>( ResourceKind::OldOrSource, objects, )?; - let prep = diff_cache.prepare_diff()?; + let prep = diff_cache + .prepare_diff() + .or_raise(|| gix_error::message("Could not prepare resources for similarity checking"))?; stats.num_similarity_checks += 1; *num_checks += 1; match prep.operation { diff --git a/gix-diff/src/tree/function.rs b/gix-diff/src/tree/function.rs index f35f42e25d2..f268091e770 100644 --- a/gix-diff/src/tree/function.rs +++ b/gix-diff/src/tree/function.rs @@ -229,7 +229,7 @@ fn catchup_rhs_with_lhs( let Err(err) = rhs_entries.next().expect("the peeked item to be present") else { unreachable!("peeked error changed before it was consumed") }; - return Err(Error::EntriesDecode(err)); + return Err(err.into()); } None => { delegate.pop_path_component(); @@ -283,7 +283,7 @@ fn catchup_lhs_with_rhs( let Err(err) = lhs_entries.next().expect("the peeked item to be present") else { unreachable!("peeked error changed before it was consumed") }; - return Err(Error::EntriesDecode(err)); + return Err(err.into()); } None => { delegate.pop_path_component(); diff --git a/gix-diff/src/tree/mod.rs b/gix-diff/src/tree/mod.rs index ccc7d3c3ed8..865849c801d 100644 --- a/gix-diff/src/tree/mod.rs +++ b/gix-diff/src/tree/mod.rs @@ -7,20 +7,43 @@ use gix_object::bstr::BString; use crate::tree::visit::Relation; /// The error returned by [`tree()`](super::tree()). -#[derive(Debug, thiserror::Error)] +#[derive(Debug)] #[expect(missing_docs)] pub enum Error { - #[error(transparent)] - Find(gix_error::Error), - #[error("The delegate cancelled the operation")] Cancelled, - #[error(transparent)] - EntriesDecode(#[from] gix_object::decode::Error), + Failure(gix_error::Error), } -impl From for Error { - fn from(err: gix_object::find::existing_iter::Error) -> Self { - Error::Find(err.into_error()) +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Cancelled => f.write_str("The delegate cancelled the operation"), + Error::Failure(err) => std::fmt::Display::fmt(err, f), + } + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::Cancelled => None, + Error::Failure(err) => Some(err), + } + } +} + +impl From> for Error +where + E: std::error::Error + Send + Sync + 'static, +{ + fn from(err: gix_error::Exn) -> Self { + Error::Failure(err.into_error()) + } +} + +impl From for Error { + fn from(err: gix_object::decode::Error) -> Self { + Error::Failure(gix_error::Error::from_error(err)) } } diff --git a/gix-diff/src/tree_with_rewrites/function.rs b/gix-diff/src/tree_with_rewrites/function.rs index fceca4e9517..0533edf1c5e 100644 --- a/gix-diff/src/tree_with_rewrites/function.rs +++ b/gix-diff/src/tree_with_rewrites/function.rs @@ -1,8 +1,9 @@ use bstr::BStr; +use gix_error::ResultExt; use gix_object::TreeRefIter; -use super::{Action, ChangeRef, Error, Options}; -use crate::{rewrites, rewrites::tracker}; +use super::{Action, ChangeRef, Options}; +use crate::{rewrites, rewrites::tracker, tree::Error}; /// Call `for_each` repeatedly with all changes that are needed to convert `lhs` to `rhs`. /// Provide a `resource_cache` to speed up obtaining blobs for similarity checks. @@ -21,18 +22,20 @@ use crate::{rewrites, rewrites::tracker}; /// `rhs` or `lhs` can be empty to indicate deletion or addition of an entire tree. /// /// Note that the rewrite outcome is only available if [rewrite-tracking was enabled](Options::rewrites). -pub fn diff( +pub fn diff( lhs: TreeRefIter<'_>, rhs: TreeRefIter<'_>, resource_cache: &mut crate::blob::Platform, tree_diff_state: &mut crate::tree::State, objects: &impl gix_object::FindObjectOrHeader, - for_each: impl FnMut(ChangeRef<'_>) -> Result, + for_each: impl FnMut(ChangeRef<'_>) -> Result, options: Options, -) -> Result, Error> -where - E: Into>, -{ +) -> Result, Error> { + fn callback_error(err: gix_error::Exn) -> Error { + err.raise(gix_error::message("The user-provided callback failed")) + .into() + } + let mut delegate = Delegate { src_tree: lhs, recorder: crate::tree::Recorder::default().track_location(options.location), @@ -46,41 +49,38 @@ where Ok(()) => { let outcome = delegate.process_tracked_changes(resource_cache)?; match delegate.err { - Some(err) => Err(Error::ForEach(err.into())), + Some(err) => Err(callback_error(err)), None => Ok(outcome), } } Err(crate::tree::Error::Cancelled) => delegate .err - .map_or(Err(Error::Diff(crate::tree::Error::Cancelled)), |err| { - Err(Error::ForEach(err.into())) - }), - Err(err) => Err(err.into()), + .map_or(Err(Error::Cancelled), |err| Err(callback_error(err))), + Err(err) => Err(err), } } -struct Delegate<'a, 'old, VisitFn, E, Objects> { +struct Delegate<'a, 'old, VisitFn, Objects> { src_tree: TreeRefIter<'old>, recorder: crate::tree::Recorder, objects: &'a Objects, visit: VisitFn, tracked: Option>, location: Option, - err: Option, + err: Option, } -impl Delegate<'_, '_, VisitFn, E, Objects> +impl Delegate<'_, '_, VisitFn, Objects> where Objects: gix_object::FindObjectOrHeader, - VisitFn: FnMut(ChangeRef<'_>) -> Result, - E: Into>, + VisitFn: FnMut(ChangeRef<'_>) -> Result, { /// Call `visit` on an attached version of `change`. fn emit_change( change: crate::tree::visit::Change, location: &BStr, visit: &mut VisitFn, - stored_err: &mut Option, + stored_err: &mut Option, ) -> crate::tree::visit::Action { use crate::tree::visit::Change::*; let change = match change { @@ -137,54 +137,55 @@ where None => return Ok(None), }; - let outcome = tracked.emit( - |dest, source| match source { - Some(source) => { - let (oid, mode) = dest.change.oid_and_entry_mode(); - let change = ChangeRef::Rewrite { - source_location: source.location, - source_entry_mode: source.entry_mode, - source_id: source.id, - source_relation: source.change.relation(), - entry_mode: mode, - id: oid.to_owned(), - relation: dest.change.relation(), - diff: source.diff, - location: dest.location, - copy: match source.kind { - tracker::visit::SourceKind::Rename => false, - tracker::visit::SourceKind::Copy => true, - }, - }; - match (self.visit)(change) { - Ok(std::ops::ControlFlow::Break(())) => std::ops::ControlFlow::Break(()), - Ok(std::ops::ControlFlow::Continue(())) => std::ops::ControlFlow::Continue(()), - Err(err) => { - self.err = Some(err); - std::ops::ControlFlow::Break(()) + let outcome = tracked + .emit( + |dest, source| match source { + Some(source) => { + let (oid, mode) = dest.change.oid_and_entry_mode(); + let change = ChangeRef::Rewrite { + source_location: source.location, + source_entry_mode: source.entry_mode, + source_id: source.id, + source_relation: source.change.relation(), + entry_mode: mode, + id: oid.to_owned(), + relation: dest.change.relation(), + diff: source.diff, + location: dest.location, + copy: match source.kind { + tracker::visit::SourceKind::Rename => false, + tracker::visit::SourceKind::Copy => true, + }, + }; + match (self.visit)(change) { + Ok(std::ops::ControlFlow::Break(())) => std::ops::ControlFlow::Break(()), + Ok(std::ops::ControlFlow::Continue(())) => std::ops::ControlFlow::Continue(()), + Err(err) => { + self.err = Some(err); + std::ops::ControlFlow::Break(()) + } } } - } - None => Self::emit_change(dest.change, dest.location, &mut self.visit, &mut self.err), - }, - diff_cache, - self.objects, - |push| { - let mut delegate = tree_to_changes::Delegate::new(push, self.location); - let state = gix_traverse::tree::breadthfirst::State::default(); - gix_traverse::tree::breadthfirst(self.src_tree, state, self.objects, &mut delegate) - .map_err(gix_error::Exn::into_error) - }, - )?; + None => Self::emit_change(dest.change, dest.location, &mut self.visit, &mut self.err), + }, + diff_cache, + self.objects, + |push| { + let mut delegate = tree_to_changes::Delegate::new(push, self.location); + let state = gix_traverse::tree::breadthfirst::State::default(); + gix_traverse::tree::breadthfirst(self.src_tree, state, self.objects, &mut delegate) + .map_err(gix_error::Exn::into_error) + }, + ) + .or_raise(|| gix_error::message("Failure during rename tracking"))?; Ok(Some(outcome)) } } -impl crate::tree::Visit for Delegate<'_, '_, VisitFn, E, Objects> +impl crate::tree::Visit for Delegate<'_, '_, VisitFn, Objects> where Objects: gix_object::FindObjectOrHeader, - VisitFn: FnMut(ChangeRef<'_>) -> Result, - E: Into>, + VisitFn: FnMut(ChangeRef<'_>) -> Result, { fn pop_front_tracked_path_and_set_current(&mut self) { self.recorder.pop_front_tracked_path_and_set_current(); diff --git a/gix-diff/src/tree_with_rewrites/mod.rs b/gix-diff/src/tree_with_rewrites/mod.rs index 28d608f5017..0c7362bfb90 100644 --- a/gix-diff/src/tree_with_rewrites/mod.rs +++ b/gix-diff/src/tree_with_rewrites/mod.rs @@ -3,18 +3,6 @@ use crate::{Rewrites, tree::recorder::Location}; mod change; pub use change::{Change, ChangeRef}; -/// The error returned by [`tree_with_rewrites()`](super::tree_with_rewrites()). -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] -pub enum Error { - #[error(transparent)] - Diff(#[from] crate::tree::Error), - #[error("The user-provided callback failed")] - ForEach(#[source] Box), - #[error("Failure during rename tracking")] - RenameTracking(#[from] crate::rewrites::tracker::emit::Error), -} - /// Returned by the [`tree_with_rewrites()`](super::tree_with_rewrites()) function to control flow. /// /// Use [`std::ops::ControlFlow::Continue`] to continue the traversal of changes. diff --git a/gix-diff/tests/diff/blob/platform.rs b/gix-diff/tests/diff/blob/platform.rs index d388a15f901..0d7775bcb84 100644 --- a/gix-diff/tests/diff/blob/platform.rs +++ b/gix-diff/tests/diff/blob/platform.rs @@ -1,7 +1,4 @@ -use gix_diff::blob::{ - Algorithm, Platform, ResourceKind, pipeline, platform, - platform::{prepare_diff, prepare_diff::Operation}, -}; +use gix_diff::blob::{Algorithm, Platform, ResourceKind, pipeline, platform, platform::prepare_diff::Operation}; use gix_object::{ bstr::{BString, ByteSlice}, tree::EntryKind, @@ -359,10 +356,13 @@ fn source_and_destination_do_not_exist() -> crate::Result { assert_eq!(new.driver_index, None); assert_eq!(new.mode, EntryKind::BlobExecutable); - assert!(matches!( - platform.prepare_diff(), - Err(prepare_diff::Error::SourceAndDestinationRemoved) - )); + assert_eq!( + platform + .prepare_diff() + .expect_err("both resources are missing") + .to_string(), + "Tried to diff resources that are both considered removed" + ); assert_eq!( format!( diff --git a/gix-diff/tests/diff/index.rs b/gix-diff/tests/diff/index.rs index 188c19f502c..3e6e8e915ac 100644 --- a/gix-diff/tests/diff/index.rs +++ b/gix-diff/tests/diff/index.rs @@ -3,6 +3,7 @@ use gix_diff::{ index::Change, rewrites::{Copies, CopySource}, }; +use gix_error::ErrorExt; use gix_object::bstr::BStr; #[test] @@ -50,16 +51,16 @@ fn empty_to_new_tree_without_rename_tracking() -> crate::Result { let err = gix_diff::index( &lhs, &rhs, - |_change| Err(std::io::Error::other("custom error")), + |_change| Err(gix_error::message("custom error").raise_erased()), None::>, &mut pathspec, &mut |_, _, _, _| true, ) .unwrap_err(); - assert_eq!( - format!("{err:?}"), - r#"Callback(Custom { kind: Other, error: "custom error" })"#, - "custom errors made visible and not squelched" + let rendered = format!("{err:?}"); + assert!( + rendered.contains("The callback indicated failure") && rendered.contains("custom error"), + "custom errors made visible and not squelched: {rendered}" ); } Ok(()) @@ -1300,10 +1301,7 @@ fn unmerged_entries_and_intent_to_add() -> crate::Result { } mod util { - use std::{ - convert::Infallible, - path::{Path, PathBuf}, - }; + use std::path::{Path, PathBuf}; use gix_diff::rewrites; @@ -1375,7 +1373,7 @@ mod util { let rewrites_info = gix_diff::index( &from, &to, - |change| -> Result<_, Infallible> { + |change| -> Result<_, gix_error::Exn> { out.push(change.into_owned()); Ok(std::ops::ControlFlow::Continue(())) }, diff --git a/gix-diff/tests/diff/tree_with_rewrites.rs b/gix-diff/tests/diff/tree_with_rewrites.rs index a26cddc964d..c6fe9dff609 100644 --- a/gix-diff/tests/diff/tree_with_rewrites.rs +++ b/gix-diff/tests/diff/tree_with_rewrites.rs @@ -4,6 +4,7 @@ use gix_diff::{ tree::{recorder::Location, visit::Relation}, tree_with_rewrites::{Change, Options}, }; +use gix_error::ErrorExt; use gix_object::{TreeRefIter, bstr::BStr}; #[test] @@ -112,7 +113,7 @@ fn empty_to_new_tree_without_rename_tracking() -> crate::Result { &mut cache, &mut Default::default(), &odb, - |_change| Err(std::io::Error::other("custom error")), + |_change| Err(gix_error::message("custom error").raise_erased()), Options::default(), ) .unwrap_err(); @@ -1888,10 +1889,7 @@ fn realistic_renames_3_without_identity() -> crate::Result { } mod util { - use std::{ - convert::Infallible, - path::{Path, PathBuf}, - }; + use std::path::{Path, PathBuf}; use gix_diff::rewrites; use gix_object::{FindExt, TreeRefIter}; @@ -1953,7 +1951,7 @@ mod util { &mut cache, &mut Default::default(), &odb, - |change| -> Result<_, Infallible> { + |change| -> Result<_, gix_error::Exn> { out.push(change.into_owned()); Ok(std::ops::ControlFlow::Continue(())) }, diff --git a/gix-merge/src/tree/function/change.rs b/gix-merge/src/tree/function/change.rs index 2ee0a1a3bad..dbddc550cce 100644 --- a/gix-merge/src/tree/function/change.rs +++ b/gix-merge/src/tree/function/change.rs @@ -8,8 +8,6 @@ //! Semantic conflict resolution and edits to the result tree belong to the //! sibling `resolve` module; this module only prepares, indexes, and matches work. -use std::convert::Infallible; - use bstr::{BString, ByteSlice}; use gix_diff::{tree::recorder::Location, tree_with_rewrites::Change}; use gix_object::FindExt; @@ -115,7 +113,7 @@ pub(super) fn collect( diff_resource_cache, diff_state, objects, - |change| -> Result<_, Infallible> { + |change| -> Result<_, gix_error::Exn> { track(change, &mut changes); Ok(std::ops::ControlFlow::Continue(())) }, diff --git a/gix-merge/src/tree/mod.rs b/gix-merge/src/tree/mod.rs index 344d37859b8..f33625b0b3c 100644 --- a/gix-merge/src/tree/mod.rs +++ b/gix-merge/src/tree/mod.rs @@ -10,7 +10,7 @@ pub enum Error { #[error("Could not find ancestor, our or their tree iterator to get started")] FindTreeIter(#[source] gix_error::Error), #[error("Failed to diff our side or their side")] - DiffTree(#[from] gix_diff::tree_with_rewrites::Error), + DiffTree(#[from] gix_diff::tree::Error), #[error("Could not apply merge result to base tree")] TreeEdit(#[source] gix_error::Error), #[error("Failed to load resource to prepare for blob merge")] diff --git a/gix-tix/src/lib.rs b/gix-tix/src/lib.rs index 22d17dd049a..4abd34ecb52 100644 --- a/gix-tix/src/lib.rs +++ b/gix-tix/src/lib.rs @@ -543,6 +543,7 @@ fn set_worktree_resources( gix::diff::blob::ResourceKind::OldOrSource, repository, ) + .map_err(gix::Exn::into_error) .context("could not prepare old worktree diff resource")?; let new_resource = new.unwrap_or(fallback); cache @@ -553,6 +554,7 @@ fn set_worktree_resources( gix::diff::blob::ResourceKind::NewOrDestination, repository, ) + .map_err(gix::Exn::into_error) .context("could not prepare new worktree diff resource")?; Ok(()) } @@ -5229,6 +5231,7 @@ fn prepare_external_diff( 0, 1, ) + .map_err(gix::Exn::into_error) .context("could not prepare external diff command") } @@ -6709,7 +6712,7 @@ fn load_staged_changes_without_lines(repository: &gix::Repository) -> Result(std::ops::ControlFlow::Continue(())) + Ok::<_, gix::Exn>(std::ops::ControlFlow::Continue(())) }, ) .context("could not obtain staged status")?; diff --git a/gix/src/object/blob.rs b/gix/src/object/blob.rs index 17860f16fdc..ef5539c00bc 100644 --- a/gix/src/object/blob.rs +++ b/gix/src/object/blob.rs @@ -17,7 +17,7 @@ pub mod diff { /// pub mod init { /// The error returned by [`object::tree::diff::Change::diff`](crate::object::tree::diff::Change::diff()). - pub type Error = gix_diff::blob::platform::set_resource::Error; + pub type Error = gix_error::Error; } /// diff --git a/gix/src/object/tree/diff/for_each.rs b/gix/src/object/tree/diff/for_each.rs index 43ae7b0f34a..db7ff16dc94 100644 --- a/gix/src/object/tree/diff/for_each.rs +++ b/gix/src/object/tree/diff/for_each.rs @@ -1,20 +1,16 @@ use gix_object::TreeRefIter; use super::{Action, Change, Platform}; -use crate::{Tree, diff::rewrites::tracker}; +use crate::Tree; /// The error return by methods on the [diff platform][Platform]. #[derive(Debug, thiserror::Error)] #[expect(missing_docs)] pub enum Error { #[error(transparent)] - Diff(#[from] gix_diff::tree_with_rewrites::Error), - #[error("The user-provided callback failed")] - ForEach(#[source] Box), + Diff(#[from] gix_diff::tree::Error), #[error(transparent)] ResourceCache(#[from] crate::repository::diff_resource_cache::Error), - #[error("Failure during rename tracking")] - RenameTracking(#[from] tracker::emit::Error), } /// Add the item to compare to. @@ -23,14 +19,11 @@ impl<'old> Platform<'_, 'old> { /// /// `other` could also be created with the [`empty_tree()`][crate::Repository::empty_tree()] method to handle the first commit /// in a repository - it doesn't have a parent, equivalent to compare 'nothing' to something. - pub fn for_each_to_obtain_tree<'new, E>( + pub fn for_each_to_obtain_tree<'new>( &mut self, other: &Tree<'new>, - for_each: impl FnMut(Change<'_, 'old, 'new>) -> Result, - ) -> Result, Error> - where - E: Into>, - { + for_each: impl FnMut(Change<'_, 'old, 'new>) -> Result, + ) -> Result, Error> { self.for_each_to_obtain_tree_inner(other, for_each, None) } @@ -44,27 +37,21 @@ impl<'old> Platform<'_, 'old> { /// /// Note that to do rename tracking like `git` does, one has to configure the `resource_cache` with /// a conversion pipeline that uses [`gix_diff::blob::pipeline::Mode::ToGit`]. - pub fn for_each_to_obtain_tree_with_cache<'new, E>( + pub fn for_each_to_obtain_tree_with_cache<'new>( &mut self, other: &Tree<'new>, resource_cache: &mut gix_diff::blob::Platform, - for_each: impl FnMut(Change<'_, 'old, 'new>) -> Result, - ) -> Result, Error> - where - E: Into>, - { + for_each: impl FnMut(Change<'_, 'old, 'new>) -> Result, + ) -> Result, Error> { self.for_each_to_obtain_tree_inner(other, for_each, Some(resource_cache)) } - fn for_each_to_obtain_tree_inner<'new, E>( + fn for_each_to_obtain_tree_inner<'new>( &mut self, other: &Tree<'new>, - mut for_each: impl FnMut(Change<'_, 'old, 'new>) -> Result, + mut for_each: impl FnMut(Change<'_, 'old, 'new>) -> Result, resource_cache: Option<&mut gix_diff::blob::Platform>, - ) -> Result, Error> - where - E: Into>, - { + ) -> Result, Error> { let repo = self.lhs.repo; let mut storage; let cache = match resource_cache { diff --git a/gix/src/object/tree/diff/mod.rs b/gix/src/object/tree/diff/mod.rs index cd0d246e8f9..53473c71e4f 100644 --- a/gix/src/object/tree/diff/mod.rs +++ b/gix/src/object/tree/diff/mod.rs @@ -205,7 +205,7 @@ impl Platform<'_, '_> { } resource_cache.clear_resource_cache_keep_allocation(); - Ok::<_, std::convert::Infallible>(std::ops::ControlFlow::Continue(())) + Ok::<_, gix_error::Exn>(std::ops::ControlFlow::Continue(())) })?; Ok(Stats { diff --git a/gix/src/repository/diff.rs b/gix/src/repository/diff.rs index 52c601cb198..77c48126ed8 100644 --- a/gix/src/repository/diff.rs +++ b/gix/src/repository/diff.rs @@ -69,7 +69,7 @@ impl Repository { &mut cache, &mut Default::default(), &self.objects, - |change| -> Result<_, std::convert::Infallible> { + |change| -> Result<_, gix_error::Exn> { out.push(change.into_owned()); Ok(std::ops::ControlFlow::Continue(())) }, diff --git a/gix/src/repository/mod.rs b/gix/src/repository/mod.rs index ea31ff98ac3..eb280155f06 100644 --- a/gix/src/repository/mod.rs +++ b/gix/src/repository/mod.rs @@ -135,7 +135,7 @@ pub mod diff_tree_to_tree { #[error(transparent)] CreateResourceCache(#[from] super::diff_resource_cache::Error), #[error(transparent)] - TreeDiff(#[from] gix_diff::tree_with_rewrites::Error), + TreeDiff(#[from] gix_diff::tree::Error), } } diff --git a/gix/src/status/iter/mod.rs b/gix/src/status/iter/mod.rs index 15e51e401c5..eeee04dc17d 100644 --- a/gix/src/status/iter/mod.rs +++ b/gix/src/status/iter/mod.rs @@ -100,7 +100,7 @@ where } else { std::ops::ControlFlow::Continue(()) }; - Ok::<_, std::convert::Infallible>(action) + Ok::<_, gix_error::Exn>(action) }, ) } @@ -176,7 +176,7 @@ where } else { std::ops::ControlFlow::Continue(()) }; - Ok::<_, std::convert::Infallible>(action) + Ok::<_, gix_error::Exn>(action) }, )?; (items, Some(tree_index)) diff --git a/gix/src/status/mod.rs b/gix/src/status/mod.rs index 010f3b0150d..97c287bd812 100644 --- a/gix/src/status/mod.rs +++ b/gix/src/status/mod.rs @@ -133,8 +133,6 @@ impl Repository { /// pub mod is_dirty { - use std::convert::Infallible; - use crate::Repository; /// The error returned by [Repository::is_dirty()]. @@ -188,7 +186,7 @@ pub mod is_dirty { crate::status::tree_index::TrackRenames::Disabled, |_, _, _| { index_is_dirty = true; - Ok::<_, Infallible>(std::ops::ControlFlow::Break(())) + Ok::<_, gix_error::Exn>(std::ops::ControlFlow::Break(())) }, )?; if index_is_dirty { diff --git a/gix/src/status/tree_index.rs b/gix/src/status/tree_index.rs index cb89faf78a0..529b1285da3 100644 --- a/gix/src/status/tree_index.rs +++ b/gix/src/status/tree_index.rs @@ -11,7 +11,13 @@ pub enum Error { #[error("Could not create diff-cache for similarity checks")] DiffResourceCache(#[from] crate::repository::diff_resource_cache::Error), #[error(transparent)] - TreeIndexDiff(#[from] gix_diff::index::Error), + TreeIndexDiff(gix_error::Error), +} + +impl From for Error { + fn from(err: gix_diff::index::Error) -> Self { + Error::TreeIndexDiff(err.into_error()) + } } /// Specify how to perform rewrite tracking [Repository::tree_index_status()]. @@ -52,7 +58,7 @@ impl Repository { /// /// * This is a low-level method - prefer the [`Repository::status()`] platform instead for access to various iterators /// over the same information. - pub fn tree_index_status<'repo, E>( + pub fn tree_index_status<'repo>( &'repo self, tree_id: &gix_hash::oid, worktree_index: &gix_index::State, @@ -62,11 +68,8 @@ impl Repository { gix_diff::index::ChangeRef<'_, '_>, &gix_index::State, &gix_index::State, - ) -> Result, - ) -> Result - where - E: Into>, - { + ) -> Result, + ) -> Result { let _span = gix_trace::coarse!("gix::tree_index_status"); let tree_index: gix_index::State = self.index_from_tree(tree_id)?.into(); let rewrites = match renames { diff --git a/gix/tests/gix/object/tree/diff.rs b/gix/tests/gix/object/tree/diff.rs index 5d7b4f6249a..acd8b9cd4c1 100644 --- a/gix/tests/gix/object/tree/diff.rs +++ b/gix/tests/gix/object/tree/diff.rs @@ -20,7 +20,7 @@ fn changes_against_tree_modified() -> crate::Result { let mut i = 0; from.changes()? - .for_each_to_obtain_tree(&to, |change| -> Result<_, Infallible> { + .for_each_to_obtain_tree(&to, |change| -> Result<_, gix_error::Exn> { let (expected_previous_entry_mode, expected_previous_data, expected_entry_mode, expected_data) = expected_modifications[i]; @@ -149,7 +149,7 @@ fn changes_against_tree_modified() -> crate::Result { } mod track_rewrites { - use std::{collections::HashMap, convert::Infallible}; + use std::collections::HashMap; use gix::{ diff::{ @@ -205,7 +205,7 @@ mod track_rewrites { .options(|opts| { opts.track_rewrites(rewrites.into()); }) - .for_each_to_obtain_tree(&to, |change| -> Result<_, Infallible> { + .for_each_to_obtain_tree(&to, |change| -> Result<_, gix_error::Exn> { if let Change::Rewrite { source_location, diff: Some(diff), From 69b33f0c1e4c9b2eb5f1d7e4dd03f9c3cb32f38a Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 20 Aug 2026 17:17:29 +0200 Subject: [PATCH 20/51] change(gix-index)!: migrate errors to gix-error rubberstamp --- Cargo.lock | 1 - gix-index/Cargo.toml | 1 - gix-index/src/decode/entries.rs | 4 +- gix-index/src/decode/header.rs | 30 +-- gix-index/src/decode/mod.rs | 71 +++---- gix-index/src/extension/decode.rs | 36 ++-- gix-index/src/extension/link.rs | 54 ++---- gix-index/src/extension/tree/verify.rs | 179 +++++++++--------- gix-index/src/file/init.rs | 73 ++++--- gix-index/src/file/verify.rs | 31 ++- gix-index/src/file/write.rs | 35 ++-- gix-index/src/init.rs | 19 +- gix-index/src/verify.rs | 43 ++--- gix-index/tests/index/file/init.rs | 20 +- gix-index/tests/index/file/read.rs | 6 +- gix-index/tests/index/fuzzed.rs | 30 ++- gix-index/tests/index/init.rs | 9 +- gix-tix/src/edit/rebase.rs | 2 + .../tests/worktree-state/checkout.rs | 3 +- gix/src/clone/checkout.rs | 10 +- gix/src/repository/index.rs | 26 +-- gix/src/repository/mod.rs | 2 +- gix/src/status/iter/types.rs | 14 +- gix/src/worktree/mod.rs | 4 +- gix/tests/gix/repository/mod.rs | 30 +++ gix/tests/gix/repository/open.rs | 5 +- 26 files changed, 385 insertions(+), 353 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 98439985bcf..490eff7d2e7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2056,7 +2056,6 @@ dependencies = [ "rustix", "serde", "smallvec", - "thiserror 2.0.18", ] [[package]] diff --git a/gix-index/Cargo.toml b/gix-index/Cargo.toml index f0093873263..a70fb1e5cfd 100644 --- a/gix-index/Cargo.toml +++ b/gix-index/Cargo.toml @@ -48,7 +48,6 @@ gix-utils = { version = "^0.3.6", path = "../gix-utils" } hashbrown = "0.17.1" fnv = "1.0.7" -thiserror = "2.0.18" memmap2 = "0.9.11" filetime = "0.2.29" bstr = { version = "1.12.0", default-features = false } diff --git a/gix-index/src/decode/entries.rs b/gix-index/src/decode/entries.rs index 241916df50a..f8bcd9350a3 100644 --- a/gix-index/src/decode/entries.rs +++ b/gix-index/src/decode/entries.rs @@ -1,5 +1,7 @@ use std::ops::Range; +use gix_error::OptionExt; + use crate::{ Entry, Version, decode::{self, header}, @@ -111,7 +113,7 @@ pub fn chunk<'a>( has_delta_paths, prev_path, ) - .ok_or(decode::Error::Entry { index: idx })?; + .ok_or_raise_erased(|| gix_error::CorruptionError::new(format!("Could not parse entry at index {idx}")))?; data = remaining; is_sparse |= entry.mode.is_sparse(); diff --git a/gix-index/src/decode/header.rs b/gix-index/src/decode/header.rs index 8de157edb01..bdacef74ec0 100644 --- a/gix-index/src/decode/header.rs +++ b/gix-index/src/decode/header.rs @@ -5,31 +5,27 @@ use crate::{Version, util::from_be_u32}; pub(crate) const SIGNATURE: &[u8] = b"DIRC"; mod error { - /// The error produced when failing to decode an index header. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error("{0}")] - Corrupt(&'static str), - #[error("Index version {0} is not supported")] - UnsupportedVersion(u32), - } + pub type Error = gix_error::Exn; } pub use error::Error; pub(crate) fn decode(data: &[u8], object_hash: gix_hash::Kind) -> Result<(Version, u32, &[u8]), Error> { + use gix_error::ErrorExt; + if data.len() < (3 * 4) + object_hash.len_in_bytes() { - return Err(Error::Corrupt( + return Err(gix_error::CorruptionError::new( "File is too small even for header with zero entries and smallest hash", - )); + ) + .raise_erased()); } let (signature, data) = data.split_at(4); if signature != SIGNATURE { - return Err(Error::Corrupt( - "Signature mismatch - this doesn't claim to be a header file", - )); + return Err( + gix_error::CorruptionError::new("Signature mismatch - this doesn't claim to be a header file") + .raise_erased(), + ); } let (version, data) = data.split_at(4); @@ -37,7 +33,11 @@ pub(crate) fn decode(data: &[u8], object_hash: gix_hash::Kind) -> Result<(Versio 2 => Version::V2, 3 => Version::V3, 4 => Version::V4, - unknown => return Err(Error::UnsupportedVersion(unknown)), + unknown => { + return Err( + gix_error::ValidationError::new(format!("Index version {unknown} is not supported")).raise_erased(), + ); + } }; let (entries, data) = data.split_at(4); let entries = from_be_u32(entries); diff --git a/gix-index/src/decode/mod.rs b/gix-index/src/decode/mod.rs index a663c4daca0..b1509ebb361 100644 --- a/gix-index/src/decode/mod.rs +++ b/gix-index/src/decode/mod.rs @@ -6,38 +6,9 @@ mod entries; /// pub mod header; -mod error { - use crate::{decode, extension}; - use std::collections::TryReserveError; - - /// The error returned by [`State::from_bytes()`][crate::State::from_bytes()]. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - Header(#[from] decode::header::Error), - #[error("Could not hash index data")] - Hasher(#[source] std::io::Error), - #[error("Index data would require more memory than can be reserved")] - OutOfMemory, - #[error("Could not parse entry at index {index}")] - Entry { index: u32 }, - #[error("Mandatory extension wasn't implemented or malformed.")] - Extension(#[from] extension::decode::Error), - #[error("Index trailer should have been {expected} bytes long, but was {actual}")] - UnexpectedTrailerLength { expected: usize, actual: usize }, - #[error("Shared index checksum mismatch")] - Verify(#[from] gix_hash::verify::Error), - } - - impl From for Error { - #[cold] - fn from(_: TryReserveError) -> Self { - Self::OutOfMemory - } - } -} -pub use error::Error; +/// The error returned by [`State::from_bytes()`][crate::State::from_bytes()]. +pub type Error = gix_error::Exn; +use gix_error::{CorruptionError, ErrorExt, ResourceExhaustionError, ResourceExhaustionKind, ResultExt, message}; use gix_features::parallel::InOrderIter; use crate::util::read_u32; @@ -81,10 +52,11 @@ impl State { let _span = gix_features::trace::detail!("gix_index::State::from_bytes()", options = ?_options); let (version, num_entries, post_header_data) = header::decode(data, object_hash)?; let start_of_extensions = extension::end_of_index_entry::decode(data, object_hash) - .map_err(gix_hash::io::from_hasher) - .map_err(|err| Error::Hasher(std::io::Error::other(err.into_error())))?; + .or_raise_erased(|| message("Could not hash index data"))?; if num_entries as usize > entries::max_entries_possible(data.len(), start_of_extensions, object_hash, version) { - return Err(header::Error::Corrupt("Declared entry count exceeds possible entries for file size").into()); + return Err( + CorruptionError::new("Declared entry count exceeds possible entries for file size").raise_erased(), + ); } let mut num_threads = gix_features::parallel::num_threads(thread_limit); @@ -98,7 +70,7 @@ impl State { ensure_in_alloc_limit( (num_entries as usize) .checked_mul(std::mem::size_of::()) - .ok_or(Error::OutOfMemory)?, + .ok_or_else(|| allocation_error(ResourceExhaustionKind::AllocationFailure))?, alloc_limit_bytes, )?; ensure_in_alloc_limit(path_backing_buffer_size, alloc_limit_bytes)?; @@ -218,7 +190,7 @@ impl State { ); (entries_res, ext_res) }); - let (ext, data) = ext_res?; + let (ext, data) = ext_res.or_erased()?; (entries_res?.0, ext, data) } None | Some(_) => { @@ -229,22 +201,26 @@ impl State { object_hash, version, )?; - let (ext, data) = extension::decode::all(data, object_hash, alloc_limit_bytes)?; + let (ext, data) = extension::decode::all(data, object_hash, alloc_limit_bytes).or_erased()?; (entries, ext, data) } }; if data.len() != object_hash.len_in_bytes() { - return Err(Error::UnexpectedTrailerLength { - expected: object_hash.len_in_bytes(), - actual: data.len(), - }); + return Err(CorruptionError::new(format!( + "Index trailer should have been {} bytes long, but was {}", + object_hash.len_in_bytes(), + data.len() + )) + .raise_erased()); } let checksum = gix_hash::ObjectId::from_bytes_or_panic(data); let checksum = (!checksum.is_null()).then_some(checksum); if let Some((expected_checksum, actual_checksum)) = expected_checksum.zip(checksum) { - actual_checksum.verify(&expected_checksum)?; + actual_checksum + .verify(&expected_checksum) + .or_raise_erased(|| message("Shared index checksum mismatch"))?; } let EntriesOutcome { entries, @@ -293,7 +269,8 @@ struct EntriesOutcome { fn vec_with_capacity(capacity: usize) -> Result, Error> { let mut vec = Vec::new(); - vec.try_reserve(capacity).map_err(|_| Error::OutOfMemory)?; + vec.try_reserve(capacity) + .or_raise_erased(|| message("Index data would require more memory than can be reserved"))?; Ok(vec) } @@ -358,7 +335,11 @@ pub(crate) fn stat(data: &[u8]) -> Option<(entry::Stat, &[u8])> { fn ensure_in_alloc_limit(size: usize, alloc_limit_bytes: Option) -> Result<(), Error> { if alloc_limit_bytes.is_some_and(|limit| size > limit) { - return Err(Error::OutOfMemory); + return Err(allocation_error(ResourceExhaustionKind::AllocationLimit)); } Ok(()) } + +fn allocation_error(kind: ResourceExhaustionKind) -> Error { + ResourceExhaustionError::new(kind, "Index data would require more memory than can be reserved").raise_erased() +} diff --git a/gix-index/src/extension/decode.rs b/gix-index/src/extension/decode.rs index 5367373bc30..6773563ef81 100644 --- a/gix-index/src/extension/decode.rs +++ b/gix-index/src/extension/decode.rs @@ -7,20 +7,8 @@ pub(crate) fn header(data: &[u8]) -> (Signature, u32, &[u8]) { } mod error { - use crate::extension; - /// The error returned when decoding extensions. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error( - "Encountered mandatory extension '{}' which isn't implemented yet", - String::from_utf8_lossy(signature) - )] - MandatoryUnimplemented { signature: extension::Signature }, - #[error("Could not parse mandatory link extension")] - Link(#[from] extension::link::decode::Error), - } + pub type Error = gix_error::Exn; } pub use error::Error; @@ -29,6 +17,8 @@ pub(crate) fn all( object_hash: gix_hash::Kind, alloc_limit_bytes: Option, ) -> Result<(Outcome, &[u8]), Error> { + use gix_error::{ErrorExt, ResultExt, message}; + let mut ext_iter = match extension::Iter::new_without_checksum(maybe_beginning_of_extensions, object_hash) { Some(iter) => iter, None => return Ok((Outcome::default(), maybe_beginning_of_extensions)), @@ -56,15 +46,29 @@ pub(crate) fn all( ext.offset_table = true; } // not relevant/obtained already mandatory if mandatory[0].is_ascii_lowercase() => match mandatory { - extension::link::SIGNATURE => ext.link = extension::link::decode(ext_data, object_hash)?.into(), + extension::link::SIGNATURE => { + ext.link = extension::link::decode(ext_data, object_hash) + .or_raise(|| message("Could not parse mandatory link extension"))? + .into(); + } extension::sparse::SIGNATURE => { if !ext_data.is_empty() { // only used as a marker, if this changes we need this implementation. - return Err(Error::MandatoryUnimplemented { signature: mandatory }); + return Err(message!( + "Encountered mandatory extension '{}' which isn't implemented yet", + String::from_utf8_lossy(&mandatory) + ) + .raise()); } ext.is_sparse = true; } - unknown => return Err(Error::MandatoryUnimplemented { signature: unknown }), + unknown => { + return Err(message!( + "Encountered mandatory extension '{}' which isn't implemented yet", + String::from_utf8_lossy(&unknown) + ) + .raise()); + } }, _unknown => {} // skip unknown extensions, too } diff --git a/gix-index/src/extension/link.rs b/gix-index/src/extension/link.rs index 463e37e9f04..8673efdf9af 100644 --- a/gix-index/src/extension/link.rs +++ b/gix-index/src/extension/link.rs @@ -14,33 +14,16 @@ pub struct Bitmaps { /// pub mod decode { - /// The error returned when decoding link extensions. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error("{0}")] - Corrupt(&'static str), - #[error("{kind} bitmap corrupt")] - BitmapDecode { - err: gix_bitmap::ewah::decode::Error, - kind: &'static str, - }, - } - - impl From for Error { - fn from(_: std::num::TryFromIntError) -> Self { - Self::Corrupt("error in bitmap iteration trying to convert from u64 to usize") - } - } + pub type Error = gix_error::Exn; } pub(crate) fn decode(data: &[u8], object_hash: gix_hash::Kind) -> Result { + use gix_error::{ErrorExt, OptionExt, ResultExt}; + let (id, data) = data .split_at_checked(object_hash.len_in_bytes()) - .ok_or(decode::Error::Corrupt( - "link extension too short to read share index checksum", - )) + .ok_or_raise(|| gix_error::CorruptionError::new("link extension too short to read share index checksum")) .map(|(id, d)| (gix_hash::ObjectId::from_bytes_or_panic(id), d))?; if data.is_empty() { @@ -51,12 +34,12 @@ pub(crate) fn decode(data: &[u8], object_hash: gix_hash::Kind) -> Result Result<(), crate::file::init::Error> { + use gix_error::ErrorExt; + + let corrupt = |message| gix_error::CorruptionError::new(message).raise_erased(); let shared_index_path = split_index .path .parent() @@ -96,29 +82,29 @@ impl Link { let shared_entry = match shared_index.entries.get_mut(replace_index) { Some(e) => e, None => { - err = decode::Error::Corrupt("replace bitmap length exceeds shared index length - more entries in bitmap than found in shared index").into(); + err = Some(corrupt("replace bitmap length exceeds shared index length - more entries in bitmap than found in shared index")); return None } }; if shared_entry.flags.contains(crate::entry::Flags::REMOVE) { - err = decode::Error::Corrupt("entry is marked as both replace and delete").into(); + err = Some(corrupt("entry is marked as both replace and delete")); return None } let split_entry = match split_index.entries.get(split_entry_index) { Some(e) => e, None => { - err = decode::Error::Corrupt("replace bitmap length exceeds split index length - more entries in bitmap than found in split index").into(); + err = Some(corrupt("replace bitmap length exceeds split index length - more entries in bitmap than found in split index")); return None } }; if !split_entry.path.is_empty() { - err = decode::Error::Corrupt("paths in split index entries that are for replacement should be empty").into(); + err = Some(corrupt("paths in split index entries that are for replacement should be empty")); return None } if shared_entry.path.is_empty() { - err = decode::Error::Corrupt("paths in shared index entries that are replaced should not be empty").into(); + err = Some(corrupt("paths in shared index entries that are replaced should not be empty")); return None } shared_entry.stat = split_entry.stat; @@ -129,10 +115,10 @@ impl Link { split_entry_index += 1; Some(()) }).is_none() && err.is_none() { - err = decode::Error::Corrupt("replace bitmap is malformed").into(); + err = Some(corrupt("replace bitmap is malformed")); } if let Some(err) = err { - return Err(err.into()); + return Err(err); } let split_index_path_backing = std::mem::take(&mut split_index.path_backing); @@ -152,17 +138,17 @@ impl Link { let shared_entry = match shared_index.entries.get_mut(delete_index) { Some(e) => e, None => { - err = decode::Error::Corrupt("delete bitmap length exceeds shared index length - more entries in bitmap than found in shared index").into(); + err = Some(corrupt("delete bitmap length exceeds shared index length - more entries in bitmap than found in shared index")); return None } }; shared_entry.flags.insert(crate::entry::Flags::REMOVE); Some(()) }).is_none() && err.is_none() { - err = decode::Error::Corrupt("delete bitmap is malformed").into(); + err = Some(corrupt("delete bitmap is malformed")); } if let Some(err) = err { - return Err(err.into()); + return Err(err); } shared_index diff --git a/gix-index/src/extension/tree/verify.rs b/gix-index/src/extension/tree/verify.rs index 04598a66727..f3935a63fe1 100644 --- a/gix-index/src/extension/tree/verify.rs +++ b/gix-index/src/extension/tree/verify.rs @@ -1,61 +1,13 @@ use std::cmp::Ordering; -use bstr::{BString, ByteSlice}; +use bstr::ByteSlice; +use gix_error::{ErrorExt, ResultExt}; use gix_object::FindExt; use crate::extension::Tree; /// The error returned by [`Tree::verify()`][crate::extension::Tree::verify()]. -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] -pub enum Error { - #[error( - "The entry {entry_id} at path '{name}' in parent tree {parent_id} wasn't found in the nodes children, making it incomplete" - )] - MissingTreeDirectory { - parent_id: gix_hash::ObjectId, - entry_id: gix_hash::ObjectId, - name: BString, - }, - #[error(transparent)] - TreeNodeNotFound(gix_error::Error), - #[error( - "The tree with id {oid} should have {expected_childcount} children, but its cached representation had {actual_childcount} of them" - )] - TreeNodeChildcountMismatch { - oid: gix_hash::ObjectId, - expected_childcount: usize, - actual_childcount: usize, - }, - #[error("The root tree was named '{name}', even though it should be empty")] - RootWithName { name: BString }, - #[error( - "Expected not more than {expected} entries to be reachable from the top-level, but actual count was {actual}" - )] - EntriesCount { actual: u32, expected: u32 }, - #[error("The combined TREE entry count exceeds the supported maximum")] - EntriesCountOverflow, - #[error("TREE entry '{name}' declared {actual} entries, but the index only contains {expected} entries")] - EntriesCountExceedsIndex { - name: BString, - actual: u32, - expected: usize, - }, - #[error( - "Parent tree '{parent_id}' contained out-of order trees prev = '{previous_path}' and next = '{current_path}'" - )] - OutOfOrder { - parent_id: gix_hash::ObjectId, - current_path: BString, - previous_path: BString, - }, -} - -impl From for Error { - fn from(err: gix_object::find::existing_iter::Error) -> Self { - Error::TreeNodeNotFound(err.into_error()) - } -} +pub type Error = gix_error::Exn; impl Tree { /// Validate the correctness of this instance. If `use_objects` is true, then `objects` will be used to access all objects. @@ -72,40 +24,52 @@ impl Tree { let mut entries = 0u32; let mut prev = None::<&Tree>; for child in children { - entries = entries - .checked_add(child.num_entries.unwrap_or(0)) - .ok_or(Error::EntriesCountOverflow)?; + entries = entries.checked_add(child.num_entries.unwrap_or(0)).ok_or_else(|| { + gix_error::CorruptionError::new("The combined TREE entry count exceeds the supported maximum") + .raise() + })?; if let Some(prev) = prev && prev.name.cmp(&child.name) != Ordering::Less { - return Err(Error::OutOfOrder { - parent_id, - previous_path: prev.name.as_bstr().into(), - current_path: child.name.as_bstr().into(), - }); + return Err(gix_error::CorruptionError::new(format!( + "Parent tree '{parent_id}' contained out-of order trees prev = '{}' and next = '{}'", + prev.name.as_bstr(), + child.name.as_bstr() + )) + .raise()); } prev = Some(child); } if let Some(buf) = object_buf.as_mut() { - let tree_entries = objects.find_tree_iter(&parent_id, buf)?; + let tree_entries = objects + .find_tree_iter(&parent_id, buf) + .or_raise(|| gix_error::CorruptionError::new("Tree node could not be found"))?; let mut num_entries = 0; - for entry in tree_entries.filter_map(Result::ok).filter(|e| e.mode.is_tree()) { + for entry in tree_entries { + let entry = entry.or_raise(|| { + gix_error::CorruptionError::new(format!("Could not decode an entry in tree {parent_id}")) + })?; + if !entry.mode.is_tree() { + continue; + } children .binary_search_by(|e| e.name.as_bstr().cmp(entry.filename)) - .map_err(|_| Error::MissingTreeDirectory { - parent_id, - entry_id: entry.oid.to_owned(), - name: entry.filename.to_owned(), + .map_err(|_| { + gix_error::CorruptionError::new(format!( + "The entry {} at path '{}' in parent tree {parent_id} wasn't found in the nodes children, making it incomplete", + entry.oid, entry.filename + )) + .raise() })?; num_entries += 1; } if num_entries != children.len() { - return Err(Error::TreeNodeChildcountMismatch { - oid: parent_id, - expected_childcount: num_entries, - actual_childcount: children.len(), - }); + return Err(gix_error::CorruptionError::new(format!( + "The tree with id {parent_id} should have {num_entries} children, but its cached representation had {} of them", + children.len() + )) + .raise()); } } for child in children { @@ -115,10 +79,10 @@ impl Tree { if let Some((actual, num_entries)) = actual_num_entries.zip(child.num_entries) && actual > num_entries { - return Err(Error::EntriesCount { - actual, - expected: num_entries, - }); + return Err(gix_error::CorruptionError::new(format!( + "Expected not more than {num_entries} entries to be reachable from the top-level, but actual count was {actual}" + )) + .raise()); } } Ok(entries.into()) @@ -126,9 +90,11 @@ impl Tree { let _span = gix_features::trace::coarse!("gix_index::extension::Tree::verify()"); if !self.name.is_empty() { - return Err(Error::RootWithName { - name: self.name.as_bstr().into(), - }); + return Err(gix_error::CorruptionError::new(format!( + "The root tree was named '{}', even though it should be empty", + self.name.as_bstr() + )) + .raise()); } let mut buf = Vec::new(); @@ -136,10 +102,10 @@ impl Tree { if let Some((actual, num_entries)) = declared_entries.zip(self.num_entries) && actual > num_entries { - return Err(Error::EntriesCount { - actual, - expected: num_entries, - }); + return Err(gix_error::CorruptionError::new(format!( + "Expected not more than {num_entries} entries to be reachable from the top-level, but actual count was {actual}" + )) + .raise()); } Ok(()) @@ -153,11 +119,11 @@ impl Tree { if let Some(actual) = self.num_entries && actual as usize > num_index_entries { - return Err(Error::EntriesCountExceedsIndex { - name: self.name.as_bstr().into(), - actual, - expected: num_index_entries, - }); + return Err(gix_error::CorruptionError::new(format!( + "TREE entry '{}' declared {actual} entries, but the index only contains {num_index_entries} entries", + self.name.as_bstr() + )) + .raise()); } for child in &self.children { @@ -167,3 +133,44 @@ impl Tree { Ok(()) } } + +#[cfg(test)] +mod tests { + use super::Tree; + + struct MalformedTree; + + impl gix_object::Find for MalformedTree { + fn try_find<'a>( + &self, + _id: &gix_hash::oid, + _buffer: &'a mut Vec, + ) -> Result>, gix_object::find::Error> { + Ok(Some(gix_object::Data::new( + b"40000 child\0", + gix_object::Kind::Tree, + gix_hash::Kind::Sha1, + ))) + } + } + + #[test] + fn malformed_object_tree_entries_are_not_ignored() { + let root_id = gix_hash::Kind::Sha1.null(); + let tree = Tree { + name: Default::default(), + id: root_id, + num_entries: Some(1), + children: vec![Tree { + name: b"child".as_slice().into(), + id: root_id, + num_entries: Some(0), + children: Vec::new(), + }], + }; + + let err = tree.verify(true, MalformedTree).expect_err("malformed entry must fail"); + assert_eq!(err.to_string(), format!("Could not decode an entry in tree {root_id}")); + assert!(err.downcast_any_ref::().is_some()); + } +} diff --git a/gix-index/src/file/init.rs b/gix-index/src/file/init.rs index 13f58f420ca..aab4e4dc9f1 100644 --- a/gix-index/src/file/init.rs +++ b/gix-index/src/file/init.rs @@ -2,24 +2,33 @@ use std::path::{Path, PathBuf}; +use gix_error::{ResultExt, message}; + use crate::{File, State, decode, extension}; -mod error { +/// The error returned by [File::at()][File::at()]. +pub type Error = gix_error::Exn; + +/// A failure to open an index file, retaining its path to distinguish primary and shared indexes. +#[derive(Debug)] +pub struct OpenError { + /// The index path that could not be opened. + pub path: PathBuf, + /// The underlying I/O error. + pub source: std::io::Error, +} - /// The error returned by [File::at()][super::File::at()]. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error("An IO error occurred while opening the index")] - Io(#[from] std::io::Error), - #[error(transparent)] - Decode(#[from] crate::decode::Error), - #[error(transparent)] - LinkExtension(#[from] crate::extension::link::decode::Error), +impl std::fmt::Display for OpenError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Could not open index file at '{}'", self.path.display()) } } -pub use error::Error; +impl std::error::Error for OpenError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + Some(&self.source) + } +} /// Initialization impl File { @@ -27,7 +36,7 @@ impl File { /// index that merely exists in memory and is empty. `skip_hash` will increase the performance by a factor of 2, at the cost of /// possibly not detecting corruption. /// - /// Note that the `path` will not be written if it doesn't exist. + /// Note that the `path` will not be written if it doesn't exist. A missing shared index remains an error. pub fn at_or_default( path: impl Into, object_hash: gix_hash::Kind, @@ -37,7 +46,11 @@ impl File { let path = path.into(); Ok(match Self::at(&path, object_hash, skip_hash, options) { Ok(f) => f, - Err(Error::Io(err)) if err.kind() == std::io::ErrorKind::NotFound => { + Err(err) + if err + .downcast_any_ref::() + .is_some_and(|err| err.path == path && err.source.kind() == std::io::ErrorKind::NotFound) => + { File::from_state(State::new(object_hash), path) } Err(err) => return Err(err), @@ -59,10 +72,16 @@ impl File { let _span = gix_features::trace::detail!("gix_index::File::at()"); let path = path.into(); let (data, mtime) = { - let mut file = std::fs::File::open(&path)?; + let mut file = std::fs::File::open(&path) + .map_err(|source| OpenError { + path: path.clone(), + source, + }) + .or_erased()?; // SAFETY: we have to take the risk of somebody changing the file underneath. Git never writes into the same file. #[expect(unsafe_code)] - let data = unsafe { memmap2::MmapOptions::new().map_copy_read_only(&file)? }; + let data = unsafe { memmap2::MmapOptions::new().map_copy_read_only(&file) } + .or_raise_erased(|| message("An IO error occurred while opening the index"))?; // Let the decoder report truncated files before trying to read their checksum. if !skip_hash && data.len() >= object_hash.len_in_bytes() { @@ -74,7 +93,9 @@ impl File { gix_hash::ObjectId::from_bytes_or_panic(&data[data.len() - object_hash.len_in_bytes()..]); if !expected.is_null() { let _span = gix_features::trace::detail!("gix::open_index::hash_index", path = ?path); - let meta = file.metadata()?; + let meta = file + .metadata() + .or_raise_erased(|| message("An IO error occurred while opening the index"))?; let num_bytes_to_hash = meta.len() - object_hash.len_in_bytes() as u64; gix_hash::bytes( &mut file, @@ -83,18 +104,20 @@ impl File { &mut gix_features::progress::Discard, &Default::default(), ) - .map_err( - |err| match err.downcast_any_ref::().map(std::io::Error::kind) { - Some(kind) => Error::Io(std::io::Error::new(kind, err.into_error())), - None => Error::Decode(decode::Error::Hasher(std::io::Error::other(err.into_error()))), - }, - )? + .or_raise_erased(|| message("Could not hash index data"))? .verify(&expected) - .map_err(decode::Error::from)?; + .or_raise_erased(|| message("Shared index checksum mismatch"))?; } } - (data, filetime::FileTime::from_last_modification_time(&file.metadata()?)) + ( + data, + filetime::FileTime::from_last_modification_time( + &file + .metadata() + .or_raise_erased(|| message("An IO error occurred while opening the index"))?, + ), + ) }; let (state, checksum) = State::from_bytes(&data, mtime, object_hash, options)?; diff --git a/gix-index/src/file/verify.rs b/gix-index/src/file/verify.rs index 5e1a17a8d7e..9af3e54da41 100644 --- a/gix-index/src/file/verify.rs +++ b/gix-index/src/file/verify.rs @@ -4,30 +4,23 @@ use crate::File; mod error { /// The error returned by [File::verify_integrity()][super::File::verify_integrity()]. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error("Could not read index file to generate hash")] - Io(#[from] std::io::Error), - #[error("Index checksum mismatch")] - Verify(#[from] gix_hash::verify::Error), - } + pub type Error = gix_error::Exn; } pub use error::Error; -impl From for Error { - fn from(err: gix_hash::io::Error) -> Self { - Error::Io(std::io::Error::other(err.into_error())) - } -} - impl File { /// Verify the integrity of the index to assure its consistency. pub fn verify_integrity(&self) -> Result<(), Error> { + use gix_error::{ResultExt, message}; + let _span = gix_features::trace::coarse!("gix_index::File::verify_integrity()"); if let Some(checksum) = self.checksum { - let num_bytes_to_hash = - self.path.metadata().map_err(gix_hash::io::from_std_io)?.len() - checksum.as_bytes().len() as u64; + let num_bytes_to_hash = self + .path + .metadata() + .or_raise(|| message("Could not read index file to generate hash"))? + .len() + - checksum.as_bytes().len() as u64; let should_interrupt = AtomicBool::new(false); gix_hash::bytes_of_file( &self.path, @@ -35,8 +28,10 @@ impl File { checksum.kind(), &mut gix_features::progress::Discard, &should_interrupt, - )? - .verify(&checksum)?; + ) + .or_raise(|| message("Could not read index file to generate hash"))? + .verify(&checksum) + .or_raise(|| message("Index checksum mismatch"))?; } Ok(()) } diff --git a/gix-index/src/file/write.rs b/gix-index/src/file/write.rs index c5faaa28e32..fc7a8a1bc33 100644 --- a/gix-index/src/file/write.rs +++ b/gix-index/src/file/write.rs @@ -1,22 +1,7 @@ use crate::{File, Version, write}; /// The error produced by [`File::write()`]. -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] -pub enum Error { - #[error(transparent)] - Io(#[from] std::io::Error), - #[error("Could not acquire lock for index file")] - AcquireLock(#[source] std::io::Error), - #[error("Could not commit lock for index file")] - CommitLock(#[from] gix_lock::commit::Error), -} - -impl From for Error { - fn from(err: gix_hash::io::Error) -> Self { - Error::Io(std::io::Error::other(err.into_error())) - } -} +pub type Error = gix_error::Exn; impl File { /// Write the index to `out` with `options`, to be readable by [`File::at()`], returning the version that was actually written @@ -71,16 +56,26 @@ impl File { /// /// [issue #2421]: https://github.com/GitoxideLabs/gitoxide/issues/2421 pub fn write(&mut self, options: write::Options) -> Result<(), Error> { + use gix_error::{ErrorExt, ResultExt, message}; + let _span = gix_features::trace::detail!("gix_index::File::write()", path = ?self.path); let mut lock = std::io::BufWriter::with_capacity( 64 * 1024, gix_lock::File::acquire_to_update_resource(&self.path, gix_lock::acquire::Fail::Immediately, None) - .map_err(|err| Error::AcquireLock(std::io::Error::other(err.into_error())))?, + .or_raise(|| message("Could not acquire lock for index file"))?, ); - let (version, digest) = self.write_to(&mut lock, options)?; + let (version, digest) = self + .write_to(&mut lock, options) + .or_raise(|| message("Could not write index"))?; match lock.into_inner() { - Ok(lock) => lock.commit()?, - Err(err) => return Err(Error::Io(err.into_error())), + Ok(lock) => lock + .commit() + .or_raise(|| message("Could not commit lock for index file"))?, + Err(err) => { + return Err(err + .into_error() + .and_raise(message("Could not flush buffered index data"))); + } }; self.state.version = version; self.checksum = Some(digest); diff --git a/gix-index/src/init.rs b/gix-index/src/init.rs index 9be2aaed5f8..f5117f00e82 100644 --- a/gix-index/src/init.rs +++ b/gix-index/src/init.rs @@ -3,6 +3,7 @@ pub mod from_tree { use std::collections::VecDeque; use bstr::{BStr, BString, ByteSlice, ByteVec}; + use gix_error::{ErrorExt, ResultExt, ValidationError, message}; use gix_object::{tree, tree::EntryKind}; use gix_traverse::tree::{Visit, depthfirst, visit::Action}; @@ -12,17 +13,7 @@ pub mod from_tree { }; /// The error returned by [State::from_tree()]. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error("The path \"{path}\" is invalid")] - InvalidComponent { - path: BString, - source: gix_validate::path::component::Error, - }, - #[error(transparent)] - Traversal(#[from] gix_error::Error), - } + pub type Error = gix_error::Exn; /// Initialization impl State { @@ -73,9 +64,11 @@ pub mod from_tree { let traversal = depthfirst(tree.to_owned(), depthfirst::State::default(), &objects, &mut delegate); if let Some((path, err)) = delegate.invalid_path.take() { - return Err(Error::InvalidComponent { path, source: err }); + return Err(err + .and_raise(ValidationError::new(format!("The path \"{path}\" is invalid"))) + .erased()); } - traversal.map_err(gix_error::Exn::into_error)?; + traversal.or_raise_erased(|| message("Tree traversal failed"))?; let CollectEntries { entries, diff --git a/gix-index/src/verify.rs b/gix-index/src/verify.rs index 03be4cbe08c..2ca132663fe 100644 --- a/gix-index/src/verify.rs +++ b/gix-index/src/verify.rs @@ -4,54 +4,35 @@ use crate::State; /// pub mod entries { - use bstr::BString; - /// The error returned by [`State::verify_entries()`][crate::State::verify_entries()]. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error( - "Entry '{current_path}' (stage = {current_stage}) at index {current_index} should order after prior entry '{previous_path}' (stage = {previous_stage})" - )] - OutOfOrder { - current_index: usize, - current_path: BString, - current_stage: u8, - previous_path: BString, - previous_stage: u8, - }, - } + pub type Error = gix_error::Exn; } /// pub mod extensions { - use crate::extension; - /// The error returned by [`State::verify_extensions()`][crate::State::verify_extensions()]. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - Tree(#[from] extension::tree::verify::Error), - } + pub type Error = gix_error::Exn; } impl State { /// Assure our entries are consistent. pub fn verify_entries(&self) -> Result<(), entries::Error> { + use gix_error::ErrorExt; + let _span = gix_features::trace::coarse!("gix_index::File::verify_entries()"); let mut previous = None::<&crate::Entry>; for (idx, entry) in self.entries.iter().enumerate() { if let Some(prev) = previous && prev.cmp(entry, self) != Ordering::Less { - return Err(entries::Error::OutOfOrder { - current_index: idx, - current_path: entry.path(self).into(), - current_stage: entry.flags.stage() as u8, - previous_path: prev.path(self).into(), - previous_stage: prev.flags.stage() as u8, - }); + return Err(gix_error::CorruptionError::new(format!( + "Entry '{}' (stage = {}) at index {idx} should order after prior entry '{}' (stage = {})", + entry.path(self), + entry.flags.stage() as u8, + prev.path(self), + prev.flags.stage() as u8 + )) + .raise()); } previous = Some(entry); } diff --git a/gix-index/tests/index/file/init.rs b/gix-index/tests/index/file/init.rs index 7c757f8bf5c..bcc31e27c9b 100644 --- a/gix-index/tests/index/file/init.rs +++ b/gix-index/tests/index/file/init.rs @@ -12,12 +12,7 @@ mod at { // Some platforms cannot memory-map an empty file and return an IO error instead. if len != 0 { assert!( - matches!( - err, - gix_index::file::init::Error::Decode(gix_index::decode::Error::Header( - gix_index::decode::header::Error::Corrupt(_) - )) - ), + err.downcast_any_ref::().is_some(), "expected a corrupt header for {object_hash:?}, {len} bytes, skip_hash={skip_hash}: {err}" ); } @@ -42,6 +37,19 @@ mod at_or_new { .expect("file exists and can be opened"); } + #[test] + fn missing_shared_index_is_an_error() -> gix_testtools::Result { + let tmp = gix_testtools::tempfile::TempDir::new()?; + let index_path = tmp.path().join("index"); + // Keep the primary split index, but leave its shared index behind. + std::fs::copy(Generated("v2_split_index").to_path(), &index_path)?; + + let err = gix_index::File::at_or_default(index_path, gix_testtools::object_hash(), false, Default::default()) + .expect_err("a missing shared index must not produce an empty index"); + assert!(err.into_error().is_not_found(), "the missing-file cause is preserved"); + Ok(()) + } + #[test] fn create_empty_in_memory_state_if_file_does_not_exist() { let index = gix_index::File::at_or_default( diff --git a/gix-index/tests/index/file/read.rs b/gix-index/tests/index/file/read.rs index 82c12bb5c3e..0eb5df783bd 100644 --- a/gix-index/tests/index/file/read.rs +++ b/gix-index/tests/index/file/read.rs @@ -689,10 +689,8 @@ fn v2_split_index() { #[test] fn v2_split_index_recursion_is_handled_gracefully() { let err = try_file("v2_split_index_recursive", false).expect_err("recursion fails gracefully"); - assert!(matches!( - err, - gix_index::file::init::Error::Decode(gix_index::decode::Error::Verify(_)) - )); + assert_eq!(err.to_string(), "Shared index checksum mismatch"); + assert!(err.downcast_any_ref::().is_some()); } #[test] diff --git a/gix-index/tests/index/fuzzed.rs b/gix-index/tests/index/fuzzed.rs index 0183c161814..58981e009f1 100644 --- a/gix-index/tests/index/fuzzed.rs +++ b/gix-index/tests/index/fuzzed.rs @@ -10,6 +10,11 @@ fn decode_fuzzed(data: &[u8]) -> Result<(gix_index::State, Option bool { + err.downcast_any_ref::() + .is_some_and(|err| err.message.starts_with(prefix)) +} + #[test] fn index_file_artifacts_run_fuzzer() { for path in artifact_paths("index_file") { @@ -44,7 +49,7 @@ fn malformed_tree_extension_is_ignored_instead_of_panicking() { Ok((state, _checksum)) => { assert!(state.tree().is_none(), "malformed optional extension must be ignored"); } - Err(gix_index::decode::Error::UnexpectedTrailerLength { .. }) => {} + Err(err) if is_corruption_with(&err, "Index trailer should have been") => {} Err(err) => panic!("unexpected decode failure: {err:?}"), } } @@ -86,7 +91,7 @@ fn impossible_untracked_cache_directory_counts_are_rejected_before_reserving() { "malformed optional extension must be ignored" ); } - Err(gix_index::decode::Error::UnexpectedTrailerLength { .. }) => {} + Err(err) if is_corruption_with(&err, "Index trailer should have been") => {} Err(err) => panic!("unexpected decode failure: {err:?}"), } } @@ -98,7 +103,7 @@ fn malformed_entry_padding_is_rejected_instead_of_panicking() { )) .expect_err("fuzzed input must stay rejected"); - assert!(matches!(err, gix_index::decode::Error::Entry { .. }), "{err:?}"); + assert!(is_corruption_with(&err, "Could not parse entry at index"), "{err:?}"); } #[test] @@ -112,7 +117,7 @@ fn malformed_untracked_cache_bitmap_is_rejected_instead_of_panicking() { "malformed optional extension must be ignored" ); } - Err(gix_index::decode::Error::UnexpectedTrailerLength { .. }) => {} + Err(err) if is_corruption_with(&err, "Index trailer should have been") => {} Err(err) => panic!("unexpected decode failure: {err:?}"), } } @@ -124,7 +129,7 @@ fn malformed_entry_padding_with_untracked_cache_is_rejected_instead_of_panicking )) .expect_err("fuzzed input must stay rejected"); - assert!(matches!(err, gix_index::decode::Error::Entry { .. }), "{err:?}"); + assert!(is_corruption_with(&err, "Could not parse entry at index"), "{err:?}"); } #[test] @@ -141,7 +146,20 @@ fn alloc_limit_constructor_rejects_oversized_allocations() { ) .expect_err("fixture should exceed tiny allocation limit"); - assert!(matches!(err, gix_index::decode::Error::OutOfMemory), "{err:?}"); + assert_eq!( + err.to_string(), + "Index data would require more memory than can be reserved" + ); + let err = err.into_error(); + assert!( + err.classify().any(|classification| { + classification.class() + == gix_error::Class::ResourceExhaustion(gix_error::ResourceExhaustionKind::AllocationLimit) + }), + "configured limits are resource exhaustion, not malformed input" + ); + assert!(!err.is_corrupted(), "configured limits aren't corruption"); + assert!(!err.can_retry(), "configured limits can't be fixed by retrying"); } fn artifact_paths(target: &str) -> Vec { diff --git a/gix-index/tests/index/init.rs b/gix-index/tests/index/init.rs index 0f46a8693c4..bbbd61d8a02 100644 --- a/gix-index/tests/index/init.rs +++ b/gix-index/tests/index/init.rs @@ -1,4 +1,4 @@ -use std::{error::Error, path::Path}; +use std::path::Path; use crate::{odb_at, scripted_fixture_read_only}; use gix_index::State; @@ -46,9 +46,12 @@ fn from_tree_validation() -> crate::Result { let git_dir = worktree_dir.join(".git"); let odb = odb_at(git_dir.join("objects"))?; - let err = State::from_tree(&tree_id, &odb, Default::default()).unwrap_err(); + let err = State::from_tree(&tree_id, &odb, Default::default()) + .unwrap_err() + .into_error(); + assert!(err.is_validation(), "invalid path components are validation errors"); assert_eq!( - err.source().expect("inner").to_string(), + err.probable_cause().to_string(), r"Path separators like / or \ are not allowed", r"Note that this effectively tests what would happen on Windows, where \ also isn't allowed" ); diff --git a/gix-tix/src/edit/rebase.rs b/gix-tix/src/edit/rebase.rs index 25b84d06e8a..9d658570e31 100644 --- a/gix-tix/src/edit/rebase.rs +++ b/gix-tix/src/edit/rebase.rs @@ -369,6 +369,7 @@ impl PersistedConflict { .context("could not check out the conflicting merge result")?; if let Err(err) = index .write(gix::index::write::Options::default()) + .map_err(gix::Exn::into_error) .context("could not write the conflicting index") { return match super::forget::apply_tree_transition(workdir, self.merged_tree, ours_tree) { @@ -2530,6 +2531,7 @@ fn reset_index_paths(repo: &gix::Repository, id: ObjectId, paths: &[BString]) -> index.remove_tree(); index .write(gix::index::write::Options::default()) + .map_err(gix::Exn::into_error) .context("could not update selected index paths") } diff --git a/gix-worktree-state/tests/worktree-state/checkout.rs b/gix-worktree-state/tests/worktree-state/checkout.rs index 087a3a64690..ce22118c901 100644 --- a/gix-worktree-state/tests/worktree-state/checkout.rs +++ b/gix-worktree-state/tests/worktree-state/checkout.rs @@ -955,7 +955,8 @@ fn checkout_index_in_tmp_dir_opts( gix_testtools::object_hash(), false, Default::default(), - )?; + ) + .map_err(gix_error::Exn::into_error)?; let odb = odb_at(git_dir.join("objects"))?.into_inner().into_arc()?; let destination = gix_testtools::tempfile::tempdir_in(std::env::current_dir()?)?; prep_dest(destination.path()).expect("preparation must succeed"); diff --git a/gix/src/clone/checkout.rs b/gix/src/clone/checkout.rs index cc741a4d821..db1be304fb8 100644 --- a/gix/src/clone/checkout.rs +++ b/gix/src/clone/checkout.rs @@ -17,12 +17,12 @@ pub mod main_worktree { #[error("Could not create index from tree at {id}")] IndexFromTree { id: gix_hash::ObjectId, - source: gix_index::init::from_tree::Error, + source: gix_error::Error, }, #[error("Couldn't obtain configuration for core.protect*")] BooleanConfig(#[from] crate::config::boolean::Error), #[error(transparent)] - WriteIndex(#[from] gix_index::file::write::Error), + WriteIndex(gix_error::Error), #[error(transparent)] CheckoutOptions(#[from] crate::config::checkout_options::Error), #[error(transparent)] @@ -114,7 +114,7 @@ pub mod main_worktree { let index = gix_index::State::from_tree(&root_tree, &repo.objects, repo.config.protect_options()?) .map_err(|err| Error::IndexFromTree { id: root_tree, - source: err, + source: err.into_error(), })?; let mut index = gix_index::File::from_state(index, repo.index_path()); @@ -141,7 +141,9 @@ pub mod main_worktree { files.show_throughput(start); bytes.show_throughput(start); - index.write(Default::default())?; + index + .write(Default::default()) + .map_err(|err| Error::WriteIndex(err.into_error()))?; Ok((self.repo.take().expect("still present").clone(), outcome)) } } diff --git a/gix/src/repository/index.rs b/gix/src/repository/index.rs index f2aca8a22d5..7a3e37c9228 100644 --- a/gix/src/repository/index.rs +++ b/gix/src/repository/index.rs @@ -45,7 +45,8 @@ impl crate::Repository { expected_checksum: None, alloc_limit_bytes: self.config.alloc_limit_bytes, }, - )?; + ) + .map_err(|err| worktree::open_index::Error::IndexFile(err.into_error()))?; Ok(index) } @@ -74,15 +75,12 @@ impl crate::Repository { pub fn index(&self) -> Result { self.try_index().and_then(|opt| match opt { Some(index) => Ok(index), - None => Err(worktree::open_index::Error::IndexFile( - gix_index::file::init::Error::Io(std::io::Error::new( - std::io::ErrorKind::NotFound, - format!( - "Could not find index file at '{index_path}' for opening.", - index_path = self.index_path().display() - ), + None => Err(worktree::open_index::Error::IndexFile(gix_error::Error::from_error( + gix_error::NotFoundError::new(format!( + "Could not find index file at '{index_path}' for opening.", + index_path = self.index_path().display() )), - )), + ))), }) } @@ -118,8 +116,12 @@ impl crate::Repository { || self.index_path().metadata().and_then(|m| m.modified()).ok(), || { self.open_index().map(Some).or_else(|err| match err { - worktree::open_index::Error::IndexFile(gix_index::file::init::Error::Io(err)) - if err.kind() == std::io::ErrorKind::NotFound => + worktree::open_index::Error::IndexFile(err) + if err + .downcast_any_ref::() + .is_some_and(|err| { + err.path == self.index_path() && err.source.kind() == std::io::ErrorKind::NotFound + }) => { Ok(None) } @@ -208,7 +210,7 @@ impl crate::Repository { gix_index::State::from_tree(tree, self, self.config.protect_options()?).map_err(|err| { super::index_from_tree::Error::IndexFromTree { id: tree.into(), - source: err, + source: err.into_error(), } })?, self.index_path(), diff --git a/gix/src/repository/mod.rs b/gix/src/repository/mod.rs index eb280155f06..9e1c1c2411f 100644 --- a/gix/src/repository/mod.rs +++ b/gix/src/repository/mod.rs @@ -399,7 +399,7 @@ pub mod index_from_tree { #[error("Could not create index from tree at {id}")] IndexFromTree { id: gix_hash::ObjectId, - source: gix_index::init::from_tree::Error, + source: gix_error::Error, }, #[error("Couldn't obtain configuration for core.protect*")] BooleanConfig(#[from] crate::config::boolean::Error), diff --git a/gix/src/status/iter/types.rs b/gix/src/status/iter/types.rs index 13585127839..3aa080d0b59 100644 --- a/gix/src/status/iter/types.rs +++ b/gix/src/status/iter/types.rs @@ -84,7 +84,7 @@ impl Outcome { /// Write the changes if there are any back to the index file. /// This can only be done once as the changes are consumed in the process, if there were any. - pub fn write_changes(&mut self) -> Option> { + pub fn write_changes(&mut self) -> Option> { let _span = gix_features::trace::coarse!("gix::status::index_worktree::Outcome::write_changes()"); let changes = self.changes.take()?; let mut index = match &self.worktree_index { @@ -105,10 +105,14 @@ impl Outcome { } } - Some(index.write(crate::index::write::Options { - extensions: Default::default(), - skip_hash: self.skip_hash, - })) + Some( + index + .write(crate::index::write::Options { + extensions: Default::default(), + skip_hash: self.skip_hash, + }) + .map_err(gix_error::Exn::into_error), + ) } } diff --git a/gix/src/worktree/mod.rs b/gix/src/worktree/mod.rs index 0d1e66b3136..2625a4b8dea 100644 --- a/gix/src/worktree/mod.rs +++ b/gix/src/worktree/mod.rs @@ -127,9 +127,7 @@ pub mod open_index { #[error(transparent)] ConfigSkipHash(#[from] crate::config::boolean::Error), #[error(transparent)] - IndexFile(#[from] gix_index::file::init::Error), - #[error(transparent)] - IndexCorrupt(#[from] gix_index::file::verify::Error), + IndexFile(#[from] gix_error::Error), } impl crate::Worktree<'_> { diff --git a/gix/tests/gix/repository/mod.rs b/gix/tests/gix/repository/mod.rs index ef8cc3242f7..bda42521f74 100644 --- a/gix/tests/gix/repository/mod.rs +++ b/gix/tests/gix/repository/mod.rs @@ -83,6 +83,36 @@ mod revision { #[cfg(feature = "index")] mod index { + #[test] + fn missing_shared_index_is_an_error() -> crate::Result { + let (repo, _tmp) = crate::basic_rw_repo()?; + let workdir = repo.workdir().expect("the fixture has a worktree"); + gix_testtools::git(workdir, "update-index --split-index")?; + assert_eq!( + repo.open_index()?.entries().len(), + 1, + "the split index is initially valid" + ); + let shared_index = gix_testtools::git(workdir, "rev-parse --shared-index-path")?; + std::fs::remove_file(workdir.join(shared_index.trim()))?; + + assert!( + !gix_testtools::run_git(workdir, &["ls-files"])?.success(), + "Git rejects a split index whose shared file is missing" + ); + assert!(repo.try_index().is_err(), "the primary index still exists"); + assert!(repo.index_or_empty().is_err(), "a broken index must not become empty"); + assert!( + repo.index_or_load_from_head().is_err(), + "a broken index must not be replaced with HEAD" + ); + assert!( + repo.index_or_load_from_head_or_empty().is_err(), + "neither fallback applies to a broken index" + ); + Ok(()) + } + #[test] fn basics() -> crate::Result { let repo = crate::named_subrepo_opts("make_basic_repo.sh", "unborn", gix::open::Options::isolated())?; diff --git a/gix/tests/gix/repository/open.rs b/gix/tests/gix/repository/open.rs index e8ec65b1c6d..6cef9cc1f0c 100644 --- a/gix/tests/gix/repository/open.rs +++ b/gix/tests/gix/repository/open.rs @@ -213,7 +213,8 @@ fn git_index_file_overrides_the_index_in_the_git_dir() -> crate::Result { #[cfg(feature = "index")] { gix::index::File::from_state(gix::index::State::new(repo.object_hash()), index_file) - .write(Default::default())?; + .write(Default::default()) + .map_err(gix::Exn::into_error)?; assert!( repo.index()?.entries().is_empty(), "the configured index is read, and it's initially empty" @@ -283,7 +284,7 @@ fn git_index_file_receives_writes_while_the_git_dir_index_is_locked() -> crate:: assert!(!index_file.exists()); std::fs::write(repo.git_dir().join("index.lock"), [])?; let mut index = (**repo.index_or_empty()?).clone(); - index.write(Default::default())?; + index.write(Default::default()).map_err(gix::Exn::into_error)?; assert!(index_file.is_file(), "the write lands on the configured index"); assert_eq!( From 46629010370f285187e7bc301e0dcdaaa25f7437 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 20 Aug 2026 17:18:38 +0200 Subject: [PATCH 21/51] change(gix-filter)!: migrate errors to gix-error rubberstamp --- Cargo.lock | 2 +- gix-filter/Cargo.toml | 2 +- gix-filter/src/driver/apply.rs | 59 +++----- gix-filter/src/driver/delayed.rs | 90 +++++------- gix-filter/src/driver/init.rs | 30 +--- gix-filter/src/driver/process/client.rs | 138 +++++++++--------- gix-filter/src/driver/process/server.rs | 96 ++++++------ gix-filter/src/driver/shutdown.rs | 15 +- gix-filter/src/eol/convert_to_git.rs | 34 ++--- gix-filter/src/eol/convert_to_worktree.rs | 11 +- gix-filter/src/ident.rs | 16 +- gix-filter/src/pipeline/convert.rs | 84 ++++------- gix-filter/src/pipeline/util.rs | 11 +- gix-filter/src/worktree/encode_to_git.rs | 36 ++--- gix-filter/src/worktree/encode_to_worktree.rs | 36 ++--- gix-filter/src/worktree/encoding.rs | 12 +- gix-filter/tests/filter/driver.rs | 40 ++++- gix-filter/tests/filter/eol/convert_to_git.rs | 4 +- .../filter/pipeline/convert_to_worktree.rs | 4 + gix-filter/tests/filter/worktree.rs | 13 +- gix-filter/tests/helpers/arrow.rs | 2 +- gix-merge/src/blob/pipeline.rs | 25 ++-- 22 files changed, 330 insertions(+), 430 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 490eff7d2e7..3349e320264 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1916,6 +1916,7 @@ dependencies = [ "encoding_rs", "gix-attributes", "gix-command", + "gix-error", "gix-filter", "gix-hash", "gix-object", @@ -1927,7 +1928,6 @@ dependencies = [ "gix-utils", "gix-worktree", "smallvec", - "thiserror 2.0.18", ] [[package]] diff --git a/gix-filter/Cargo.toml b/gix-filter/Cargo.toml index d0c0816020d..e73ac6754e2 100644 --- a/gix-filter/Cargo.toml +++ b/gix-filter/Cargo.toml @@ -52,7 +52,7 @@ gix-attributes = { version = "^0.35.0", path = "../gix-attributes" } encoding_rs = "0.8.32" bstr = { version = "1.12.0", default-features = false, features = ["std"] } -thiserror = "2.0.18" +gix-error = { version = "^0.3.0", path = "../gix-error" } smallvec = "1.15.1" diff --git a/gix-filter/src/driver/apply.rs b/gix-filter/src/driver/apply.rs index 931e5bf30c1..f2643a92967 100644 --- a/gix-filter/src/driver/apply.rs +++ b/gix-filter/src/driver/apply.rs @@ -4,7 +4,7 @@ use bstr::{BStr, BString}; use crate::{ Driver, driver, - driver::{Operation, Process, State, process, process::client::invoke}, + driver::{Operation, Process, State, process}, }; /// What to do if delay is supported by a process filter. @@ -24,26 +24,7 @@ pub enum Delay { } /// The error returned by [State::apply()][super::State::apply()]. -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] -pub enum Error { - #[error(transparent)] - Init(#[from] driver::init::Error), - #[error("Could not write entire object to driver")] - WriteSource(#[from] std::io::Error), - #[error("Filter process delayed an entry even though that was not requested")] - DelayNotAllowed, - #[error("Failed to invoke '{command}' command")] - ProcessInvoke { - source: process::client::invoke::Error, - command: String, - }, - #[error("The invoked command '{command}' in process indicated an error: {status:?}")] - ProcessStatus { - status: driver::process::Status, - command: String, - }, -} +pub type Error = gix_error::Exn; /// Additional information for use in the [`State::apply()`] method. #[derive(Debug, Copy, Clone)] @@ -108,7 +89,12 @@ impl State { delay: Delay, ctx: Context<'_, '_>, ) -> Result>, Error> { - match self.maybe_launch_process(driver, operation, ctx.rela_path)? { + use gix_error::{ErrorExt, ResultExt, message}; + + match self + .maybe_launch_process(driver, operation, ctx.rela_path) + .or_erased()? + { Some(Process::SingleFile { mut child, command }) => { // To avoid deadlock when the filter immediately echoes input to output (like `cat`), // we need to write to stdin and read from stdout concurrently. If we write all data @@ -128,12 +114,14 @@ impl State { // (`convert.c::filter_buffer_or_fd()`), avoiding an input-sized allocation in that // case. Find a way to similarly pump the borrowed reader concurrently. let mut input_data = Vec::new(); - std::io::copy(src, &mut input_data)?; + std::io::copy(src, &mut input_data) + .or_raise_erased(|| message("Could not write entire object to driver"))?; let stdin = child.stdin.take().expect("configured"); let input_data: Arc<[u8]> = input_data.into(); let fallback = (!driver.required).then(|| Arc::clone(&input_data)); - let write_thread = WriterThread::write_all_in_background(input_data, stdin)?; + let write_thread = WriterThread::write_all_in_background(input_data, stdin) + .or_raise_erased(|| message("Could not write entire object to driver"))?; Ok(Some(MaybeDelayed::Immediate(Box::new(ReadFilterOutput { inner: child.stdout.take(), @@ -171,18 +159,19 @@ impl State { let status = match invoke_result { Ok(status) => status, Err(err) => { - let invoke::Error::Io(io_err) = &err; - handle_io_err(io_err, &mut self.running, key.0.as_ref()); - return Err(Error::ProcessInvoke { - command: command.into(), - source: err, - }); + if let Some(io_err) = err.downcast_any_ref::() { + handle_io_err(io_err, &mut self.running, key.0.as_ref()); + } + return Err(err.raise(message!("Failed to invoke '{command}' command")).erased()); } }; if status.is_delayed() { if matches!(delay, Delay::Forbid) { - return Err(Error::DelayNotAllowed); + return Err( + message("Filter process delayed an entry even though that was not requested") + .raise_erased(), + ); } Ok(Some(MaybeDelayed::Delayed(key))) } else if status.is_success() { @@ -204,10 +193,10 @@ impl State { client.into_child().kill().ok(); } } - Err(Error::ProcessStatus { - command: command.into(), - status, - }) + Err( + message!("The invoked command '{command}' in process indicated an error: {status:?}") + .raise_erased(), + ) } } None => Ok(None), diff --git a/gix-filter/src/driver/delayed.rs b/gix-filter/src/driver/delayed.rs index c33e1eee114..fc38257cd77 100644 --- a/gix-filter/src/driver/delayed.rs +++ b/gix-filter/src/driver/delayed.rs @@ -7,42 +7,14 @@ use crate::{ /// pub mod list { - use crate::driver; - /// The error returned by [State::list_delayed_paths()][super::State::list_delayed_paths()]. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error("Could not get process named '{}' which should be running and tracked", wanted.0)] - ProcessMissing { wanted: driver::Key }, - #[error("Failed to run 'list_available_blobs' command")] - ProcessInvoke(#[from] driver::process::client::invoke::without_content::Error), - #[error("The invoked command 'list_available_blobs' in process indicated an error: {status:?}")] - ProcessStatus { status: driver::process::Status }, - } + pub type Error = gix_error::Exn; } /// pub mod fetch { - use crate::driver; - /// The error returned by [State::fetch_delayed()][super::State::fetch_delayed()]. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error("Could not get process named '{}' which should be running and tracked", wanted.0)] - ProcessMissing { wanted: driver::Key }, - #[error("Failed to run '{command}' command")] - ProcessInvoke { - command: String, - source: driver::process::client::invoke::Error, - }, - #[error("The invoked command '{command}' in process indicated an error: {status:?}")] - ProcessStatus { - status: driver::process::Status, - command: String, - }, - } + pub type Error = gix_error::Exn; } /// Operations related to delayed filtering. @@ -58,12 +30,14 @@ impl State { /// Usually if the process sends the "abort" status, we will not use a certain capability again. Here it's unclear what capability /// that is and what to do, so we leave the process running and do nothing else (just like `git`). pub fn list_delayed_paths(&mut self, process: &driver::Key) -> Result, list::Error> { - let client = self - .running - .get_mut(&process.0) - .ok_or_else(|| list::Error::ProcessMissing { - wanted: process.clone(), - })?; + use gix_error::{ErrorExt, OptionExt, message}; + + let client = self.running.get_mut(&process.0).ok_or_raise(|| { + message!( + "Could not get process named '{}' which should be running and tracked", + process.0 + ) + })?; let mut out = Vec::new(); let result = client.invoke_without_content("list_available_blobs", &mut None.into_iter(), &mut |line| { @@ -74,10 +48,10 @@ impl State { let status = match result { Ok(res) => res, Err(err) => { - if let driver::process::client::invoke::without_content::Error::Io(err) = &err { - handle_io_err(err, &mut self.running, process.0.as_ref()); + if let Some(io_err) = err.downcast_any_ref::() { + handle_io_err(io_err, &mut self.running, process.0.as_ref()); } - return Err(err.into()); + return Err(err.raise(message("Failed to run 'list_available_blobs' command"))); } }; @@ -92,7 +66,10 @@ impl State { client.into_child().kill().ok(); } } - Err(list::Error::ProcessStatus { status }) + Err( + message!("The invoked command 'list_available_blobs' in process indicated an error: {status:?}") + .raise(), + ) } } @@ -105,12 +82,14 @@ impl State { path: &BStr, operation: Operation, ) -> Result { - let client = self - .running - .get_mut(&process.0) - .ok_or_else(|| fetch::Error::ProcessMissing { - wanted: process.clone(), - })?; + use gix_error::{ErrorExt, OptionExt, message}; + + let client = self.running.get_mut(&process.0).ok_or_raise(|| { + message!( + "Could not get process named '{}' which should be running and tracked", + process.0 + ) + })?; let result = client.invoke( operation.as_str(), @@ -120,12 +99,10 @@ impl State { let status = match result { Ok(status) => status, Err(err) => { - let driver::process::client::invoke::Error::Io(io_err) = &err; - handle_io_err(io_err, &mut self.running, process.0.as_ref()); - return Err(fetch::Error::ProcessInvoke { - command: operation.as_str().into(), - source: err, - }); + if let Some(io_err) = err.downcast_any_ref::() { + handle_io_err(io_err, &mut self.running, process.0.as_ref()); + } + return Err(err.raise(message!("Failed to run '{}' command", operation.as_str()))); } }; if status.is_success() { @@ -147,10 +124,11 @@ impl State { client.into_child().kill().ok(); } } - Err(fetch::Error::ProcessStatus { - command: operation.as_str().into(), - status, - }) + Err(message!( + "The invoked command '{}' in process indicated an error: {status:?}", + operation.as_str() + ) + .raise()) } } } diff --git a/gix-filter/src/driver/init.rs b/gix-filter/src/driver/init.rs index eaa11ad5a3b..176e87e9449 100644 --- a/gix-filter/src/driver/init.rs +++ b/gix-filter/src/driver/init.rs @@ -8,20 +8,7 @@ use crate::{ }; /// The error returned by [State::maybe_launch_process()][super::State::maybe_launch_process()]. -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] -pub enum Error { - #[error("Failed to spawn driver: {command:?}")] - SpawnCommand { - source: std::io::Error, - command: std::process::Command, - }, - #[error("Process handshake with command {command:?} failed")] - ProcessHandshake { - source: process::client::handshake::Error, - command: std::process::Command, - }, -} +pub type Error = gix_error::Exn; /// Lifecycle impl State { @@ -41,12 +28,9 @@ impl State { Some(c) => c, None => { let (child, cmd) = spawn_driver(process.clone(), &self.context)?; - process::Client::handshake(child, "git-filter", &[2], &["clean", "smudge", "delay"]).map_err( - |err| Error::ProcessHandshake { - source: err, - command: cmd, - }, - )? + use gix_error::{ResultExt, message}; + process::Client::handshake(child, "git-filter", &[2], &["clean", "smudge", "delay"]) + .or_raise(|| message!("Process handshake with command {cmd:?} failed"))? } }; @@ -101,10 +85,8 @@ fn spawn_driver( let child = match cmd.spawn() { Ok(child) => child, Err(err) => { - return Err(Error::SpawnCommand { - source: err, - command: cmd, - }); + use gix_error::ErrorExt; + return Err(err.and_raise(gix_error::message!("Failed to spawn driver: {cmd:?}"))); } }; Ok((child, cmd)) diff --git a/gix-filter/src/driver/process/client.rs b/gix-filter/src/driver/process/client.rs index 40594770753..30bd593773f 100644 --- a/gix-filter/src/driver/process/client.rs +++ b/gix-filter/src/driver/process/client.rs @@ -11,47 +11,18 @@ use crate::driver::{ /// pub mod handshake { /// The error returned by [Client::handshake()][super::Client::handshake()]. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error("Failed to read or write to the process")] - Io(#[from] std::io::Error), - #[error("{msg} '{actual}'")] - Protocol { msg: String, actual: String }, - #[error("The server sent the '{name}' capability which isn't among the ones we desire can support")] - UnsupportedCapability { name: String }, - } + pub type Error = gix_error::Exn; } /// pub mod invoke { /// The error returned by [Client::invoke()][super::Client::invoke()]. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error("Failed to read or write to the process")] - Io(#[from] std::io::Error), - } + pub type Error = gix_error::Exn; /// pub mod without_content { /// The error returned by [Client::invoke_without_content()][super::super::Client::invoke_without_content()]. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error("Failed to read or write to the process")] - Io(#[from] std::io::Error), - #[error(transparent)] - PacketlineDecode(#[from] gix_packetline::decode::Error), - } - - impl From for Error { - fn from(value: super::Error) -> Self { - match value { - super::Error::Io(err) => Error::Io(err), - } - } - } + pub type Error = gix_error::Exn; } } @@ -66,13 +37,18 @@ impl Client { versions: &[usize], desired_capabilities: &[&str], ) -> Result { + use gix_error::{ErrorExt, ResultExt, message}; + let mut out = Writer::new(process.stdin.take().expect("configured stdin when spawning")); - out.write_all(format!("{welcome_prefix}-client").as_bytes())?; + out.write_all(format!("{welcome_prefix}-client").as_bytes()) + .or_raise(|| message("Failed to read or write to the process"))?; for version in versions { - out.write_all(format!("version={version}").as_bytes())?; + out.write_all(format!("version={version}").as_bytes()) + .or_raise(|| message("Failed to read or write to the process"))?; } - encode::flush_to_write(out.inner_mut())?; - out.flush()?; + encode::flush_to_write(out.inner_mut()).or_raise(|| message("Failed to read or write to the process"))?; + out.flush() + .or_raise(|| message("Failed to read or write to the process"))?; let mut input = StreamingPeekableIter::new( process.stdout.take().expect("configured stdout when spawning"), @@ -81,56 +57,58 @@ impl Client { ); let mut read = input.as_read(); let mut buf = String::new(); - read.read_line_to_string(&mut buf)?; + read.read_line_to_string(&mut buf) + .or_raise(|| message("Failed to read or write to the process"))?; if buf .strip_prefix(welcome_prefix) .is_none_or(|rest| rest.trim_end() != "-server") { - return Err(handshake::Error::Protocol { - msg: format!("Wanted '{welcome_prefix}-server, got "), - actual: buf, - }); + return Err(message!("Wanted '{welcome_prefix}-server, got '{buf}'").raise()); } buf.clear(); - read.read_line_to_string(&mut buf)?; + read.read_line_to_string(&mut buf) + .or_raise(|| message("Failed to read or write to the process"))?; let chosen_version = match buf .strip_prefix("version=") .and_then(|version| usize::from_str(version.trim_end()).ok()) { Some(version) => version, None => { - return Err(handshake::Error::Protocol { - msg: "Needed 'version=', got ".into(), - actual: buf, - }); + return Err(message!("Needed 'version=', got '{buf}'").raise()); } }; if !versions.contains(&chosen_version) { - return Err(handshake::Error::Protocol { - msg: format!("Server offered {chosen_version}, we only support "), - actual: versions.iter().map(ToString::to_string).collect::>().join(", "), - }); + return Err(message!( + "Server offered {chosen_version}, we only support '{}'", + versions.iter().map(ToString::to_string).collect::>().join(", ") + ) + .raise()); } - if read.read_line_to_string(&mut buf)? != 0 { - return Err(handshake::Error::Protocol { - msg: "expected flush packet, got".into(), - actual: buf, - }); + if read + .read_line_to_string(&mut buf) + .or_raise(|| message("Failed to read or write to the process"))? + != 0 + { + return Err(message!("expected flush packet, got '{buf}'").raise()); } for capability in desired_capabilities { - out.write_all(format!("capability={capability}").as_bytes())?; + out.write_all(format!("capability={capability}").as_bytes()) + .or_raise(|| message("Failed to read or write to the process"))?; } - encode::flush_to_write(out.inner_mut())?; - out.flush()?; + encode::flush_to_write(out.inner_mut()).or_raise(|| message("Failed to read or write to the process"))?; + out.flush() + .or_raise(|| message("Failed to read or write to the process"))?; read.reset_with(&[gix_packetline::PacketLineRef::Flush]); let mut capabilities = HashSet::new(); loop { buf.clear(); - let num_read = read.read_line_to_string(&mut buf)?; + let num_read = read + .read_line_to_string(&mut buf) + .or_raise(|| message("Failed to read or write to the process"))?; if num_read == 0 { break; } @@ -138,7 +116,10 @@ impl Client { Some(cap) => { let cap = cap.trim_end(); if !desired_capabilities.contains(&cap) { - return Err(handshake::Error::UnsupportedCapability { name: cap.into() }); + return Err(message!( + "The server sent the '{cap}' capability which isn't among the ones we desire can support" + ) + .raise()); } capabilities.insert(cap.to_owned()); } @@ -163,11 +144,17 @@ impl Client { meta: &mut dyn Iterator, content: &mut dyn std::io::Read, ) -> Result { + use gix_error::{ResultExt, message}; + self.send_command_and_meta(command, meta)?; - std::io::copy(content, &mut self.input)?; - encode::flush_to_write(self.input.inner_mut())?; - self.input.flush()?; - Ok(self.read_status()?) + std::io::copy(content, &mut self.input).or_raise(|| message("Failed to read or write to the process"))?; + encode::flush_to_write(self.input.inner_mut()) + .or_raise(|| message("Failed to read or write to the process"))?; + self.input + .flush() + .or_raise(|| message("Failed to read or write to the process"))?; + self.read_status() + .or_raise(|| message("Failed to read or write to the process")) } /// Invoke `command` while passing `meta` data, but don't send any content, and return their status. @@ -180,15 +167,21 @@ impl Client { meta: &mut dyn Iterator, inspect_line: &mut dyn FnMut(&BStr), ) -> Result { + use gix_error::{ResultExt, message}; + self.send_command_and_meta(command, meta)?; while let Some(data) = self.out.read_line() { - let line = data??; + let line = data + .or_raise(|| message("Failed to read from the process"))? + .or_raise(|| message("Failed to decode a packet line from the process"))?; if let Some(line) = line.as_text() { inspect_line(line.as_bstr()); } } self.out.reset_with(&[gix_packetline::PacketLineRef::Flush]); - let status = self.read_status()?; + let status = self + .read_status() + .or_raise(|| message("Failed to read or write to the process"))?; Ok(status) } @@ -214,16 +207,23 @@ impl Client { command: &str, meta: &mut dyn Iterator, ) -> Result<(), invoke::Error> { - self.input.write_all(format!("command={command}").as_bytes())?; + use gix_error::{ResultExt, message}; + + self.input + .write_all(format!("command={command}").as_bytes()) + .or_raise(|| message("Failed to read or write to the process"))?; let mut buf = BString::default(); for (key, value) in meta { buf.clear(); buf.push_str(key); buf.push(b'='); buf.push_str(&value); - self.input.write_all(&buf)?; + self.input + .write_all(&buf) + .or_raise(|| message("Failed to read or write to the process"))?; } - encode::flush_to_write(self.input.inner_mut())?; + encode::flush_to_write(self.input.inner_mut()) + .or_raise(|| message("Failed to read or write to the process"))?; Ok(()) } } diff --git a/gix-filter/src/driver/process/server.rs b/gix-filter/src/driver/process/server.rs index 1ce68521d6f..c2eb296c9ef 100644 --- a/gix-filter/src/driver/process/server.rs +++ b/gix-filter/src/driver/process/server.rs @@ -16,34 +16,14 @@ pub struct Request<'a> { /// pub mod next_request { - use bstr::BString; - /// The error returned by [Server::next_request()][super::Server::next_request()]. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error("Failed to read from the client")] - Io(#[from] std::io::Error), - #[error("{msg} '{actual}'")] - Protocol { msg: String, actual: BString }, - #[error(transparent)] - PacketlineDecode(#[from] gix_packetline::decode::Error), - } + pub type Error = gix_error::Exn; } /// pub mod handshake { /// The error returned by [Server::handshake()][super::Server::handshake()]. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error("Failed to read or write to the client")] - Io(#[from] std::io::Error), - #[error("{msg} '{actual}'")] - Protocol { msg: String, actual: String }, - #[error("Could not select supported version from the one sent by the client: {}", actual.iter().map(ToString::to_string).collect::>().join(", "))] - VersionMismatch { actual: Vec }, - } + pub type Error = gix_error::Exn; } impl Server { @@ -64,6 +44,8 @@ impl Server { pick_version: &mut dyn FnMut(&[usize]) -> Option, available_capabilities: &[&str], ) -> Result { + use gix_error::{ErrorExt, OptionExt, ResultExt, message}; + let mut input = StreamingPeekableIter::new( stdin.lock(), &[gix_packetline::PacketLineRef::Flush], @@ -71,21 +53,21 @@ impl Server { ); let mut read = input.as_read(); let mut buf = String::new(); - read.read_line_to_string(&mut buf)?; + read.read_line_to_string(&mut buf) + .or_raise(|| message("Failed to read or write to the client"))?; if buf .strip_prefix(welcome_prefix) .is_none_or(|rest| rest.trim_end() != "-client") { - return Err(handshake::Error::Protocol { - msg: format!("Expected '{welcome_prefix}-client, got"), - actual: buf, - }); + return Err(message!("Expected '{welcome_prefix}-client, got '{buf}'").raise()); } let mut versions = Vec::new(); loop { buf.clear(); - let num_read = read.read_line_to_string(&mut buf)?; + let num_read = read + .read_line_to_string(&mut buf) + .or_raise(|| message("Failed to read or write to the client"))?; if num_read == 0 { break; } @@ -96,26 +78,33 @@ impl Server { { Some(version) => version, None => { - return Err(handshake::Error::Protocol { - msg: "Expected 'version=', got".into(), - actual: buf, - }); + return Err(message!("Expected 'version=', got '{buf}'").raise()); } }, ); } - let version = pick_version(&versions).ok_or(handshake::Error::VersionMismatch { actual: versions })?; + let version = pick_version(&versions).ok_or_raise(|| { + message!( + "Could not select supported version from the one sent by the client: {}", + versions.iter().map(ToString::to_string).collect::>().join(", ") + ) + })?; read.reset_with(&[gix_packetline::PacketLineRef::Flush]); let mut out = Writer::new(stdout.lock()); - out.write_all(format!("{welcome_prefix}-server").as_bytes())?; - out.write_all(format!("version={version}").as_bytes())?; - encode::flush_to_write(out.inner_mut())?; - out.flush()?; + out.write_all(format!("{welcome_prefix}-server").as_bytes()) + .or_raise(|| message("Failed to read or write to the client"))?; + out.write_all(format!("version={version}").as_bytes()) + .or_raise(|| message("Failed to read or write to the client"))?; + encode::flush_to_write(out.inner_mut()).or_raise(|| message("Failed to read or write to the client"))?; + out.flush() + .or_raise(|| message("Failed to read or write to the client"))?; let mut capabilities = HashSet::new(); loop { buf.clear(); - let num_read = read.read_line_to_string(&mut buf)?; + let num_read = read + .read_line_to_string(&mut buf) + .or_raise(|| message("Failed to read or write to the client"))?; if num_read == 0 { break; } @@ -131,10 +120,12 @@ impl Server { } for cap in &capabilities { - out.write_all(format!("capability={cap}").as_bytes())?; + out.write_all(format!("capability={cap}").as_bytes()) + .or_raise(|| message("Failed to read or write to the client"))?; } - encode::flush_to_write(out.inner_mut())?; - out.flush()?; + encode::flush_to_write(out.inner_mut()).or_raise(|| message("Failed to read or write to the client"))?; + out.flush() + .or_raise(|| message("Failed to read or write to the client"))?; drop(read); Ok(Server { @@ -156,42 +147,37 @@ impl Server { /// Note that the process is supposed to shut-down once there are no more requests, and `git` will wait /// until it has finished. pub fn next_request(&mut self) -> Result>, next_request::Error> { + use gix_error::{ErrorExt, OptionExt, ResultExt, message}; + let mut buf = String::new(); let mut read = self.input.as_read(); match read.read_line_to_string(&mut buf) { Ok(_) => {} Err(err) if err.kind() == std::io::ErrorKind::UnexpectedEof => return Ok(None), - Err(err) => return Err(err.into()), + Err(err) => return Err(err.and_raise(message("Failed to read from the client"))), } let command = match buf.strip_prefix("command=").map(str::trim_end).map(ToOwned::to_owned) { Some(cmd) => cmd, None => { - return Err(next_request::Error::Protocol { - msg: "Wanted 'command=', got ".into(), - actual: buf.into(), - }); + return Err(message!("Wanted 'command=', got '{buf}'").raise()); } }; let mut meta = Vec::with_capacity(1); while let Some(res) = read.read_data_line() { - let line = res??; + let line = res + .or_raise(|| message("Failed to read from the client"))? + .or_raise(|| message("Failed to decode packet line"))?; let line = line .as_bstr() - .ok_or_else(|| next_request::Error::Protocol { - msg: "expected data line, got ".into(), - actual: format!("{line:?}").into(), - })? + .ok_or_raise(|| message!("expected data line, got '{line:?}'"))? .trim(); let mut tokens = line.splitn(2, |b| *b == b'='); let (key, value) = tokens .next() .zip(tokens.next()) - .ok_or_else(|| next_request::Error::Protocol { - msg: "Expected 'key=value' metadata, got".into(), - actual: line.into(), - })?; + .ok_or_raise(|| message!("Expected 'key=value' metadata, got '{}'", line.as_bstr()))?; assert!(tokens.next().is_none(), "configured to yield at most two tokens"); meta.push((key.as_bstr().to_string(), value.into())); } diff --git a/gix-filter/src/driver/shutdown.rs b/gix-filter/src/driver/shutdown.rs index cbb85c12d56..0a4c0cbc1e4 100644 --- a/gix-filter/src/driver/shutdown.rs +++ b/gix-filter/src/driver/shutdown.rs @@ -1,4 +1,5 @@ use bstr::BString; +use gix_error::ErrorExt; use crate::driver::State; @@ -10,14 +11,7 @@ pub struct Outcome { } /// A filter process that exited unsuccessfully during shutdown. -#[derive(Debug, thiserror::Error)] -#[error("Filter process {command:?} failed with {status}")] -pub struct Error { - /// The command that launched the process. - pub command: BString, - /// Its unsuccessful exit status. - pub status: std::process::ExitStatus, -} +pub type Error = gix_error::Exn; impl Outcome { /// Return this outcome if all observed processes exited successfully, or the first failure otherwise. @@ -32,10 +26,7 @@ impl Outcome { .filter(|status| !status.success()) .map(|status| (command, status)) }) { - return Err(Error { - command: command.clone(), - status: *status, - }); + return Err(gix_error::message!("Filter process {command:?} failed with {status}").raise()); } Ok(self) } diff --git a/gix-filter/src/eol/convert_to_git.rs b/gix-filter/src/eol/convert_to_git.rs index eb50924a709..328e9f38bda 100644 --- a/gix-filter/src/eol/convert_to_git.rs +++ b/gix-filter/src/eol/convert_to_git.rs @@ -1,4 +1,4 @@ -use std::path::{Path, PathBuf}; +use std::path::Path; /// Additional context for use with [`convert_to_git`][super::convert_to_git()]. #[derive(Default, Copy, Clone)] @@ -27,20 +27,10 @@ pub enum RoundTripCheck<'a> { } /// The error returned by [convert_to_git()][super::convert_to_git()]. -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] -pub enum Error { - #[error("{msg} in '{}'", path.display())] - RoundTrip { msg: &'static str, path: PathBuf }, - #[error("Could not obtain index object to check line endings for")] - FetchObjectFromIndex(#[source] Box), - #[error("Could not allocate buffer")] - OutOfMemory(#[from] std::collections::TryReserveError), -} +pub type Error = gix_error::Exn; /// A function that writes a buffer like `fn(&mut buf)` with by tes of an object in the index that is the one that should be converted. -pub type IndexObjectFn<'a> = - dyn FnMut(&mut Vec) -> Result, Box> + 'a; +pub type IndexObjectFn<'a> = dyn FnMut(&mut Vec) -> Result, gix_error::Exn> + 'a; pub(crate) mod function { use bstr::ByteSlice; @@ -72,6 +62,8 @@ pub(crate) mod function { config, }: Options<'_>, ) -> Result { + use gix_error::{ErrorExt, ResultExt, message}; + if digest == AttributesDigest::Binary || src.is_empty() { return Ok(false); } @@ -84,7 +76,9 @@ pub(crate) mod function { return Ok(false); } - if let Some(()) = index_object(buf).map_err(Error::FetchObjectFromIndex)? { + if let Some(()) = + index_object(buf).or_raise(|| message("Could not obtain index object to check line endings for"))? + { let has_crlf_in_index = buf .find_byte(b'\r') .map(|_| Stats::from_bytes(buf)) @@ -112,10 +106,7 @@ pub(crate) mod function { // CRLF would not be restored by checkout match round_trip_check { RoundTripCheck::Fail { rela_path } => { - return Err(Error::RoundTrip { - msg: "CRLF would be replaced by LF", - path: rela_path.to_owned(), - }); + return Err(message!("CRLF would be replaced by LF in '{}'", rela_path.display()).raise()); } #[allow(unused_variables, reason = "Used when tracing is enabled at compile time.")] RoundTripCheck::Warn { rela_path } => { @@ -129,10 +120,7 @@ pub(crate) mod function { // CRLF would be added by checkout match round_trip_check { RoundTripCheck::Fail { rela_path } => { - return Err(Error::RoundTrip { - msg: "LF would be replaced by CRLF", - path: rela_path.to_owned(), - }); + return Err(message!("LF would be replaced by CRLF in '{}'", rela_path.display()).raise()); } #[allow(unused_variables, reason = "Used when tracing is enabled at compile time.")] RoundTripCheck::Warn { rela_path } => { @@ -149,7 +137,7 @@ pub(crate) mod function { return Ok(false); } - clear_and_set_capacity(buf, src.len() - stats.crlf)?; + clear_and_set_capacity(buf, src.len() - stats.crlf).or_raise(|| message("Could not allocate buffer"))?; if stats.lone_cr == 0 { buf.extend(src.iter().filter(|b| **b != b'\r')); } else { diff --git a/gix-filter/src/eol/convert_to_worktree.rs b/gix-filter/src/eol/convert_to_worktree.rs index 206a4673572..3eb76a8c9aa 100644 --- a/gix-filter/src/eol/convert_to_worktree.rs +++ b/gix-filter/src/eol/convert_to_worktree.rs @@ -6,12 +6,7 @@ use crate::{ }; /// The error produced by [`convert_to_worktree()`]. -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] -pub enum Error { - #[error("Could not allocate buffer")] - OutOfMemory(#[from] std::collections::TryReserveError), -} +pub type Error = gix_error::Exn; /// Convert all `\n` in `src` to `crlf` if `digest` and `config` indicate it, returning `true` if `buf` holds the result, or `false` /// if no change was made after all. @@ -21,6 +16,8 @@ pub fn convert_to_worktree( buf: &mut Vec, config: Configuration, ) -> Result { + use gix_error::{ResultExt, message}; + if src.is_empty() || digest.to_eol(config) != Some(Mode::CrLf) { return Ok(false); } @@ -29,7 +26,7 @@ pub fn convert_to_worktree( return Ok(false); } - clear_and_set_capacity(buf, src.len() + stats.lone_lf)?; + clear_and_set_capacity(buf, src.len() + stats.lone_lf).or_raise(|| message("Could not allocate buffer"))?; let mut ofs = 0; while let Some(pos) = src[ofs..].find_byteset(b"\r\n") { diff --git a/gix-filter/src/ident.rs b/gix-filter/src/ident.rs index 4e0d8573fd9..54cc75e8e94 100644 --- a/gix-filter/src/ident.rs +++ b/gix-filter/src/ident.rs @@ -43,14 +43,7 @@ pub fn undo(src: &[u8], buf: &mut Vec) -> Result; } /// Substitute all occurrences of `$Id$` with `$Id: $` if present in `src` and write all changes to `buf`, @@ -62,15 +55,18 @@ pub mod apply { /// `Git` also tries to cleanup 'stray' substituted `$Id: $`, but we don't do that, sticking exactly to what ought to be done. /// The respective code is up to 16 years old and one might assume that `git` by now handles checking and checkout filters correctly. pub fn apply(src: &[u8], object_hash: gix_hash::Kind, buf: &mut Vec) -> Result { + use gix_error::{ResultExt, message}; + const HASH_LEN: usize = ": ".len() + gix_hash::Kind::longest().len_in_hex(); let mut id = None; let mut ofs = 0; while let Some(pos) = src[ofs..].find(b"$Id$") { let id = match id { None => { - let new_id = gix_object::compute_hash(object_hash, gix_object::Kind::Blob, src)?; + let new_id = gix_object::compute_hash(object_hash, gix_object::Kind::Blob, src) + .or_raise(|| message("Could not hash blob"))?; id = new_id.into(); - clear_and_set_capacity(buf, src.len() + HASH_LEN)?; // pre-allocate for one ID + clear_and_set_capacity(buf, src.len() + HASH_LEN).or_raise(|| message("Could not allocate buffer"))?; // pre-allocate for one ID new_id } Some(id) => id.to_owned(), diff --git a/gix-filter/src/pipeline/convert.rs b/gix-filter/src/pipeline/convert.rs index 8d3615913fe..e48fed9e231 100644 --- a/gix-filter/src/pipeline/convert.rs +++ b/gix-filter/src/pipeline/convert.rs @@ -6,17 +6,8 @@ use crate::{Pipeline, driver, eol, ident, pipeline::util::Configuration, worktre /// pub mod configuration { - use bstr::BString; - /// Errors related to the configuration of filter attributes. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error("The encoding named '{name}' isn't available")] - UnknownEncoding { name: BString }, - #[error("Encodings must be names, like UTF-16, and cannot be booleans.")] - InvalidEncoding, - } + pub type Error = gix_error::ValidationError; } /// @@ -25,22 +16,7 @@ pub mod to_git { pub type IndexObjectFn<'a> = dyn FnMut(&mut Vec) -> Result, gix_object::find::Error> + 'a; /// The error returned by [Pipeline::convert_to_git()][super::Pipeline::convert_to_git()]. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - Eol(#[from] crate::eol::convert_to_git::Error), - #[error(transparent)] - Worktree(#[from] crate::worktree::encode_to_git::Error), - #[error(transparent)] - Driver(#[from] crate::driver::apply::Error), - #[error(transparent)] - Configuration(#[from] super::configuration::Error), - #[error("Copy of driver process output to memory failed")] - ReadProcessOutputToBuffer(#[from] std::io::Error), - #[error("Could not allocate buffer")] - OutOfMemory(#[from] std::collections::TryReserveError), - } + pub type Error = gix_error::Exn; } /// @@ -67,20 +43,7 @@ pub mod to_worktree { } /// The error returned by [Pipeline::convert_to_worktree()][super::Pipeline::convert_to_worktree()]. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - Ident(#[from] crate::ident::apply::Error), - #[error(transparent)] - Eol(#[from] crate::eol::convert_to_worktree::Error), - #[error(transparent)] - Worktree(#[from] crate::worktree::encode_to_worktree::Error), - #[error(transparent)] - Driver(#[from] crate::driver::apply::Error), - #[error(transparent)] - Configuration(#[from] super::configuration::Error), - } + pub type Error = gix_error::Exn; } /// Access @@ -99,6 +62,8 @@ impl Pipeline { where R: std::io::Read, { + use gix_error::{ResultExt, message}; + let bstr_rela_path = gix_path::to_unix_separators_on_windows(gix_path::into_bstr(rela_path)); let Configuration { driver, @@ -113,7 +78,8 @@ impl Pipeline { attributes, self.options.eol_config, false, - )?; + ) + .or_erased()?; let mut in_src_buffer = false; // this is just an approximation, but it's as good as it gets without reading the actual input. @@ -126,7 +92,8 @@ impl Pipeline { round_trip_check: None, config: self.options.eol_config, }, - )?; + ) + .or_erased()?; if let Some(driver) = driver && let Some(mut read) = self.processes.apply( @@ -143,12 +110,14 @@ impl Pipeline { return Ok(ToGitOutcome::Process(read)); } self.bufs.clear(); - read.read_to_end(&mut self.bufs.src)?; + read.read_to_end(&mut self.bufs.src) + .or_raise_erased(|| message("Copy of driver process output to memory failed"))?; in_src_buffer = true; } if !in_src_buffer && (apply_ident_filter || encoding.is_some() || would_convert_eol) { self.bufs.clear(); - src.read_to_end(&mut self.bufs.src)?; + src.read_to_end(&mut self.bufs.src) + .or_raise_erased(|| message("Copy of driver process output to memory failed"))?; in_src_buffer = true; } @@ -162,7 +131,8 @@ impl Pipeline { } else { worktree::encode_to_git::RoundTripCheck::Skip }, - )?; + ) + .or_erased()?; self.bufs.swap(); } @@ -170,16 +140,21 @@ impl Pipeline { &self.bufs.src, digest, &mut self.bufs.dest, - &mut |buf| index_object(buf).map_err(Into::into), + &mut |buf| index_object(buf), eol::convert_to_git::Options { round_trip_check: self.options.crlf_roundtrip_check.to_eol_roundtrip_check(rela_path), config: self.options.eol_config, }, - )? { + ) + .or_erased()? + { self.bufs.swap(); } - if apply_ident_filter && ident::undo(&self.bufs.src, &mut self.bufs.dest)? { + if apply_ident_filter + && ident::undo(&self.bufs.src, &mut self.bufs.dest) + .or_raise_erased(|| message("Could not allocate buffer"))? + { self.bufs.swap(); } Ok(if in_src_buffer { @@ -206,6 +181,8 @@ impl Pipeline { unknown_encoding, }: to_worktree::Options, ) -> Result, to_worktree::Error> { + use gix_error::ResultExt; + let Configuration { driver, digest, @@ -219,16 +196,17 @@ impl Pipeline { attributes, self.options.eol_config, unknown_encoding == to_worktree::UnknownEncoding::Ignore, - )?; + ) + .or_erased()?; let mut bufs = self.bufs.use_foreign_src(src); let (src, dest) = bufs.src_and_dest(); - if apply_ident_filter && ident::apply(src, self.options.object_hash, dest)? { + if apply_ident_filter && ident::apply(src, self.options.object_hash, dest).or_erased()? { bufs.swap(); } let (src, dest) = bufs.src_and_dest(); - if eol::convert_to_worktree(src, digest, dest, self.options.eol_config)? { + if eol::convert_to_worktree(src, digest, dest, self.options.eol_config).or_erased()? { bufs.swap(); } @@ -239,7 +217,9 @@ impl Pipeline { Err(_err) if unknown_encoding == to_worktree::UnknownEncoding::Ignore => { gix_trace::warn!(err = %_err, "Ignoring failed worktree encoding"); } - Err(err) => return Err(err.into()), + Err(err) => { + return Err(err.erased()); + } } } diff --git a/gix-filter/src/pipeline/util.rs b/gix-filter/src/pipeline/util.rs index 77860906b38..a4ad362a255 100644 --- a/gix-filter/src/pipeline/util.rs +++ b/gix-filter/src/pipeline/util.rs @@ -43,7 +43,9 @@ impl<'driver> Configuration<'driver> { ignore_unknown: bool, ) -> Result, configuration::Error> { match attr.assignment.state { - StateRef::Set | StateRef::Unset => Err(configuration::Error::InvalidEncoding), + StateRef::Set | StateRef::Unset => Err(gix_error::ValidationError::new( + "Encodings must be names, like UTF-16, and cannot be booleans.", + )), StateRef::Value(name) => match encoding_rs::Encoding::for_label(name.as_bstr()) { Some(encoding) => Ok({ // The working-tree-encoding is the encoding we have to expect in the working tree. @@ -58,9 +60,10 @@ impl<'driver> Configuration<'driver> { gix_trace::warn!(encoding = %name.as_bstr(), "Ignoring unavailable worktree encoding"); Ok(None) } - None => Err(configuration::Error::UnknownEncoding { - name: name.as_bstr().to_owned(), - }), + None => Err(gix_error::ValidationError::new(format!( + "The encoding named '{}' isn't available", + name.as_bstr() + ))), }, StateRef::Unspecified => Ok(None), } diff --git a/gix-filter/src/worktree/encode_to_git.rs b/gix-filter/src/worktree/encode_to_git.rs index 3937cbeb373..a4b4ae77e84 100644 --- a/gix-filter/src/worktree/encode_to_git.rs +++ b/gix-filter/src/worktree/encode_to_git.rs @@ -8,19 +8,7 @@ pub enum RoundTripCheck { } /// The error returned by [`encode_to_git()][super::encode_to_git()]. -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] -pub enum Error { - #[error("Cannot convert input of {input_len} bytes to UTF-8 without overflowing")] - Overflow { input_len: usize }, - #[error("The input was malformed and could not be decoded as '{encoding}'")] - Malformed { encoding: &'static str }, - #[error("Encoding from '{src_encoding}' to '{dest_encoding}' and back is not the same")] - RoundTrip { - src_encoding: &'static str, - dest_encoding: &'static str, - }, -} +pub type Error = gix_error::ValidationError; pub(crate) mod function { use encoding_rs::DecoderResult; @@ -38,7 +26,12 @@ pub(crate) mod function { let mut decoder = src_encoding.new_decoder_with_bom_removal(); let buf_len = decoder .max_utf8_buffer_length_without_replacement(src.len()) - .ok_or(Error::Overflow { input_len: src.len() })?; + .ok_or_else(|| { + gix_error::ValidationError::new(format!( + "Cannot convert input of {} bytes to UTF-8 without overflowing", + src.len() + )) + })?; buf.clear(); buf.resize(buf_len, 0); let (res, read, written) = decoder.decode_to_utf8_without_replacement(src, buf, true); @@ -55,9 +48,10 @@ pub(crate) mod function { unreachable!("we assure that the output buffer is big enough as per the encoder's estimate") } DecoderResult::Malformed(_, _) => { - return Err(Error::Malformed { - encoding: src_encoding.name(), - }); + return Err(gix_error::ValidationError::new(format!( + "The input was malformed and could not be decoded as '{}'", + src_encoding.name() + ))); } } @@ -68,10 +62,10 @@ pub(crate) mod function { let str = unsafe { std::str::from_utf8_unchecked(buf) }; let (should_equal_src, _actual_encoding, _had_errors) = src_encoding.encode(str); if should_equal_src != src { - return Err(Error::RoundTrip { - src_encoding: src_encoding.name(), - dest_encoding: "UTF-8", - }); + return Err(gix_error::ValidationError::new(format!( + "Encoding from '{}' to 'UTF-8' and back is not the same", + src_encoding.name() + ))); } } RoundTripCheck::Skip => {} diff --git a/gix-filter/src/worktree/encode_to_worktree.rs b/gix-filter/src/worktree/encode_to_worktree.rs index cd5915c44e1..a8bd0b88e46 100644 --- a/gix-filter/src/worktree/encode_to_worktree.rs +++ b/gix-filter/src/worktree/encode_to_worktree.rs @@ -1,17 +1,5 @@ /// The error returned by [`encode_to_worktree()][super::encode_to_worktree()]. -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] -pub enum Error { - #[error("Cannot convert input of {input_len} UTF-8 bytes to target encoding without overflowing")] - Overflow { input_len: usize }, - #[error("Input was not UTF-8 encoded")] - InputAsUtf8(#[from] std::str::Utf8Error), - #[error("The character '{character}' could not be mapped to the {worktree_encoding}")] - Unmappable { - character: char, - worktree_encoding: &'static str, - }, -} +pub type Error = gix_error::Exn; pub(crate) mod function { use encoding_rs::EncoderResult; @@ -26,15 +14,22 @@ pub(crate) mod function { worktree_encoding: &'static encoding_rs::Encoding, buf: &mut Vec, ) -> Result<(), Error> { + use gix_error::{ErrorExt, ResultExt}; + let mut encoder = worktree_encoding.new_encoder(); let buf_len = encoder .max_buffer_length_from_utf8_if_no_unmappables(src_utf8.len()) - .ok_or(Error::Overflow { - input_len: src_utf8.len(), + .ok_or_else(|| { + gix_error::ValidationError::new(format!( + "Cannot convert input of {} UTF-8 bytes to target encoding without overflowing", + src_utf8.len() + )) + .raise() })?; buf.clear(); buf.resize(buf_len, 0); - let src = std::str::from_utf8(src_utf8)?; + let src = std::str::from_utf8(src_utf8) + .or_raise(|| gix_error::ValidationError::new("Input was not UTF-8 encoded"))?; let (res, read, written) = encoder.encode_from_utf8_without_replacement(src, buf, true); match res { EncoderResult::InputEmpty => { @@ -49,10 +44,11 @@ pub(crate) mod function { unreachable!("we assure that the output buffer is big enough as per the encoder's estimate") } EncoderResult::Unmappable(c) => { - return Err(Error::Unmappable { - worktree_encoding: worktree_encoding.name(), - character: c, - }); + return Err(gix_error::ValidationError::new(format!( + "The character '{c}' could not be mapped to the {}", + worktree_encoding.name() + )) + .raise()); } } Ok(()) diff --git a/gix-filter/src/worktree/encoding.rs b/gix-filter/src/worktree/encoding.rs index ae1dbc2062a..9bf302d214a 100644 --- a/gix-filter/src/worktree/encoding.rs +++ b/gix-filter/src/worktree/encoding.rs @@ -3,15 +3,8 @@ use encoding_rs::Encoding; /// pub mod for_label { - use bstr::BString; - /// The error returned by [for_label()][super::for_label()]. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error("An encoding named '{name}' is not known")] - Unknown { name: BString }, - } + pub type Error = gix_error::ValidationError; } /// Try to produce a new `Encoding` for `label` or report an error if it is not known. @@ -26,6 +19,7 @@ pub fn for_label<'a>(label: impl Into<&'a BStr>) -> Result<&'static Encoding, fo if label == "latin-1" { label = "ISO-8859-1".into(); } - let enc = Encoding::for_label(label.as_ref()).ok_or_else(|| for_label::Error::Unknown { name: label.into() })?; + let enc = Encoding::for_label(label.as_ref()) + .ok_or_else(|| gix_error::ValidationError::new(format!("An encoding named '{label}' is not known")))?; Ok(enc) } diff --git a/gix-filter/tests/filter/driver.rs b/gix-filter/tests/filter/driver.rs index cc5ae421bd4..8ae8ea1f78e 100644 --- a/gix-filter/tests/filter/driver.rs +++ b/gix-filter/tests/filter/driver.rs @@ -89,8 +89,11 @@ mod shutdown { let outcome = state.shutdown(Mode::WaitForProcesses)?; assert_eq!(outcome.processes.len(), 1, "we only launch one process"); + let (command, status) = &outcome.processes[0]; + let status = status.expect("waiting for the process records its exit status"); + let expected = format!("Filter process {command:?} failed with {status}"); let err = outcome.into_result().expect_err("the non-zero exit status is an error"); - assert!(!err.status.success(), "the failed status is retained"); + assert_eq!(err, expected, "the failed command and status are retained"); Ok(()) } @@ -182,9 +185,10 @@ pub(crate) mod apply { Ok(_) => panic!("expecting an error as invalid context was passed"), Err(err) => err, }; - assert!( - matches!(err, gix_filter::driver::apply::Error::ProcessInvoke { .. }), - "{err:?}: cannot invoke if failure is requested" + assert_eq!( + err.to_string(), + "Failed to invoke 'smudge' command", + "cannot invoke if failure is requested" ); let mut filtered = state @@ -213,8 +217,18 @@ pub(crate) mod apply { .invoke("next-smudge-aborts", &mut None.into_iter(), &mut &b""[..])? .is_success() ); - assert!( - matches!(state.apply(&driver, &mut std::io::empty(), Operation::Smudge, context_from_path("any")), Err(driver::apply::Error::ProcessStatus {status: driver::process::Status::Named(name), ..}) if name == "abort") + let err = state + .apply( + &driver, + &mut std::io::empty(), + Operation::Smudge, + context_from_path("any"), + ) + .err() + .expect("the process reports its requested abort status"); + assert_eq!( + err.to_string(), + "The invoked command 'smudge' in process indicated an error: Named(\"abort\")" ); assert!( state @@ -245,8 +259,18 @@ pub(crate) mod apply { )? .is_success() ); - assert!( - matches!(state.apply(&driver, &mut std::io::empty(), Operation::Smudge, context_from_path("any")), Err(driver::apply::Error::ProcessStatus {status: driver::process::Status::Named(name), ..}) if name == "send-term-signal") + let err = state + .apply( + &driver, + &mut std::io::empty(), + Operation::Smudge, + context_from_path("any"), + ) + .err() + .expect("the process reports its requested failure status"); + assert_eq!( + err.to_string(), + "The invoked command 'smudge' in process indicated an error: Named(\"send-term-signal\")" ); let mut filtered = state .apply(&driver, &mut &b"hi\n"[..], Operation::Smudge, context_from_path("any"))? diff --git a/gix-filter/tests/filter/eol/convert_to_git.rs b/gix-filter/tests/filter/eol/convert_to_git.rs index 466ab322737..ae05127abc8 100644 --- a/gix-filter/tests/filter/eol/convert_to_git.rs +++ b/gix-filter/tests/filter/eol/convert_to_git.rs @@ -190,10 +190,10 @@ fn round_trip_check() -> crate::Result { Ok(()) } -fn no_call(_buf: &mut Vec) -> Result, Box> { +fn no_call(_buf: &mut Vec) -> Result, gix_error::Exn> { unreachable!("index function will not be called") } -fn no_object_in_index(_buf: &mut Vec) -> Result, Box> { +fn no_object_in_index(_buf: &mut Vec) -> Result, gix_error::Exn> { Ok(None) } diff --git a/gix-filter/tests/filter/pipeline/convert_to_worktree.rs b/gix-filter/tests/filter/pipeline/convert_to_worktree.rs index 38940492dfe..b89a669df81 100644 --- a/gix-filter/tests/filter/pipeline/convert_to_worktree.rs +++ b/gix-filter/tests/filter/pipeline/convert_to_worktree.rs @@ -225,5 +225,9 @@ fn unknown_encoding_can_be_an_error() -> gix_testtools::Result { .err() .expect("unknown encodings can be rejected explicitly"); assert_eq!(err.to_string(), "The encoding named 'not-an-encoding' isn't available"); + assert!( + err.downcast_any_ref::().is_some(), + "unknown encodings are validation failures" + ); Ok(()) } diff --git a/gix-filter/tests/filter/worktree.rs b/gix-filter/tests/filter/worktree.rs index 99762087319..1f309f5a14c 100644 --- a/gix-filter/tests/filter/worktree.rs +++ b/gix-filter/tests/filter/worktree.rs @@ -14,10 +14,7 @@ mod encoding { fn utf32_is_not_supported() { for enc in ["UTF-32BE", "UTF-32LE", "UTF-32", "UTF-32LE-BOM", "UTF-32BE-BOM"] { assert!( - matches!( - worktree::encoding::for_label(enc).unwrap_err(), - worktree::encoding::for_label::Error::Unknown { .. } - ), + worktree::encoding::for_label(enc).unwrap_err().message.contains(enc), "it's not needed for the web and this crate is meant for use in firefox" ); } @@ -43,10 +40,10 @@ mod encoding { fn various_utf_16_with_bom_suffix_are_unsupported() { for label in ["UTF-16BE-BOM", "UTF-16LE-BOM"] { assert!( - matches!( - worktree::encoding::for_label(label).unwrap_err(), - worktree::encoding::for_label::Error::Unknown { .. } - ), + worktree::encoding::for_label(label) + .unwrap_err() + .message + .contains(label), "git supports these and has special handling, but we have not for now. Git has no tests for that either." ); } diff --git a/gix-filter/tests/helpers/arrow.rs b/gix-filter/tests/helpers/arrow.rs index 98a8e859538..3feab639df3 100644 --- a/gix-filter/tests/helpers/arrow.rs +++ b/gix-filter/tests/helpers/arrow.rs @@ -9,7 +9,7 @@ use gix_filter::driver::process; /// Visible marker used to demonstrate clean/smudge round trips in filter-driver tests. static PREFIX: &str = "➡"; -fn main() -> Result<(), Box> { +fn main() -> Result<(), Box> { let mut args = std::env::args(); let sub_command = args.nth(1).ok_or("Need sub-command")?; let next_arg = args.next(); // possibly %f diff --git a/gix-merge/src/blob/pipeline.rs b/gix-merge/src/blob/pipeline.rs index 61de3575787..38d22766f34 100644 --- a/gix-merge/src/blob/pipeline.rs +++ b/gix-merge/src/blob/pipeline.rs @@ -133,9 +133,7 @@ pub mod convert_to_mergeable { #[error(transparent)] FindObject(#[from] gix_error::Error), #[error(transparent)] - ConvertToWorktree(#[from] gix_filter::pipeline::convert::to_worktree::Error), - #[error(transparent)] - ConvertToGit(#[from] gix_filter::pipeline::convert::to_git::Error), + ConvertToWorktree(gix_error::Error), #[error("Memory allocation failed")] OutOfMemory(#[from] TryReserveError), } @@ -282,15 +280,18 @@ impl Pipeline { if convert == Mode::Renormalize { { - let res = self.filter.convert_to_worktree( - out, - rela_path, - attributes, - to_worktree::Options { - can_delay: Delay::Forbid, - unknown_encoding: to_worktree::UnknownEncoding::Fail, - }, - )?; + let res = self + .filter + .convert_to_worktree( + out, + rela_path, + attributes, + to_worktree::Options { + can_delay: Delay::Forbid, + unknown_encoding: to_worktree::UnknownEncoding::Fail, + }, + ) + .map_err(|err| convert_to_mergeable::Error::ConvertToWorktree(err.into_error()))?; match res { ToWorktreeOutcome::Unchanged(_) => {} From 7d42d81eb3463b6e64142cb8306d8d176124b8d7 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 20 Aug 2026 17:20:10 +0200 Subject: [PATCH 22/51] change(gix-merge)!: migrate errors to gix-error rubberstamp --- Cargo.lock | 1 - gix-merge/Cargo.toml | 1 - gix-merge/fuzz/Cargo.toml | 1 + gix-merge/fuzz/fuzz_targets/tree.rs | 2 +- gix-merge/src/blob/pipeline.rs | 140 ++++++++----------- gix-merge/src/blob/platform/merge.rs | 119 +++++++--------- gix-merge/src/blob/platform/prepare_merge.rs | 38 +++-- gix-merge/src/blob/platform/set_resource.rs | 61 +++----- gix-merge/src/commit/function.rs | 31 ++-- gix-merge/src/commit/mod.rs | 26 +--- gix-merge/src/commit/virtual_merge_base.rs | 61 ++++---- gix-merge/src/tree/function/change.rs | 6 +- gix-merge/src/tree/function/resolve.rs | 16 +-- gix-merge/src/tree/mod.rs | 29 +--- gix-merge/src/tree/utils.rs | 58 ++++---- gix-merge/tests/merge/blob/pipeline.rs | 15 +- gix-merge/tests/merge/blob/platform.rs | 2 +- 17 files changed, 251 insertions(+), 356 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3349e320264..436f7a2b556 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2127,7 +2127,6 @@ dependencies = [ "pretty_assertions", "serde", "termtree", - "thiserror 2.0.18", ] [[package]] diff --git a/gix-merge/Cargo.toml b/gix-merge/Cargo.toml index 99475ecb461..311994b01d4 100644 --- a/gix-merge/Cargo.toml +++ b/gix-merge/Cargo.toml @@ -45,7 +45,6 @@ gix-index = { version = "^0.55.0", path = "../gix-index" } gix-error = { version = "^0.3.0", path = "../gix-error" } imara-diff = { package = "gix-imara-diff", version = "^0.2.5", path = "../gix-imara-diff" } -thiserror = "2.0.18" bstr = { version = "1.12.0", default-features = false } nonempty = "0.12.0" serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"] } diff --git a/gix-merge/fuzz/Cargo.toml b/gix-merge/fuzz/Cargo.toml index 736d6d57e7e..ba4117e10fa 100644 --- a/gix-merge/fuzz/Cargo.toml +++ b/gix-merge/fuzz/Cargo.toml @@ -14,6 +14,7 @@ libfuzzer-sys = "0.4" arbitrary = { version = "1.3.2", features = ["derive"] } imara-diff = { package = "gix-imara-diff", version = "0.2.0", path = "../../gix-imara-diff" } gix-diff = { path = "../../gix-diff" } +gix-error = { path = "../../gix-error" } gix-filter = { path = "../../gix-filter" } gix-hash = { path = "../../gix-hash", features = ["sha1"] } gix-merge = { path = "..", features = ["sha1"] } diff --git a/gix-merge/fuzz/fuzz_targets/tree.rs b/gix-merge/fuzz/fuzz_targets/tree.rs index 3699ef75862..54aff409b28 100644 --- a/gix-merge/fuzz/fuzz_targets/tree.rs +++ b/gix-merge/fuzz/fuzz_targets/tree.rs @@ -289,7 +289,7 @@ fn fuzz(data: &[u8]) { Ok(outcome) => outcome, // Resolving a binary add/add conflict with its absent ancestor cannot // produce a resource. This is a valid configuration-dependent error. - Err(gix_merge::tree::Error::MergeResourceNotFound) => continue, + Err(err) if err.downcast_any_ref::().is_some() => continue, Err(err) => panic!("generated trees and objects are valid: {err:?}"), }; outcome diff --git a/gix-merge/src/blob/pipeline.rs b/gix-merge/src/blob/pipeline.rs index 38d22766f34..34caf9db933 100644 --- a/gix-merge/src/blob/pipeline.rs +++ b/gix-merge/src/blob/pipeline.rs @@ -4,12 +4,11 @@ use std::{ }; use bstr::BStr; -use gix_error::{ErrorExt, NotFoundError}; use gix_filter::{ driver::apply::{Delay, MaybeDelayed}, pipeline::convert::{ToGitOutcome, ToWorktreeOutcome, to_worktree}, }; -use gix_object::{FindExt, tree::EntryKind}; +use gix_object::tree::EntryKind; use super::{Pipeline, ResourceKind}; @@ -113,36 +112,8 @@ pub enum Data { /// pub mod convert_to_mergeable { - use std::collections::TryReserveError; - - use bstr::BString; - use gix_object::tree::EntryKind; - /// The error returned by [Pipeline::convert_to_mergeable()](super::Pipeline::convert_to_mergeable()). - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error("Entry at '{rela_path}' must be regular file or symlink, but was {actual:?}")] - InvalidEntryKind { rela_path: BString, actual: EntryKind }, - #[error("Entry at '{rela_path}' could not be read as symbolic link")] - ReadLink { rela_path: BString, source: std::io::Error }, - #[error("Entry at '{rela_path}' could not be opened for reading or read from")] - OpenOrRead { rela_path: BString, source: std::io::Error }, - #[error("Entry at '{rela_path}' could not be copied from a filter process to a memory buffer")] - StreamCopy { rela_path: BString, source: std::io::Error }, - #[error(transparent)] - FindObject(#[from] gix_error::Error), - #[error(transparent)] - ConvertToWorktree(gix_error::Error), - #[error("Memory allocation failed")] - OutOfMemory(#[from] TryReserveError), - } - - impl From for Error { - fn from(err: gix_object::find::existing_object::Error) -> Self { - Error::FindObject(err.into_error()) - } - } + pub type Error = gix_error::Exn; } /// Conversion @@ -180,11 +151,13 @@ impl Pipeline { convert: Mode, out: &mut Vec, ) -> Result, convert_to_mergeable::Error> { + use gix_error::{ErrorExt, NotFoundError, OptionExt, ResultExt, message}; + if !matches!(mode, EntryKind::Blob | EntryKind::BlobExecutable) { - return Err(convert_to_mergeable::Error::InvalidEntryKind { - rela_path: rela_path.to_owned(), - actual: mode, - }); + return Err(gix_error::ValidationError::new(format!( + "Entry at '{rela_path}' must be regular file or symlink, but was {mode:?}" + )) + .raise_erased()); } out.clear(); @@ -195,11 +168,8 @@ impl Pipeline { self.path.push(gix_path::from_bstr(rela_path)); let size_in_bytes = (self.options.large_file_threshold_bytes > 0) .then(|| { - none_if_missing(self.path.metadata().map(|md| md.len())).map_err(|err| { - convert_to_mergeable::Error::OpenOrRead { - rela_path: rela_path.to_owned(), - source: err, - } + none_if_missing(self.path.metadata().map(|md| md.len())).or_raise_erased(|| { + message!("Entry at '{rela_path}' could not be opened for reading or read from") }) }) .transpose()?; @@ -207,11 +177,8 @@ impl Pipeline { Some(None) => None, // missing as identified by the size check Some(Some(size)) if size > self.options.large_file_threshold_bytes => Some(Data::TooLarge { size }), _ => { - let file = none_if_missing(std::fs::File::open(&self.path)).map_err(|err| { - convert_to_mergeable::Error::OpenOrRead { - rela_path: rela_path.to_owned(), - source: err, - } + let file = none_if_missing(std::fs::File::open(&self.path)).or_raise_erased(|| { + message!("Entry at '{rela_path}' could not be opened for reading or read from") })?; if let Some(file) = file { @@ -232,24 +199,23 @@ impl Pipeline { match res { ToGitOutcome::Unchanged(mut file) => { - file.read_to_end(out).map_err(|err| { - convert_to_mergeable::Error::OpenOrRead { - rela_path: rela_path.to_owned(), - source: err, - } + file.read_to_end(out).or_raise_erased(|| { + message!( + "Entry at '{rela_path}' could not be opened for reading or read from" + ) })?; } ToGitOutcome::Process(mut stream) => { - stream.read_to_end(out).map_err(|err| { - convert_to_mergeable::Error::OpenOrRead { - rela_path: rela_path.to_owned(), - source: err, - } + stream.read_to_end(out).or_raise_erased(|| { + message!( + "Entry at '{rela_path}' could not be opened for reading or read from" + ) })?; } ToGitOutcome::Buffer(buf) => { out.clear(); - out.try_reserve(buf.len())?; + out.try_reserve(buf.len()) + .or_raise_erased(|| message("Memory allocation failed"))?; out.extend_from_slice(buf); } } @@ -268,44 +234,50 @@ impl Pipeline { let data = if id.is_null() { None } else { - let header = objects.try_header(id)?.ok_or_else(|| { - NotFoundError::new(format!("An object with id {id} could not be found")).raise_erased() - })?; + let header = objects + .try_header(id) + .or_raise_erased(|| message!("Could not find object header for {id}"))? + .ok_or_raise_erased(|| { + NotFoundError::new(format!("An object with id {id} could not be found")) + })?; let is_binary = self.options.large_file_threshold_bytes > 0 && header.size > self.options.large_file_threshold_bytes; let data = if is_binary { Data::TooLarge { size: header.size } } else { - objects.find(id, out)?; + objects + .try_find(id, out) + .or_erased() + .or_raise_erased(|| message!("Could not find object {id}"))? + .ok_or_raise_erased(|| { + NotFoundError::new(format!("An object with id {id} could not be found")) + })?; if convert == Mode::Renormalize { { - let res = self - .filter - .convert_to_worktree( - out, - rela_path, - attributes, - to_worktree::Options { - can_delay: Delay::Forbid, - unknown_encoding: to_worktree::UnknownEncoding::Fail, - }, - ) - .map_err(|err| convert_to_mergeable::Error::ConvertToWorktree(err.into_error()))?; + let res = self.filter.convert_to_worktree( + out, + rela_path, + attributes, + to_worktree::Options { + can_delay: Delay::Forbid, + unknown_encoding: to_worktree::UnknownEncoding::Fail, + }, + )?; match res { ToWorktreeOutcome::Unchanged(_) => {} ToWorktreeOutcome::Buffer(src) => { out.clear(); - out.try_reserve(src.len())?; + out.try_reserve(src.len()) + .or_raise_erased(|| message("Memory allocation failed"))?; out.extend_from_slice(src); } ToWorktreeOutcome::Process(MaybeDelayed::Immediate(mut stream)) => { - std::io::copy(&mut stream, out).map_err(|err| { - convert_to_mergeable::Error::StreamCopy { - rela_path: rela_path.to_owned(), - source: err, - } + std::io::copy(&mut stream, out).or_raise_erased(|| { + message!( + "Entry at '{rela_path}' could not be copied from a filter process to a memory buffer" + ) })?; } ToWorktreeOutcome::Process(MaybeDelayed::Delayed(_)) => { @@ -324,16 +296,14 @@ impl Pipeline { match res { ToGitOutcome::Unchanged(_) => {} ToGitOutcome::Process(mut stream) => { - stream - .read_to_end(out) - .map_err(|err| convert_to_mergeable::Error::OpenOrRead { - rela_path: rela_path.to_owned(), - source: err, - })?; + stream.read_to_end(out).or_raise_erased(|| { + message!("Entry at '{rela_path}' could not be opened for reading or read from") + })?; } ToGitOutcome::Buffer(buf) => { out.clear(); - out.try_reserve(buf.len())?; + out.try_reserve(buf.len()) + .or_raise_erased(|| message("Memory allocation failed"))?; out.extend_from_slice(buf); } } diff --git a/gix-merge/src/blob/platform/merge.rs b/gix-merge/src/blob/platform/merge.rs index 2769962c825..286c9ff26db 100644 --- a/gix-merge/src/blob/platform/merge.rs +++ b/gix-merge/src/blob/platform/merge.rs @@ -16,21 +16,7 @@ pub struct Options { } /// The error returned by [`PlatformRef::merge()`]. -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] -pub enum Error { - #[error(transparent)] - PrepareExternalDriver(#[from] inner::prepare_external_driver::Error), - #[error("Failed to launch external merge driver: {cmd}")] - SpawnExternalDriver { cmd: String, source: std::io::Error }, - #[error("External merge driver failed with non-zero exit status {status:?}: {cmd}")] - ExternalDriverFailure { - status: std::process::ExitStatus, - cmd: String, - }, - #[error("IO failed when dealing with merge-driver output")] - ExternalDriverIO(#[from] std::io::Error), -} +pub type Error = gix_error::Exn; /// The product of a [`PlatformRef::prepare_external_driver()`] operation. /// @@ -76,28 +62,7 @@ pub(super) mod inner { }; /// The error returned by [PlatformRef::prepare_external_driver()](PlatformRef::prepare_external_driver()). - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error("The resource of kind {kind:?} was too large to be processed")] - ResourceTooLarge { kind: ResourceKind }, - #[error( - "Tempfile to store content of '{rela_path}' ({kind:?}) for passing to external merge command could not be created" - )] - CreateTempfile { - rela_path: BString, - kind: ResourceKind, - source: std::io::Error, - }, - #[error( - "Could not write content of '{rela_path}' ({kind:?}) to tempfile for passing to external merge command" - )] - WriteTempfile { - rela_path: BString, - kind: ResourceKind, - source: std::io::Error, - }, - } + pub type Error = gix_error::Exn; /// Plumbing impl<'parent> PlatformRef<'parent> { @@ -125,6 +90,8 @@ pub(super) mod inner { }: builtin_driver::text::Labels<'_>, context: gix_command::Context, ) -> Result { + use gix_error::{OptionExt, ResultExt, ValidationError, message}; + fn write_data( data: &[u8], directory: &Path, @@ -139,14 +106,23 @@ pub(super) mod inner { Ok((file, path)) } - let base = self.ancestor.data.as_slice().ok_or(Error::ResourceTooLarge { - kind: ResourceKind::CommonAncestorOrBase, + let base = self.ancestor.data.as_slice().ok_or_raise_erased(|| { + ValidationError::new(format!( + "The resource of kind {:?} was too large to be processed", + ResourceKind::CommonAncestorOrBase + )) })?; - let ours = self.current.data.as_slice().ok_or(Error::ResourceTooLarge { - kind: ResourceKind::CurrentOrOurs, + let ours = self.current.data.as_slice().ok_or_raise_erased(|| { + ValidationError::new(format!( + "The resource of kind {:?} was too large to be processed", + ResourceKind::CurrentOrOurs + )) })?; - let theirs = self.other.data.as_slice().ok_or(Error::ResourceTooLarge { - kind: ResourceKind::OtherOrTheirs, + let theirs = self.other.data.as_slice().ok_or_raise_erased(|| { + ValidationError::new(format!( + "The resource of kind {:?} was too large to be processed", + ResourceKind::OtherOrTheirs + )) })?; let tmp_dir = context @@ -154,20 +130,26 @@ pub(super) mod inner { .as_deref() .or(context.git_dir.as_deref()) .unwrap_or(Path::new("")); - let (base_tmp, base_path) = write_data(base, tmp_dir).map_err(|err| Error::CreateTempfile { - rela_path: self.ancestor.rela_path.into(), - kind: ResourceKind::CommonAncestorOrBase, - source: err, + let (base_tmp, base_path) = write_data(base, tmp_dir).or_raise_erased(|| { + message!( + "Tempfile to store content of '{}' ({:?}) for passing to external merge command could not be created", + self.ancestor.rela_path, + ResourceKind::CommonAncestorOrBase + ) })?; - let (ours_tmp, ours_path) = write_data(ours, tmp_dir).map_err(|err| Error::CreateTempfile { - rela_path: self.current.rela_path.into(), - kind: ResourceKind::CurrentOrOurs, - source: err, + let (ours_tmp, ours_path) = write_data(ours, tmp_dir).or_raise_erased(|| { + message!( + "Tempfile to store content of '{}' ({:?}) for passing to external merge command could not be created", + self.current.rela_path, + ResourceKind::CurrentOrOurs + ) })?; - let (theirs_tmp, theirs_path) = write_data(theirs, tmp_dir).map_err(|err| Error::CreateTempfile { - rela_path: self.other.rela_path.into(), - kind: ResourceKind::OtherOrTheirs, - source: err, + let (theirs_tmp, theirs_path) = write_data(theirs, tmp_dir).or_raise_erased(|| { + message!( + "Tempfile to store content of '{}' ({:?}) for passing to external merge command could not be created", + self.other.rela_path, + ResourceKind::OtherOrTheirs + ) })?; let mut cmd = BString::from(Vec::with_capacity(merge_command.len())); @@ -416,21 +398,28 @@ impl<'parent> PlatformRef<'parent> { labels: builtin_driver::text::Labels<'_>, context: &gix_command::Context, ) -> Result<(inner::builtin_merge::Pick, Resolution), Error> { + use gix_error::{ErrorExt, ResultExt, message}; + match self.configured_driver() { Ok(driver) => { - let mut cmd = self.prepare_external_driver(driver.command.clone(), labels, context.clone())?; - let status = cmd.status().map_err(|err| Error::SpawnExternalDriver { - cmd: format!("{:?}", cmd.cmd), - source: err, - })?; + let mut cmd = self + .prepare_external_driver(driver.command.clone(), labels, context.clone()) + .or_raise(|| message("Failed to prepare external merge driver"))?; + let status = cmd + .status() + .or_raise(|| message!("Failed to launch external merge driver: {:?}", cmd.cmd))?; if !status.success() { - return Err(Error::ExternalDriverFailure { - cmd: format!("{:?}", cmd.cmd), - status, - }); + return Err(message!( + "External merge driver failed with non-zero exit status {status:?}: {:?}", + cmd.cmd + ) + .raise()); } out.clear(); - cmd.open_result_file()?.read_to_end(out)?; + cmd.open_result_file() + .or_raise(|| message("IO failed when dealing with merge-driver output"))? + .read_to_end(out) + .or_raise(|| message("IO failed when dealing with merge-driver output"))?; Ok((inner::builtin_merge::Pick::Buffer, Resolution::Complete)) } Err(builtin) => { diff --git a/gix-merge/src/blob/platform/prepare_merge.rs b/gix-merge/src/blob/platform/prepare_merge.rs index 9742b425ca5..c3c79ca0a15 100644 --- a/gix-merge/src/blob/platform/prepare_merge.rs +++ b/gix-merge/src/blob/platform/prepare_merge.rs @@ -1,6 +1,7 @@ use std::{num::NonZeroU8, str::FromStr}; -use bstr::{BStr, BString, ByteSlice}; +use bstr::{BStr, ByteSlice}; +use gix_error::{OptionExt, ResultExt, message}; use gix_filter::attributes; use crate::blob::{ @@ -10,18 +11,7 @@ use crate::blob::{ }; /// The error returned by [Platform::prepare_merge_state()](Platform::prepare_merge()). -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] -pub enum Error { - #[error("The 'current', 'ancestor' or 'other' resource for the merge operation were not set")] - UnsetResource, - #[error("Failed to obtain attributes for {kind:?} resource at '{rela_path}'")] - Attributes { - rela_path: BString, - kind: ResourceKind, - source: std::io::Error, - }, -} +pub type Error = gix_error::Exn; /// Preparation impl Platform { @@ -38,17 +28,25 @@ impl Platform { objects: &impl gix_object::Find, mut options: merge::Options, ) -> Result, Error> { - let current = self.current.as_ref().ok_or(Error::UnsetResource)?; - let ancestor = self.ancestor.as_ref().ok_or(Error::UnsetResource)?; - let other = self.other.as_ref().ok_or(Error::UnsetResource)?; + let current = self.current.as_ref().ok_or_raise(|| { + message("The 'current', 'ancestor' or 'other' resource for the merge operation were not set") + })?; + let ancestor = self.ancestor.as_ref().ok_or_raise(|| { + message("The 'current', 'ancestor' or 'other' resource for the merge operation were not set") + })?; + let other = self.other.as_ref().ok_or_raise(|| { + message("The 'current', 'ancestor' or 'other' resource for the merge operation were not set") + })?; let entry = self .attr_stack .at_entry(current.rela_path.as_bstr(), None, objects) - .map_err(|err| Error::Attributes { - source: err, - kind: ResourceKind::CurrentOrOurs, - rela_path: current.rela_path.clone(), + .or_raise(|| { + message!( + "Failed to obtain attributes for {:?} resource at '{}'", + ResourceKind::CurrentOrOurs, + current.rela_path + ) })?; entry.matching_attributes(&mut self.attrs); let mut attrs = self.attrs.iter_selected(); diff --git a/gix-merge/src/blob/platform/set_resource.rs b/gix-merge/src/blob/platform/set_resource.rs index 35706837040..de33539d132 100644 --- a/gix-merge/src/blob/platform/set_resource.rs +++ b/gix-merge/src/blob/platform/set_resource.rs @@ -1,28 +1,10 @@ -use bstr::{BStr, BString}; +use bstr::BStr; +use gix_error::{ErrorExt, ResultExt, message}; -use crate::blob::{Platform, ResourceKind, pipeline, platform::Resource}; +use crate::blob::{Platform, ResourceKind, platform::Resource}; /// The error returned by [Platform::set_resource](Platform::set_resource). -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] -pub enum Error { - #[error("Can only diff blobs, not {mode:?}")] - InvalidMode { mode: gix_object::tree::EntryKind }, - #[error("Failed to read {kind:?} worktree data from '{rela_path}'")] - Io { - rela_path: BString, - kind: ResourceKind, - source: std::io::Error, - }, - #[error("Failed to obtain attributes for {kind:?} resource at '{rela_path}'")] - Attributes { - rela_path: BString, - kind: ResourceKind, - source: std::io::Error, - }, - #[error(transparent)] - ConvertToMergeable(#[from] pipeline::convert_to_mergeable::Error), -} +pub type Error = gix_error::Exn; /// Preparation impl Platform { @@ -49,16 +31,12 @@ impl Platform { mode, gix_object::tree::EntryKind::Blob | gix_object::tree::EntryKind::BlobExecutable ) { - return Err(Error::InvalidMode { mode }); + return Err(message!("Can only diff blobs, not {mode:?}").raise()); } let entry = self .attr_stack .at_entry(rela_path, None, objects) - .map_err(|err| Error::Attributes { - source: err, - kind, - rela_path: rela_path.to_owned(), - })?; + .or_raise(|| message!("Failed to obtain attributes for {kind:?} resource at '{rela_path}'"))?; let storage = match kind { ResourceKind::OtherOrTheirs => &mut self.other, @@ -67,18 +45,21 @@ impl Platform { }; let mut buf_storage = Vec::new(); - let out = self.filter.convert_to_mergeable( - &id, - mode, - rela_path, - kind, - &mut |_, out| { - let _ = entry.matching_attributes(out); - }, - objects, - self.filter_mode, - storage.as_mut().map_or(&mut buf_storage, |s| &mut s.buffer), - )?; + let out = self + .filter + .convert_to_mergeable( + &id, + mode, + rela_path, + kind, + &mut |_, out| { + let _ = entry.matching_attributes(out); + }, + objects, + self.filter_mode, + storage.as_mut().map_or(&mut buf_storage, |s| &mut s.buffer), + ) + .or_raise(|| message("Failed to convert resource to mergeable form"))?; match storage { None => { diff --git a/gix-merge/src/commit/function.rs b/gix-merge/src/commit/function.rs index 1cccf582e78..31b738dfa27 100644 --- a/gix-merge/src/commit/function.rs +++ b/gix-merge/src/commit/function.rs @@ -1,5 +1,6 @@ use std::borrow::Cow; +use gix_error::{ErrorExt, ResultExt, message}; use gix_object::FindExt; use crate::{ @@ -53,7 +54,8 @@ pub fn commit<'objects>( abbreviate_hash: &mut dyn FnMut(&gix_hash::oid) -> String, options: Options, ) -> Result, Error> { - let merge_bases = gix_revision::merge_base(our_commit, &[their_commit], graph)?; + let merge_bases = gix_revision::merge_base(our_commit, &[their_commit], graph) + .or_raise(|| message("Failed to obtain the merge base between the two commits to be merged"))?; let mut virtual_merge_bases = Vec::new(); let mut state = gix_diff::tree::State::default(); let mut commit_to_tree = @@ -61,12 +63,14 @@ pub fn commit<'objects>( let (merge_base_tree_id, ancestor_name): (_, Cow<'_, str>) = match merge_bases.clone() { Some(base_commit) if base_commit.len() == 1 => ( - commit_to_tree(*base_commit.first())?, + commit_to_tree(*base_commit.first()) + .or_raise(|| message("Could not find ancestor, our or their commit to extract tree from"))?, abbreviate_hash(base_commit.first()).into(), ), Some(base_commits) => { let virtual_base_tree = if options.use_first_merge_base { - commit_to_tree(*base_commits.first())? + commit_to_tree(*base_commits.first()) + .or_raise(|| message("Could not find ancestor, our or their commit to extract tree from"))? } else { let mut base_commits: Vec<_> = base_commits.into(); let first = base_commits.pop().expect("at least two"); @@ -81,7 +85,8 @@ pub fn commit<'objects>( objects, abbreviate_hash, options.tree_merge.clone(), - )?; + ) + .or_raise(|| message("Could not create virtual merge base"))?; virtual_merge_bases = Vec::from(out.virtual_merge_bases); out.tree_id }; @@ -91,10 +96,7 @@ pub fn commit<'objects>( if options.allow_missing_merge_base { (gix_hash::ObjectId::empty_tree(our_commit.kind()), "empty tree".into()) } else { - return Err(Error::NoMergeBase { - our_commit_id: our_commit, - their_commit_id: their_commit, - }); + return Err(message!("No common ancestor between {our_commit} and {their_commit}").raise()); } } }; @@ -104,8 +106,14 @@ pub fn commit<'objects>( labels.ancestor = Some(ancestor_name.as_ref().into()); } - let our_tree_id = objects.find_commit(&our_commit, &mut state.buf1)?.tree(); - let their_tree_id = objects.find_commit(&their_commit, &mut state.buf1)?.tree(); + let our_tree_id = objects + .find_commit(&our_commit, &mut state.buf1) + .or_raise(|| message("Could not find ancestor, our or their commit to extract tree from"))? + .tree(); + let their_tree_id = objects + .find_commit(&their_commit, &mut state.buf1) + .or_raise(|| message("Could not find ancestor, our or their commit to extract tree from"))? + .tree(); let outcome = crate::tree( &merge_base_tree_id, @@ -118,7 +126,8 @@ pub fn commit<'objects>( diff_resource_cache, blob_merge, options.tree_merge, - )?; + ) + .or_raise(|| message("Could not merge trees"))?; Ok(super::Outcome { tree_merge: outcome, diff --git a/gix-merge/src/commit/mod.rs b/gix-merge/src/commit/mod.rs index 9667691bea1..5f288641f58 100644 --- a/gix-merge/src/commit/mod.rs +++ b/gix-merge/src/commit/mod.rs @@ -1,29 +1,5 @@ /// The error returned by [`commit()`](crate::commit()). -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] -pub enum Error { - #[error("Failed to obtain the merge base between the two commits to be merged")] - MergeBase(#[from] gix_revision::merge_base::Error), - #[error(transparent)] - VirtualMergeBase(#[from] virtual_merge_base::Error), - #[error(transparent)] - MergeTree(#[from] crate::tree::Error), - #[error("No common ancestor between {our_commit_id} and {their_commit_id}")] - NoMergeBase { - /// The commit on our side that was to be merged. - our_commit_id: gix_hash::ObjectId, - /// The commit on their side that was to be merged. - their_commit_id: gix_hash::ObjectId, - }, - #[error("Could not find ancestor, our or their commit to extract tree from")] - FindCommit(#[source] gix_error::Error), -} - -impl From for Error { - fn from(err: gix_object::find::existing_object::Error) -> Self { - Error::FindCommit(err.into_error()) - } -} +pub type Error = gix_error::Exn; /// A way to configure [`commit()`](crate::commit()). #[derive(Default, Debug, Clone)] diff --git a/gix-merge/src/commit/virtual_merge_base.rs b/gix-merge/src/commit/virtual_merge_base.rs index 679df033e11..636d7a4321e 100644 --- a/gix-merge/src/commit/virtual_merge_base.rs +++ b/gix-merge/src/commit/virtual_merge_base.rs @@ -12,30 +12,10 @@ pub struct Outcome { } /// The error returned by [`commit::merge_base()`](crate::commit::virtual_merge_base()). -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] -pub enum Error { - #[error(transparent)] - MergeTree(#[from] crate::tree::Error), - #[error("Failed to write tree for merged merge-base or virtual commit")] - WriteObject(gix_object::write::Error), - #[error("Failed to decode a commit needed to build a virtual merge-base")] - DecodeCommit(#[from] gix_object::decode::Error), - #[error( - "Conflicts occurred when trying to resolve multiple merge-bases by merging them. This is most certainly a bug." - )] - VirtualMergeBaseConflict, - #[error("Could not find commit to use as basis for a virtual commit")] - FindCommit(#[source] gix_error::Error), -} - -impl From for Error { - fn from(err: gix_object::find::existing_object::Error) -> Self { - Error::FindCommit(err.into_error()) - } -} +pub type Error = gix_error::Exn; pub(super) mod function { + use gix_error::{ErrorExt, ResultExt, message}; use gix_object::FindExt; use super::Error; @@ -101,13 +81,17 @@ pub(super) mod function { content_merge: treat_as_unresolved::ContentMerge::Markers, tree_merge: treat_as_unresolved::TreeMerge::Undecidable, }) { - return Err(Error::VirtualMergeBaseConflict.into()); + return Err(message( + "Conflicts occurred when trying to resolve multiple merge-bases by merging them. This is most certainly a bug.", + ) + .raise()); } let merged_tree_id = out .tree_merge .tree .write(|tree| objects.write(tree)) - .map_err(Error::WriteObject)?; + .or_erased() + .or_raise(|| message("Failed to write tree for merged merge-base or virtual commit"))?; tree_id = Some(merged_tree_id); merged_commit_id = create_virtual_commit(objects, merged_commit_id, next_commit_id, merged_tree_id)?; @@ -120,13 +104,15 @@ pub(super) mod function { virtual_merge_bases: nonempty::NonEmpty::from_vec(virtual_merge_bases) .expect("the virtual merge-base process always creates at least one commit"), commit_id: merged_commit_id, - tree_id: tree_id.map_or_else( - || { - let mut buf = Vec::new(); - objects.find_commit(&merged_commit_id, &mut buf).map(|c| c.tree()) - }, - Ok, - )?, + tree_id: tree_id + .map_or_else( + || { + let mut buf = Vec::new(); + objects.find_commit(&merged_commit_id, &mut buf).map(|c| c.tree()) + }, + Ok, + ) + .or_raise(|| message("Could not find commit to use as basis for a virtual commit"))?, }) } @@ -137,10 +123,17 @@ pub(super) mod function { tree_id: gix_hash::ObjectId, ) -> Result { let mut buf = Vec::new(); - let commit_ref = objects.find_commit(&parent_a, &mut buf)?; - let mut commit = commit_ref.to_owned()?; + let commit_ref = objects + .find_commit(&parent_a, &mut buf) + .or_raise(|| message("Could not find commit to use as basis for a virtual commit"))?; + let mut commit = commit_ref + .to_owned() + .or_raise(|| message("Failed to decode a commit needed to build a virtual merge-base"))?; commit.parents = vec![parent_a, parent_b].into(); commit.tree = tree_id; - objects.write(&commit).map_err(Error::WriteObject) + objects + .write(&commit) + .or_erased() + .or_raise(|| message("Failed to write tree for merged merge-base or virtual commit")) } } diff --git a/gix-merge/src/tree/function/change.rs b/gix-merge/src/tree/function/change.rs index dbddc550cce..6311be9ca7f 100644 --- a/gix-merge/src/tree/function/change.rs +++ b/gix-merge/src/tree/function/change.rs @@ -10,6 +10,7 @@ use bstr::{BString, ByteSlice}; use gix_diff::{tree::recorder::Location, tree_with_rewrites::Change}; +use gix_error::ResultExt; use gix_object::FindExt; use crate::tree::{ @@ -106,7 +107,7 @@ pub(super) fn collect( if base_tree != side_tree { let side_tree = objects .find_tree_iter(side_tree, side_buf) - .map_err(|err| Error::FindTreeIter(err.into_error()))?; + .or_raise_erased(|| gix_error::message("Tree merge failed"))?; gix_diff::tree_with_rewrites( gix_object::TreeRefIter::from_bytes(base_buf, base_tree.kind()), side_tree, @@ -121,7 +122,8 @@ pub(super) fn collect( location: Some(Location::Path), rewrites, }, - )?; + ) + .or_raise_erased(|| gix_error::message("Tree merge failed"))?; } Ok(SideState::from_changes(changes)) } diff --git a/gix-merge/src/tree/function/resolve.rs b/gix-merge/src/tree/function/resolve.rs index e0d27ceb96d..0214ef14f0c 100644 --- a/gix-merge/src/tree/function/resolve.rs +++ b/gix-merge/src/tree/function/resolve.rs @@ -6,6 +6,7 @@ use std::borrow::Cow; use bstr::{BString, ByteSlice}; use gix_diff::tree_with_rewrites::Change; +use gix_error::ResultExt; use gix_hash::ObjectId; use gix_object::{ FindExt, tree, @@ -87,27 +88,24 @@ use super::change::{MatchKind, collect as collect_changes, matching as matching_ /// /// Note that `objects` *should* have an object cache to greatly accelerate tree-retrieval. #[expect(clippy::too_many_arguments)] -pub fn tree<'objects, E>( +pub fn tree<'objects>( base_tree: &gix_hash::oid, our_tree: &gix_hash::oid, their_tree: &gix_hash::oid, mut labels: crate::blob::builtin_driver::text::Labels<'_>, objects: &'objects impl gix_object::FindObjectOrHeader, - mut write_blob_to_odb: impl FnMut(&[u8]) -> Result, + mut write_blob_to_odb: impl FnMut(&[u8]) -> Result, diff_state: &mut gix_diff::tree::State, diff_resource_cache: &mut gix_diff::blob::Platform, blob_merge: &mut crate::blob::Platform, options: Options, -) -> Result, Error> -where - E: Into>, -{ +) -> Result, Error> { let _span = gix_trace::coarse!("gix_merge::tree", ?base_tree, ?our_tree, ?their_tree, ?labels); let (mut base_buf, mut side_buf) = (Vec::new(), Vec::new()); let mut editor = { let ancestor_tree = objects .find_tree(base_tree, &mut base_buf) - .map_err(|err| Error::FindTree(err.into_error()))?; + .or_raise_erased(|| gix_error::message("Tree merge failed"))?; tree::Editor::new(ancestor_tree.to_owned(), objects, base_tree.kind()) }; let resolve_tree_conflicts = options.tree_conflicts; @@ -2034,7 +2032,7 @@ fn apply_change_and_mark( editor: &mut tree::Editor<'_>, change: &Change, disposition: &mut ChangeDisposition, -) -> Result<(), tree::editor::Error> { +) -> Result<(), Error> { apply_change(editor, change, None)?; *disposition = ChangeDisposition::Applied; Ok(()) @@ -2052,7 +2050,7 @@ fn apply_our_resolution( Original => (local_ours, local_ours_disposition), Swapped => (local_theirs, local_theirs_disposition), }; - Ok(apply_change_and_mark(editor, ours, disposition)?) + apply_change_and_mark(editor, ours, disposition) } fn involves_submodule(a: &EntryMode, b: &EntryMode) -> bool { diff --git a/gix-merge/src/tree/mod.rs b/gix-merge/src/tree/mod.rs index f33625b0b3c..fc72f4fe8e8 100644 --- a/gix-merge/src/tree/mod.rs +++ b/gix-merge/src/tree/mod.rs @@ -2,34 +2,7 @@ use bstr::BString; use gix_diff::{Rewrites, tree_with_rewrites::Change}; /// The error returned by [`tree()`](crate::tree()). -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] -pub enum Error { - #[error("Could not find ancestor, our or their tree to get started")] - FindTree(#[source] gix_error::Error), - #[error("Could not find ancestor, our or their tree iterator to get started")] - FindTreeIter(#[source] gix_error::Error), - #[error("Failed to diff our side or their side")] - DiffTree(#[from] gix_diff::tree::Error), - #[error("Could not apply merge result to base tree")] - TreeEdit(#[source] gix_error::Error), - #[error("Failed to load resource to prepare for blob merge")] - BlobMergeSetResource(#[from] crate::blob::platform::set_resource::Error), - #[error(transparent)] - BlobMergePrepare(#[from] crate::blob::platform::prepare_merge::Error), - #[error(transparent)] - BlobMerge(#[from] crate::blob::platform::merge::Error), - #[error("Failed to write merged blob content as blob to the object database")] - WriteBlobToOdb(Box), - #[error("The merge was performed, but the binary merge result couldn't be selected as it wasn't found")] - MergeResourceNotFound, -} - -impl From for Error { - fn from(err: gix_object::tree::editor::Error) -> Self { - Error::TreeEdit(err.into_error()) - } -} +pub type Error = gix_error::Exn; /// The outcome produced by [`tree()`](crate::tree()). #[derive(Clone)] diff --git a/gix-merge/src/tree/utils.rs b/gix-merge/src/tree/utils.rs index 0b6138652b2..aa40bac7c5c 100644 --- a/gix-merge/src/tree/utils.rs +++ b/gix-merge/src/tree/utils.rs @@ -9,6 +9,7 @@ use std::collections::HashMap; use bstr::{BStr, BString, ByteSlice, ByteVec}; use gix_diff::tree_with_rewrites::{Change, ChangeRef}; +use gix_error::{NotFoundError, OptionExt, ResultExt, message}; use gix_hash::ObjectId; use gix_object::{ tree, @@ -119,21 +120,18 @@ pub fn unique_path_in_tree( /// Perform a merge between two blobs and return the result of its object id. #[expect(clippy::too_many_arguments)] -pub fn perform_blob_merge( +pub fn perform_blob_merge( mut labels: crate::blob::builtin_driver::text::Labels<'_>, objects: &impl gix_object::FindObjectOrHeader, blob_merge: &mut crate::blob::Platform, buf: &mut Vec, - write_blob_to_odb: &mut impl FnMut(&[u8]) -> Result, + write_blob_to_odb: &mut impl FnMut(&[u8]) -> Result, (our_location, our_id, our_mode): (&BString, ObjectId, EntryMode), (their_location, their_id, their_mode): (&BString, ObjectId, EntryMode), (previous_location, previous_id, previous_mode): (&BString, ObjectId, EntryMode), (extra_markers, outer_side): (u8, ConflictMapping), options: &Options, -) -> Result<(ObjectId, crate::blob::Resolution), Error> -where - E: Into>, -{ +) -> Result<(ObjectId, crate::blob::Resolution), Error> { if our_id == their_id { // This can happen if the merge modes are different. debug_assert_ne!( @@ -159,21 +157,27 @@ where ConflictMapping::Original => (ResourceKind::CurrentOrOurs, ResourceKind::OtherOrTheirs), ConflictMapping::Swapped => (ResourceKind::OtherOrTheirs, ResourceKind::CurrentOrOurs), }; - blob_merge.set_resource(our_id, our_mode.kind(), our_location.as_bstr(), our_kind, objects)?; - blob_merge.set_resource( - their_id, - their_mode.kind(), - their_location.as_bstr(), - their_kind, - objects, - )?; - blob_merge.set_resource( - previous_id, - previous_mode.kind(), - previous_location.as_bstr(), - ResourceKind::CommonAncestorOrBase, - objects, - )?; + blob_merge + .set_resource(our_id, our_mode.kind(), our_location.as_bstr(), our_kind, objects) + .or_erased()?; + blob_merge + .set_resource( + their_id, + their_mode.kind(), + their_location.as_bstr(), + their_kind, + objects, + ) + .or_erased()?; + blob_merge + .set_resource( + previous_id, + previous_mode.kind(), + previous_location.as_bstr(), + ResourceKind::CommonAncestorOrBase, + objects, + ) + .or_erased()?; fn combined(side: &BStr, location: &BString) -> BString { let mut buf = side.to_owned(); @@ -202,17 +206,21 @@ where other: other.as_ref().map(|n| n.as_bstr()), } }; - let mut prep = blob_merge.prepare_merge(objects, options.blob_merge)?; + let mut prep = blob_merge.prepare_merge(objects, options.blob_merge).or_erased()?; if let crate::blob::builtin_driver::text::Conflict::Keep { marker_size, .. } = &mut prep.options.text.conflict { *marker_size = marker_size.saturating_add(extra_markers.saturating_add(options.marker_size_multiplier.saturating_mul(2))); } - let (pick, resolution) = prep.merge(buf, labels, &options.blob_merge_command_ctx)?; + let (pick, resolution) = prep.merge(buf, labels, &options.blob_merge_command_ctx).or_erased()?; let merged_blob_id = prep .id_by_pick(pick, buf, write_blob_to_odb) - .map_err(|err| Error::WriteBlobToOdb(err.into()))? - .ok_or(Error::MergeResourceNotFound)?; + .or_raise_erased(|| message("Failed to write merged blob content as blob to the object database"))? + .ok_or_raise_erased(|| { + NotFoundError::new( + "The merge was performed, but the binary merge result couldn't be selected as it wasn't found", + ) + })?; Ok((merged_blob_id, resolution)) } diff --git a/gix-merge/tests/merge/blob/pipeline.rs b/gix-merge/tests/merge/blob/pipeline.rs index d50fd60b0c6..0520e90d755 100644 --- a/gix-merge/tests/merge/blob/pipeline.rs +++ b/gix-merge/tests/merge/blob/pipeline.rs @@ -56,9 +56,13 @@ fn without_transformation() -> crate::Result { ) .unwrap_err(); + assert_eq!( + err.to_string(), + "Entry at 'link' must be regular file or symlink, but was Link" + ); assert!( - matches!(err, pipeline::convert_to_mergeable::Error::InvalidEntryKind {rela_path,actual} - if rela_path == link_name && actual == EntryKind::Link) + err.downcast_any_ref::().is_some(), + "an unsupported entry kind is a validation failure" ); assert_eq!( buf.len(), @@ -297,12 +301,7 @@ fn non_existing() -> crate::Result { ) .unwrap_err(); assert!( - matches!( - err, - gix_merge::blob::pipeline::convert_to_mergeable::Error::FindObject( - err - ) if err.is_not_found(), - ), + err.downcast_any_ref::().is_some(), "missing object database ids are always an error (even though missing objects on disk are allowed)" ); Ok(()) diff --git a/gix-merge/tests/merge/blob/platform.rs b/gix-merge/tests/merge/blob/platform.rs index 9653192be21..a209c96197c 100644 --- a/gix-merge/tests/merge/blob/platform.rs +++ b/gix-merge/tests/merge/blob/platform.rs @@ -570,7 +570,7 @@ cat "%B" >> "%A""# .prepare_external_driver("bogus".into(), Default::default(), Default::default()) .unwrap_err(); assert!( - matches!(err, platform::prepare_external_driver::Error::ResourceTooLarge { .. }), + err.downcast_any_ref::().is_some(), "however, for external drivers, resources can still be too much to handle, until we learn how to stream them" ); Ok(()) From 5d71252e4acc161924de66d11b07888b13a142b2 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 20 Aug 2026 17:22:37 +0200 Subject: [PATCH 23/51] change(gix-protocol)!: migrate errors to gix-error rubberstamp --- Cargo.lock | 1 - gitoxide-core/src/pack/receive.rs | 19 ++- gix-protocol/Cargo.toml | 1 - gix-protocol/src/command.rs | 28 +--- gix-protocol/src/fetch/error.rs | 38 +----- gix-protocol/src/fetch/function.rs | 126 ++++++++++++------ gix-protocol/src/fetch/negotiate.rs | 83 ++++++------ gix-protocol/src/fetch/refmap/init.rs | 34 +++-- gix-protocol/src/fetch/response/io.rs | 77 +++++++++-- gix-protocol/src/fetch/response/mod.rs | 86 ++++-------- gix-protocol/src/fetch/types.rs | 5 +- gix-protocol/src/handshake/function.rs | 42 ++++-- gix-protocol/src/handshake/mod.rs | 37 +---- gix-protocol/src/handshake/refs/async_io.rs | 11 +- .../src/handshake/refs/blocking_io.rs | 11 +- gix-protocol/src/handshake/refs/mod.rs | 25 +--- gix-protocol/src/handshake/refs/shared.rs | 123 +++++++++++------ gix-protocol/src/ls_refs.rs | 93 ++++++++----- gix-protocol/tests/protocol/fetch/_impl.rs | 54 +++++--- gix-protocol/tests/protocol/fetch/mod.rs | 20 +-- gix-protocol/tests/protocol/fetch/ref_map.rs | 11 +- gix-protocol/tests/protocol/fetch/response.rs | 13 +- gix-protocol/tests/protocol/fetch/v2.rs | 27 ++-- gix-protocol/tests/protocol/mod.rs | 2 +- gix/src/clone/fetch/mod.rs | 27 ++-- gix/src/env.rs | 5 +- gix/src/remote/connection/fetch/error.rs | 4 +- .../remote/connection/fetch/receive_pack.rs | 11 +- gix/src/remote/connection/ref_map.rs | 32 +++-- gix/tests/gix/clone.rs | 11 +- .../no-repo/pack/clone/fail-ambiguous-host | 5 +- .../no-repo/pack/clone/fail-ambiguous-path | 5 +- .../fail-ambiguous-username/explicit-ssh | 5 +- .../fail-ambiguous-username/implicit-ssh | 5 +- .../no-repo/pack/receive/fail-ambiguous-host | 5 +- .../no-repo/pack/receive/fail-ambiguous-path | 5 +- .../fail-ambiguous-username/explicit-ssh | 5 +- .../fail-ambiguous-username/implicit-ssh | 5 +- 38 files changed, 592 insertions(+), 505 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 436f7a2b556..557c256f6c4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2343,7 +2343,6 @@ dependencies = [ "gix-utils", "nonempty", "serde", - "thiserror 2.0.18", ] [[package]] diff --git a/gitoxide-core/src/pack/receive.rs b/gitoxide-core/src/pack/receive.rs index a1ae86385cc..811e2f729cc 100644 --- a/gitoxide-core/src/pack/receive.rs +++ b/gitoxide-core/src/pack/receive.rs @@ -5,6 +5,7 @@ use std::{ }; use crate::{OutputFormat, net, pack::receive::protocol::fetch::negotiate}; +use gix::error::{ResultExt, message}; #[cfg(feature = "async-client")] use gix::protocol::transport::client::async_io::connect; #[cfg(feature = "blocking-client")] @@ -71,7 +72,8 @@ where vec![("agent".into(), Some(agent.clone()))], &mut progress, ) - .await?; + .await + .map_err(gix::Exn::into_error)?; if wanted_refs.is_empty() { wanted_refs.push("refs/heads/*:refs/remotes/origin/*".into()); } @@ -90,15 +92,20 @@ where extra_refspecs: vec![], }; - let fetch_refmap = handshake.prepare_lsrefs_or_extract_refmap(user_agent.clone(), true, context)?; + let fetch_refmap = handshake + .prepare_lsrefs_or_extract_refmap(user_agent.clone(), true, context) + .map_err(gix::Exn::into_error)?; #[cfg(feature = "async-client")] let refmap = fetch_refmap .fetch_async(&mut progress, &mut transport.inner, trace_packetlines) - .await?; + .await + .map_err(gix::Exn::into_error)?; #[cfg(feature = "blocking-client")] - let refmap = fetch_refmap.fetch_blocking(&mut progress, &mut transport.inner, trace_packetlines)?; + let refmap = fetch_refmap + .fetch_blocking(&mut progress, &mut transport.inner, trace_packetlines) + .map_err(gix::Exn::into_error)?; if refmap.is_missing_required_mapping() { return Err(Error::NoMapping { @@ -124,6 +131,7 @@ where ctx.object_hash, ctx.format, ) + .or_raise_erased(|| message("Failed to receive the pack")) .map(|_| true) }, progress, @@ -141,7 +149,8 @@ where reject_shallow_remote: true, }, ) - .await?; + .await + .map_err(gix::Exn::into_error)?; Ok(()) } diff --git a/gix-protocol/Cargo.toml b/gix-protocol/Cargo.toml index d91ef6a3b2b..6f80dd9674a 100644 --- a/gix-protocol/Cargo.toml +++ b/gix-protocol/Cargo.toml @@ -92,7 +92,6 @@ gix-credentials = { version = "^0.40.0", path = "../gix-credentials", optional = gix-refspec = { version = "^0.45.1", path = "../gix-refspec", optional = true } gix-lock = { version = "^24.0.0", path = "../gix-lock", optional = true } -thiserror = "2.0.18" nonempty = "0.12.0" serde = { version = "1.0.114", optional = true, default-features = false, features = [ "derive", diff --git a/gix-protocol/src/command.rs b/gix-protocol/src/command.rs index ab128e0a04e..909e8d6a3d0 100644 --- a/gix-protocol/src/command.rs +++ b/gix-protocol/src/command.rs @@ -176,16 +176,13 @@ mod with_io { arguments: &[BString], features: &[Feature], ) -> Result<(), validate_argument_prefixes::Error> { - use validate_argument_prefixes::Error; + use gix_error::message; let allowed = self.all_argument_prefixes(); for arg in arguments { if allowed.iter().any(|allowed| arg.starts_with(allowed.as_bytes())) { continue; } - return Err(Error::UnsupportedArgument { - command: self.as_str(), - argument: arg.clone(), - }); + return Err(message!("{}: argument {arg} is not known or allowed", self.as_str()).into()); } match version { gix_transport::Protocol::V0 | gix_transport::Protocol::V1 => { @@ -196,10 +193,7 @@ mod with_io { { continue; } - return Err(Error::UnsupportedCapability { - command: self.as_str(), - feature: feature.to_string(), - }); + return Err(message!("{}: capability {feature} is not supported", self.as_str()).into()); } } gix_transport::Protocol::V2 => { @@ -220,10 +214,7 @@ mod with_io { match *feature { "agent" | "object-format" => {} _ => { - return Err(Error::UnsupportedCapability { - command: self.as_str(), - feature: feature.to_string(), - }); + return Err(message!("{}: capability {feature} is not supported", self.as_str()).into()); } } } @@ -235,17 +226,8 @@ mod with_io { /// pub mod validate_argument_prefixes { - use bstr::BString; - /// The error returned by [Command::validate_argument_prefixes()](super::Command::validate_argument_prefixes()). - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error("{command}: argument {argument} is not known or allowed")] - UnsupportedArgument { command: &'static str, argument: BString }, - #[error("{command}: capability {feature} is not supported")] - UnsupportedCapability { command: &'static str, feature: String }, - } + pub type Error = gix_error::ValidationError; } } #[cfg(any(test, feature = "async-client", feature = "blocking-client"))] diff --git a/gix-protocol/src/fetch/error.rs b/gix-protocol/src/fetch/error.rs index b8116047e65..8032b7d4540 100644 --- a/gix-protocol/src/fetch/error.rs +++ b/gix-protocol/src/fetch/error.rs @@ -1,38 +1,2 @@ /// The error returned by [`fetch()`](crate::fetch()). -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] -pub enum Error { - #[error("Could not decode server reply")] - FetchResponse(#[from] crate::fetch::response::Error), - #[error(transparent)] - Negotiate(#[from] crate::fetch::negotiate::Error), - #[error(transparent)] - Client(#[from] crate::transport::client::Error), - #[error("Server lack feature {feature:?}: {description}")] - MissingServerFeature { - feature: &'static str, - description: &'static str, - }, - #[error("Could not write 'shallow' file to incorporate remote updates after fetching")] - WriteShallowFile(#[source] gix_error::Error), - #[error("Could not read 'shallow' file to send current shallow boundary")] - ReadShallowFile(#[source] gix_error::Error), - #[error("'shallow' file could not be locked in preparation for writing changes")] - LockShallowFile(#[source] std::io::Error), - #[error("Receiving objects from shallow remotes is prohibited due to the value of `clone.rejectShallow`")] - RejectShallowRemote, - #[error("Failed to consume the pack sent by the remote")] - ConsumePack(#[source] Box), - #[error("Failed to read remaining bytes in stream")] - ReadRemainingBytes(#[source] std::io::Error), -} - -impl crate::transport::IsSpuriousError for Error { - fn is_spurious(&self) -> bool { - match self { - Error::FetchResponse(err) => err.is_spurious(), - Error::Client(err) => err.is_spurious(), - _ => false, - } - } -} +pub type Error = gix_error::Exn; diff --git a/gix-protocol/src/fetch/function.rs b/gix-protocol/src/fetch/function.rs index b2fb8b33d50..72c0ddcc22b 100644 --- a/gix-protocol/src/fetch/function.rs +++ b/gix-protocol/src/fetch/function.rs @@ -3,6 +3,7 @@ use std::{ sync::atomic::{AtomicBool, Ordering}, }; +use gix_error::{ErrorExt, ResultExt, RetryableError, ValidationError, message}; use gix_features::progress::DynNestedProgress; use crate::fetch::{ @@ -36,9 +37,13 @@ use crate::transport::client::blocking_io::{ExtendedBufRead, HandleProgress, Tra /// Return `Ok(None)` if there was nothing to do because all remote refs are at the same state as they are locally, /// or there was nothing wanted, or `Ok(Some(outcome))` to inform about all the changes that were made. #[crate::bisync::bisync] -pub async fn fetch( +pub async fn fetch( negotiate: &mut impl Negotiate, - consume_pack: impl FnOnce(&mut dyn std::io::BufRead, &mut dyn DynNestedProgress, &AtomicBool) -> Result, + consume_pack: impl FnOnce( + &mut dyn std::io::BufRead, + &mut dyn DynNestedProgress, + &AtomicBool, + ) -> Result, mut progress: P, should_interrupt: &AtomicBool, Context { @@ -58,7 +63,6 @@ where P: gix_features::progress::NestedProgress, P::SubProgress: 'static, T: Transport, - E: Into>, { let _span = gix_trace::coarse!("gix_protocol::fetch()"); let v1_shallow_updates = handshake.v1_shallow_updates.take(); @@ -76,12 +80,10 @@ where let mut arguments = Arguments::new(protocol_version, fetch_features, trace_packetlines); if matches!(tags, Tags::Included) { if !arguments.can_use_include_tag() { - return Err(Error::MissingServerFeature { - feature: "include-tag", - description: - // NOTE: if this is an issue, we could probably do what's proposed here. - "To make this work we would have to implement another pass to fetch attached tags separately", - }); + return Err(ValidationError::new( + "Server lack feature \"include-tag\": To make this work we would have to implement another pass to fetch attached tags separately", + ) + .raise_erased()); } arguments.use_include_tag(); } @@ -91,7 +93,9 @@ where "negotiate", protocol_version = handshake.server_protocol_version as usize ); - let action = negotiate.mark_complete_and_common_ref()?; + let action = negotiate + .mark_complete_and_common_ref() + .or_raise_erased(|| message("Failed to prepare fetch negotiation"))?; let mut previous_response = None::; match &action { negotiate::Action::NoChange | negotiate::Action::SkipToRefUpdate => Ok(None), @@ -109,26 +113,28 @@ where progress.step(); progress.set_name(format!("negotiate (round {})", rounds.len() + 1)); if should_interrupt.load(Ordering::Relaxed) { - return Err(Error::Negotiate(negotiate::Error::NegotiationFailed { - rounds: rounds.len(), - })); + return Err(RetryableError::new(gix_error::message!( + "We were unable to figure out what objects the server should send after {} round(s)", + rounds.len() + )) + .raise_erased()); } - let is_done = match negotiate.one_round(&mut state, &mut arguments, previous_response.as_ref()) { - Ok((round, is_done)) => { - rounds.push(round); - is_done - } - Err(err) => { - return Err(err.into()); - } - }; - let mut reader = arguments.send(transport, is_done).await?; + let (round, is_done) = negotiate + .one_round(&mut state, &mut arguments, previous_response.as_ref()) + .or_raise_erased(|| message("Failed to negotiate objects with the server"))?; + rounds.push(round); + let mut reader = arguments + .send(transport, is_done) + .await + .map_err(|err| transport_error(err, "Failed to send fetch arguments"))?; if sideband_all { setup_remote_progress(&mut progress, &mut reader, should_interrupt); } let response = - crate::fetch::Response::from_line_reader(protocol_version, &mut reader, is_done, !is_done).await?; + crate::fetch::Response::from_line_reader(protocol_version, &mut reader, is_done, !is_done) + .await + .or_raise_erased(|| message("Could not decode server reply"))?; let has_pack = response.has_pack(); previous_response = Some(response); if has_pack { @@ -147,7 +153,10 @@ where previous_response.append_v1_shallow_updates(v1_shallow_updates); if !previous_response.shallow_updates().is_empty() && shallow_lock.is_none() { if reject_shallow_remote { - return Err(Error::RejectShallowRemote); + return Err(ValidationError::new( + "Receiving objects from shallow remotes is prohibited due to the value of `clone.rejectShallow`", + ) + .raise_erased()); } shallow_lock = acquire_shallow_lock(&shallow_file).map(Some)?; } @@ -159,7 +168,9 @@ where // Assure the final flush packet is consumed. let has_read_to_end = reader.stopped_at().is_some(); if !has_read_to_end { - read_remaining(&mut reader).await.map_err(Error::ReadRemainingBytes)?; + read_remaining(&mut reader) + .await + .or_raise_erased(|| message("Failed to read remaining bytes in stream"))?; } } drop(reader); @@ -168,7 +179,9 @@ where && !previous_response.shallow_updates().is_empty() { gix_shallow::write(shallow_lock, shallow_commits, previous_response.shallow_updates()) - .map_err(|err| Error::WriteShallowFile(err.into_error()))?; + .or_raise_erased(|| { + message("Could not write 'shallow' file to incorporate remote updates after fetching") + })?; } Ok(Some(Outcome { last_response: previous_response, @@ -179,35 +192,33 @@ where } #[crate::bisync::only_async] -fn consume_received_pack( +fn consume_received_pack( reader: R, - consume: impl FnOnce(&mut dyn std::io::BufRead, &mut dyn DynNestedProgress, &AtomicBool) -> Result, + consume: impl FnOnce(&mut dyn std::io::BufRead, &mut dyn DynNestedProgress, &AtomicBool) -> Result, progress: &mut dyn DynNestedProgress, should_interrupt: &AtomicBool, ) -> Result<(R, bool), Error> where R: crate::futures_io::AsyncBufRead + Unpin, - E: Into>, { let mut reader = crate::futures_lite::io::BlockOn::new(reader); - let may_read_to_end = - consume(&mut reader, progress, should_interrupt).map_err(|err| Error::ConsumePack(err.into()))?; + let may_read_to_end = consume(&mut reader, progress, should_interrupt) + .or_raise_erased(|| message("Failed to consume the pack sent by the remote"))?; Ok((reader.into_inner(), may_read_to_end)) } #[crate::bisync::only_sync] -fn consume_received_pack( +fn consume_received_pack( mut reader: R, - consume: impl FnOnce(&mut dyn std::io::BufRead, &mut dyn DynNestedProgress, &AtomicBool) -> Result, + consume: impl FnOnce(&mut dyn std::io::BufRead, &mut dyn DynNestedProgress, &AtomicBool) -> Result, progress: &mut dyn DynNestedProgress, should_interrupt: &AtomicBool, ) -> Result<(R, bool), Error> where R: std::io::BufRead, - E: Into>, { - let may_read_to_end = - consume(&mut reader, progress, should_interrupt).map_err(|err| Error::ConsumePack(err.into()))?; + let may_read_to_end = consume(&mut reader, progress, should_interrupt) + .or_raise_erased(|| message("Failed to consume the pack sent by the remote"))?; Ok((reader, may_read_to_end)) } @@ -225,7 +236,7 @@ fn read_remaining(reader: &mut impl std::io::Read) -> std::io::Result<()> { fn acquire_shallow_lock(shallow_file: &Path) -> Result { gix_lock::File::acquire_to_update_resource(shallow_file, gix_lock::acquire::Fail::Immediately, None) - .map_err(|err| Error::LockShallowFile(std::io::Error::other(err.into_error()))) + .or_raise_erased(|| message("'shallow' file could not be locked in preparation for writing changes")) } fn add_shallow_args( @@ -236,13 +247,14 @@ fn add_shallow_args( let expect_change = *shallow != Shallow::NoChange; let shallow_lock = expect_change.then(|| acquire_shallow_lock(shallow_file)).transpose()?; - let shallow_commits = gix_shallow::read(shallow_file).map_err(|err| Error::ReadShallowFile(err.into_error()))?; + let shallow_commits = gix_shallow::read(shallow_file) + .or_raise_erased(|| message("Could not read 'shallow' file to send current shallow boundary"))?; if (shallow_commits.is_some() || expect_change) && !args.can_use_shallow() { // NOTE: if this is an issue, we can always unshallow the repo ourselves. - return Err(Error::MissingServerFeature { - feature: "shallow", - description: "shallow clones need server support to remain shallow, otherwise bigger than expected packs are sent effectively unshallowing the repository", - }); + return Err(ValidationError::new( + "Server lack feature \"shallow\": shallow clones need server support to remain shallow, otherwise bigger than expected packs are sent effectively unshallowing the repository", + ) + .raise_erased()); } if let Some(shallow_commits) = &shallow_commits { for commit in shallow_commits.iter() { @@ -274,6 +286,16 @@ fn add_shallow_args( Ok((shallow_commits, shallow_lock)) } +pub(crate) fn transport_error(err: crate::transport::client::Error, context: &'static str) -> Error { + use crate::transport::IsSpuriousError; + + if err.is_spurious() { + RetryableError::new(err).and_raise(message(context)).erased() + } else { + err.and_raise(message(context)).erased() + } +} + fn setup_remote_progress<'a>( progress: &mut dyn gix_features::progress::DynNestedProgress, reader: &mut Box + Unpin + 'a>, @@ -291,3 +313,21 @@ fn setup_remote_progress<'a>( } }) as HandleProgress<'a>)); } + +#[cfg(test)] +mod tests { + #[test] + fn transport_errors_keep_retryability() { + let err = super::transport_error( + crate::transport::client::Error::Io(std::io::ErrorKind::ConnectionReset.into()), + "transport failed", + ) + .into_error(); + + assert!(err.can_retry(), "the transport classifier remains visible"); + assert!( + err.downcast_any_ref::().is_some(), + "the original I/O error remains in the chain" + ); + } +} diff --git a/gix-protocol/src/fetch/negotiate.rs b/gix-protocol/src/fetch/negotiate.rs index 864bb843ce4..7e16cb24976 100644 --- a/gix-protocol/src/fetch/negotiate.rs +++ b/gix-protocol/src/fetch/negotiate.rs @@ -8,6 +8,7 @@ use std::borrow::Cow; use gix_date::SecondsSinceUnixEpoch; +use gix_error::{ResultExt, message}; use gix_negotiate::Flags; use gix_ref::file::ReferenceExt; @@ -16,30 +17,7 @@ use crate::fetch::{RefMap, Shallow, Tags, refmap}; type Queue = gix_revwalk::PriorityQueue; /// The error returned during [`one_round()`] or [`mark_complete_and_common_ref()`]. -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] -pub enum Error { - #[error("We were unable to figure out what objects the server should send after {rounds} round(s)")] - NegotiationFailed { rounds: usize }, - #[error(transparent)] - LookupCommitInGraph(gix_error::Error), - #[error(transparent)] - OpenPackedRefsBuffer(#[from] gix_ref::packed::buffer::open::Error), - #[error(transparent)] - IO(#[from] std::io::Error), - #[error(transparent)] - InitRefIter(#[from] gix_ref::file::iter::loose_then_packed::Error), - #[error(transparent)] - PeelToId(#[from] gix_ref::peel::to_id::Error), - #[error(transparent)] - AlternateRefsAndObjects(Box), -} - -impl From for Error { - fn from(err: gix_revwalk::graph::get_or_insert_default::Error) -> Self { - Error::LookupCommitInGraph(err.into_error()) - } -} +pub type Error = gix_error::Exn; /// Determines what should be done after [preparing the commit-graph for negotiation](mark_complete_and_common_ref). #[must_use] @@ -124,10 +102,10 @@ pub struct Round { /// - `f(mapping) -> bool` returns `true` if the given mapping should not participate in change tracking. /// - [`make_refmapping_ignore_predicate()`] is a typical implementation for this. #[expect(clippy::too_many_arguments)] -pub fn mark_complete_and_common_ref( +pub fn mark_complete_and_common_ref( objects: &(impl gix_object::Find + gix_object::FindHeader + gix_object::Exists), refs: &gix_ref::file::Store, - alternates: impl FnOnce() -> Result, + alternates: impl FnOnce() -> Result, negotiator: &mut dyn gix_negotiate::Negotiator, graph: &mut gix_negotiate::Graph<'_, '_>, ref_map: &RefMap, @@ -135,7 +113,6 @@ pub fn mark_complete_and_common_ref( mapping_is_ignored: impl Fn(&refmap::Mapping) -> bool, ) -> Result where - E: Into>, Out: Iterator, F: gix_object::Find, { @@ -184,7 +161,8 @@ where if let Some(commit) = want_id .and_then(|id| graph.get_or_insert_commit(id.into(), |_| {}).transpose()) - .transpose()? + .transpose() + .or_raise(|| message("Could not look up commit in graph"))? { remote_ref_target_known[mapping_idx] = true; cutoff_date = cutoff_date.unwrap_or_default().max(commit.commit_time).into(); @@ -210,7 +188,7 @@ where // (`git` is conditional here based on `deepen`, but it doesn't make sense and it's hard to extract from history when that happened). let mut queue = Queue::new(); mark_all_refs_in_repo(refs, objects, graph, &mut queue, Flags::COMPLETE)?; - for (alt_refs, alt_objs) in alternates().map_err(|err| Error::AlternateRefsAndObjects(err.into()))? { + for (alt_refs, alt_objs) in alternates().or_raise(|| message("Could not obtain alternate refs and objects"))? { mark_all_refs_in_repo(&alt_refs, &alt_objs, graph, &mut queue, Flags::COMPLETE)?; } // Keep track of the tips, which happen to be on our queue right, before we traverse the graph with cutoff. @@ -238,7 +216,9 @@ where .filter(|(c, _)| c.data.flags.contains(Flags::COMPLETE)) .map(|(_, id)| id) { - negotiator.known_common(common_id.into(), graph)?; + negotiator + .known_common(common_id.into(), graph) + .or_raise(|| message("Could not mark common commit"))?; } } Ok(()) @@ -248,7 +228,9 @@ where // reason we cached the set of tips. gix_trace::detail!("mark tips", num_tips = tips.len()).into_scope(|| -> Result<_, Error> { for tip in tips.iter_unordered() { - negotiator.add_tip(*tip, graph)?; + negotiator + .add_tip(*tip, graph) + .or_raise(|| message("Could not add negotiation tip"))?; } Ok(()) })?; @@ -359,7 +341,8 @@ fn mark_recent_complete_commits( .get_or_insert_commit(parent_id, |md| { was_complete = md.flags.contains(Flags::COMPLETE); md.flags |= Flags::COMPLETE; - })? + }) + .or_raise(|| message("Could not look up commit in graph"))? .filter(|_| !was_complete) { queue.insert(parent.commit_time, parent_id); @@ -377,22 +360,30 @@ fn mark_all_refs_in_repo( mark: Flags, ) -> Result<(), Error> { let _span = gix_trace::detail!("mark_all_refs"); - for local_ref in store.iter()?.all()? { - let mut local_ref = local_ref?; - let id = - match local_ref.peel_to_id_packed(store, objects, store.cached_packed_buffer()?.as_ref().map(|b| &***b)) { - Ok(id) => id, - Err(gix_ref::peel::to_id::Error::FollowToObject(gix_ref::peel::to_object::Error::Follow( - gix_ref::file::find::existing::Error::NotFound { .. }, - ))) => continue, - Err(err) => return Err(err.into()), - }; + for local_ref in store + .iter() + .or_raise(|| message("Could not open packed refs"))? + .all() + .or_raise(|| message("Could not initialize ref iterator"))? + { + let mut local_ref = local_ref.or_raise(|| message("Could not read reference"))?; + let packed = store + .cached_packed_buffer() + .or_raise(|| message("Could not open packed refs"))?; + let id = match local_ref.peel_to_id_packed(store, objects, packed.as_ref().map(|b| &***b)) { + Ok(id) => id, + Err(gix_ref::peel::to_id::Error::FollowToObject(gix_ref::peel::to_object::Error::Follow( + gix_ref::file::find::existing::Error::NotFound { .. }, + ))) => continue, + Err(err) => return Err(err).or_raise(|| message("Could not peel reference to ID")), + }; let mut is_complete = false; if let Some(commit) = graph .get_or_insert_commit(id, |md| { is_complete = md.flags.contains(Flags::COMPLETE); md.flags |= mark; - })? + }) + .or_raise(|| message("Could not look up commit in graph"))? .filter(|_| !is_complete) { queue.insert(commit.commit_time, id); @@ -470,7 +461,9 @@ pub fn one_round( match ack { Acknowledgement::Common(id) => { seen_ack = true; - negotiator.in_common_with_remote(*id, graph)?; + negotiator + .in_common_with_remote(*id, graph) + .or_raise(|| message("Could not mark remote-common commit"))?; if let Some(common) = &mut state.common_commits { common.push(*id); } @@ -494,7 +487,7 @@ pub fn one_round( let mut haves_added = 0; for have_id in (0..state.haves_to_send).map_while(|_| negotiator.next_have(graph)) { - arguments.have(have_id?); + arguments.have(have_id.or_raise(|| message("Could not obtain next negotiation commit"))?); haves_added += 1; } // Note that we are differing from the git implementation, which does an extra-round of with no new haves sent at all. diff --git a/gix-protocol/src/fetch/refmap/init.rs b/gix-protocol/src/fetch/refmap/init.rs index b006a894bd2..15d0eda839c 100644 --- a/gix-protocol/src/fetch/refmap/init.rs +++ b/gix-protocol/src/fetch/refmap/init.rs @@ -1,4 +1,5 @@ -use bstr::{BString, ByteSlice}; +use bstr::ByteSlice; +use gix_error::{ErrorExt, ResultExt, ValidationError, message}; use gix_transport::client::Capabilities; use crate::{ @@ -10,16 +11,7 @@ use crate::{ }; /// The error returned by [`crate::Handshake::prepare_lsrefs_or_extract_refmap()`]. -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] -pub enum Error { - #[error("The object format {format:?} as used by the remote is unsupported")] - UnknownObjectFormat { format: BString }, - #[error(transparent)] - MappingValidation(#[from] gix_refspec::match_group::validate::Error), - #[error(transparent)] - ListRefs(#[from] crate::ls_refs::Error), -} +pub type Error = gix_error::Exn; /// For use in [`RefMap::from_refs()`]. #[derive(Debug, Clone)] @@ -63,7 +55,8 @@ impl RefMap { object, } })) - .validated()?; + .validated() + .or_raise_erased(|| message("Failed to validate mappings between remote and local references"))?; let mappings = res.mappings; let mappings = mappings @@ -105,14 +98,17 @@ impl RefMap { /// In builds whose `gix-hash` lacks the `sha1` feature, it's treated as unknown object format error. fn extract_object_hash(capabilities: &Capabilities) -> Result { let object_format = match capabilities.capability("object-format").and_then(|c| c.value()) { - Some(object_format) => object_format.to_str().map_err(|_| Error::UnknownObjectFormat { - format: object_format.into(), + Some(object_format) => object_format.to_str().or_raise_erased(|| { + ValidationError::new_with_input("The object format used by the remote isn't valid UTF-8", object_format) })?, None => "sha1", }; - object_format - .parse::() - .map_err(|_| Error::UnknownObjectFormat { - format: object_format.into(), - }) + match object_format.parse::() { + Ok(kind) => Ok(kind), + Err(err) => Err(ValidationError::new_with_input( + format!("The object format used by the remote is unsupported: {err}"), + object_format, + ) + .raise_erased()), + } } diff --git a/gix-protocol/src/fetch/response/io.rs b/gix-protocol/src/fetch/response/io.rs index a9d9766cfb8..f58046cb369 100644 --- a/gix-protocol/src/fetch/response/io.rs +++ b/gix-protocol/src/fetch/response/io.rs @@ -4,7 +4,8 @@ use std::io; use crate::transport::client::async_io::ExtendedBufRead; #[crate::bisync::only_sync] use crate::transport::client::blocking_io::ExtendedBufRead; -use gix_transport::{Protocol, client::MessageKind}; +use gix_error::{CorruptionError, ErrorExt, RetryableError, message}; +use gix_transport::{Protocol, client, client::MessageKind}; use crate::fetch::{ Response, response, @@ -19,7 +20,7 @@ async fn parse_v2_section<'a, T>( parse: impl Fn(&str) -> Result, ) -> Result { line.clear(); - while reader.readline_str(line).await? != 0 { + while reader.readline_str(line).await.map_err(read_error)? != 0 { res.push(parse(line)?); line.clear(); } @@ -70,8 +71,8 @@ impl Response { // to deal with this correctly. // For now this is acceptable, as V2 can be used as a workaround, which also is the default. Some(Err(err)) if err.kind() == io::ErrorKind::UnexpectedEof => break 'lines false, - Some(Err(err)) => return Err(err.into()), - Some(Ok(Err(err))) => return Err(err.into()), + Some(Err(err)) => return Err(read_error(err)), + Some(Ok(Err(err))) => return Err(transport_error(err)), None => { // maybe we saw a shallow flush packet, let's reset and retry debug_assert_eq!( @@ -79,12 +80,12 @@ impl Response { Some(MessageKind::Flush), "If this isn't a flush packet, we don't know what's going on" ); - reader.readline_str(&mut line).await?; + reader.readline_str(&mut line).await.map_err(read_error)?; reader.reset(Protocol::V1); match reader.peek_data_line().await { Some(Ok(Ok(line))) => String::from_utf8_lossy(line), - Some(Err(err)) => return Err(err.into()), - Some(Ok(Err(err))) => return Err(err.into()), + Some(Err(err)) => return Err(read_error(err)), + Some(Ok(Err(err))) => return Err(transport_error(err)), None => break 'lines false, // EOF } } @@ -94,7 +95,7 @@ impl Response { break 'lines true; } assert_ne!( - reader.readline_str(&mut line).await?, + reader.readline_str(&mut line).await.map_err(read_error)?, 0, "consuming a peeked line works" ); @@ -123,8 +124,8 @@ impl Response { let mut wanted_refs = Vec::::new(); let has_pack = 'section: loop { line.clear(); - if reader.readline_str(&mut line).await? == 0 { - return Err(response::Error::Io(io::Error::new( + if reader.readline_str(&mut line).await.map_err(read_error)? == 0 { + return Err(read_error(io::Error::new( io::ErrorKind::UnexpectedEof, "Could not read message headline", ))); @@ -150,7 +151,11 @@ impl Response { // what follows is the packfile itself, which can be read with a sideband enabled reader break 'section true; } - _ => return Err(response::Error::UnknownSectionHeader { header: line }), + _ => { + return Err( + CorruptionError::new(format!("Unknown or unsupported header: {line:?}")).raise_erased() + ); + } } }; Ok(Response { @@ -163,3 +168,53 @@ impl Response { } } } + +fn read_error(err: io::Error) -> response::Error { + let err = if err.kind() == io::ErrorKind::Other { + match err.into_inner() { + Some(err) => match err.downcast::() { + Ok(err) => return (*err).and_raise(message("Failed to read from line reader")).erased(), + Err(err) => io::Error::other(err), + }, + None => io::ErrorKind::Other.into(), + } + } else { + err + }; + transport_error(err.into()) +} + +fn transport_error(err: client::Error) -> response::Error { + use crate::transport::IsSpuriousError; + + let context = message("Failed to read from line reader"); + let can_retry = + !matches!(&err, client::Error::Io(err) if err.kind() == io::ErrorKind::OutOfMemory) && err.is_spurious(); + if can_retry { + RetryableError::new(err).and_raise(context).erased() + } else { + err.and_raise(context).erased() + } +} + +#[cfg(test)] +mod tests { + #[test] + fn line_reader_io_uses_transport_retry_policy_except_for_memory_exhaustion() { + let err = super::read_error(std::io::ErrorKind::ConnectionAborted.into()).into_error(); + assert!( + err.can_retry(), + "connection failures remain retryable while reading packet lines" + ); + + let err = super::read_error(std::io::ErrorKind::OutOfMemory.into()).into_error(); + assert!( + !err.can_retry(), + "memory exhaustion isn't retryable by transport policy" + ); + assert!(err.classify().any(|classification| { + classification.class() + == gix_error::Class::ResourceExhaustion(gix_error::ResourceExhaustionKind::AllocationFailure) + })); + } +} diff --git a/gix-protocol/src/fetch/response/mod.rs b/gix-protocol/src/fetch/response/mod.rs index 61200b4bae4..358a4e211ce 100644 --- a/gix-protocol/src/fetch/response/mod.rs +++ b/gix-protocol/src/fetch/response/mod.rs @@ -1,50 +1,14 @@ use bstr::BString; -use gix_transport::{Protocol, client}; +use gix_error::{CorruptionError, ErrorExt, ResultExt, ValidationError}; +use gix_transport::Protocol; use crate::{command::Feature, fetch::Response}; /// The error returned in the [response module][crate::fetch::response]. -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] -pub enum Error { - #[error("Failed to read from line reader")] - Io(#[source] std::io::Error), - #[error(transparent)] - UploadPack(#[from] gix_transport::packetline::read::Error), - #[error(transparent)] - Transport(#[from] client::Error), - #[error("Currently we require feature {feature:?}, which is not supported by the server")] - MissingServerCapability { feature: &'static str }, - #[error("Encountered an unknown line prefix in {line:?}")] - UnknownLineType { line: String }, - #[error("Unknown or unsupported header: {header:?}")] - UnknownSectionHeader { header: String }, -} +pub type Error = gix_error::Exn; -impl From for Error { - fn from(err: std::io::Error) -> Self { - if err.kind() == std::io::ErrorKind::Other { - match err.into_inner() { - Some(err) => match err.downcast::() { - Ok(err) => Error::UploadPack(*err), - Err(err) => Error::Io(std::io::Error::other(err)), - }, - None => Error::Io(std::io::ErrorKind::Other.into()), - } - } else { - Error::Io(err) - } - } -} - -impl gix_transport::IsSpuriousError for Error { - fn is_spurious(&self) -> bool { - match self { - Error::Io(err) => err.is_spurious(), - Error::Transport(err) => err.is_spurious(), - _ => false, - } - } +fn unknown_line(line: &str) -> Error { + CorruptionError::new(format!("Encountered an unknown line prefix in {line:?}")).raise_erased() } /// An 'ACK' line received from the server. @@ -76,14 +40,14 @@ pub fn shallow_update_from_line(line: &str) -> Result { match line.trim_end().split_once(' ') { Some((prefix, id)) => { let id = gix_hash::ObjectId::from_hex(id.as_bytes()) - .map_err(|_| Error::UnknownLineType { line: line.to_owned() })?; + .or_raise_erased(|| CorruptionError::new(format!("Encountered an unknown line prefix in {line:?}")))?; Ok(match prefix { "shallow" => ShallowUpdate::Shallow(id), "unshallow" => ShallowUpdate::Unshallow(id), - _ => return Err(Error::UnknownLineType { line: line.to_owned() }), + _ => return Err(unknown_line(line)), }) } - None => Err(Error::UnknownLineType { line: line.to_owned() }), + None => Err(unknown_line(line)), } } @@ -97,22 +61,23 @@ impl Acknowledgement { "NAK" => Acknowledgement::Nak, // V1 "ACK" => { let id = match id { - Some(id) => gix_hash::ObjectId::from_hex(id.as_bytes()) - .map_err(|_| Error::UnknownLineType { line: line.to_owned() })?, - None => return Err(Error::UnknownLineType { line: line.to_owned() }), + Some(id) => gix_hash::ObjectId::from_hex(id.as_bytes()).or_raise_erased(|| { + CorruptionError::new(format!("Encountered an unknown line prefix in {line:?}")) + })?, + None => return Err(unknown_line(line)), }; if let Some(description) = description { match description { "common" => {} "ready" => return Ok(Acknowledgement::Ready), - _ => return Err(Error::UnknownLineType { line: line.to_owned() }), + _ => return Err(unknown_line(line)), } } Acknowledgement::Common(id) } - _ => return Err(Error::UnknownLineType { line: line.to_owned() }), + _ => return Err(unknown_line(line)), }), - (None, _, _) => Err(Error::UnknownLineType { line: line.to_owned() }), + (None, _, _) => Err(unknown_line(line)), } } /// Returns the hash of the acknowledged object if this instance acknowledges a common one. @@ -129,11 +94,12 @@ impl WantedRef { pub fn from_line(line: &str) -> Result { match line.trim_end().split_once(' ') { Some((id, path)) => { - let id = gix_hash::ObjectId::from_hex(id.as_bytes()) - .map_err(|_| Error::UnknownLineType { line: line.to_owned() })?; + let id = gix_hash::ObjectId::from_hex(id.as_bytes()).or_raise_erased(|| { + CorruptionError::new(format!("Encountered an unknown line prefix in {line:?}")) + })?; Ok(WantedRef { id, path: path.into() }) } - None => Err(Error::UnknownLineType { line: line.to_owned() }), + None => Err(unknown_line(line)), } } } @@ -155,18 +121,20 @@ impl Response { let has = |name: &str| features.iter().any(|f| f.0 == name); // Let's focus on V2 standards, and simply not support old servers to keep our code simpler if !has("multi_ack_detailed") { - return Err(Error::MissingServerCapability { - feature: "multi_ack_detailed", - }); + return Err(ValidationError::new( + "Currently we require feature \"multi_ack_detailed\", which is not supported by the server", + ) + .raise_erased()); } // It's easy to NOT do sideband for us, but then again, everyone supports it. // CORRECTION: If sideband is off, it would send the packfile without packet line encoding, // which is nothing we ever want to deal with (despite it being more efficient). In V2, this // is not even an option anymore, sidebands are always present. if !has("side-band") && !has("side-band-64k") { - return Err(Error::MissingServerCapability { - feature: "side-band OR side-band-64k", - }); + return Err(ValidationError::new( + "Currently we require feature \"side-band OR side-band-64k\", which is not supported by the server", + ) + .raise_erased()); } } Protocol::V2 => {} diff --git a/gix-protocol/src/fetch/types.rs b/gix-protocol/src/fetch/types.rs index 718dd33ffdc..705115362b4 100644 --- a/gix-protocol/src/fetch/types.rs +++ b/gix-protocol/src/fetch/types.rs @@ -44,11 +44,10 @@ mod with_fetch { use crate::fetch::{self, negotiate, refmap}; /// For use in [`fetch`](crate::fetch()). - pub struct NegotiateContext<'a, 'b, 'c, Objects, Alternates, AlternatesOut, AlternatesErr, Find> + pub struct NegotiateContext<'a, 'b, 'c, Objects, Alternates, AlternatesOut, Find> where Objects: gix_object::Find + gix_object::FindHeader + gix_object::Exists, - Alternates: FnOnce() -> Result, - AlternatesErr: Into>, + Alternates: FnOnce() -> Result, AlternatesOut: Iterator, Find: gix_object::Find, { diff --git a/gix-protocol/src/handshake/function.rs b/gix-protocol/src/handshake/function.rs index 511246e99ad..9c65947b077 100644 --- a/gix-protocol/src/handshake/function.rs +++ b/gix-protocol/src/handshake/function.rs @@ -1,5 +1,7 @@ use crate::bisync::bisync; +use gix_error::{ErrorExt, ResultExt, ValidationError, message}; use gix_features::{progress, progress::Progress}; +use gix_transport::IsSpuriousError; use gix_transport::{Service, client}; use super::Error; @@ -61,20 +63,31 @@ where www_authenticate, ..Default::default() })) - .map_err(|err| Error::Credentials(err.into_error()))? - .ok_or(Error::EmptyCredentials)?; - transport.set_identity(identity)?; + .or_raise_erased(|| message("Failed to obtain credentials"))? + .ok_or_else(|| { + message( + "No credentials were returned at all as if the credential helper isn't functioning unknowingly", + ) + .raise_erased() + })?; + transport + .set_identity(identity) + .or_raise_erased(|| message("Could not set transport identity"))?; progress.step(); progress.set_name("handshake (authenticated)".into()); match transport.handshake(service, &extra_parameters).await { Ok(v) => { - authenticate(next.store()).map_err(|err| Error::Credentials(err.into_error()))?; + authenticate(next.store()).or_raise_erased(|| message("Failed to store credentials"))?; Ok(v) } // Still no permission? Reject the credentials. Err(client::Error::Io(err)) if err.kind() == std::io::ErrorKind::PermissionDenied => { - authenticate(next.erase()).map_err(|err| Error::Credentials(err.into_error()))?; - return Err(Error::InvalidCredentials { url, source: err }); + authenticate(next.erase()).or_raise_erased(|| message("Failed to erase credentials"))?; + return Err(err + .and_raise(message!( + "Credentials provided for \"{url}\" were not accepted by the remote" + )) + .erased()); } // Otherwise, do nothing, as we don't know if it actually got to try the credentials. // If they were previously stored, they remain. In the worst case, the user has to enter them again @@ -83,12 +96,21 @@ where } } Err(err) => Err(err), - }?; + } + .map_err(|err| { + let context = message("Transport handshake failed"); + if err.is_spurious() { + gix_error::RetryableError::new(err).and_raise(context).erased() + } else { + err.and_raise(context).erased() + } + })?; if !supported_versions.is_empty() && !supported_versions.contains(&actual_protocol) { - return Err(Error::TransportProtocolPolicyViolation { - actual_version: actual_protocol, - }); + return Err(ValidationError::new(format!( + "The transport didn't accept the advertised server version {actual_protocol:?} and closed the connection client side" + )) + .raise_erased()); } let parsed_refs = match refs { diff --git a/gix-protocol/src/handshake/mod.rs b/gix-protocol/src/handshake/mod.rs index b578e34d198..70517d14e0f 100644 --- a/gix-protocol/src/handshake/mod.rs +++ b/gix-protocol/src/handshake/mod.rs @@ -72,6 +72,7 @@ pub(crate) mod hero { #[cfg(feature = "fetch")] mod fetch { use crate::{Handshake, command::Feature, fetch::RefMap, ls_refs::RefPrefixes}; + use gix_error::{ResultExt, message}; use gix_features::progress::Progress; /// Intermediate state while potentially fetching a refmap after the handshake. @@ -104,7 +105,8 @@ pub(crate) mod hero { let capabilities = cmd.capabilities; let remote_refs = cmd .$invoke(transport, &mut progress, trace_packetlines) - .await?; + .await + .or_raise_erased(|| message("Failed to list references on the remote"))?; RefMap::from_refs(remote_refs, capabilities, cx) } }; @@ -161,39 +163,8 @@ pub(crate) mod hero { #[cfg(feature = "handshake")] mod error { - use bstr::BString; - use gix_transport::client; - - use crate::handshake::refs; - /// The error returned by [`handshake()`][crate::handshake()]. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error("Failed to obtain credentials")] - Credentials(#[source] gix_error::Error), - #[error("No credentials were returned at all as if the credential helper isn't functioning unknowingly")] - EmptyCredentials, - #[error("Credentials provided for \"{url}\" were not accepted by the remote")] - InvalidCredentials { url: BString, source: std::io::Error }, - #[error(transparent)] - Transport(#[from] client::Error), - #[error( - "The transport didn't accept the advertised server version {actual_version:?} and closed the connection client side" - )] - TransportProtocolPolicyViolation { actual_version: gix_transport::Protocol }, - #[error(transparent)] - ParseRefs(#[from] refs::parse::Error), - } - - impl gix_transport::IsSpuriousError for Error { - fn is_spurious(&self) -> bool { - match self { - Error::Transport(err) => err.is_spurious(), - _ => false, - } - } - } + pub type Error = gix_error::Exn; } #[cfg(feature = "handshake")] diff --git a/gix-protocol/src/handshake/refs/async_io.rs b/gix-protocol/src/handshake/refs/async_io.rs index b338149111e..bbb43e92623 100644 --- a/gix-protocol/src/handshake/refs/async_io.rs +++ b/gix-protocol/src/handshake/refs/async_io.rs @@ -2,6 +2,7 @@ use crate::fetch::response::ShallowUpdate; use crate::handshake::{Ref, refs, refs::parse::Error}; use crate::transport::client::async_io::ReadlineBufRead; +use gix_error::{ResultExt, message}; /// Parse refs from the given input line by line. Protocol V2 is required for this to succeed. pub async fn from_v2_refs(in_refs: &mut dyn ReadlineBufRead) -> Result, Error> { @@ -9,9 +10,10 @@ pub async fn from_v2_refs(in_refs: &mut dyn ReadlineBufRead) -> Result, while let Some(line) = in_refs .readline() .await - .transpose()? .transpose() - .map_err(|err| Error::DecodePacketline(gix_error::Error::from_error(err)))? + .or_raise_erased(|| message("Could not read advertised ref"))? + .transpose() + .or_raise_erased(|| message("Could not decode advertised ref"))? .and_then(|l| l.as_bstr()) { out_refs.push(refs::shared::parse_v2(line)?); @@ -39,9 +41,10 @@ pub async fn from_v1_refs_received_as_part_of_handshake_and_capabilities<'a>( while let Some(line) = in_refs .readline() .await - .transpose()? .transpose() - .map_err(|err| Error::DecodePacketline(gix_error::Error::from_error(err)))? + .or_raise_erased(|| message("Could not read advertised ref"))? + .transpose() + .or_raise_erased(|| message("Could not decode advertised ref"))? .and_then(|l| l.as_bstr()) { refs::shared::parse_v1( diff --git a/gix-protocol/src/handshake/refs/blocking_io.rs b/gix-protocol/src/handshake/refs/blocking_io.rs index 3c9257ca050..0c431fcc76b 100644 --- a/gix-protocol/src/handshake/refs/blocking_io.rs +++ b/gix-protocol/src/handshake/refs/blocking_io.rs @@ -3,15 +3,17 @@ use crate::{ fetch::response::ShallowUpdate, handshake::{Ref, refs, refs::parse::Error}, }; +use gix_error::{ResultExt, message}; /// Parse refs from the given input line by line. Protocol V2 is required for this to succeed. pub fn from_v2_refs(in_refs: &mut dyn ReadlineBufRead) -> Result, Error> { let mut out_refs = Vec::new(); while let Some(line) = in_refs .readline() - .transpose()? .transpose() - .map_err(|err| Error::DecodePacketline(gix_error::Error::from_error(err)))? + .or_raise_erased(|| message("Could not read advertised ref"))? + .transpose() + .or_raise_erased(|| message("Could not decode advertised ref"))? .and_then(|l| l.as_bstr()) { out_refs.push(refs::shared::parse_v2(line)?); @@ -37,9 +39,10 @@ pub fn from_v1_refs_received_as_part_of_handshake_and_capabilities<'a>( while let Some(line) = in_refs .readline() - .transpose()? .transpose() - .map_err(|err| Error::DecodePacketline(gix_error::Error::from_error(err)))? + .or_raise_erased(|| message("Could not read advertised ref"))? + .transpose() + .or_raise_erased(|| message("Could not decode advertised ref"))? .and_then(|l| l.as_bstr()) { refs::shared::parse_v1( diff --git a/gix-protocol/src/handshake/refs/mod.rs b/gix-protocol/src/handshake/refs/mod.rs index 0261c7dfbaa..92b3f43a5d8 100644 --- a/gix-protocol/src/handshake/refs/mod.rs +++ b/gix-protocol/src/handshake/refs/mod.rs @@ -4,31 +4,8 @@ use super::Ref; /// pub mod parse { - use bstr::BString; - /// The error returned when parsing References/refs from the server response. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - Io(#[from] std::io::Error), - #[error(transparent)] - DecodePacketline(gix_error::Error), - #[error(transparent)] - Id(#[from] gix_hash::decode::Error), - #[error("{symref:?} could not be parsed. A symref is expected to look like :.")] - MalformedSymref { symref: BString }, - #[error("{0:?} could not be parsed. A V1 ref line should be ' '.")] - MalformedV1RefLine(BString), - #[error( - "{0:?} could not be parsed. A V2 ref line should be ' [ (peeled|symref-target):'." - )] - MalformedV2RefLine(BString), - #[error("The ref attribute {attribute:?} is unknown. Found in line {line:?}")] - UnknownAttribute { attribute: BString, line: BString }, - #[error("{message}")] - InvariantViolation { message: &'static str }, - } + pub type Error = gix_error::Exn; } impl Ref { diff --git a/gix-protocol/src/handshake/refs/shared.rs b/gix-protocol/src/handshake/refs/shared.rs index d5792621a39..84720de4916 100644 --- a/gix-protocol/src/handshake/refs/shared.rs +++ b/gix-protocol/src/handshake/refs/shared.rs @@ -1,4 +1,5 @@ use bstr::{BStr, BString, ByteSlice}; +use gix_error::{CorruptionError, ErrorExt, ResultExt}; use crate::{ fetch::response::ShallowUpdate, @@ -104,13 +105,17 @@ pub(crate) fn from_capabilities<'a>( } }); for symref in symref_values { - let (left, right) = symref.split_at(symref.find_byte(b':').ok_or_else(|| Error::MalformedSymref { - symref: symref.to_owned(), + let (left, right) = symref.split_at(symref.find_byte(b':').ok_or_else(|| { + CorruptionError::new(format!( + "{symref:?} could not be parsed. A symref is expected to look like :." + )) + .raise_erased() })?); if left.is_empty() || right.is_empty() { - return Err(Error::MalformedSymref { - symref: symref.to_owned(), - }); + return Err(CorruptionError::new(format!( + "{symref:?} could not be parsed. A symref is expected to look like :." + )) + .raise_erased()); } out_refs.push(InternalRef::SymbolicForLookup { path: left.into(), @@ -130,14 +135,18 @@ pub(in crate::handshake::refs) fn parse_v1( line: &BStr, ) -> Result<(), Error> { let trimmed = line.trim_end(); - let (hex_hash, path) = trimmed.split_at( - trimmed - .find(b" ") - .ok_or_else(|| Error::MalformedV1RefLine(trimmed.to_owned().into()))?, - ); + let (hex_hash, path) = trimmed.split_at(trimmed.find(b" ").ok_or_else(|| { + CorruptionError::new(format!( + "{trimmed:?} could not be parsed. A V1 ref line should be ' '." + )) + .raise_erased() + })?); let path = &path[1..]; if path.is_empty() { - return Err(Error::MalformedV1RefLine(trimmed.to_owned().into())); + return Err(CorruptionError::new(format!( + "{trimmed:?} could not be parsed. A V1 ref line should be ' '." + )) + .raise_erased()); } match path.strip_suffix(b"^{}") { Some(stripped) => { @@ -145,33 +154,36 @@ pub(in crate::handshake::refs) fn parse_v1( // this is a special dummy-ref just for the sake of getting capabilities across in a repo that is empty. return Ok(()); } - let (previous_path, tag) = - out_refs - .pop() - .and_then(InternalRef::unpack_direct) - .ok_or(Error::InvariantViolation { - message: "Expecting peeled refs to be preceded by direct refs", - })?; + let (previous_path, tag) = out_refs.pop().and_then(InternalRef::unpack_direct).ok_or_else(|| { + CorruptionError::new("Expecting peeled refs to be preceded by direct refs").raise_erased() + })?; if previous_path != stripped { - return Err(Error::InvariantViolation { - message: "Expecting peeled refs to have the same base path as the previous, unpeeled one", - }); + return Err(CorruptionError::new( + "Expecting peeled refs to have the same base path as the previous, unpeeled one", + ) + .raise_erased()); } out_refs.push(InternalRef::Peeled { path: previous_path, tag, - object: gix_hash::ObjectId::from_hex(hex_hash.as_bytes())?, + object: gix_hash::ObjectId::from_hex(hex_hash.as_bytes()) + .or_raise_erased(|| CorruptionError::new("Could not decode peeled object ID"))?, }); } None => { let object = match gix_hash::ObjectId::from_hex(hex_hash.as_bytes()) { Ok(id) => id, Err(_) if hex_hash.as_bstr() == "shallow" => { - let id = gix_hash::ObjectId::from_hex(path)?; + let id = gix_hash::ObjectId::from_hex(path) + .or_raise_erased(|| CorruptionError::new("Could not decode shallow object ID"))?; out_shallow.push(ShallowUpdate::Shallow(id)); return Ok(()); } - Err(err) => return Err(err.into()), + Err(err) => { + return Err(err + .and_raise(CorruptionError::new("Could not decode object ID")) + .erased()); + } }; match out_refs .iter() @@ -205,10 +217,16 @@ pub(in crate::handshake::refs) fn parse_v2(line: &BStr) -> Result { let id = if hex_hash == b"unborn" { None } else { - Some(gix_hash::ObjectId::from_hex(hex_hash.as_bytes())?) + Some( + gix_hash::ObjectId::from_hex(hex_hash.as_bytes()) + .or_raise_erased(|| CorruptionError::new("Could not decode object ID"))?, + ) }; if path.is_empty() { - return Err(Error::MalformedV2RefLine(trimmed.to_owned().into())); + return Err(CorruptionError::new(format!( + "{trimmed:?} could not be parsed. A V2 ref line should be ' [ (peeled|symref-target):'." + )) + .raise_erased()); } let mut symref_target = None; let mut peeled = None; @@ -217,43 +235,59 @@ pub(in crate::handshake::refs) fn parse_v2(line: &BStr) -> Result { match (tokens.next(), tokens.next()) { (Some(attribute), Some(value)) => { if value.is_empty() { - return Err(Error::MalformedV2RefLine(trimmed.to_owned().into())); + return Err(CorruptionError::new(format!( + "{trimmed:?} could not be parsed. A V2 ref line should be ' [ (peeled|symref-target):'." + )) + .raise_erased()); } match attribute { b"peeled" => { - peeled = Some(gix_hash::ObjectId::from_hex(value.as_bytes())?); + peeled = Some( + gix_hash::ObjectId::from_hex(value.as_bytes()) + .or_raise_erased(|| CorruptionError::new("Could not decode peeled object ID"))?, + ); } b"symref-target" => { symref_target = Some(value); } _ => { - return Err(Error::UnknownAttribute { - attribute: attribute.to_owned().into(), - line: trimmed.to_owned().into(), - }); + return Err(CorruptionError::new(format!( + "The ref attribute {attribute:?} is unknown. Found in line {trimmed:?}" + )) + .raise_erased()); } } } - _ => return Err(Error::MalformedV2RefLine(trimmed.to_owned().into())), + _ => { + return Err(CorruptionError::new(format!( + "{trimmed:?} could not be parsed. A V2 ref line should be ' [ (peeled|symref-target):'." + )) + .raise_erased()); + } } } if tokens.next().is_some() { - return Err(Error::MalformedV2RefLine(trimmed.to_owned().into())); + return Err(CorruptionError::new(format!( + "{trimmed:?} could not be parsed. A V2 ref line should be ' [ (peeled|symref-target):'." + )) + .raise_erased()); } Ok(match (symref_target, peeled) { (Some(target_name), peeled) => match target_name { b"(null)" => match peeled { None => Ref::Direct { full_ref_name: path.into(), - object: id.ok_or(Error::InvariantViolation { - message: "got 'unborn' while (null) was a symref target", + object: id.ok_or_else(|| { + CorruptionError::new("got 'unborn' while (null) was a symref target") + .raise_erased() })?, }, Some(peeled) => Ref::Peeled { full_ref_name: path.into(), object: peeled, - tag: id.ok_or(Error::InvariantViolation { - message: "got 'unborn' while (null) was a symref target", + tag: id.ok_or_else(|| { + CorruptionError::new("got 'unborn' while (null) was a symref target") + .raise_erased() })?, }, }, @@ -273,19 +307,22 @@ pub(in crate::handshake::refs) fn parse_v2(line: &BStr) -> Result { (None, Some(peeled)) => Ref::Peeled { full_ref_name: path.into(), object: peeled, - tag: id.ok_or(Error::InvariantViolation { - message: "got 'unborn' as tag target", + tag: id.ok_or_else(|| { + CorruptionError::new("got 'unborn' as tag target").raise_erased() })?, }, (None, None) => Ref::Direct { - object: id.ok_or(Error::InvariantViolation { - message: "got 'unborn' as object name of direct reference", + object: id.ok_or_else(|| { + CorruptionError::new("got 'unborn' as object name of direct reference").raise_erased() })?, full_ref_name: path.into(), }, }) } - _ => Err(Error::MalformedV2RefLine(trimmed.to_owned().into())), + _ => Err(CorruptionError::new(format!( + "{trimmed:?} could not be parsed. A V2 ref line should be ' [ (peeled|symref-target):'." + )) + .raise_erased()), } } diff --git a/gix-protocol/src/ls_refs.rs b/gix-protocol/src/ls_refs.rs index 9b25c365b40..1fed1a0f95d 100644 --- a/gix-protocol/src/ls_refs.rs +++ b/gix-protocol/src/ls_refs.rs @@ -1,30 +1,7 @@ #[cfg(any(feature = "blocking-client", feature = "async-client"))] mod error { - use crate::handshake::refs::parse; - /// The error returned by invoking a [`super::function::LsRefsCommand`]. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - Io(#[from] std::io::Error), - #[error(transparent)] - Transport(#[from] gix_transport::client::Error), - #[error(transparent)] - Parse(#[from] parse::Error), - #[error(transparent)] - ArgumentValidation(#[from] crate::command::validate_argument_prefixes::Error), - } - - impl gix_transport::IsSpuriousError for Error { - fn is_spurious(&self) -> bool { - match self { - Error::Io(err) => err.is_spurious(), - Error::Transport(err) => err.is_spurious(), - _ => false, - } - } - } + pub type Error = gix_error::Exn; } #[cfg(any(feature = "blocking-client", feature = "async-client"))] pub use error::Error; @@ -37,6 +14,7 @@ pub(crate) mod function { use std::collections::HashSet; use bstr::{BString, ByteVec}; + use gix_error::{ResultExt, message}; use gix_features::progress::Progress; use gix_transport::client::Capabilities; @@ -143,12 +121,14 @@ pub(crate) mod function { trace: bool, ) -> Result, Error> { let _span = gix_features::trace::detail!("gix_protocol::LsRefsCommand::invoke()", mode = $mode); - Command::LsRefs.validate_argument_prefixes( - gix_transport::Protocol::V2, - self.capabilities, - &self.arguments, - &self.features, - )?; + Command::LsRefs + .validate_argument_prefixes( + gix_transport::Protocol::V2, + self.capabilities, + &self.arguments, + &self.features, + ) + .or_raise_erased(|| message("Invalid ls-refs arguments or capabilities"))?; progress.step(); progress.set_name("list refs".into()); @@ -163,8 +143,9 @@ pub(crate) mod function { }, trace, ) - .await?; - Ok($from_v2_refs(&mut remote_refs).await?) + .await + .map_err(|err| crate::fetch::function::transport_error(err, "Could not invoke ls-refs"))?; + $from_v2_refs(&mut remote_refs).await } }; } @@ -223,11 +204,57 @@ pub(crate) mod function { } #[cfg(test)] - mod ref_prefixes { + mod tests { use bstr::{BString, ByteSlice}; use super::RefPrefixes; + #[cfg(feature = "blocking-client")] + #[test] + fn invoke_preserves_transport_retryability() { + use std::io::{self, ErrorKind}; + + struct FailingWriter(ErrorKind); + impl io::Write for FailingWriter { + fn write(&mut self, _buf: &[u8]) -> io::Result { + Err(self.0.into()) + } + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } + } + + let capabilities = super::Capabilities::from_lines("version 2\nls-refs\nagent=test\n".into()) + .expect("valid V2 capabilities"); + for (kind, retryable) in [ + (ErrorKind::BrokenPipe, true), + (ErrorKind::ConnectionReset, true), + (ErrorKind::PermissionDenied, false), + ] { + let transport = gix_transport::client::git::blocking_io::Connection::new( + &[][..], + FailingWriter(kind), + gix_transport::Protocol::V2, + "repo.git", + None::<(&str, _)>, + gix_transport::client::git::ConnectMode::Process, + false, + ); + let err = super::LsRefsCommand::new(None, &capabilities, ("agent", Some("test".into()))) + .invoke_blocking(transport, &mut gix_features::progress::Discard, false) + .expect_err("the transport write fails") + .into_error(); + assert_eq!(err.can_retry(), retryable, "preserve retry policy for {kind:?}"); + assert_eq!( + err.downcast_any_ref::() + .expect("retain the I/O cause") + .kind(), + kind, + "the original transport failure remains available" + ); + } + } + #[test] fn extend_preserves_first_seen_order_and_deduplicates_prefixes() { let mut prefixes = RefPrefixes::new(); diff --git a/gix-protocol/tests/protocol/fetch/_impl.rs b/gix-protocol/tests/protocol/fetch/_impl.rs index 82bab99993e..08be6d25202 100644 --- a/gix-protocol/tests/protocol/fetch/_impl.rs +++ b/gix-protocol/tests/protocol/fetch/_impl.rs @@ -12,6 +12,7 @@ pub enum RefsAction { mod fetch_fn { use crate::bisync::bisync; + use gix_error::{ErrorExt, ResultExt, message}; use gix_features::progress::NestedProgress; use gix_protocol::{ Command, LsRefsCommand, credentials, @@ -96,7 +97,8 @@ mod fetch_fn { delegate.handshake_extra_parameters(), &mut progress, ) - .await?; + .await + .or_raise_erased(|| message("Protocol handshake failed"))?; let agent = gix_protocol::agent(agent); let refs = match refs { @@ -108,20 +110,23 @@ mod fetch_fn { { LsRefsCommand::new(None, &capabilities, ("agent", Some(agent.clone()))) .invoke_async(&mut transport, &mut progress, trace) - .await? + .await + .or_raise_erased(|| message("Failed to list remote references"))? } #[cfg(feature = "blocking-client")] { - LsRefsCommand::new(None, &capabilities, ("agent", Some(agent.clone()))).invoke_blocking( - &mut transport, - &mut progress, - trace, - )? + LsRefsCommand::new(None, &capabilities, ("agent", Some(agent.clone()))) + .invoke_blocking(&mut transport, &mut progress, trace) + .or_raise_erased(|| message("Failed to list remote references"))? } } Err(err) => { - indicate_end_of_interaction(transport, trace).await?; - return Err(err.into()); + indicate_end_of_interaction(transport, trace) + .await + .or_raise_erased(|| message("Failed to end the interaction"))?; + return Err(err + .and_raise(message("Failed to prepare listing remote references")) + .erased()); } }, }; @@ -133,7 +138,9 @@ mod fetch_fn { return if matches!(protocol_version, gix_transport::Protocol::V1) || matches!(fetch_mode, FetchConnection::TerminateOnSuccessfulCompletion) { - indicate_end_of_interaction(transport, trace).await.map_err(Into::into) + indicate_end_of_interaction(transport, trace) + .await + .or_raise_erased(|| message("Failed to end the interaction")) } else { Ok(()) }; @@ -144,8 +151,10 @@ mod fetch_fn { .expect("BUG: delegates must always produce valid arguments"); } Err(err) => { - indicate_end_of_interaction(transport, trace).await?; - return Err(err.into()); + indicate_end_of_interaction(transport, trace) + .await + .or_raise_erased(|| message("Failed to end the interaction"))?; + return Err(err.and_raise(message("Failed to prepare the fetch")).erased()); } } @@ -159,8 +168,13 @@ mod fetch_fn { progress.step(); progress.set_name(format!("negotiate (round {round})")); round += 1; - let action = delegate.negotiate(&refs, &mut arguments, previous_response.as_ref())?; - let mut reader = arguments.send(&mut transport, action == Action::Cancel).await?; + let action = delegate + .negotiate(&refs, &mut arguments, previous_response.as_ref()) + .or_raise_erased(|| message("Fetch negotiation failed"))?; + let mut reader = arguments + .send(&mut transport, action == Action::Cancel) + .await + .or_raise_erased(|| message("Failed to send fetch arguments"))?; if sideband_all { setup_remote_progress(&mut progress, &mut reader); } @@ -170,14 +184,18 @@ mod fetch_fn { true, /* hack, telling us we don't want this delegate approach anymore */ false, /* just as much of a hack which causes us to expect a pack immediately */ ) - .await?; + .await + .or_raise_erased(|| message("Could not decode server reply"))?; previous_response = if response.has_pack() { progress.step(); progress.set_name("receiving pack".into()); if !sideband_all { setup_remote_progress(&mut progress, &mut reader); } - delegate.receive_pack(reader, progress, &refs, &response).await?; + delegate + .receive_pack(reader, progress, &refs, &response) + .await + .or_raise_erased(|| message("Failed to receive pack"))?; break 'negotiation; } else { match action { @@ -189,7 +207,9 @@ mod fetch_fn { if matches!(protocol_version, gix_transport::Protocol::V2) && matches!(fetch_mode, FetchConnection::TerminateOnSuccessfulCompletion) { - indicate_end_of_interaction(transport, trace).await?; + indicate_end_of_interaction(transport, trace) + .await + .or_raise_erased(|| message("Failed to end the interaction"))?; } Ok(()) } diff --git a/gix-protocol/tests/protocol/fetch/mod.rs b/gix-protocol/tests/protocol/fetch/mod.rs index 476cab84c13..4cc2884e680 100644 --- a/gix-protocol/tests/protocol/fetch/mod.rs +++ b/gix-protocol/tests/protocol/fetch/mod.rs @@ -16,26 +16,8 @@ use _impl::{Action, DelegateBlocking, RefsAction}; mod ref_map; mod error { - use std::io; - - use gix_protocol::{fetch::response, handshake, ls_refs}; - use gix_transport::client; - /// The error used in [`fetch()`][crate::fetch()]. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - Handshake(#[from] handshake::Error), - #[error("Could not access repository or failed to read streaming pack file")] - Io(#[from] io::Error), - #[error(transparent)] - Transport(#[from] client::Error), - #[error(transparent)] - LsRefs(#[from] ls_refs::Error), - #[error(transparent)] - Response(#[from] response::Error), - } + pub type Error = gix_error::Exn; } pub use error::Error; diff --git a/gix-protocol/tests/protocol/fetch/ref_map.rs b/gix-protocol/tests/protocol/fetch/ref_map.rs index 6accb81d2a1..10db3903a84 100644 --- a/gix-protocol/tests/protocol/fetch/ref_map.rs +++ b/gix-protocol/tests/protocol/fetch/ref_map.rs @@ -30,10 +30,13 @@ mod from_refs { fn unknown_object_format_errors() { let caps = caps_with(b"symref=HEAD:refs/heads/main object-format=sha999 agent=git/2.54.0"); let err = RefMap::from_refs(Vec::new(), &caps, ctx()).expect_err("unknown format must error"); - assert!( - matches!(err, refmap::init::Error::UnknownObjectFormat { ref format } if format == "sha999"), - "Any `object-format` value we don't recognize must surface as an `UnknownObjectFormat` error - rather than silently defaulting to Sha1, so callers can refuse to fetch from unsupported servers" + let err = err + .downcast_any_ref::() + .expect("unknown formats are validation errors"); + assert_eq!( + err.input.as_ref().map(|input| input.as_slice()), + Some(b"sha999".as_slice()), + "the unsupported format is retained for callers" ); } diff --git a/gix-protocol/tests/protocol/fetch/response.rs b/gix-protocol/tests/protocol/fetch/response.rs index 159b8d1367b..299a6fe510a 100644 --- a/gix-protocol/tests/protocol/fetch/response.rs +++ b/gix-protocol/tests/protocol/fetch/response.rs @@ -380,12 +380,13 @@ mod v2 { let mut sidebands = provider.as_read_without_sidebands(); match fetch::Response::from_line_reader(Protocol::V2, &mut sidebands, true, true).await { Ok(_) => panic!("need error response"), - Err(err) => match err { - fetch::response::Error::UploadPack(err) => { - assert_eq!(err.message, "segmentation fault\n"); - } - err => panic!("we expect upload pack errors, got {err:#?}"), - }, + Err(err) => { + let err = err.into_error(); + let packetline_err = err + .downcast_any_ref::() + .unwrap_or_else(|| panic!("the remote ERR packet is retained in the error chain: {err:#?}")); + assert_eq!(packetline_err.message, "segmentation fault\n"); + } } } diff --git a/gix-protocol/tests/protocol/fetch/v2.rs b/gix-protocol/tests/protocol/fetch/v2.rs index 22d8c137633..d11ca60d48e 100644 --- a/gix-protocol/tests/protocol/fetch/v2.rs +++ b/gix-protocol/tests/protocol/fetch/v2.rs @@ -4,8 +4,7 @@ use gix_protocol::handshake; use gix_transport::Protocol; use crate::fetch::{ - _impl::FetchConnection, CloneDelegate, CloneRefInWantDelegate, Error, LsRemoteDelegate, helper_unused, oid, - transport, + _impl::FetchConnection, CloneDelegate, CloneRefInWantDelegate, LsRemoteDelegate, helper_unused, oid, transport, }; #[crate::bisync::bisync] @@ -51,13 +50,11 @@ async fn clone_abort_prep() -> crate::Result { .as_bytes() .as_bstr() ); - match err { - Error::Io(err) => { - assert_eq!(err.kind(), std::io::ErrorKind::Other); - assert_eq!(err.get_ref().expect("other error").to_string(), "hello world"); - } - _ => panic!("should not have another error here"), - } + let err = err + .downcast_any_ref::() + .expect("delegate I/O error is retained"); + assert_eq!(err.kind(), std::io::ErrorKind::Other); + assert_eq!(err.get_ref().expect("other error").to_string(), "hello world"); Ok(()) } @@ -150,13 +147,11 @@ async fn ls_remote_abort_in_prep_ls_refs() -> crate::Result { transport.into_inner().1.as_bstr(), b"0044git-upload-pack does/not/matter\x00\x00version=2\x00value-only\x00key=value\x000000".as_bstr() ); - match err { - Error::Io(err) => { - assert_eq!(err.kind(), std::io::ErrorKind::Other); - assert_eq!(err.get_ref().expect("other error").to_string(), "hello world"); - } - err => panic!("should not have another error here, got: {err}"), - } + let err = err + .downcast_any_ref::() + .expect("delegate I/O error is retained"); + assert_eq!(err.kind(), std::io::ErrorKind::Other); + assert_eq!(err.get_ref().expect("other error").to_string(), "hello world"); Ok(()) } diff --git a/gix-protocol/tests/protocol/mod.rs b/gix-protocol/tests/protocol/mod.rs index 498a93a4f06..5bb5ba471cf 100644 --- a/gix-protocol/tests/protocol/mod.rs +++ b/gix-protocol/tests/protocol/mod.rs @@ -1,4 +1,4 @@ -pub type Result = std::result::Result<(), Box>; +pub type Result = std::result::Result<(), Box>; pub fn fixture_bytes(path: &str) -> Vec { std::fs::read(std::path::PathBuf::from("tests").join("fixtures").join(path)) diff --git a/gix/src/clone/fetch/mod.rs b/gix/src/clone/fetch/mod.rs index 6591b0e8e6f..131903dd558 100644 --- a/gix/src/clone/fetch/mod.rs +++ b/gix/src/clone/fetch/mod.rs @@ -6,6 +6,10 @@ use gix_ref::Category; use crate::config::tree::Key; +fn mapping_validation(err: &gix_error::Error) -> Option<&gix_refspec::match_group::validate::Error> { + err.downcast_any_ref() +} + /// The error returned by [`PrepareFetch::fetch_only()`]. #[derive(Debug, thiserror::Error)] #[expect(missing_docs)] @@ -320,17 +324,18 @@ impl PrepareFetch { }; match connection.prepare_fetch(&repo, &mut progress, fetch_opts.clone()).await { Ok(prepare) => prepare, - Err(remote::fetch::prepare::Error::RefMap(remote::ref_map::Error::InitRefMap( - gix_protocol::fetch::refmap::init::Error::MappingValidation(err), - ))) if err.issues.len() == 1 - && fetch_opts.extra_refspecs.contains(&head_refspec) - && matches!( - err.issues.first(), - Some(gix_refspec::match_group::validate::Issue::Conflict { - destination_full_ref_name, - .. - }) if *destination_full_ref_name == head_local_tracking_branch - ) => + Err(remote::fetch::prepare::Error::RefMap(remote::ref_map::Error::InitRefMap(err))) + if mapping_validation(&err).is_some_and(|err| { + err.issues.len() == 1 + && fetch_opts.extra_refspecs.contains(&head_refspec) + && matches!( + err.issues.first(), + Some(gix_refspec::match_group::validate::Issue::Conflict { + destination_full_ref_name, + .. + }) if *destination_full_ref_name == head_local_tracking_branch + ) + }) => { let head_refspec_idx = fetch_opts .extra_refspecs diff --git a/gix/src/env.rs b/gix/src/env.rs index 1d633c86723..00f53b5c9d0 100644 --- a/gix/src/env.rs +++ b/gix/src/env.rs @@ -122,9 +122,10 @@ pub mod collate { Error::Fetch( crate::remote::fetch::Error::PackThreads(_) | crate::remote::fetch::Error::PackIndexVersion(_) - | crate::remote::fetch::Error::RemovePackKeepFile { .. } - | crate::remote::fetch::Error::Fetch(gix_protocol::fetch::Error::Negotiate(_)), + | crate::remote::fetch::Error::RemovePackKeepFile { .. }, ) => true, + #[cfg(any(feature = "async-network-client", feature = "blocking-network-client"))] + Error::Fetch(crate::remote::fetch::Error::Fetch(err)) => err.is_corrupted(), _ => false, } } diff --git a/gix/src/remote/connection/fetch/error.rs b/gix/src/remote/connection/fetch/error.rs index bd06a1306da..4d7fff1585d 100644 --- a/gix/src/remote/connection/fetch/error.rs +++ b/gix/src/remote/connection/fetch/error.rs @@ -6,7 +6,7 @@ use crate::config; #[expect(missing_docs)] pub enum Error { #[error(transparent)] - Fetch(#[from] gix_protocol::fetch::Error), + Fetch(#[from] gix_error::Error), #[error("The value to configure pack threads should be 0 to auto-configure or the amount of threads to use")] PackThreads(#[from] config::unsigned_integer::Error), #[error("The value to configure the pack index version should be 1 or 2")] @@ -41,7 +41,7 @@ pub enum Error { impl gix_protocol::transport::IsSpuriousError for Error { fn is_spurious(&self) -> bool { match self { - Error::Fetch(err) => err.is_spurious(), + Error::Fetch(err) => err.can_retry(), Error::Client(err) => err.is_spurious(), _ => false, } diff --git a/gix/src/remote/connection/fetch/receive_pack.rs b/gix/src/remote/connection/fetch/receive_pack.rs index 6ff2e0fc75d..53d7f182a00 100644 --- a/gix/src/remote/connection/fetch/receive_pack.rs +++ b/gix/src/remote/connection/fetch/receive_pack.rs @@ -1,5 +1,6 @@ use std::{ops::DerefMut, path::PathBuf, sync::atomic::AtomicBool}; +use gix_error::{ResultExt, message}; use gix_odb::store::RefreshMode; use gix_protocol::fetch::{Arguments, negotiate}; #[cfg(feature = "async-network-client")] @@ -178,7 +179,7 @@ where let res = gix_protocol::fetch( &mut negotiate, - |reader, progress, should_interrupt| -> Result { + |reader, progress, should_interrupt| -> Result { let mut may_read_to_end = false; write_pack_bundle = if matches!(self.dry_run, fetch::DryRun::No) { let res = gix_pack::Bundle::write_to_directory( @@ -192,7 +193,8 @@ where })), repo.object_hash(), write_pack_options, - )?; + ) + .or_raise_erased(|| message("Failed to write the received pack"))?; may_read_to_end = true; Some(res) } else { @@ -205,7 +207,8 @@ where context, fetch_options, ) - .await?; + .await + .map_err(gix_error::Exn::into_error)?; let negotiate = res.map(|v| outcome::Negotiate { graph: graph.detach(), rounds: v.negotiate.rounds, @@ -277,7 +280,7 @@ impl gix_protocol::fetch::Negotiate for Negotiate<'_, '_, '_> { { let alternates = std::mem::take(&mut self.alternates); let open_options = self.open_options.clone(); - move || -> Result<_, std::convert::Infallible> { + move || { Ok(alternates .into_iter() .filter_map(move |path| { diff --git a/gix/src/remote/connection/ref_map.rs b/gix/src/remote/connection/ref_map.rs index 1a5afe55026..97b5b818206 100644 --- a/gix/src/remote/connection/ref_map.rs +++ b/gix/src/remote/connection/ref_map.rs @@ -14,7 +14,7 @@ use crate::{ #[expect(missing_docs)] pub enum Error { #[error(transparent)] - InitRefMap(#[from] gix_protocol::fetch::refmap::init::Error), + InitRefMap(gix_error::Error), #[error("Failed to configure the transport before connecting to {url:?}")] GatherTransportConfig { url: BString, @@ -23,7 +23,7 @@ pub enum Error { #[error("Failed to configure the transport layer")] ConfigureTransport(#[from] Box), #[error(transparent)] - Handshake(#[from] gix_protocol::handshake::Error), + Handshake(gix_error::Error), #[error(transparent)] Transport(#[from] gix_protocol::transport::client::Error), #[error(transparent)] @@ -34,7 +34,7 @@ impl gix_protocol::transport::IsSpuriousError for Error { fn is_spurious(&self) -> bool { match self { Error::Transport(err) => err.is_spurious(), - Error::Handshake(err) => err.is_spurious(), + Error::Handshake(err) => err.can_retry(), _ => false, } } @@ -160,26 +160,36 @@ where handshake_parameters, &mut progress, ) - .await?; + .await + .map_err(gix_error::Exn::into_error) + .map_err(Error::Handshake)?; let context = fetch::refmap::init::Context { fetch_refspecs: self.remote.fetch_specs.clone(), extra_refspecs, }; - let fetch_refmap = handshake.prepare_lsrefs_or_extract_refmap( - repo.config.user_agent_tuple(), - prefix_from_spec_as_filter_on_remote, - context, - )?; + let fetch_refmap = handshake + .prepare_lsrefs_or_extract_refmap( + repo.config.user_agent_tuple(), + prefix_from_spec_as_filter_on_remote, + context, + ) + .map_err(gix_error::Exn::into_error) + .map_err(Error::InitRefMap)?; #[cfg(feature = "async-network-client")] let ref_map = fetch_refmap .fetch_async(progress, &mut self.transport.inner, self.trace) - .await?; + .await + .map_err(gix_error::Exn::into_error) + .map_err(Error::InitRefMap)?; #[cfg(feature = "blocking-network-client")] - let ref_map = fetch_refmap.fetch_blocking(progress, &mut self.transport.inner, self.trace)?; + let ref_map = fetch_refmap + .fetch_blocking(progress, &mut self.transport.inner, self.trace) + .map_err(gix_error::Exn::into_error) + .map_err(Error::InitRefMap)?; self.handshake = Some(handshake); Ok(ref_map) diff --git a/gix/tests/gix/clone.rs b/gix/tests/gix/clone.rs index 755c1e54e68..9a6dcb629a9 100644 --- a/gix/tests/gix/clone.rs +++ b/gix/tests/gix/clone.rs @@ -295,13 +295,12 @@ mod blocking_io { )? .fetch_only(gix::progress::Discard, &AtomicBool::default()) .unwrap_err(); + let gix::clone::fetch::Error::Fetch(gix::remote::fetch::Error::Fetch(err)) = err else { + panic!("expected the protocol fetch to reject the shallow remote") + }; + assert!(err.is_validation(), "the configured rejection is a validation failure"); assert!( - matches!( - err, - gix::clone::fetch::Error::Fetch(gix::remote::fetch::Error::Fetch( - gix_protocol::fetch::Error::RejectShallowRemote - )) - ), + err.to_string().contains("clone.rejectShallow"), "we can avoid fetching from remotes with this setting" ); Ok(()) diff --git a/tests/snapshots/plumbing/no-repo/pack/clone/fail-ambiguous-host b/tests/snapshots/plumbing/no-repo/pack/clone/fail-ambiguous-host index 52e6b005a91..0a99e122c4c 100644 --- a/tests/snapshots/plumbing/no-repo/pack/clone/fail-ambiguous-host +++ b/tests/snapshots/plumbing/no-repo/pack/clone/fail-ambiguous-host @@ -1 +1,4 @@ - Error: Host name '-oProxyCommand=open$IFS-aCalculator' could be mistaken for a command-line argument \ No newline at end of file + Error: Transport handshake failed + +Caused by: + Host name '-oProxyCommand=open$IFS-aCalculator' could be mistaken for a command-line argument \ No newline at end of file diff --git a/tests/snapshots/plumbing/no-repo/pack/clone/fail-ambiguous-path b/tests/snapshots/plumbing/no-repo/pack/clone/fail-ambiguous-path index 5ce09d4e1a9..62ed36c1588 100644 --- a/tests/snapshots/plumbing/no-repo/pack/clone/fail-ambiguous-path +++ b/tests/snapshots/plumbing/no-repo/pack/clone/fail-ambiguous-path @@ -1 +1,4 @@ - Error: The repository path '-oProxyCommand=open$IFS-aCalculator/bar' could be mistaken for a command-line argument \ No newline at end of file + Error: Transport handshake failed + +Caused by: + The repository path '-oProxyCommand=open$IFS-aCalculator/bar' could be mistaken for a command-line argument \ No newline at end of file diff --git a/tests/snapshots/plumbing/no-repo/pack/clone/fail-ambiguous-username/explicit-ssh b/tests/snapshots/plumbing/no-repo/pack/clone/fail-ambiguous-username/explicit-ssh index 78d252596ad..d4c35412b29 100644 --- a/tests/snapshots/plumbing/no-repo/pack/clone/fail-ambiguous-username/explicit-ssh +++ b/tests/snapshots/plumbing/no-repo/pack/clone/fail-ambiguous-username/explicit-ssh @@ -1 +1,4 @@ - Error: Username '-Fconfigfile' could be mistaken for a command-line argument \ No newline at end of file + Error: Transport handshake failed + +Caused by: + Username '-Fconfigfile' could be mistaken for a command-line argument \ No newline at end of file diff --git a/tests/snapshots/plumbing/no-repo/pack/clone/fail-ambiguous-username/implicit-ssh b/tests/snapshots/plumbing/no-repo/pack/clone/fail-ambiguous-username/implicit-ssh index 78d252596ad..d4c35412b29 100644 --- a/tests/snapshots/plumbing/no-repo/pack/clone/fail-ambiguous-username/implicit-ssh +++ b/tests/snapshots/plumbing/no-repo/pack/clone/fail-ambiguous-username/implicit-ssh @@ -1 +1,4 @@ - Error: Username '-Fconfigfile' could be mistaken for a command-line argument \ No newline at end of file + Error: Transport handshake failed + +Caused by: + Username '-Fconfigfile' could be mistaken for a command-line argument \ No newline at end of file diff --git a/tests/snapshots/plumbing/no-repo/pack/receive/fail-ambiguous-host b/tests/snapshots/plumbing/no-repo/pack/receive/fail-ambiguous-host index 473adbb4a27..0935826475c 100644 --- a/tests/snapshots/plumbing/no-repo/pack/receive/fail-ambiguous-host +++ b/tests/snapshots/plumbing/no-repo/pack/receive/fail-ambiguous-host @@ -1 +1,4 @@ -Error: Host name '-oProxyCommand=open$IFS-aCalculator' could be mistaken for a command-line argument \ No newline at end of file +Error: Transport handshake failed + +Caused by: + Host name '-oProxyCommand=open$IFS-aCalculator' could be mistaken for a command-line argument \ No newline at end of file diff --git a/tests/snapshots/plumbing/no-repo/pack/receive/fail-ambiguous-path b/tests/snapshots/plumbing/no-repo/pack/receive/fail-ambiguous-path index 2d3c844e17f..fb4123a2d29 100644 --- a/tests/snapshots/plumbing/no-repo/pack/receive/fail-ambiguous-path +++ b/tests/snapshots/plumbing/no-repo/pack/receive/fail-ambiguous-path @@ -1 +1,4 @@ -Error: The repository path '-oProxyCommand=open$IFS-aCalculator/bar' could be mistaken for a command-line argument \ No newline at end of file +Error: Transport handshake failed + +Caused by: + The repository path '-oProxyCommand=open$IFS-aCalculator/bar' could be mistaken for a command-line argument \ No newline at end of file diff --git a/tests/snapshots/plumbing/no-repo/pack/receive/fail-ambiguous-username/explicit-ssh b/tests/snapshots/plumbing/no-repo/pack/receive/fail-ambiguous-username/explicit-ssh index 8a9fca8771d..a3798370070 100644 --- a/tests/snapshots/plumbing/no-repo/pack/receive/fail-ambiguous-username/explicit-ssh +++ b/tests/snapshots/plumbing/no-repo/pack/receive/fail-ambiguous-username/explicit-ssh @@ -1 +1,4 @@ -Error: Username '-Fconfigfile' could be mistaken for a command-line argument \ No newline at end of file +Error: Transport handshake failed + +Caused by: + Username '-Fconfigfile' could be mistaken for a command-line argument \ No newline at end of file diff --git a/tests/snapshots/plumbing/no-repo/pack/receive/fail-ambiguous-username/implicit-ssh b/tests/snapshots/plumbing/no-repo/pack/receive/fail-ambiguous-username/implicit-ssh index 8a9fca8771d..a3798370070 100644 --- a/tests/snapshots/plumbing/no-repo/pack/receive/fail-ambiguous-username/implicit-ssh +++ b/tests/snapshots/plumbing/no-repo/pack/receive/fail-ambiguous-username/implicit-ssh @@ -1 +1,4 @@ -Error: Username '-Fconfigfile' could be mistaken for a command-line argument \ No newline at end of file +Error: Transport handshake failed + +Caused by: + Username '-Fconfigfile' could be mistaken for a command-line argument \ No newline at end of file From f28f7df44c88e4dc6e7892856cfd454dbcbd36bc Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 20 Aug 2026 17:23:18 +0200 Subject: [PATCH 24/51] change(gix-pack)!: migrate errors to gix-error rubberstamp --- Cargo.lock | 1 - gitoxide-core/src/pack/create.rs | 9 +- gitoxide-core/src/pack/explode.rs | 28 ++-- gitoxide-core/src/pack/index.rs | 1 + gitoxide-core/src/pack/verify.rs | 10 +- gix-odb/src/store_impls/dynamic/find.rs | 19 ++- gix-odb/src/store_impls/dynamic/header.rs | 9 +- gix-odb/src/store_impls/dynamic/load_one.rs | 5 +- gix-odb/src/store_impls/dynamic/mod.rs | 7 + gix-odb/src/store_impls/dynamic/types.rs | 10 +- gix-odb/src/store_impls/dynamic/verify.rs | 41 ++--- gix-pack/Cargo.toml | 1 - gix-pack/src/bundle/find.rs | 4 +- gix-pack/src/bundle/init.rs | 30 ++-- gix-pack/src/bundle/mod.rs | 2 +- gix-pack/src/bundle/write/error.rs | 17 +-- gix-pack/src/bundle/write/mod.rs | 27 ++-- gix-pack/src/cache/delta/from_offsets.rs | 83 ++++------- gix-pack/src/cache/delta/mod.rs | 22 +-- gix-pack/src/cache/delta/traverse/mod.rs | 60 ++------ gix-pack/src/cache/delta/traverse/resolve.rs | 110 +++++++------- gix-pack/src/cache/delta/tree.rs | 67 +++++---- gix-pack/src/cache/mod.rs | 2 +- gix-pack/src/data/delta.rs | 65 +++----- gix-pack/src/data/entry/decode.rs | 73 +++++---- gix-pack/src/data/file/decode/entry.rs | 97 +++++++----- gix-pack/src/data/file/decode/header.rs | 43 +++--- gix-pack/src/data/file/decode/mod.rs | 54 ++++--- gix-pack/src/data/file/init.rs | 13 +- gix-pack/src/data/header.rs | 19 +-- gix-pack/src/data/input/bytes_to_entries.rs | 48 ++++-- gix-pack/src/data/input/entries_to_bytes.rs | 11 +- gix-pack/src/data/input/entry.rs | 8 +- .../data/input/lookup_ref_delta_objects.rs | 10 +- gix-pack/src/data/input/types.rs | 23 +-- gix-pack/src/data/output/bytes.rs | 39 ++--- gix-pack/src/data/output/count/objects/mod.rs | 21 ++- .../src/data/output/count/objects/types.rs | 23 +-- .../src/data/output/entry/iter_from_counts.rs | 40 ++--- gix-pack/src/data/output/entry/mod.rs | 27 ++-- gix-pack/src/index/init.rs | 97 +++++------- gix-pack/src/index/traverse/error.rs | 46 +----- gix-pack/src/index/traverse/mod.rs | 109 +++++++------- gix-pack/src/index/traverse/reduce.rs | 27 +--- gix-pack/src/index/traverse/with_index.rs | 25 ++-- gix-pack/src/index/traverse/with_lookup.rs | 20 +-- gix-pack/src/index/verify.rs | 67 ++++----- gix-pack/src/index/write/error.rs | 29 +--- gix-pack/src/index/write/mod.rs | 39 +++-- gix-pack/src/multi_index/chunk.rs | 80 +++++----- gix-pack/src/multi_index/init.rs | 128 +++++++--------- gix-pack/src/multi_index/verify.rs | 140 +++++------------- gix-pack/src/multi_index/write.rs | 40 ++--- gix-pack/src/verify.rs | 28 ++-- gix-pack/tests/pack/bundle.rs | 42 +++--- gix-pack/tests/pack/data/file.rs | 66 +++++++-- gix-pack/tests/pack/data/fuzzed.rs | 14 +- gix-pack/tests/pack/data/input.rs | 48 +++--- .../pack/data/output/count_and_entries.rs | 12 +- gix-pack/tests/pack/index.rs | 27 ++-- gix-pack/tests/pack/iter.rs | 10 +- gix-pack/tests/pack/multi_index/fuzzed.rs | 55 ++++--- gix-transport/tests/async-transport.rs | 2 +- .../tests/blocking-transport-http.rs | 2 +- gix-transport/tests/blocking-transport.rs | 2 +- 65 files changed, 1070 insertions(+), 1264 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 557c256f6c4..8dfbe1bca2a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2246,7 +2246,6 @@ dependencies = [ "parking_lot", "serde", "smallvec", - "thiserror 2.0.18", "uluru", ] diff --git a/gitoxide-core/src/pack/create.rs b/gitoxide-core/src/pack/create.rs index ef4e1144f04..d5170fee45b 100644 --- a/gitoxide-core/src/pack/create.rs +++ b/gitoxide-core/src/pack/create.rs @@ -1,6 +1,7 @@ use std::{ffi::OsStr, io, path::Path, str::FromStr, time::Instant}; use anyhow::anyhow; +use gix::error::ResultExt; use gix::{ Count, NestedProgress, Progress, hash, hash::ObjectId, interrupt, objs::bstr::ByteVec, odb::pack, parallel::InOrderIter, prelude::Finalize, progress, traverse, @@ -106,7 +107,7 @@ where P: NestedProgress, P::SubProgress: 'static, { - type ObjectIdIter = dyn Iterator>> + Send; + type ObjectIdIter = dyn Iterator> + Send; let repo = gix::discover(repository_path)?; let pack_compression = repo.pack_compression()?; @@ -133,7 +134,7 @@ where let handle = repo.objects.into_shared_arc().to_cache_arc(); let iter = Box::new( traverse::commit::Simple::new(tips, handle.clone()) - .map(|res| res.map_err(|err| Box::new(err.into_error()) as Box<_>).map(|c| c.id)) + .map(|res| res.map(|c| c.id)) .inspect(move |_| progress.inc()), ); (handle, iter) @@ -149,8 +150,8 @@ where .lines() .map(|hex_id| { hex_id - .map_err(|err| Box::new(err) as Box<_>) - .and_then(|hex_id| ObjectId::from_hex(hex_id.as_bytes()).map_err(Into::into)) + .or_erased() + .and_then(|hex_id| ObjectId::from_hex(hex_id.as_bytes()).or_erased()) }) .inspect(move |_| progress.inc()), ), diff --git a/gitoxide-core/src/pack/explode.rs b/gitoxide-core/src/pack/explode.rs index bf938fc1af6..c860db41b50 100644 --- a/gitoxide-core/src/pack/explode.rs +++ b/gitoxide-core/src/pack/explode.rs @@ -6,7 +6,7 @@ use std::{ }; use anyhow::{Result, anyhow}; -use gix::error::{ErrorExt, NotFoundError, message}; +use gix::error::{ErrorExt, NotFoundError, ResultExt, message}; use gix::{ NestedProgress, hash::ObjectId, @@ -159,12 +159,14 @@ pub fn pack_or_pack_index( use anyhow::Context; let path = pack_path.as_ref(); - let bundle = pack::Bundle::at(path, object_hash).with_context(|| { - format!( - "Could not find .idx or .pack file from given file at '{}'", - path.display() - ) - })?; + let bundle = pack::Bundle::at(path, object_hash) + .map_err(gix::Exn::into_error) + .with_context(|| { + format!( + "Could not find .idx or .pack file from given file at '{}'", + path.display() + ) + })?; if !object_path.as_ref().is_none_or(|p| p.as_ref().is_dir()) { return Err(anyhow!( @@ -210,7 +212,7 @@ pub fn pack_or_pack_index( "Failed to write {object_kind} object {}", index_entry.oid )) - .into_error() + .erased() })?; if let Err(err) = written_id.verify(&index_entry.oid) { if let object::Kind::Tree = object_kind { @@ -221,7 +223,7 @@ pub fn pack_or_pack_index( } else { return Err(err .and_raise(message!("{object_kind} object wasn't re-encoded without change")) - .into_error()); + .erased()); } } if let Some(verifier) = loose_odb.as_ref() { @@ -231,16 +233,15 @@ pub fn pack_or_pack_index( err.and_raise(message!( "The recently written file for loose object {written_id} could not be read" )) - .into_error() + .erased() })? .ok_or_else(|| { NotFoundError::new(format!( "The recently written file for loose object {written_id} could not be found" )) - .raise() - .into_error() + .raise_erased() })?; - obj.verify_checksum(&written_id).map_err(gix::Exn::into_error)?; + obj.verify_checksum(&written_id).or_erased()?; } Ok(()) } @@ -253,6 +254,7 @@ pub fn pack_or_pack_index( make_pack_lookup_cache: pack::cache::lru::StaticLinkedList::<64>::default, }, ) + .map_err(gix::Exn::into_error) .with_context(|| "Failed to explode the entire pack - some loose objects may have been created nonetheless")?; let (index_path, data_path) = (bundle.index.path().to_owned(), bundle.pack.path().to_owned()); diff --git a/gitoxide-core/src/pack/index.rs b/gitoxide-core/src/pack/index.rs index e22ccee4add..3e0af6a4470 100644 --- a/gitoxide-core/src/pack/index.rs +++ b/gitoxide-core/src/pack/index.rs @@ -112,6 +112,7 @@ pub fn from_pack( options, ), } + .map_err(gix::Exn::into_error) .with_context(|| "Failed to write pack and index")?; match format { OutputFormat::Human => drop(human_output(out, res)), diff --git a/gitoxide-core/src/pack/verify.rs b/gitoxide-core/src/pack/verify.rs index 3aefaee09eb..edf3988cc03 100644 --- a/gitoxide-core/src/pack/verify.rs +++ b/gitoxide-core/src/pack/verify.rs @@ -119,13 +119,16 @@ where }; let res = match ext { "pack" => { - let pack = odb::pack::data::File::at(path, object_hash).with_context(|| "Could not open pack file")?; + let pack = odb::pack::data::File::at(path, object_hash) + .map_err(gix::Exn::into_error) + .with_context(|| "Could not open pack file")?; pack.verify_checksum(&mut progress.add_child("Sha1 of pack"), should_interrupt) .map(|id| (id, None))? } "idx" => { - let idx = - odb::pack::index::File::at(path, object_hash).with_context(|| "Could not open pack index file")?; + let idx = odb::pack::index::File::at(path, object_hash) + .map_err(gix::Exn::into_error) + .with_context(|| "Could not open pack index file")?; let packfile_path = path.with_extension("pack"); let pack = odb::pack::data::File::at(&packfile_path, object_hash) .map_err(|e| { @@ -154,6 +157,7 @@ where should_interrupt, ) .map(|o| (o.actual_index_checksum, o.pack_traverse_statistics)) + .map_err(gix::Exn::into_error) .with_context(|| "Verification failure")? } "" => match path.file_name() { diff --git a/gix-odb/src/store_impls/dynamic/find.rs b/gix-odb/src/store_impls/dynamic/find.rs index d09eddd9946..97aab6e0636 100644 --- a/gix-odb/src/store_impls/dynamic/find.rs +++ b/gix-odb/src/store_impls/dynamic/find.rs @@ -6,6 +6,8 @@ use gix_pack::cache::DecodeEntry; use crate::store::{handle, load_index}; pub(crate) mod error { + use gix_error::Error as GixError; + use crate::{loose, pack}; /// Returned by [`Handle::try_find()`][gix_pack::Find::try_find()] @@ -15,7 +17,7 @@ pub(crate) mod error { #[error("An error occurred while obtaining an object from the loose object store")] Loose(#[from] loose::find::Error), #[error("An error occurred while obtaining an object from the packed object store")] - Pack(#[from] pack::data::decode::Error), + Pack(#[from] GixError), #[error(transparent)] LoadIndex(#[from] crate::store::load_index::Error), #[error(transparent)] @@ -47,6 +49,12 @@ pub(crate) mod error { }, } + impl From for Error { + fn from(err: pack::data::decode::Error) -> Self { + Error::Pack(err.into_error()) + } + } + #[derive(Copy, Clone)] pub(crate) struct DeltaBaseRecursion<'a> { pub depth: usize, @@ -172,7 +180,13 @@ where entry_size: r.compressed_size + header_size, }), )), - Err(gix_pack::data::decode::Error::DeltaBaseUnresolved(base_id)) => { + Err(err) => { + let Some(base_id) = err + .downcast_any_ref::() + .map(|err| err.0) + else { + return Err(err.into()); + }; // Only with multi-pack indices it's allowed to jump to refer to other packs within this // multi-pack. Otherwise this would constitute a thin pack which is only allowed in transit. // However, if we somehow end up with that, we will resolve it safely, even though we could @@ -276,7 +290,6 @@ where ) }) } - Err(err) => Err(err), }?; if idx != 0 { diff --git a/gix-odb/src/store_impls/dynamic/header.rs b/gix-odb/src/store_impls/dynamic/header.rs index 14f13bad62f..39695af906a 100644 --- a/gix-odb/src/store_impls/dynamic/header.rs +++ b/gix-odb/src/store_impls/dynamic/header.rs @@ -80,7 +80,13 @@ where }) }) { Ok(header) => Ok(header.into()), - Err(gix_pack::data::decode::Error::DeltaBaseUnresolved(base_id)) => { + Err(err) => { + let Some(base_id) = err + .downcast_any_ref::() + .map(|err| err.0) + else { + return Err(err.into()); + }; // Only with multi-pack indices it's allowed to jump to refer to other packs within this // multi-pack. Otherwise this would constitute a thin pack which is only allowed in transit. // However, if we somehow end up with that, we will resolve it safely, even though we could @@ -150,7 +156,6 @@ where }) .map(Into::into) } - Err(err) => Err(err), }?; if idx != 0 { diff --git a/gix-odb/src/store_impls/dynamic/load_one.rs b/gix-odb/src/store_impls/dynamic/load_one.rs index 9ef6969697c..5ca625be86e 100644 --- a/gix-odb/src/store_impls/dynamic/load_one.rs +++ b/gix-odb/src/store_impls/dynamic/load_one.rs @@ -32,10 +32,7 @@ impl super::Store { pack.id = id.to_intrinsic_pack_id(); Arc::new(pack) }) - .map_err(|err| match err { - gix_pack::data::header::decode::Error::Io { source, .. } => source, - other => std::io::Error::other(other), - }) + .map_err(super::exn_to_io) } let slot = &self.files[id.index]; diff --git a/gix-odb/src/store_impls/dynamic/mod.rs b/gix-odb/src/store_impls/dynamic/mod.rs index 25ff75d88b5..854f8e67237 100644 --- a/gix-odb/src/store_impls/dynamic/mod.rs +++ b/gix-odb/src/store_impls/dynamic/mod.rs @@ -3,6 +3,13 @@ use std::{cell::RefCell, ops::Deref}; use crate::Store; +fn exn_to_io(err: gix_error::Exn) -> std::io::Error { + let kind = err + .downcast_any_ref::() + .map_or(std::io::ErrorKind::Other, std::io::Error::kind); + std::io::Error::new(kind, err.into_error()) +} + /// This effectively acts like a handle but exists to be usable from the actual `crate::Handle` implementation which adds caches on top. /// Each store is quickly cloned and contains thread-local state for shared packs. pub struct Handle diff --git a/gix-odb/src/store_impls/dynamic/types.rs b/gix-odb/src/store_impls/dynamic/types.rs index d6ed872b0f3..8fa42f4d59d 100644 --- a/gix-odb/src/store_impls/dynamic/types.rs +++ b/gix-odb/src/store_impls/dynamic/types.rs @@ -320,19 +320,13 @@ impl IndexAndPacks { IndexAndPacks::Index(bundle) => bundle.index.load_strict(|path| { gix_pack::index::File::at(path, object_hash) .map(Arc::new) - .map_err(|err| match err { - gix_pack::index::init::Error::Io { source, .. } => source, - err => std::io::Error::other(err), - }) + .map_err(super::exn_to_io) }), IndexAndPacks::MultiIndex(bundle) => { bundle.multi_index.load_strict(|path| { gix_pack::multi_index::File::at(path, alloc_limit_bytes) .map(Arc::new) - .map_err(|err| match err { - gix_pack::multi_index::init::Error::Io { source, .. } => source, - err => std::io::Error::other(err), - }) + .map_err(super::exn_to_io) })?; if let Some(multi_index) = bundle.multi_index.loaded() { bundle.data = Self::index_names_to_pack_paths(multi_index); diff --git a/gix-odb/src/store_impls/dynamic/verify.rs b/gix-odb/src/store_impls/dynamic/verify.rs index e58a503d6d4..896d29e367f 100644 --- a/gix-odb/src/store_impls/dynamic/verify.rs +++ b/gix-odb/src/store_impls/dynamic/verify.rs @@ -26,17 +26,17 @@ pub mod integrity { #[expect(missing_docs)] pub enum Error { #[error(transparent)] - MultiIndexIntegrity(#[from] pack::index::traverse::Error), + MultiIndexIntegrity(gix_error::Error), #[error(transparent)] - IndexIntegrity(#[from] pack::index::traverse::Error), + IndexIntegrity(gix_error::Error), #[error(transparent)] - IndexOpen(#[from] pack::index::init::Error), + IndexOpen(gix_error::Error), #[error(transparent)] LooseObjectStoreIntegrity(#[from] crate::loose::verify::integrity::Error), #[error(transparent)] - MultiIndexOpen(#[from] pack::multi_index::init::Error), + MultiIndexOpen(gix_error::Error), #[error(transparent)] - PackOpen(#[from] pack::data::init::Error), + PackOpen(gix_error::Error), #[error(transparent)] InitializeODB(#[from] crate::store::load_index::Error), #[error("The disk on state changed while performing the operation, and we observed the change.")] @@ -158,7 +158,8 @@ impl super::Store { let index = match bundle.index.loaded() { Some(index) => index.deref(), None => { - index = pack::index::File::at(bundle.index.path(), self.object_hash)?; + index = pack::index::File::at(bundle.index.path(), self.object_hash) + .map_err(|err| integrity::Error::IndexOpen(err.into_error()))?; &index } }; @@ -166,7 +167,8 @@ impl super::Store { let data = match bundle.data.loaded() { Some(pack) => pack.deref(), None => { - pack = pack::data::File::at(bundle.data.path(), self.object_hash)? + pack = pack::data::File::at(bundle.data.path(), self.object_hash) + .map_err(|err| integrity::Error::PackOpen(err.into_error()))? .with_alloc_limit_bytes(self.alloc_limit_bytes); &pack } @@ -175,14 +177,16 @@ impl super::Store { "verify index".into(), integrity::ProgressId::VerifyIndex(Default::default()).into(), ); - let outcome = index.verify_integrity( - Some(pack::index::verify::PackContext { - data, - options: options.clone(), - }), - &mut child_progress, - should_interrupt, - )?; + let outcome = index + .verify_integrity( + Some(pack::index::verify::PackContext { + data, + options: options.clone(), + }), + &mut child_progress, + should_interrupt, + ) + .map_err(|err| integrity::Error::IndexIntegrity(err.into_error()))?; statistics.push(IndexStatistics { path: bundle.index.path().to_owned(), statistics: SingleOrMultiStatistics::Single( @@ -198,7 +202,8 @@ impl super::Store { let index = match bundle.multi_index.loaded() { Some(index) => index.deref(), None => { - index = pack::multi_index::File::at(bundle.multi_index.path(), self.alloc_limit_bytes)?; + index = pack::multi_index::File::at(bundle.multi_index.path(), self.alloc_limit_bytes) + .map_err(|err| integrity::Error::MultiIndexOpen(err.into_error()))?; &index } }; @@ -206,7 +211,9 @@ impl super::Store { "verify multi-index".into(), integrity::ProgressId::VerifyMultiIndex(Default::default()).into(), ); - let outcome = index.verify_integrity(&mut child_progress, should_interrupt, options.clone())?; + let outcome = index + .verify_integrity(&mut child_progress, should_interrupt, options.clone()) + .map_err(|err| integrity::Error::MultiIndexIntegrity(err.into_error()))?; let index_dir = bundle.multi_index.path().parent().expect("file in a directory"); statistics.push(IndexStatistics { diff --git a/gix-pack/Cargo.toml b/gix-pack/Cargo.toml index 309520da6fe..742300bae7f 100644 --- a/gix-pack/Cargo.toml +++ b/gix-pack/Cargo.toml @@ -61,7 +61,6 @@ gix-diff = { version = "^0.67.1", path = "../gix-diff", default-features = false memmap2 = "0.9.11" smallvec = "1.15.1" parking_lot = { version = "0.12.4", default-features = false, optional = true } -thiserror = "2.0.18" crossbeam-deque = { version = "0.8.6", optional = true } # for caching diff --git a/gix-pack/src/bundle/find.rs b/gix-pack/src/bundle/find.rs index d8f38f321e9..48fd5637238 100644 --- a/gix-pack/src/bundle/find.rs +++ b/gix-pack/src/bundle/find.rs @@ -1,3 +1,5 @@ +use gix_error::ResultExt; + impl crate::Bundle { /// Find an object with the given [`ObjectId`](gix_hash::ObjectId) and place its data into `out`. /// `inflate` is used to decompress objects, and will be reset before first use, but not after the last use. @@ -35,7 +37,7 @@ impl crate::Bundle { cache: &mut dyn crate::cache::DecodeEntry, ) -> Result<(gix_object::Data<'a>, crate::data::entry::Location), crate::data::decode::Error> { let ofs = self.index.pack_offset_at_index(idx); - let pack_entry = self.pack.entry(ofs)?; + let pack_entry = self.pack.entry(ofs).or_erased()?; let header_size = pack_entry.header_size(); self.pack .decode_entry( diff --git a/gix-pack/src/bundle/init.rs b/gix-pack/src/bundle/init.rs index a06c9e0d32b..8da30def0f6 100644 --- a/gix-pack/src/bundle/init.rs +++ b/gix-pack/src/bundle/init.rs @@ -1,18 +1,11 @@ -use std::path::{Path, PathBuf}; +use std::path::Path; + +use gix_error::{ErrorExt, OptionExt, ValidationError}; use crate::Bundle; /// Returned by [`Bundle::at()`] -#[derive(thiserror::Error, Debug)] -#[expect(missing_docs)] -pub enum Error { - #[error("An 'idx' extension is expected of an index file: '{0}'")] - InvalidPath(PathBuf), - #[error(transparent)] - Pack(#[from] crate::data::header::decode::Error), - #[error(transparent)] - Index(#[from] crate::index::init::Error), -} +pub type Error = gix_error::Exn; /// Initialization impl Bundle { @@ -30,7 +23,12 @@ impl Bundle { let ext = path .extension() .and_then(std::ffi::OsStr::to_str) - .ok_or_else(|| Error::InvalidPath(path.to_owned()))?; + .ok_or_raise_erased(|| { + ValidationError::new(format!( + "An 'idx' extension is expected of an index file: '{}'", + path.display() + )) + })?; Ok(match ext { "idx" => Self { index: crate::index::File::at(path, object_hash)?, @@ -40,7 +38,13 @@ impl Bundle { pack: crate::data::File::at(path, object_hash)?, index: crate::index::File::at(path.with_extension("idx"), object_hash)?, }, - _ => return Err(Error::InvalidPath(path.to_owned())), + _ => { + return Err(ValidationError::new(format!( + "An 'idx' extension is expected of an index file: '{}'", + path.display() + )) + .raise_erased()); + } }) } } diff --git a/gix-pack/src/bundle/mod.rs b/gix-pack/src/bundle/mod.rs index d8ef1107d70..278ca94496b 100644 --- a/gix-pack/src/bundle/mod.rs +++ b/gix-pack/src/bundle/mod.rs @@ -33,7 +33,7 @@ pub mod verify { progress: &mut dyn DynNestedProgress, should_interrupt: &AtomicBool, options: crate::index::verify::integrity::Options, - ) -> Result> + ) -> Result where C: crate::cache::DecodeEntry, F: Fn() -> C + Send + Clone, diff --git a/gix-pack/src/bundle/write/error.rs b/gix-pack/src/bundle/write/error.rs index da44181bb4f..9ef6d75fb15 100644 --- a/gix-pack/src/bundle/write/error.rs +++ b/gix-pack/src/bundle/write/error.rs @@ -1,17 +1,2 @@ -use std::io; - -use gix_tempfile::handle::Writable; - /// The error returned by [`Bundle::write_to_directory()`][crate::Bundle::write_to_directory()] -#[derive(thiserror::Error, Debug)] -#[expect(missing_docs)] -pub enum Error { - #[error("An IO error occurred when reading the pack or creating a temporary file")] - Io(#[from] io::Error), - #[error(transparent)] - PackIter(#[from] crate::data::input::Error), - #[error("Could not move a temporary file into its desired place")] - Persist(#[from] gix_tempfile::handle::persist::Error), - #[error(transparent)] - IndexWrite(#[from] crate::index::write::Error), -} +pub type Error = gix_error::Exn; diff --git a/gix-pack/src/bundle/write/mod.rs b/gix-pack/src/bundle/write/mod.rs index a50da98b3df..dca3e79d1d4 100644 --- a/gix-pack/src/bundle/write/mod.rs +++ b/gix-pack/src/bundle/write/mod.rs @@ -6,6 +6,7 @@ use std::{ sync::{Arc, atomic::AtomicBool}, }; +use gix_error::{ResultExt, message}; use gix_features::{interrupt, progress, progress::Progress}; use gix_tempfile::{AutoRemove, ContainingDirectory}; @@ -80,8 +81,10 @@ impl crate::Bundle { let data_file = Arc::new(parking_lot::Mutex::new(io::BufWriter::with_capacity( 64 * 1024, match directory.as_ref() { - Some(directory) => gix_tempfile::new(directory, ContainingDirectory::Exists, AutoRemove::Tempfile)?, - None => gix_tempfile::new(std::env::temp_dir(), ContainingDirectory::Exists, AutoRemove::Tempfile)?, + Some(directory) => gix_tempfile::new(directory, ContainingDirectory::Exists, AutoRemove::Tempfile) + .or_raise_erased(|| message("Could not create temporary pack file"))?, + None => gix_tempfile::new(std::env::temp_dir(), ContainingDirectory::Exists, AutoRemove::Tempfile) + .or_raise_erased(|| message("Could not create temporary pack file"))?, }, ))); let (pack_entries_iter, pack_version): ( @@ -191,8 +194,10 @@ impl crate::Bundle { }; let data_file = Arc::new(parking_lot::Mutex::new(io::BufWriter::new(match directory.as_ref() { - Some(directory) => gix_tempfile::new(directory, ContainingDirectory::Exists, AutoRemove::Tempfile)?, - None => gix_tempfile::new(std::env::temp_dir(), ContainingDirectory::Exists, AutoRemove::Tempfile)?, + Some(directory) => gix_tempfile::new(directory, ContainingDirectory::Exists, AutoRemove::Tempfile) + .or_raise_erased(|| message("Could not create temporary pack file"))?, + None => gix_tempfile::new(std::env::temp_dir(), ContainingDirectory::Exists, AutoRemove::Tempfile) + .or_raise_erased(|| message("Could not create temporary pack file"))?, }))); let eight_pages = 4096 * 8; let (pack_entries_iter, pack_version): ( @@ -291,7 +296,8 @@ impl crate::Bundle { Ok(match directory { Some(directory) => { let directory = directory.as_ref(); - let mut index_file = gix_tempfile::new(directory, ContainingDirectory::Exists, AutoRemove::Tempfile)?; + let mut index_file = gix_tempfile::new(directory, ContainingDirectory::Exists, AutoRemove::Tempfile) + .or_raise_erased(|| message("Could not create temporary index file"))?; let outcome = crate::index::write_data_iter_to_stream( index_kind, @@ -327,13 +333,15 @@ impl crate::Bundle { } else { let keep_path = data_path.with_extension("keep"); - std::fs::write(&keep_path, b"")?; + std::fs::write(&keep_path, b"") + .or_raise_erased(|| message("Could not create pack keep file"))?; 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)?; + .or_erased()? + .persist(&data_path) + .or_raise_erased(|| message("Could not persist pack file"))?; Some(keep_path) }; if !index_path.is_file() { @@ -341,7 +349,8 @@ impl crate::Bundle { .persist(&index_path) .inspect_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()); - })?; + }) + .or_raise_erased(|| message("Could not persist pack index"))?; } WriteOutcome { outcome, diff --git a/gix-pack/src/cache/delta/from_offsets.rs b/gix-pack/src/cache/delta/from_offsets.rs index 773fa3994c2..6633d1c99ed 100644 --- a/gix-pack/src/cache/delta/from_offsets.rs +++ b/gix-pack/src/cache/delta/from_offsets.rs @@ -5,25 +5,13 @@ use std::{ time::Instant, }; +use gix_error::{ErrorExt, ResultExt, RetryableError, message}; use gix_features::progress::{self, Progress}; use crate::{cache::delta::Tree, data}; /// Returned by [`Tree::from_offsets_in_pack()`] -#[derive(thiserror::Error, Debug)] -#[allow(missing_docs)] -pub enum Error { - #[error("{message}")] - Io { source: io::Error, message: &'static str }, - #[error(transparent)] - Header(#[from] crate::data::header::decode::Error), - #[error("Could find object with id {id} in this pack. Thin packs are not supported")] - UnresolvedRefDelta { id: gix_hash::ObjectId }, - #[error(transparent)] - Tree(#[from] crate::cache::delta::Error), - #[error("Interrupted")] - Interrupted, -} +pub type Error = gix_error::Exn; /// Generate tree from certain input impl Tree { @@ -49,10 +37,7 @@ impl Tree { ) -> Result { let mut r = io::BufReader::with_capacity( 8192 * 8, // this value directly corresponds to performance, 8k (default) is about 4x slower than 64k - fs::File::open(pack_path).map_err(|err| Error::Io { - source: err, - message: "open pack path", - })?, + fs::File::open(pack_path).or_raise_erased(|| message("open pack path"))?, ); let anticipated_num_objects = data_sorted_by_offsets @@ -67,9 +52,8 @@ impl Tree { { // safety check - assure ourselves it's a pack we can handle let mut buf = [0u8; data::header::SIZE]; - r.read_exact(&mut buf).map_err(|err| Error::Io { - source: err, - message: "reading header buffer with at least 12 bytes failed - pack file truncated?", + r.read_exact(&mut buf).or_raise_erased(|| { + message("reading header buffer with at least 12 bytes failed - pack file truncated?") })?; crate::data::header::decode(&buf)?; } @@ -84,10 +68,8 @@ impl Tree { if let Some(previous_offset) = previous_cursor_position { Self::advance_cursor_to_pack_offset(&mut r, pack_offset, previous_offset)?; } - let entry = crate::data::Entry::from_read(&mut r, pack_offset, hash_len).map_err(|err| Error::Io { - source: err, - message: "EOF while parsing header", - })?; + let entry = crate::data::Entry::from_read(&mut r, pack_offset, hash_len) + .or_raise_erased(|| message("EOF while parsing header"))?; previous_cursor_position = Some(pack_offset + entry.header_size() as u64); use crate::data::entry::Header::*; @@ -96,32 +78,29 @@ impl Tree { tree.add_root(pack_offset, data)?; } RefDelta { base_id } => { - resolve_in_pack_id(base_id.as_ref()) - .ok_or(Error::UnresolvedRefDelta { id: base_id }) - .and_then(|base_pack_offset| { - tree.add_child(base_pack_offset, pack_offset, data).map_err(Into::into) - })?; + let base_pack_offset = resolve_in_pack_id(base_id.as_ref()).ok_or_else(|| { + gix_error::NotFoundError::new(format!( + "Could find object with id {base_id} in this pack. Thin packs are not supported" + )) + .raise_erased() + })?; + tree.add_child(base_pack_offset, pack_offset, data)?; } OfsDelta { base_distance } => { let Some(base_pack_offset) = crate::data::entry::Header::verified_base_pack_offset(pack_offset, base_distance) else { - return Err(Error::Io { - source: io::Error::new( - io::ErrorKind::InvalidData, - format!( - "OFS_DELTA base distance {base_distance} is invalid for pack offset {pack_offset}" - ), - ), - message: "invalid OFS_DELTA base distance", - }); + return Err(gix_error::CorruptionError::new(format!( + "OFS_DELTA base distance {base_distance} is invalid for pack offset {pack_offset}" + )) + .raise_erased()); }; tree.add_child(base_pack_offset, pack_offset, data)?; } } progress.inc(); if idx % 10_000 == 0 && should_interrupt.load(Ordering::SeqCst) { - return Err(Error::Interrupted); + return Err(RetryableError::new(message("Interrupted")).raise_erased()); } } @@ -140,29 +119,23 @@ impl Tree { if bytes_to_skip == 0 { return Ok(()); } - let buf = r.fill_buf().map_err(|err| Error::Io { - source: err, - message: "skip bytes", - })?; + let buf = r.fill_buf().or_raise_erased(|| message("skip bytes"))?; if buf.is_empty() { // This means we have reached the end of file and can't make progress anymore, before we have satisfied our need // for more - return Err(Error::Io { - source: io::Error::new( - io::ErrorKind::UnexpectedEof, - "ran out of bytes before reading desired amount of bytes", - ), - message: "index file is damaged or corrupt", - }); + return Err(io::Error::new( + io::ErrorKind::UnexpectedEof, + "ran out of bytes before reading desired amount of bytes", + ) + .and_raise(message("index file is damaged or corrupt")) + .erased()); } if bytes_to_skip <= u64::try_from(buf.len()).expect("sensible buffer size") { // SAFETY: bytes_to_skip <= buf.len() <= usize::MAX r.consume(bytes_to_skip as usize); } else { - r.seek(SeekFrom::Start(pack_offset)).map_err(|err| Error::Io { - source: err, - message: "seek to next entry", - })?; + r.seek(SeekFrom::Start(pack_offset)) + .or_raise_erased(|| message("seek to next entry"))?; } Ok(()) } diff --git a/gix-pack/src/cache/delta/mod.rs b/gix-pack/src/cache/delta/mod.rs index 2d43846f557..d3f3a90475a 100644 --- a/gix-pack/src/cache/delta/mod.rs +++ b/gix-pack/src/cache/delta/mod.rs @@ -1,25 +1,5 @@ /// Returned when using various methods on a [`Tree`] -#[derive(thiserror::Error, Debug)] -#[allow(missing_docs)] -pub enum Error { - #[error("The pack delta tree is too large to fit in memory")] - OutOfMemory, - #[error( - "Pack offsets must only increment. The previous pack offset was {last_pack_offset}, the current one is {pack_offset}" - )] - InvariantIncreasingPackOffset { - /// The last seen pack offset - last_pack_offset: crate::data::Offset, - /// The invariant violating offset - pack_offset: crate::data::Offset, - }, -} - -impl From for Error { - fn from(_: std::collections::TryReserveError) -> Self { - Error::OutOfMemory - } -} +pub type Error = gix_error::Exn; /// /// A tree that allows one-time iteration over all nodes and their children, consuming it in the process, /// while being shareable among threads without a lock. diff --git a/gix-pack/src/cache/delta/traverse/mod.rs b/gix-pack/src/cache/delta/traverse/mod.rs index c41b9043202..80cb5d76156 100644 --- a/gix-pack/src/cache/delta/traverse/mod.rs +++ b/gix-pack/src/cache/delta/traverse/mod.rs @@ -1,5 +1,6 @@ -use std::{collections::TryReserveError, sync::atomic::AtomicBool}; +use std::sync::atomic::AtomicBool; +use gix_error::{ErrorExt, NotFoundError, RetryableError, message}; use gix_features::{ progress::{self, DynNestedProgress, Progress}, threading, @@ -18,49 +19,15 @@ pub(crate) mod util; pub(super) type SharedRefDeltaChildren = OwnShared>; /// Returned by [`Tree::traverse()`] -#[derive(thiserror::Error, Debug)] -#[allow(missing_docs)] -pub enum Error { - #[error("{message}")] - ZlibInflate { - source: gix_error::Error, - message: &'static str, - }, - #[error("The resolver failed to obtain the pack entry bytes for the entry at {pack_offset}")] - ResolveFailed { pack_offset: u64 }, - #[error(transparent)] - EntryType(#[from] crate::data::entry::decode::Error), - #[error("One of the object inspectors failed")] - Inspect(#[from] Box), - #[error("Interrupted")] - Interrupted, - #[error("Entry too large to fit in memory")] - OutOfMemory, - #[error( - "The base at {base_pack_offset} was referred to by a ref-delta, but it was never added to the tree as if the pack was still thin." - )] - OutOfPackRefDelta { - /// The base's offset which was from a resolved ref-delta that didn't actually get added to the tree - base_pack_offset: crate::data::Offset, - }, - #[error("The ref-delta base object {base_id} could not be found")] - UnresolvedRefDelta { - /// The id named by one or more unresolved ref-delta entries. - base_id: gix_hash::ObjectId, - }, - #[error("Failed to hash an object while resolving in-pack ref-deltas")] - ObjectHash(#[from] gix_hash::hasher::Error), - #[error("Failed to spawn thread when switching to work-stealing mode")] - SpawnThread(#[from] std::io::Error), - #[error(transparent)] - Delta(#[from] crate::data::delta::apply::Error), +pub type Error = gix_error::Exn; + +#[cold] +pub(super) fn allocation_error(kind: gix_error::ResourceExhaustionKind) -> Error { + gix_error::ResourceExhaustionError::new(kind, "Entry too large to fit in memory").raise_erased() } -impl From for Error { - #[cold] - fn from(_: TryReserveError) -> Self { - Self::OutOfMemory - } +pub(super) fn interrupted() -> Error { + RetryableError::new(message("Interrupted")).raise_erased() } /// Additional context passed to the `inspect_object(…)` function of the [`Tree::traverse()`] method. @@ -124,7 +91,7 @@ where /// This method returns a vector of all tree items, along with their potentially modified custom node data. /// /// _Note_ that this method consumed the Tree to assure safe parallel traversal with mutation support. - pub fn traverse( + pub fn traverse( mut self, resolve: F, resolve_data: &R, @@ -142,8 +109,7 @@ where where F: for<'r> Fn(EntryRange, &'r R) -> Option<&'r [u8]> + Send + Clone, R: Send + Sync, - MBFN: FnMut(&mut T, &dyn Progress, Context<'_>) -> Result<(), E> + Send + Clone, - E: std::error::Error + Send + Sync + 'static, + MBFN: FnMut(&mut T, &dyn Progress, Context<'_>) -> Result<(), gix_error::Exn> + Send + Clone, { self.set_pack_entries_end_and_resolve_ref_offsets(pack_entries_end)?; @@ -186,7 +152,9 @@ where if let Some(ref_delta_children) = ref_delta_children && let Some((base_id, _children)) = threading::lock(&ref_delta_children).first_key_value() { - return Err(Error::UnresolvedRefDelta { base_id: *base_id }); + return Err( + NotFoundError::new(format!("The ref-delta base object {base_id} could not be found")).raise_erased(), + ); } object_progress.show_throughput(start); diff --git a/gix-pack/src/cache/delta/traverse/resolve.rs b/gix-pack/src/cache/delta/traverse/resolve.rs index e0b83a2d20e..605f623ac04 100644 --- a/gix-pack/src/cache/delta/traverse/resolve.rs +++ b/gix-pack/src/cache/delta/traverse/resolve.rs @@ -1,5 +1,6 @@ use std::sync::atomic::{AtomicBool, Ordering}; +use gix_error::{ErrorExt, ResourceExhaustionKind, ResultExt, message}; use gix_features::{ progress::Progress, threading::{self, OwnShared}, @@ -7,7 +8,7 @@ use gix_features::{ use crate::{ cache::delta::{ - traverse::{Context, Error, util::ItemSliceSync}, + traverse::{Context, Error, allocation_error, interrupted, util::ItemSliceSync}, tree::Item, }, data, @@ -84,7 +85,8 @@ fn attach_ref_delta_children( } let kind = entry.header.as_kind().expect("a fully resolved object has a base kind"); - let id = gix_object::compute_hash(object_hash, kind, decompressed)?; + let id = gix_object::compute_hash(object_hash, kind, decompressed) + .or_raise_erased(|| message("Failed to hash an object while resolving in-pack ref-deltas"))?; if let Some(children) = threading::lock(ref_delta_children).remove(&id) { node.add_children(children); } @@ -127,7 +129,7 @@ struct WorkItem<'a, T: Send> { /// SAFETY: `items` and `child_items` must originate from the same [`crate::cache::delta::Tree`]. #[expect(clippy::too_many_arguments, unsafe_code)] #[deny(unsafe_op_in_unsafe_fn)] -pub(super) unsafe fn all( +pub(super) unsafe fn all( items: &mut [Item], child_items: &ItemSliceSync<'_, Item>, thread_limit: Option, @@ -147,8 +149,7 @@ where T: Send, R: Send + Sync, F: for<'r> Fn(EntryRange, &'r R) -> Option<&'r [u8]> + Send + Clone, - MBFN: FnMut(&mut T, &dyn Progress, Context<'_>) -> Result<(), E> + Send + Clone, - E: std::error::Error + Send + Sync + 'static, + MBFN: FnMut(&mut T, &dyn Progress, Context<'_>) -> Result<(), gix_error::Exn> + Send + Clone, { let work = items .iter_mut() @@ -209,7 +210,7 @@ where /// node run before roots that were already waiting. #[cfg(not(feature = "parallel"))] #[expect(clippy::too_many_arguments)] -fn resolve_serial( +fn resolve_serial( mut work: Vec>, objects: gix_features::progress::StepShared, size: gix_features::progress::StepShared, @@ -226,15 +227,14 @@ where T: Send, R: Send + Sync, F: for<'r> Fn(EntryRange, &'r R) -> Option<&'r [u8]> + Send + Clone, - MBFN: FnMut(&mut T, &dyn Progress, Context<'_>) -> Result<(), E> + Send + Clone, - E: std::error::Error + Send + Sync + 'static, + MBFN: FnMut(&mut T, &dyn Progress, Context<'_>) -> Result<(), gix_error::Exn> + Send + Clone, { let mut delta_bytes = Vec::new(); let mut fully_resolved_delta_bytes = Vec::new(); let mut inflate = gix_zlib::Inflate::default(); while let Some(task) = work.pop() { if should_interrupt.load(Ordering::Relaxed) { - return Err(Error::Interrupted); + return Err(interrupted()); } resolve_task( task, @@ -266,7 +266,7 @@ where /// more descendants, and exits only when no work remains. #[cfg(feature = "parallel")] #[expect(clippy::too_many_arguments)] -fn resolve_parallel( +fn resolve_parallel( num_threads: usize, work: Vec>, objects: gix_features::progress::StepShared, @@ -284,8 +284,7 @@ where T: Send, R: Send + Sync, F: for<'r> Fn(EntryRange, &'r R) -> Option<&'r [u8]> + Send + Clone, - MBFN: FnMut(&mut T, &dyn Progress, Context<'_>) -> Result<(), E> + Send + Clone, - E: std::error::Error + Send + Sync + 'static, + MBFN: FnMut(&mut T, &dyn Progress, Context<'_>) -> Result<(), gix_error::Exn> + Send + Clone, { use std::sync::atomic::AtomicUsize; @@ -328,7 +327,7 @@ where } if should_interrupt.load(Ordering::Relaxed) { abort.store(true, Ordering::Relaxed); - return Err(Error::Interrupted); + return Err(interrupted()); } let Some(task) = steal(&worker, stealers, roots) else { if remaining.load(Ordering::Acquire) == 0 { @@ -379,7 +378,9 @@ where std::panic::resume_unwind(payload); } } - return Err(Error::SpawnThread(err)); + return Err(err + .and_raise(message("Failed to spawn thread when switching to work-stealing mode")) + .erased()); } } } @@ -442,7 +443,7 @@ fn steal( /// serial traversal pushes that item onto its `Vec`, while parallel traversal pushes it onto the current worker's deque /// and updates the shared count of unfinished work. #[expect(clippy::too_many_arguments)] -fn resolve_task<'a, T, F, MBFN, E, R>( +fn resolve_task<'a, T, F, MBFN, R>( WorkItem { level, mut node, @@ -466,8 +467,7 @@ where T: Send, R: Send + Sync, F: for<'r> Fn(EntryRange, &'r R) -> Option<&'r [u8]> + Send, - MBFN: FnMut(&mut T, &dyn Progress, Context<'_>) -> Result<(), E> + Send, - E: std::error::Error + Send + Sync + 'static, + MBFN: FnMut(&mut T, &dyn Progress, Context<'_>) -> Result<(), gix_error::Exn> + Send, { let is_root = parent.is_none(); // Root buffers either become shared bases or are dropped after inspection. Keeping leaf-root allocations out of @@ -483,22 +483,24 @@ where object_hash, alloc_limit_bytes, )?; - let (base_size, consumed) = data::delta::decode_header_size(delta_bytes)?; + let (base_size, consumed) = data::delta::decode_header_size(delta_bytes).or_erased()?; let base_size = decoded_size_limited(base_size, alloc_limit_bytes)?; if parent.bytes.len() != base_size { - return Err(data::delta::apply::Error::Corrupt { - message: "delta base size does not match base object size", - } - .into()); + return Err(data::delta::apply::Error::new( + "Corrupt delta data: delta base size does not match base object size", + ) + .raise_erased()); } - let (result_size, result_header_size) = data::delta::decode_header_size(&delta_bytes[consumed..])?; + let (result_size, result_header_size) = + data::delta::decode_header_size(&delta_bytes[consumed..]).or_erased()?; let result_size = decoded_size_limited(result_size, alloc_limit_bytes)?; resize_with_limit(fully_resolved_delta_bytes, result_size, alloc_limit_bytes)?; data::delta::apply( &parent.bytes, fully_resolved_delta_bytes, &delta_bytes[consumed + result_header_size..], - )?; + ) + .or_erased()?; entry.header = parent.entry.header; (entry, entry_end) } else { @@ -568,8 +570,8 @@ where /// /// `modify_base` receives mutable access to the node's associated data and a [`Context`] containing the parsed entry, /// its end offset, the resolved object bytes, and its delta-tree level. Only a successful inspection increments the -/// object counter and the total number of resolved bytes; inspector errors abort traversal as [`Error::Inspect`]. -fn inspect( +/// object counter and the total number of resolved bytes; inspector errors abort traversal. +fn inspect( node: &mut Node<'_, T>, level: u16, resolved: &ResolvedBase, @@ -580,8 +582,7 @@ fn inspect( ) -> Result<(), Error> where T: Send, - MBFN: FnMut(&mut T, &dyn Progress, Context<'_>) -> Result<(), E> + Send, - E: std::error::Error + Send + Sync + 'static, + MBFN: FnMut(&mut T, &dyn Progress, Context<'_>) -> Result<(), gix_error::Exn> + Send, { modify_base( node.data(), @@ -593,7 +594,7 @@ where level, }, ) - .map_err(|err| Box::new(err) as Box)?; + .or_raise_erased(|| message("One of the object inspectors failed"))?; objects.fetch_add(1, Ordering::Relaxed); size.fetch_add(resolved.bytes.len(), Ordering::Relaxed); Ok(()) @@ -615,10 +616,14 @@ fn decompress_from_resolver( where F: for<'r> Fn(EntryRange, &'r R) -> Option<&'r [u8]> + Send, { - let bytes = resolve(slice.clone(), resolve_data).ok_or(Error::ResolveFailed { - pack_offset: slice.start, + let bytes = resolve(slice.clone(), resolve_data).ok_or_else(|| { + gix_error::message!( + "The resolver failed to obtain the pack entry bytes for the entry at {}", + slice.start + ) + .raise_erased() })?; - let entry = data::Entry::from_bytes(bytes, slice.start, object_hash)?; + let entry = data::Entry::from_bytes(bytes, slice.start, object_hash).or_erased()?; let compressed = &bytes[entry.header_size()..]; let decompressed_len = decoded_size_limited(entry.decompressed_size, alloc_limit_bytes)?; decompress_all_at_once_with(inflate, compressed, decompressed_len, out, alloc_limit_bytes)?; @@ -634,26 +639,28 @@ fn decompress_all_at_once_with( ) -> Result<(), Error> { resize_with_limit(out, decompressed_len, alloc_limit_bytes)?; inflate.reset(); - inflate.once(b, out).map_err(|err| Error::ZlibInflate { - source: err.into_error(), - message: "Failed to decompress entry", - })?; + inflate + .once(b, out) + .or_raise_erased(|| message("Failed to decompress entry"))?; Ok(()) } fn decoded_size_limited(size: u64, alloc_limit_bytes: Option) -> Result { - let size: usize = size.try_into().map_err(|_| Error::OutOfMemory)?; + let size: usize = size + .try_into() + .map_err(|_| allocation_error(ResourceExhaustionKind::AllocationFailure))?; if alloc_limit_bytes.is_some_and(|limit| size > limit) { - return Err(Error::OutOfMemory); + return Err(allocation_error(ResourceExhaustionKind::AllocationLimit)); } Ok(size) } fn resize_with_limit(out: &mut Vec, len: usize, alloc_limit_bytes: Option) -> Result<(), Error> { if alloc_limit_bytes.is_some_and(|limit| len > limit) { - return Err(Error::OutOfMemory); + return Err(allocation_error(ResourceExhaustionKind::AllocationLimit)); } - out.try_reserve(len.saturating_sub(out.len()))?; + out.try_reserve(len.saturating_sub(out.len())) + .or_raise_erased(|| message("Entry too large to fit in memory"))?; out.resize(len, 0); Ok(()) } @@ -708,7 +715,7 @@ mod tests { if context.level == 1 { calls_at_first_child.fetch_min(resolve_calls.load(Ordering::Relaxed), Ordering::Relaxed); } - Ok::<_, std::io::Error>(()) + Ok::<_, gix_error::Exn>(()) }, ) .expect("valid delta tree"); @@ -750,7 +757,7 @@ mod tests { std::thread::sleep(Duration::from_millis(20)); active.fetch_sub(1, Ordering::Relaxed); } - Ok::<_, std::io::Error>(()) + Ok::<_, gix_error::Exn>(()) }, ) .expect("valid delta tree"); @@ -771,8 +778,9 @@ mod tests { let err = traverse_with_limit(tree, &pack).expect_err("entry size exceeds the allocation cap"); - assert!( - matches!(err, traverse::Error::OutOfMemory), + assert_eq!( + err.to_string(), + "Entry too large to fit in memory", "declared decompressed sizes above the cap must be rejected before allocation" ); } @@ -800,8 +808,9 @@ mod tests { let err = traverse_with_limit(tree, &pack).expect_err("delta base size exceeds the allocation cap"); - assert!( - matches!(err, traverse::Error::OutOfMemory), + assert_eq!( + err.to_string(), + "Entry too large to fit in memory", "delta base sizes above the cap must be rejected before comparing them with the decoded base" ); } @@ -829,8 +838,9 @@ mod tests { let err = traverse_with_limit(tree, &pack).expect_err("delta result size exceeds the allocation cap"); - assert!( - matches!(err, traverse::Error::OutOfMemory), + assert_eq!( + err.to_string(), + "Entry too large to fit in memory", "delta result sizes above the cap must be rejected before resizing the output buffer" ); } @@ -842,7 +852,7 @@ mod tests { Some(1), Some(0), |slice, pack| pack.get(slice.start as usize..slice.end as usize), - |(), _progress, _context| Ok::<_, std::io::Error>(()), + |(), _progress, _context| Ok::<_, gix_error::Exn>(()), ) } @@ -857,7 +867,7 @@ mod tests { where F: for<'r> Fn(data::EntryRange, &'r Vec) -> Option<&'r [u8]> + Send + Clone, MBFN: - FnMut(&mut (), &dyn progress::Progress, traverse::Context<'_>) -> Result<(), std::io::Error> + Send + Clone, + FnMut(&mut (), &dyn progress::Progress, traverse::Context<'_>) -> Result<(), gix_error::Exn> + Send + Clone, { let should_interrupt = AtomicBool::new(false); let mut size_progress = progress::Discard; diff --git a/gix-pack/src/cache/delta/tree.rs b/gix-pack/src/cache/delta/tree.rs index 07f3497e58d..9544de3f7b9 100644 --- a/gix-pack/src/cache/delta/tree.rs +++ b/gix-pack/src/cache/delta/tree.rs @@ -1,5 +1,7 @@ use std::collections::BTreeMap; +use gix_error::{CorruptionError, ErrorExt, ResourceExhaustionError, ResourceExhaustionKind, ResultExt}; + use super::{Error, Tree, traverse}; /// Maps each referenced base object ID to indices in `Tree::child_items` of ref-deltas waiting for it. @@ -45,21 +47,29 @@ pub(super) enum NodeKind { Child, } +fn allocation_error(kind: ResourceExhaustionKind) -> ResourceExhaustionError { + ResourceExhaustionError::new(kind, "The pack delta tree is too large to fit in memory") +} + impl Tree { /// Instantiate a empty tree capable of storing `num_objects` amounts of items. pub(crate) fn with_capacity(num_objects: usize, alloc_limit_bytes: Option) -> Result { let capacity = num_objects / 2; let allocation_bytes = capacity .checked_mul(std::mem::size_of::>()) - .ok_or(Error::OutOfMemory)?; + .ok_or_else(|| allocation_error(ResourceExhaustionKind::AllocationFailure).raise_erased())?; if alloc_limit_bytes.is_some_and(|limit| allocation_bytes > limit) { - return Err(Error::OutOfMemory); + return Err(allocation_error(ResourceExhaustionKind::AllocationLimit).raise_erased()); } let mut root_items = Vec::new(); - root_items.try_reserve_exact(capacity)?; + root_items + .try_reserve_exact(capacity) + .or_raise_erased(|| allocation_error(ResourceExhaustionKind::AllocationFailure))?; let mut child_items = Vec::new(); - child_items.try_reserve_exact(capacity)?; + child_items + .try_reserve_exact(capacity) + .or_raise_erased(|| allocation_error(ResourceExhaustionKind::AllocationFailure))?; Ok(Tree { root_items, child_items, @@ -91,10 +101,11 @@ impl Tree { }; let item = &mut items.last_mut().expect("last seen won't lie"); if offset <= item.offset { - return Err(Error::InvariantIncreasingPackOffset { - last_pack_offset: item.offset, - pack_offset: offset, - }); + return Err(CorruptionError::new(format!( + "Pack offsets must only increment. The previous pack offset was {}, the current one is {offset}", + item.offset + )) + .raise_erased()); } item.next_offset = offset; Ok(()) @@ -117,9 +128,10 @@ impl Tree { } else if let Ok(i) = self.root_items.binary_search_by_key(&parent_offset, |i| i.offset) { self.root_items[i].children.push(child_index as u32); } else { - return Err(traverse::Error::OutOfPackRefDelta { - base_pack_offset: parent_offset, - }); + return Err(gix_error::message!( + "The base at {parent_offset} was referred to by a ref-delta, but it was never added to the tree as if the pack was still thin." + ) + .raise_erased()); } } } @@ -213,17 +225,22 @@ impl Tree { mod tests { #[test] fn allocation_failure_is_reported() { - let result = super::Tree::<()>::with_capacity(usize::MAX, None); - assert!( - matches!(result, Err(super::Error::OutOfMemory)), - "an impossible attacker-controlled capacity must return an allocation error" + let err = super::Tree::<()>::with_capacity(usize::MAX, None) + .err() + .expect("an impossible attacker-controlled capacity must return an allocation error"); + assert_eq!( + err.downcast_any_ref::() + .map(gix_error::ResourceExhaustionError::kind), + Some(gix_error::ResourceExhaustionKind::AllocationFailure) ); - assert!( - matches!( - super::Tree::<()>::with_capacity(2, Some(0)), - Err(super::Error::OutOfMemory) - ), - "the configured allocation limit must apply to delta-tree storage" + + let err = super::Tree::<()>::with_capacity(2, Some(0)) + .err() + .expect("the configured allocation limit must apply to delta-tree storage"); + assert_eq!( + err.downcast_any_ref::() + .map(gix_error::ResourceExhaustionError::kind), + Some(gix_error::ResourceExhaustionKind::AllocationLimit) ); } @@ -241,17 +258,17 @@ mod tests { use gix_testtools::fixture_path; #[test] - fn v1() -> Result<(), Box> { + fn v1() -> gix_testtools::Result { tree(INDEX_V1, PACK_FOR_INDEX_V1) } #[test] - fn v2() -> Result<(), Box> { + fn v2() -> gix_testtools::Result { tree(SMALL_PACK_INDEX, SMALL_PACK) } #[test] - fn invalid_ofs_delta_base_distance_is_reported() -> Result<(), Box> { + fn invalid_ofs_delta_base_distance_is_reported() -> gix_testtools::Result { let first_entry_offset = pack::data::header::SIZE as pack::data::Offset; let pack_file = gix_testtools::tempfile::NamedTempFile::new()?; let mut pack_data = pack::data::header::encode(pack::data::Version::V2, 1).to_vec(); @@ -275,7 +292,7 @@ mod tests { Ok(()) } - fn tree(index_path: &str, pack_path: &str) -> Result<(), Box> { + fn tree(index_path: &str, pack_path: &str) -> gix_testtools::Result { let idx = pack::index::File::at(fixture_path(index_path), gix_hash::Kind::Sha1)?; crate::cache::delta::Tree::from_offsets_in_pack( &fixture_path(pack_path), diff --git a/gix-pack/src/cache/mod.rs b/gix-pack/src/cache/mod.rs index 4fb4dbdbbb6..f6c443af3b9 100644 --- a/gix-pack/src/cache/mod.rs +++ b/gix-pack/src/cache/mod.rs @@ -66,7 +66,7 @@ pub mod object; /// # fn build_index_less( /// # pack_path: &std::path::Path, /// # offsets: Vec, -/// # ) -> Result<(), Box> { +/// # ) -> Result<(), gix_pack::cache::delta::from_offsets::Error> { /// // Build the delta tree straight from the pack — no `.idx` required: /// let tree = Tree::from_offsets_in_pack( /// pack_path, diff --git a/gix-pack/src/data/delta.rs b/gix-pack/src/data/delta.rs index fb01e928d7f..d184db9e1c4 100644 --- a/gix-pack/src/data/delta.rs +++ b/gix-pack/src/data/delta.rs @@ -1,18 +1,11 @@ /// pub mod apply { /// Returned when failing to apply deltas. - #[derive(thiserror::Error, Debug)] - #[expect(missing_docs)] - pub enum Error { - #[error("Corrupt delta data: {message}")] - Corrupt { message: &'static str }, - #[error("Encountered unsupported command code: 0")] - UnsupportedCommandCode, - #[error("Delta copy from base: byte slices must match")] - DeltaCopyBaseSliceMismatch, - #[error("Delta copy data: byte slices must match")] - DeltaCopyDataSliceMismatch, - } + pub type Error = gix_error::CorruptionError; +} + +fn corrupt(message: &'static str) -> apply::Error { + apply::Error::new(format!("Corrupt delta data: {message}")) } /// Given the decompressed pack delta `d`, decode a size in bytes (either the base object size or the result object size) @@ -23,9 +16,7 @@ pub(crate) fn decode_header_size(d: &[u8]) -> Result<(u64, usize), apply::Error> let mut consumed = 0; for cmd in d.iter() { if shift >= u64::BITS { - return Err(apply::Error::Corrupt { - message: "delta header size uses more bits than fit into u64", - }); + return Err(corrupt("delta header size uses more bits than fit into u64")); } consumed += 1; size |= (u64::from(*cmd) & 0x7f) << shift; @@ -34,16 +25,14 @@ pub(crate) fn decode_header_size(d: &[u8]) -> Result<(u64, usize), apply::Error> return Ok((size, consumed)); } } - Err(apply::Error::Corrupt { - message: "delta header size is truncated", - }) + Err(corrupt("delta header size is truncated")) } pub(crate) fn apply(base: &[u8], mut target: &mut [u8], data: &[u8]) -> Result<(), apply::Error> { fn next_byte(data: &[u8], i: &mut usize) -> Result { - let byte = *data.get(*i).ok_or(apply::Error::Corrupt { - message: "delta copy instruction is truncated", - })?; + let byte = *data + .get(*i) + .ok_or_else(|| corrupt("delta copy instruction is truncated"))?; *i += 1; Ok(byte) } @@ -79,33 +68,29 @@ pub(crate) fn apply(base: &[u8], mut target: &mut [u8], data: &[u8]) -> Result<( size = 0x10000; // 65536 } let ofs = ofs as usize; - let end = ofs.checked_add(size as usize).ok_or(apply::Error::Corrupt { - message: "delta copy range overflows", - })?; + let end = ofs + .checked_add(size as usize) + .ok_or_else(|| corrupt("delta copy range overflows"))?; std::io::Write::write( &mut target, - base.get(ofs..end).ok_or(apply::Error::Corrupt { - message: "delta copy range exceeds base object size", - })?, + base.get(ofs..end) + .ok_or_else(|| corrupt("delta copy range exceeds base object size"))?, ) - .map_err(|_e| apply::Error::DeltaCopyBaseSliceMismatch)?; + .map_err(|_| apply::Error::new("Delta copy from base: byte slices must match"))?; } 0 => { - return Err(apply::Error::Corrupt { - message: "delta command 0 is reserved and invalid", - }); + return Err(corrupt("delta command 0 is reserved and invalid")); } size => { - let end = i.checked_add(*size as usize).ok_or(apply::Error::Corrupt { - message: "delta insert range overflows", - })?; + let end = i + .checked_add(*size as usize) + .ok_or_else(|| corrupt("delta insert range overflows"))?; std::io::Write::write( &mut target, - data.get(i..end).ok_or(apply::Error::Corrupt { - message: "delta insert data is truncated", - })?, + data.get(i..end) + .ok_or_else(|| corrupt("delta insert data is truncated"))?, ) - .map_err(|_e| apply::Error::DeltaCopyDataSliceMismatch)?; + .map_err(|_| apply::Error::new("Delta copy data: byte slices must match"))?; i = end; } } @@ -116,9 +101,7 @@ pub(crate) fn apply(base: &[u8], mut target: &mut [u8], data: &[u8]) -> Result<( "delta instructions were not consumed completely, should be impossible" ); if !target.is_empty() { - return Err(apply::Error::Corrupt { - message: "delta instructions produced fewer bytes than promised", - }); + return Err(corrupt("delta instructions produced fewer bytes than promised")); } Ok(()) diff --git a/gix-pack/src/data/entry/decode.rs b/gix-pack/src/data/entry/decode.rs index ccd4f130bc1..8b1edbeda43 100644 --- a/gix-pack/src/data/entry/decode.rs +++ b/gix-pack/src/data/entry/decode.rs @@ -6,15 +6,10 @@ use super::{BLOB, COMMIT, OFS_DELTA, REF_DELTA, TAG, TREE}; use crate::data; /// The error returned by [data::Entry::from_bytes()]. -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] -pub enum Error { - #[error("Object type {type_id} is unsupported")] - UnsupportedType { type_id: u8 }, - #[error("Pack entry is truncated: {message}")] - Corrupt { message: &'static str }, - #[error("Pack entry header value overflowed while decoding")] - Overflow, +pub type Error = gix_error::CorruptionError; + +fn corrupt(message: &'static str) -> Error { + Error::new(format!("Pack entry is truncated: {message}")) } /// Decoding @@ -38,13 +33,10 @@ impl data::Entry { let hash = d .get(consumed..) .and_then(|d| d.get(..hash_len)) - .ok_or(Error::Corrupt { - message: "ref-delta base object id", - })?; + .ok_or_else(|| corrupt("ref-delta base object id"))?; let delta = RefDelta { - base_id: gix_hash::ObjectId::try_from(hash).map_err(|_| Error::Corrupt { - message: "unsupported object hash length", - })?, + base_id: gix_hash::ObjectId::try_from(hash) + .map_err(|_| corrupt("unsupported object hash length"))?, }; consumed += hash_len; delta @@ -53,7 +45,7 @@ impl data::Entry { TREE => Tree, COMMIT => Commit, TAG => Tag, - other => return Err(Error::UnsupportedType { type_id: other }), + other => return Err(Error::new(format!("Object type {other} is unsupported"))), }; Ok(data::Entry { header: object, @@ -81,6 +73,7 @@ impl data::Entry { let mut buf = gix_hash::Kind::buf(); let hash = &mut buf[..hash_len]; r.read_exact(hash)?; + #[allow(clippy::redundant_slicing)] let delta = RefDelta { base_id: gix_hash::ObjectId::from_bytes_or_panic(&hash[..]), }; @@ -104,9 +97,9 @@ impl data::Entry { } fn encoded_header_size(consumed: usize) -> Result { - consumed.try_into().map_err(|_| Error::Corrupt { - message: "entry header size does not fit into u16", - }) + consumed + .try_into() + .map_err(|_| corrupt("entry header size does not fit into u16")) } #[inline] @@ -136,20 +129,24 @@ fn streaming_parse_header_info(read: &mut dyn io::Read) -> Result<(u8, u64, usiz /// Parses the header of a pack-entry, yielding object type id, decompressed object size, and consumed bytes #[inline] fn parse_header_info(data: &[u8]) -> Result<(u8, u64, usize), Error> { - let mut c = *data.first().ok_or(Error::Corrupt { - message: "need a pack entry header, got empty input", - })?; + let mut c = *data + .first() + .ok_or_else(|| corrupt("need a pack entry header, got empty input"))?; let mut i = 1; let type_id = (c >> 4) & 0b0000_0111; let mut size = u64::from(c) & 0b0000_1111; let mut shift = 4u32; while c & 0b1000_0000 != 0 { - c = *data.get(i).ok_or(Error::Corrupt { - message: "pack entry header continuation byte", - })?; + c = *data + .get(i) + .ok_or_else(|| corrupt("pack entry header continuation byte"))?; i += 1; - let component = u64::from(c & 0b0111_1111).checked_shl(shift).ok_or(Error::Overflow)?; - size = size.checked_add(component).ok_or(Error::Overflow)?; + let component = u64::from(c & 0b0111_1111) + .checked_shl(shift) + .ok_or_else(|| Error::new("Pack entry header value overflowed while decoding"))?; + size = size + .checked_add(component) + .ok_or_else(|| Error::new("Pack entry header value overflowed while decoding"))?; shift += 7; } Ok((type_id, size, i)) @@ -157,21 +154,19 @@ fn parse_header_info(data: &[u8]) -> Result<(u8, u64, usize), Error> { fn parse_leb64(data: &[u8]) -> Result<(u64, usize), Error> { let mut i = 0; - let mut c = *data.first().ok_or(Error::Corrupt { - message: "an ofs-delta base distance", - })?; + let mut c = *data.first().ok_or_else(|| corrupt("an ofs-delta base distance"))?; i += 1; let mut value = u64::from(c) & 0x7f; while c & 0x80 != 0 { - c = *data.get(i).ok_or(Error::Corrupt { - message: "an ofs-delta base distance continuation byte", - })?; + c = *data + .get(i) + .ok_or_else(|| corrupt("an ofs-delta base distance continuation byte"))?; i += 1; value = value .checked_add(1) .and_then(|value| value.checked_shl(7)) .and_then(|value| value.checked_add(u64::from(c) & 0x7f)) - .ok_or(Error::Overflow)?; + .ok_or_else(|| Error::new("Pack entry header value overflowed while decoding"))?; } Ok((value, i)) } @@ -242,11 +237,11 @@ mod tests { #[test] fn oversized_encoded_header_size_is_rejected() { - assert!( - matches!( - encoded_header_size(usize::from(u16::MAX) + 1), - Err(Error::Corrupt { message }) if message == "entry header size does not fit into u16" - ), + assert_eq!( + encoded_header_size(usize::from(u16::MAX) + 1) + .expect_err("the encoded size exceeds u16") + .to_string(), + "Pack entry is truncated: entry header size does not fit into u16", "entry header lengths that cannot be stored in the Entry metadata must be rejected" ); } diff --git a/gix-pack/src/data/file/decode/entry.rs b/gix-pack/src/data/file/decode/entry.rs index 9c5043bd5bc..c82e63d78d5 100644 --- a/gix-pack/src/data/file/decode/entry.rs +++ b/gix-pack/src/data/file/decode/entry.rs @@ -1,9 +1,14 @@ use smallvec::SmallVec; use std::ops::Range; +use gix_error::{ErrorExt, ResourceExhaustionKind, ResultExt, ValidationError, message}; + use crate::{ cache, data, - data::{File, delta, file::decode::Error}, + data::{ + File, delta, + file::decode::{DeltaBaseUnresolved, Error, allocation_error}, + }, }; /// A return value of a resolve function, which given an [`ObjectId`][gix_hash::ObjectId] determines where an object can be found. @@ -90,9 +95,12 @@ where inflate: &mut gix_zlib::Inflate, out: &mut [u8], ) -> Result { - let size: usize = entry.decompressed_size.try_into().map_err(|_| Error::OutOfMemory)?; + let size: usize = entry + .decompressed_size + .try_into() + .map_err(|_| allocation_error(ResourceExhaustionKind::AllocationFailure))?; if out.len() < size { - return Err(Error::OutOfMemory); + return Err(ValidationError::new("Output buffer is too small for the decompressed entry").raise_erased()); } self.decompress_entry_from_data_offset(entry.data_offset, inflate, &mut out[..size]) } @@ -103,9 +111,9 @@ where pub fn entry(&self, offset: data::Offset) -> Result { let pack_offset: usize = offset.try_into().expect("offset representable by machine"); if pack_offset > self.data.len() { - return Err(data::entry::decode::Error::Corrupt { - message: "an entry offset pointing beyond pack data", - }); + return Err(data::entry::decode::Error::new( + "Pack entry is truncated: an entry offset pointing beyond pack data", + )); } let object_data = &self.data[pack_offset..]; @@ -144,10 +152,10 @@ where let (status, consumed_in, consumed_out) = self.decompress_entry_from_data_offset_unchecked(data_offset, inflate, out)?; if status != gix_zlib::Status::StreamEnd || consumed_out != out.len() { - return Err(data::entry::decode::Error::Corrupt { - message: "pack entry decompressed size does not match entry header", - } - .into()); + return Err(data::entry::decode::Error::new( + "Pack entry is truncated: pack entry decompressed size does not match entry header", + ) + .raise_erased()); } Ok((consumed_in, consumed_out)) } @@ -164,16 +172,16 @@ where ) -> Result<(gix_zlib::Status, usize, usize), Error> { let offset: usize = data_offset.try_into().expect("offset representable by machine"); if offset >= self.data.len() { - return Err(data::entry::decode::Error::Corrupt { - message: "an entry data offset pointing beyond pack data", - } - .into()); + return Err(data::entry::decode::Error::new( + "Pack entry is truncated: an entry data offset pointing beyond pack data", + ) + .raise_erased()); } inflate.reset(); inflate .once(&self.data[offset..], out) - .map_err(|err| Error::ZlibInflate(err.into_error())) + .or_raise_erased(|| message("Failed to decompress pack entry")) } /// Decode an entry, resolving delta's as needed, while growing the `out` vector if there is not enough @@ -200,7 +208,8 @@ where Tree | Blob | Commit | Tag => { let size = self.decoded_object_size(entry.decompressed_size)?; if let Some(additional) = size.checked_sub(out.len()) { - out.try_reserve(additional)?; + out.try_reserve(additional) + .or_raise_erased(|| message("Entry too large to fit in memory"))?; } out.resize(size, 0); self.decompress_entry(&entry, inflate, out.as_mut_slice()) @@ -252,12 +261,12 @@ where // TODO: is this assumption actually true? total_delta_data_size = total_delta_data_size .checked_add(cursor.decompressed_size) - .ok_or(Error::OutOfMemory)?; + .ok_or_else(|| allocation_error(ResourceExhaustionKind::AllocationFailure))?; if self .alloc_limit_bytes .is_some_and(|limit| total_delta_data_size > limit as u64) { - return Err(Error::OutOfMemory); + return Err(allocation_error(ResourceExhaustionKind::AllocationLimit)); } let decompressed_size = self.decoded_object_size(cursor.decompressed_size)?; chain.push(Delta { @@ -273,11 +282,15 @@ where use crate::data::entry::Header; cursor = match cursor.header { Header::OfsDelta { base_distance } => { - self.entry(cursor.checked_base_pack_offset(base_distance).ok_or( - crate::data::entry::decode::Error::Corrupt { - message: "an ofs-delta base distance pointing before pack start", - }, - )?)? + let offset = cursor + .checked_base_pack_offset(base_distance) + .ok_or_else(|| { + crate::data::entry::decode::Error::new( + "Pack entry is truncated: an ofs-delta base distance pointing before pack start", + ) + }) + .or_erased()?; + self.entry(offset).or_erased()? } Header::RefDelta { base_id } => match resolve(base_id.as_ref(), out) { Some(ResolvedBase::InPack(entry)) => entry, @@ -286,7 +299,7 @@ where object_kind = Some(kind); break; } - None => return Err(Error::DeltaBaseUnresolved(base_id)), + None => return Err(DeltaBaseUnresolved(base_id).raise_erased()), }, _ => unreachable!("cursor.is_delta() only allows deltas here"), }; @@ -305,7 +318,9 @@ where // First pass will decompress all delta data and keep it in our output buffer // []... // so that we can find the biggest result size. - let total_delta_data_size: usize = total_delta_data_size.try_into().map_err(|_| Error::OutOfMemory)?; + let total_delta_data_size: usize = total_delta_data_size + .try_into() + .map_err(|_| allocation_error(ResourceExhaustionKind::AllocationFailure))?; let chain_len = chain.len(); let actual_base_size = match base_buffer_size { @@ -319,9 +334,10 @@ where start: delta_start, end: delta_start .checked_add(total_delta_data_size) - .ok_or(Error::OutOfMemory)?, + .ok_or_else(|| allocation_error(ResourceExhaustionKind::AllocationFailure))?, }; - out.try_reserve(delta_range.end.saturating_sub(out.len()))?; + out.try_reserve(delta_range.end.saturating_sub(out.len())) + .or_raise_erased(|| message("Entry too large to fit in memory"))?; out.resize(delta_range.end, 0); let mut instructions = &mut out[delta_range.clone()]; @@ -340,18 +356,18 @@ where } let current_delta = &instructions[..consumed_out]; - let (base_size, offset) = delta::decode_header_size(current_delta)?; + let (base_size, offset) = delta::decode_header_size(current_delta).or_erased()?; let mut bytes_consumed_by_header = offset; delta.base_size = self.decoded_object_size(base_size)?; if delta.base_size != expected_base_size { - return Err(delta::apply::Error::Corrupt { - message: "delta base size does not match base object size", - } - .into()); + return Err(delta::apply::Error::new( + "Corrupt delta data: delta base size does not match base object size", + ) + .raise_erased()); } biggest_result_size = biggest_result_size.max(base_size); - let (result_size, offset) = delta::decode_header_size(¤t_delta[offset..])?; + let (result_size, offset) = delta::decode_header_size(¤t_delta[offset..]).or_erased()?; bytes_consumed_by_header += offset; biggest_result_size = biggest_result_size.max(result_size); delta.result_size = self.decoded_object_size(result_size)?; @@ -377,8 +393,9 @@ where let out_size = first_buffer_size .checked_add(second_buffer_size) .and_then(|size| size.checked_add(total_delta_data_size)) - .ok_or(Error::OutOfMemory)?; - out.try_reserve(out_size.saturating_sub(out.len()))?; + .ok_or_else(|| allocation_error(ResourceExhaustionKind::AllocationFailure))?; + out.try_reserve(out_size.saturating_sub(out.len())) + .or_raise_erased(|| message("Entry too large to fit in memory"))?; out.resize(out_size, 0); // Now 'rescue' the deltas, because in the next step we possibly overwrite that portion @@ -386,7 +403,7 @@ where let second_buffer_end = { let end = first_buffer_size .checked_add(second_buffer_size) - .ok_or(Error::OutOfMemory)?; + .ok_or_else(|| allocation_error(ResourceExhaustionKind::AllocationFailure))?; // Move the decompressed delta instructions behind the two work buffers so they remain intact // while we repurpose the front of `out` for base-object materialization and delta application. out.copy_within(delta_range, end); @@ -430,7 +447,7 @@ where if delta_idx + 1 == chain_len { last_result_size = Some(result_size); } - delta::apply(&source_buf[..base_size], &mut target_buf[..result_size], data)?; + delta::apply(&source_buf[..base_size], &mut target_buf[..result_size], data).or_erased()?; // use the target as source for the next delta std::mem::swap(&mut source_buf, &mut target_buf); } @@ -475,9 +492,11 @@ where /// Convert user-controlled sizes from pack data into allocation sizes while enforcing the configured allocation cap. fn decoded_object_size(size: u64, alloc_limit_bytes: Option) -> Result { - let size: usize = size.try_into().map_err(|_| Error::OutOfMemory)?; + let size: usize = size + .try_into() + .map_err(|_| allocation_error(ResourceExhaustionKind::AllocationFailure))?; if alloc_limit_bytes.is_some_and(|limit| size > limit) { - return Err(Error::OutOfMemory); + return Err(allocation_error(ResourceExhaustionKind::AllocationLimit)); } Ok(size) } diff --git a/gix-pack/src/data/file/decode/header.rs b/gix-pack/src/data/file/decode/header.rs index c8f8d86c56b..db3b69dbe62 100644 --- a/gix-pack/src/data/file/decode/header.rs +++ b/gix-pack/src/data/file/decode/header.rs @@ -1,6 +1,11 @@ +use gix_error::{ErrorExt, ResultExt}; + use crate::{ data, - data::{File, delta, file::decode::Error}, + data::{ + File, delta, + file::decode::{DeltaBaseUnresolved, Error}, + }, }; /// A return value of a resolve function, which given an [`ObjectId`][gix_hash::ObjectId] determines where an object can be found. @@ -73,11 +78,15 @@ where if first_delta_decompressed_size.is_none() { first_delta_decompressed_size = Some(self.decode_delta_object_size(inflate, &entry)?); } - entry = self.entry(entry.checked_base_pack_offset(base_distance).ok_or( - crate::data::entry::decode::Error::Corrupt { - message: "an ofs-delta base distance pointing before pack start", - }, - )?)?; + let offset = entry + .checked_base_pack_offset(base_distance) + .ok_or_else(|| { + crate::data::entry::decode::Error::new( + "Pack entry is truncated: an ofs-delta base distance pointing before pack start", + ) + }) + .or_erased()?; + entry = self.entry(offset).or_erased()?; } RefDelta { base_id } => { num_deltas += 1; @@ -96,7 +105,7 @@ where num_deltas: origin_num_deltas.unwrap_or_default() + num_deltas, }); } - None => return Err(Error::DeltaBaseUnresolved(base_id)), + None => return Err(DeltaBaseUnresolved(base_id).raise_erased()), } } } @@ -120,20 +129,20 @@ where self.decompress_entry_from_data_offset_unchecked(entry.data_offset, inflate, &mut buf[..max_size])?; if status == gix_zlib::Status::StreamEnd { if consumed_out as u64 != entry.decompressed_size { - return Err(data::entry::decode::Error::Corrupt { - message: "pack entry decompressed to fewer bytes than declared in the entry header", - } - .into()); + return Err(data::entry::decode::Error::new( + "Pack entry is truncated: pack entry decompressed to fewer bytes than declared in the entry header", + ) + .raise_erased()); } } else if entry.decompressed_size == max_size as u64 { - return Err(data::entry::decode::Error::Corrupt { - message: "pack entry decompressed to more bytes than declared in the entry header", - } - .into()); + return Err(data::entry::decode::Error::new( + "Pack entry is truncated: pack entry decompressed to more bytes than declared in the entry header", + ) + .raise_erased()); } let buf = &buf[..consumed_out]; - let (_base_size, offset) = delta::decode_header_size(buf)?; - let (result_size, _offset) = delta::decode_header_size(&buf[offset..])?; + let (_base_size, offset) = delta::decode_header_size(buf).or_erased()?; + let (result_size, _offset) = delta::decode_header_size(&buf[offset..]).or_erased()?; Ok(result_size) } } diff --git a/gix-pack/src/data/file/decode/mod.rs b/gix-pack/src/data/file/decode/mod.rs index 3ce41c3e287..76654fb9cec 100644 --- a/gix-pack/src/data/file/decode/mod.rs +++ b/gix-pack/src/data/file/decode/mod.rs @@ -1,31 +1,43 @@ -use std::collections::TryReserveError; - /// pub mod entry; /// pub mod header; +/// A ref-delta base that could not be resolved. +/// +/// Its source preserves the missing-object classification when converted to [`gix_error::Error`]. +#[derive(Debug)] +pub struct DeltaBaseUnresolved( + /// The object ID named by the ref-delta. + pub gix_hash::ObjectId, +); + +impl std::fmt::Display for DeltaBaseUnresolved { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "A delta chain could not be followed as the ref base with id {} could not be found", + self.0 + ) + } +} + +impl std::error::Error for DeltaBaseUnresolved { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + static NOT_FOUND: gix_error::NotFoundError = gix_error::NotFoundError { + message: std::borrow::Cow::Borrowed("Delta base object not found"), + }; + Some(&NOT_FOUND) + } +} + /// Returned by [`File::decode_header()`][crate::data::File::decode_header()], /// [`File::decode_entry()`][crate::data::File::decode_entry()] and . /// [`File::decompress_entry()`][crate::data::File::decompress_entry()] -#[derive(thiserror::Error, Debug)] -#[expect(missing_docs)] -pub enum Error { - #[error("Failed to decompress pack entry")] - ZlibInflate(#[source] gix_error::Error), - #[error("A delta chain could not be followed as the ref base with id {0} could not be found")] - DeltaBaseUnresolved(gix_hash::ObjectId), - #[error(transparent)] - EntryType(#[from] crate::data::entry::decode::Error), - #[error("Entry too large to fit in memory")] - OutOfMemory, - #[error(transparent)] - Delta(#[from] crate::data::delta::apply::Error), -} +pub type Error = gix_error::Exn; -impl From for Error { - #[cold] - fn from(_: TryReserveError) -> Self { - Self::OutOfMemory - } +#[cold] +pub(super) fn allocation_error(kind: gix_error::ResourceExhaustionKind) -> Error { + use gix_error::ErrorExt; + gix_error::ResourceExhaustionError::new(kind, "Entry too large to fit in memory").raise_erased() } diff --git a/gix-pack/src/data/file/init.rs b/gix-pack/src/data/file/init.rs index 561e68b3e89..3ae11f9de11 100644 --- a/gix-pack/src/data/file/init.rs +++ b/gix-pack/src/data/file/init.rs @@ -1,5 +1,7 @@ use std::path::{Path, PathBuf}; +use gix_error::{CorruptionError, ErrorExt, ResultExt, message}; + use crate::data; /// Instantiation @@ -17,10 +19,8 @@ impl data::File { } fn at_inner(path: &Path, object_hash: gix_hash::Kind) -> Result { - let data = crate::mmap::read_only(path).map_err(|e| data::header::decode::Error::Io { - source: e, - path: path.to_owned(), - })?; + let data = crate::mmap::read_only(path) + .or_raise_erased(|| message!("Could not open pack data file at '{}'", path.display()))?; Self::from_data(data, path.to_owned(), object_hash) } } @@ -38,9 +38,10 @@ where let pack_len = data.len(); let id = gix_features::hash::crc32(path.as_os_str().to_string_lossy().as_bytes()); if pack_len < data::header::SIZE + hash_len { - return Err(data::header::decode::Error::Corrupt(format!( + return Err(CorruptionError::new(format!( "Pack data of size {pack_len} is too small for even an empty pack with shortest hash" - ))); + )) + .raise_erased()); } let (kind, num_objects) = data::header::decode( &data[..data::header::SIZE] diff --git a/gix-pack/src/data/header.rs b/gix-pack/src/data/header.rs index fae3f31ab7a..6887feda4d4 100644 --- a/gix-pack/src/data/header.rs +++ b/gix-pack/src/data/header.rs @@ -1,4 +1,5 @@ use crate::data; +use gix_error::ErrorExt; pub(crate) const N32_SIZE: usize = std::mem::size_of::(); @@ -9,13 +10,13 @@ pub const SIZE: usize = b"PACK".len() + N32_SIZE * 2; pub fn decode(data: &[u8; SIZE]) -> Result<(data::Version, u32), decode::Error> { let mut ofs = 0; if &data[ofs..ofs + b"PACK".len()] != b"PACK" { - return Err(decode::Error::Corrupt("Pack data type not recognized".into())); + return Err(gix_error::CorruptionError::new("Pack data type not recognized").raise_erased()); } ofs += N32_SIZE; let kind = match crate::read_u32(&data[ofs..ofs + N32_SIZE]) { 2 => data::Version::V2, 3 => data::Version::V3, - v => return Err(decode::Error::UnsupportedVersion(v)), + v => return Err(gix_error::ValidationError::new(format!("Unsupported pack version: {v}")).raise_erased()), }; ofs += N32_SIZE; let num_objects = crate::read_u32(&data[ofs..ofs + N32_SIZE]); @@ -42,17 +43,5 @@ pub fn encode(version: data::Version, num_objects: u32) -> [u8; SIZE] { /// pub mod decode { /// Returned by [`decode()`][super::decode()]. - #[derive(thiserror::Error, Debug)] - #[expect(missing_docs)] - pub enum Error { - #[error("Could not open pack file at '{path}'")] - Io { - source: std::io::Error, - path: std::path::PathBuf, - }, - #[error("{0}")] - Corrupt(String), - #[error("Unsupported pack version: {0}")] - UnsupportedVersion(u32), - } + pub type Error = gix_error::Exn; } diff --git a/gix-pack/src/data/input/bytes_to_entries.rs b/gix-pack/src/data/input/bytes_to_entries.rs index 7fed68ef0fd..afe2bab79df 100644 --- a/gix-pack/src/data/input/bytes_to_entries.rs +++ b/gix-pack/src/data/input/bytes_to_entries.rs @@ -1,5 +1,6 @@ use std::{fs, io}; +use gix_error::{CorruptionError, ErrorExt, ResultExt, message}; use gix_hash::{Hasher, ObjectId}; use gix_zlib::Decompress; @@ -52,7 +53,7 @@ where object_hash: gix_hash::Kind, ) -> Result, input::Error> { let mut header_data = [0u8; crate::data::header::SIZE]; - read.read_exact(&mut header_data).map_err(gix_hash::io::from_std_io)?; + read.read_exact(&mut header_data).map_err(io_error)?; let (version, num_objects) = crate::data::header::decode(&header_data)?; Ok(BytesToEntriesIter { @@ -92,7 +93,7 @@ where } None => crate::data::Entry::from_read(&mut self.read, self.offset, self.hash_len), } - .map_err(gix_hash::io::from_std_io)?; + .map_err(io_error)?; // Decompress object to learn its compressed bytes let compressed_buf = self.compressed_buf.take().unwrap_or_else(|| Vec::with_capacity(4096)); @@ -112,12 +113,13 @@ where decompressor: &mut self.decompressor, }; - let bytes_copied = io::copy(&mut decompressed_reader, &mut io::sink()).map_err(gix_hash::io::from_std_io)?; + let bytes_copied = io::copy(&mut decompressed_reader, &mut io::sink()).map_err(io_error)?; if bytes_copied != entry.decompressed_size { - return Err(input::Error::IncompletePack { - actual: bytes_copied, - expected: entry.decompressed_size, - }); + return Err(CorruptionError::new(format!( + "pack is incomplete: it was decompressed into {bytes_copied} bytes but {} bytes where expected.", + entry.decompressed_size + )) + .raise_erased()); } let pack_offset = self.offset; @@ -139,7 +141,7 @@ where let header_len = entry .header .write_to(bytes_copied, &mut header_buf.as_mut()) - .map_err(gix_hash::io::from_std_io)?; + .map_err(io_error)?; let state = gix_features::hash::crc32_update(0, &header_buf[..header_len]); Some(gix_features::hash::crc32_update(state, &compressed)) } else { @@ -174,21 +176,30 @@ where if let Err(err) = self.read.read_exact(id.as_mut_slice()) && self.mode != input::Mode::Restore { - return Err(input::Error::Io(err)); + return Err(io_error(err)); } if let Some(hash) = self.hash.take() { - let actual_id = hash.try_finalize().map_err(gix_hash::io::from_hasher)?; + let actual_id = hash + .try_finalize() + .map_err(gix_hash::io::from_hasher) + .map_err(hash_io_error)?; if self.mode == input::Mode::Restore { id = actual_id; } else { - actual_id.verify(&id)?; + actual_id + .verify(&id) + .or_raise_erased(|| message("Failed to verify pack checksum in trailer"))?; } } Some(id) } else if self.mode == input::Mode::Restore { let hash = self.hash.clone().expect("in restore mode a hash is set"); - Some(hash.try_finalize().map_err(gix_hash::io::from_hasher)?) + Some( + hash.try_finalize() + .map_err(gix_hash::io::from_hasher) + .map_err(hash_io_error)?, + ) } else { None }) @@ -273,8 +284,7 @@ where { /// Returns an iterator over [`Entries`][crate::data::input::Entry], without making use of the memory mapping. pub fn streaming_iter(&self) -> Result, input::Error> { - let reader = - io::BufReader::with_capacity(4096 * 8, fs::File::open(&self.path).map_err(gix_hash::io::from_std_io)?); + let reader = io::BufReader::with_capacity(4096 * 8, fs::File::open(&self.path).map_err(io_error)?); BytesToEntriesIter::new_from_header( reader, input::Mode::Verify, @@ -284,6 +294,16 @@ where } } +fn io_error(err: io::Error) -> input::Error { + err.and_raise(message("An IO operation failed while streaming an entry")) + .erased() +} + +fn hash_io_error(err: gix_hash::io::Error) -> input::Error { + err.raise(message("An IO operation failed while streaming an entry")) + .erased() +} + /// The boxed variant is faster for what we do (moving the decompressor in and out a lot) pub struct DecompressRead<'a, R> { /// The reader from which bytes should be decompressed. diff --git a/gix-pack/src/data/input/entries_to_bytes.rs b/gix-pack/src/data/input/entries_to_bytes.rs index 67774661268..f448abe78f7 100644 --- a/gix-pack/src/data/input/entries_to_bytes.rs +++ b/gix-pack/src/data/input/entries_to_bytes.rs @@ -1,5 +1,7 @@ use std::iter::Peekable; +use gix_error::message; + use crate::data::input; /// An implementation of [`Iterator`] to write [encoded entries][input::Entry] to an inner implementation each time @@ -135,7 +137,7 @@ where Ok(entry) } }) - .map_err(input::Error::from), + .map_err(hash_io_error), Err(err) => { self.is_done = true; Err(err) @@ -143,7 +145,7 @@ where }), None => match self.write_header_and_digest(None) { Ok(_) => None, - Err(err) => Some(Err(err.into())), + Err(err) => Some(Err(hash_io_error(err))), }, } } @@ -152,3 +154,8 @@ where self.input.size_hint() } } + +fn hash_io_error(err: gix_hash::io::Error) -> input::Error { + err.raise(message("An IO operation failed while streaming an entry")) + .erased() +} diff --git a/gix-pack/src/data/input/entry.rs b/gix-pack/src/data/input/entry.rs index f5b3ea3e462..43281f48b3b 100644 --- a/gix-pack/src/data/input/entry.rs +++ b/gix-pack/src/data/input/entry.rs @@ -1,5 +1,7 @@ use std::io::Write; +use gix_error::{ErrorExt, message}; + use crate::data::{entry::Header, input}; impl input::Entry { @@ -59,7 +61,11 @@ fn compress_data(obj: &gix_object::Data<'_>, compression: gix_zlib::Compression) let mut out = gix_zlib::stream::deflate::Write::new(Vec::new(), compression); if let Err(err) = std::io::copy(&mut &*obj.data, &mut out) { match err.kind() { - std::io::ErrorKind::Other => return Err(input::Error::Io(err)), + std::io::ErrorKind::Other => { + return Err(err + .and_raise(message("An IO operation failed while streaming an entry")) + .erased()); + } err => { unreachable!("Should never see other errors than zlib, but got {:?}", err) } diff --git a/gix-pack/src/data/input/lookup_ref_delta_objects.rs b/gix-pack/src/data/input/lookup_ref_delta_objects.rs index 21e88748dc7..c3ea3dd5f0b 100644 --- a/gix-pack/src/data/input/lookup_ref_delta_objects.rs +++ b/gix-pack/src/data/input/lookup_ref_delta_objects.rs @@ -1,3 +1,4 @@ +use gix_error::ErrorExt; use gix_hash::ObjectId; use crate::data::{entry::Header, input}; @@ -133,10 +134,11 @@ where let Some(base_pack_offset) = Header::verified_base_pack_offset(entry.pack_offset, base_distance) else { - return Some(Err(input::Error::InvalidBaseDistance { - pack_offset: entry.pack_offset, - distance: base_distance, - })); + return Some(Err(gix_error::CorruptionError::new(format!( + "The OFS_DELTA base distance {base_distance} is invalid for pack offset {}", + entry.pack_offset + )) + .raise_erased())); }; match self .inserted_entry_length_at_offset diff --git a/gix-pack/src/data/input/types.rs b/gix-pack/src/data/input/types.rs index 4c7a7ba2168..ba48b2f937d 100644 --- a/gix-pack/src/data/input/types.rs +++ b/gix-pack/src/data/input/types.rs @@ -1,27 +1,6 @@ /// Returned by [`BytesToEntriesIter::new_from_header()`][crate::data::input::BytesToEntriesIter::new_from_header()] and as part /// of `Item` of [`BytesToEntriesIter`][crate::data::input::BytesToEntriesIter]. -#[derive(thiserror::Error, Debug)] -#[expect(missing_docs)] -pub enum Error { - #[error("An IO operation failed while streaming an entry")] - Io(#[from] std::io::Error), - #[error(transparent)] - PackParse(#[from] crate::data::header::decode::Error), - #[error("Failed to verify pack checksum in trailer")] - Verify(#[from] gix_hash::verify::Error), - #[error("pack is incomplete: it was decompressed into {actual} bytes but {expected} bytes where expected.")] - IncompletePack { actual: u64, expected: u64 }, - #[error("The object {object_id} could not be decoded or wasn't found")] - NotFound { object_id: gix_hash::ObjectId }, - #[error("The OFS_DELTA base distance {distance} is invalid for pack offset {pack_offset}")] - InvalidBaseDistance { pack_offset: u64, distance: u64 }, -} - -impl From for Error { - fn from(err: gix_hash::io::Error) -> Self { - Error::Io(std::io::Error::other(err.into_error())) - } -} +pub type Error = gix_error::Exn; /// Iteration Mode #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)] diff --git a/gix-pack/src/data/output/bytes.rs b/gix-pack/src/data/output/bytes.rs index 51f338980e2..fe66e52f318 100644 --- a/gix-pack/src/data/output/bytes.rs +++ b/gix-pack/src/data/output/bytes.rs @@ -1,28 +1,11 @@ use std::io::Write; +use gix_error::{ResultExt, message}; + use crate::{data::output, exact_vec}; /// The error returned by `next()` in the [`FromEntriesIter`] iterator. -#[expect(missing_docs)] -#[derive(Debug, thiserror::Error)] -pub enum Error -where - E: std::error::Error + 'static, -{ - #[error(transparent)] - Io(#[from] std::io::Error), - #[error(transparent)] - Input(E), -} - -impl From for Error -where - E: std::error::Error + 'static, -{ - fn from(err: gix_hash::io::Error) -> Self { - Error::Io(std::io::Error::other(err.into_error())) - } -} +pub type Error = gix_error::Exn; /// An implementation of [`Iterator`] to write [encoded entries][output::Entry] to an inner implementation each time /// `next()` is called. @@ -48,11 +31,10 @@ pub struct FromEntriesIter { is_done: bool, } -impl FromEntriesIter +impl FromEntriesIter where - I: Iterator, E>>, + I: Iterator, gix_error::Exn>>, W: std::io::Write, - E: std::error::Error + 'static, { /// Create a new instance reading [entries][output::Entry] from an `input` iterator and write pack data bytes to /// `output` writer, resembling a pack of `version` with exactly `num_entries` amount of objects contained in it. @@ -92,7 +74,7 @@ where self.trailer } - fn next_inner(&mut self) -> Result> { + fn next_inner(&mut self) -> Result { let previous_written = self.written; if let Some((version, num_entries)) = self.header_info.take() { let header_bytes = crate::data::header::encode(version, num_entries); @@ -103,7 +85,7 @@ where } match self.input.next() { Some(entries) => { - for entry in entries.map_err(Error::Input)? { + for entry in entries.or_raise_erased(|| message("Pack entry input iterator failed"))? { if entry.is_invalid() { self.pack_offsets_and_validity.push((0, false)); continue; @@ -144,14 +126,13 @@ where } } -impl Iterator for FromEntriesIter +impl Iterator for FromEntriesIter where - I: Iterator, E>>, + I: Iterator, gix_error::Exn>>, W: std::io::Write, - E: std::error::Error + 'static, { /// The amount of bytes written to `out` if `Ok` or the error `E` received from the input. - type Item = Result>; + type Item = Result; fn next(&mut self) -> Option { if self.is_done { diff --git a/gix-pack/src/data/output/count/objects/mod.rs b/gix-pack/src/data/output/count/objects/mod.rs index 06230dbf3d0..369f6a2cdf5 100644 --- a/gix-pack/src/data/output/count/objects/mod.rs +++ b/gix-pack/src/data/output/count/objects/mod.rs @@ -31,7 +31,7 @@ mod tree; /// * more configuration pub fn objects( db: Find, - objects_ids: Box>> + Send>, + objects_ids: Box> + Send>, objects: &dyn gix_features::progress::Count, should_interrupt: &AtomicBool, Options { @@ -92,7 +92,7 @@ where /// Like [`objects()`] but using a single thread only to mostly save on the otherwise required overhead. pub fn objects_unthreaded( db: &dyn crate::Find, - object_ids: &mut dyn Iterator>>, + object_ids: &mut dyn Iterator>, objects: &dyn gix_features::progress::Count, should_interrupt: &AtomicBool, input_object_expansion: ObjectExpansion, @@ -119,6 +119,7 @@ mod expand { sync::atomic::{AtomicBool, Ordering}, }; + use gix_error::{ErrorExt, ResultExt, RetryableError, message}; use gix_hash::{ObjectId, oid}; use gix_object::{CommitRefIter, Data, TagRefIter}; @@ -137,7 +138,7 @@ mod expand { db: &dyn crate::Find, input_object_expansion: ObjectExpansion, seen_objs: &impl util::InsertImmutable, - oids: &mut dyn Iterator>>, + oids: &mut dyn Iterator>, buf1: &mut Vec, buf2: &mut Vec, objects: &gix_features::progress::AtomicStep, @@ -157,10 +158,10 @@ mod expand { let stats = &mut outcome; for id in oids { if should_interrupt.load(Ordering::Relaxed) { - return Err(Error::Interrupted); + return Err(RetryableError::new(message("Operation interrupted")).raise_erased()); } - let id = id.map_err(Error::InputIteration)?; + let id = id.or_raise_erased(|| message("Could not iterate input objects"))?; let (obj, location) = db.find(&id, buf1)?; stats.input_objects += 1; match input_object_expansion { @@ -197,7 +198,7 @@ mod expand { parent_commit_ids.push(id); } Ok(_) => break, - Err(err) => return Err(Error::CommitDecode(err)), + Err(err) => return Err(err.raise_erased()), } } let (obj, location) = db.find(&tree_id, buf1)?; @@ -215,8 +216,7 @@ mod expand { &mut tree_traversal_state, &objects, &mut traverse_delegate, - ) - .map_err(|err| Error::TreeTraverse(err.into_error()))?; + )?; out = objects.dissolve(stats); &traverse_delegate.non_trees } else { @@ -260,7 +260,7 @@ mod expand { &objects, &mut changes_delegate, ) - .map_err(Error::TreeChanges)?; + .or_raise_erased(|| message("Could not compare trees while generating pack"))?; stats.decoded_objects += objects.into_count(); } &changes_delegate.objects @@ -289,8 +289,7 @@ mod expand { &mut tree_traversal_state, &objects, &mut traverse_delegate, - ) - .map_err(|err| Error::TreeTraverse(err.into_error()))?; + )?; out = objects.dissolve(stats); } for id in &traverse_delegate.non_trees { diff --git a/gix-pack/src/data/output/count/objects/types.rs b/gix-pack/src/data/output/count/objects/types.rs index c9494d3b5dd..c96e4ce2faf 100644 --- a/gix-pack/src/data/output/count/objects/types.rs +++ b/gix-pack/src/data/output/count/objects/types.rs @@ -78,25 +78,4 @@ impl Default for Options { } /// The error returned by the pack generation iterator [`bytes::FromEntriesIter`][crate::data::output::bytes::FromEntriesIter]. -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] -pub enum Error { - #[error(transparent)] - CommitDecode(gix_object::decode::Error), - #[error(transparent)] - FindExisting(gix_error::Error), - #[error(transparent)] - InputIteration(Box), - #[error(transparent)] - TreeTraverse(gix_error::Error), - #[error(transparent)] - TreeChanges(gix_diff::tree::Error), - #[error("Operation interrupted")] - Interrupted, -} - -impl From for Error { - fn from(err: gix_object::find::existing::Error) -> Self { - Error::FindExisting(err.into_error()) - } -} +pub type Error = gix_error::Exn; 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 2147e755c4b..d500d5ef6b2 100644 --- a/gix-pack/src/data/output/entry/iter_from_counts.rs +++ b/gix-pack/src/data/output/entry/iter_from_counts.rs @@ -1,6 +1,7 @@ pub(crate) mod function { use std::{cmp::Ordering, sync::Arc}; + use gix_error::{ResultExt, message}; use gix_features::{ parallel, parallel::SequenceId, @@ -208,19 +209,27 @@ pub(crate) mod function { stats.objects_copied_from_pack += 1; entry } - None => match db.try_find(&count.id, buf).map_err(Error::Find)? { - Some((obj, _location)) => { - stats.decoded_and_recompressed_objects += 1; - output::Entry::from_data(count, &obj, compression) + None => { + match db.try_find(&count.id, buf).or_erased().or_raise_erased(|| { + message("Could not find object while generating pack") + })? { + Some((obj, _location)) => { + stats.decoded_and_recompressed_objects += 1; + output::Entry::from_data(count, &obj, compression) + } + None => { + stats.missing_objects += 1; + Ok(output::Entry::invalid()) + } } - None => { - stats.missing_objects += 1; - Ok(output::Entry::invalid()) - } - }, + } } } - None => match db.try_find(&count.id, buf).map_err(Error::Find)? { + None => match db + .try_find(&count.id, buf) + .or_erased() + .or_raise_erased(|| message("Could not find object while generating pack"))? + { Some((obj, _location)) => { stats.decoded_and_recompressed_objects += 1; output::Entry::from_data(count, &obj, compression) @@ -317,8 +326,6 @@ mod reduce { } mod types { - use crate::data::output::entry; - /// Information gathered during the run of [`iter_from_counts()`][crate::data::output::entry::iter_from_counts()]. #[derive(Default, PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] @@ -405,14 +412,7 @@ mod types { } /// The error returned by the pack generation function [`iter_from_counts()`][crate::data::output::entry::iter_from_counts()]. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - Find(gix_object::find::Error), - #[error(transparent)] - NewEntry(#[from] entry::Error), - } + pub type Error = gix_error::Exn; /// The progress ids used in [`write_to_directory()`][crate::Bundle::write_to_directory()]. /// diff --git a/gix-pack/src/data/output/entry/mod.rs b/gix-pack/src/data/output/entry/mod.rs index 647e02a97a2..edb3a9ecb59 100644 --- a/gix-pack/src/data/output/entry/mod.rs +++ b/gix-pack/src/data/output/entry/mod.rs @@ -1,5 +1,6 @@ use std::io::Write; +use gix_error::{ErrorExt, ResultExt, message}; use gix_hash::ObjectId; use crate::{data, data::output, find}; @@ -32,14 +33,7 @@ pub enum Kind { } /// The error returned by [`output::Entry::from_data()`]. -#[expect(missing_docs)] -#[derive(Debug, thiserror::Error)] -pub enum Error { - #[error("{0}")] - ZlibDeflate(#[from] std::io::Error), - #[error(transparent)] - EntryType(#[from] crate::data::entry::decode::Error), -} +pub type Error = gix_error::Exn; impl output::Entry { /// An object which can be identified as invalid easily which happens if objects didn't exist even if they were referred to. @@ -78,7 +72,7 @@ impl output::Entry { let pack_offset_must_be_zero = 0; let pack_entry = match data::Entry::from_bytes(&entry.data, pack_offset_must_be_zero, count.id.kind()) { Ok(e) => e, - Err(err) => return Some(Err(err.into())), + Err(err) => return Some(Err(err.raise_erased())), }; use crate::data::entry::Header::*; @@ -92,10 +86,10 @@ impl output::Entry { let Some(base_offset) = crate::data::entry::Header::verified_base_pack_offset(pack_location.pack_offset, base_distance) else { - return Some(Err(crate::data::entry::decode::Error::Corrupt { - message: "an ofs-delta base distance pointing before pack start", - } - .into())); + return Some(Err(gix_error::CorruptionError::new( + "an ofs-delta base distance pointing before pack start", + ) + .raise_erased())); }; potential_bases .binary_search_by(|e| { @@ -153,11 +147,14 @@ impl output::Entry { let mut out = gix_zlib::stream::deflate::Write::new(Vec::new(), compression); if let Err(err) = std::io::copy(&mut &*obj.data, &mut out) { match err.kind() { - std::io::ErrorKind::Other => return Err(Error::ZlibDeflate(err)), + std::io::ErrorKind::Other => { + return Err(err.and_raise(message("Failed to compress pack entry")).erased()); + } err => unreachable!("Should never see other errors than zlib, but got {:?}", err), } } - out.flush()?; + out.flush() + .or_raise_erased(|| message("Failed to finish compressing pack entry"))?; out.into_inner() }, }) diff --git a/gix-pack/src/index/init.rs b/gix-pack/src/index/init.rs index aa1b01318e4..26104ce7f5c 100644 --- a/gix-pack/src/index/init.rs +++ b/gix-pack/src/index/init.rs @@ -1,23 +1,18 @@ use std::{ + borrow::Cow, mem::size_of, path::{Path, PathBuf}, }; +use gix_error::{ErrorExt, ResultExt, ValidationError, message}; + use crate::index::{self, FAN_LEN, V2_SIGNATURE, Version}; /// Returned by [`index::File::at()`]. -#[derive(thiserror::Error, Debug)] -#[expect(missing_docs)] -pub enum Error { - #[error("Could not open pack index file at '{path}'")] - Io { - source: std::io::Error, - path: std::path::PathBuf, - }, - #[error("{message}")] - Corrupt { message: String }, - #[error("Unsupported index version: {version})")] - UnsupportedVersion { version: u32 }, +pub type Error = gix_error::Exn; + +fn corrupt(message: impl Into>) -> Error { + gix_error::CorruptionError::new(message).raise_erased() } const N32_SIZE: usize = size_of::(); @@ -33,10 +28,8 @@ impl index::File { } fn at_inner(path: &Path, object_hash: gix_hash::Kind) -> Result { - let data = crate::mmap::read_only(path).map_err(|source| Error::Io { - source, - path: path.to_owned(), - })?; + let data = crate::mmap::read_only(path) + .or_raise_erased(|| message!("Could not open pack index file at '{}'", path.display()))?; Self::from_data(data, path.to_owned(), object_hash) } } @@ -52,9 +45,9 @@ where let footer_size = hash_len * 2; if idx_len < FAN_LEN * N32_SIZE + footer_size { - return Err(Error::Corrupt { - message: format!("Pack index of size {idx_len} is too small for even an empty index"), - }); + return Err(corrupt(format!( + "Pack index of size {idx_len} is too small for even an empty index" + ))); } let (kind, fan, num_objects) = { let (kind, d) = { @@ -70,7 +63,9 @@ where let (vd, dr) = d.split_at(N32_SIZE); let version = crate::read_u32(vd); if version != Version::V2 as u32 { - return Err(Error::UnsupportedVersion { version }); + return Err( + ValidationError::new(format!("Unsupported index version: {version})")).raise_erased() + ); } dr } else { @@ -109,9 +104,7 @@ fn read_fan(d: &[u8]) -> ([u32; FAN_LEN], usize) { fn validate_fan(fan: &[u32; FAN_LEN]) -> Result<(), Error> { if !crate::fan_is_monotonically_increasing(fan) { - return Err(Error::Corrupt { - message: "Pack index fan-out table must be monotonically increasing".into(), - }); + return Err(corrupt("Pack index fan-out table must be monotonically increasing")); } Ok(()) } @@ -124,34 +117,28 @@ fn validate_size(data: &[u8], kind: Version, num_objects: u32, hash_len: usize) .checked_mul(N32_SIZE) .and_then(|size| size.checked_add(num_objects.checked_mul(N32_SIZE + hash_len)?)) .and_then(|size| size.checked_add(footer_size)) - .ok_or_else(|| Error::Corrupt { - message: "Pack index size overflowed while validating version 1 layout".into(), - })?, + .ok_or_else(|| corrupt("Pack index size overflowed while validating version 1 layout"))?, Version::V2 => { let v2_header_size = V2_SIGNATURE.len() + N32_SIZE + FAN_LEN * N32_SIZE; - let oid_bytes = num_objects.checked_mul(hash_len).ok_or_else(|| Error::Corrupt { - message: "Pack index size overflowed while validating object ids".into(), - })?; - let table_bytes = num_objects.checked_mul(N32_SIZE).ok_or_else(|| Error::Corrupt { - message: "Pack index size overflowed while validating 32-bit tables".into(), - })?; + let oid_bytes = num_objects + .checked_mul(hash_len) + .ok_or_else(|| corrupt("Pack index size overflowed while validating object ids"))?; + let table_bytes = num_objects + .checked_mul(N32_SIZE) + .ok_or_else(|| corrupt("Pack index size overflowed while validating 32-bit tables"))?; let offset32_start = v2_header_size .checked_add(oid_bytes) .and_then(|size| size.checked_add(table_bytes)) - .ok_or_else(|| Error::Corrupt { - message: "Pack index size overflowed while locating 32-bit offsets".into(), - })?; - let offset32_end = offset32_start.checked_add(table_bytes).ok_or_else(|| Error::Corrupt { - message: "Pack index size overflowed while locating 32-bit offsets".into(), - })?; + .ok_or_else(|| corrupt("Pack index size overflowed while locating 32-bit offsets"))?; + let offset32_end = offset32_start + .checked_add(table_bytes) + .ok_or_else(|| corrupt("Pack index size overflowed while locating 32-bit offsets"))?; if offset32_end > data.len() { - return Err(Error::Corrupt { - message: format!( - "Pack index of size {} is too small for {} objects in version 2", - data.len(), - num_objects - ), - }); + return Err(corrupt(format!( + "Pack index of size {} is too small for {} objects in version 2", + data.len(), + num_objects + ))); } let (large_offsets, max_large_offset_index) = data[offset32_start..offset32_end] .as_chunks::() @@ -170,16 +157,12 @@ fn validate_size(data: &[u8], kind: Version, num_objects: u32, hash_len: usize) .and_then(|size| size.checked_add(table_bytes)) .and_then(|size| size.checked_add(large_offsets.checked_mul(size_of::())?)) .and_then(|size| size.checked_add(footer_size)) - .ok_or_else(|| Error::Corrupt { - message: "Pack index size overflowed while validating version 2 layout".into(), - }) + .ok_or_else(|| corrupt("Pack index size overflowed while validating version 2 layout")) .and_then(|expected_size| { if large_offsets > 0 && max_large_offset_index >= large_offsets { - return Err(Error::Corrupt { - message: format!( + return Err(corrupt(format!( "Pack index references large offset {max_large_offset_index}, but only {large_offsets} large offsets are present" - ), - }); + ))); } Ok(expected_size) })? @@ -188,12 +171,10 @@ fn validate_size(data: &[u8], kind: Version, num_objects: u32, hash_len: usize) if data.len() != expected_size { // Aborting here is needed for protection against malformed inputs, or the offset access done later can panic // as it's done without explicit error handling. - return Err(Error::Corrupt { - message: format!( - "Pack index size is incorrect, expected {expected_size} bytes for {num_objects} objects in version {kind:?}, but got {} bytes", - data.len() - ), - }); + return Err(corrupt(format!( + "Pack index size is incorrect, expected {expected_size} bytes for {num_objects} objects in version {kind:?}, but got {} bytes", + data.len() + ))); } Ok(()) } diff --git a/gix-pack/src/index/traverse/error.rs b/gix-pack/src/index/traverse/error.rs index 1dcd08dd3c3..d828a6458c6 100644 --- a/gix-pack/src/index/traverse/error.rs +++ b/gix-pack/src/index/traverse/error.rs @@ -1,44 +1,2 @@ -use crate::index; - -/// Returned by [`index::File::traverse_with_index()`] and [`index::File::traverse_with_lookup`] -#[derive(thiserror::Error, Debug)] -#[expect(missing_docs)] -pub enum Error { - #[error("One of the traversal processors failed")] - Processor(#[source] E), - #[error("Failed to verify index file checksum")] - IndexVerify(#[source] index::verify::checksum::Error), - #[error("The pack delta tree index could not be built")] - Tree(#[from] crate::cache::delta::from_offsets::Error), - #[error("The tree traversal failed")] - TreeTraversal(#[from] crate::cache::delta::traverse::Error), - #[error(transparent)] - EntryType(#[from] crate::data::entry::decode::Error), - #[error("Object {id} at offset {offset} could not be decoded")] - PackDecode { - id: gix_hash::ObjectId, - offset: u64, - source: crate::data::decode::Error, - }, - #[error("The packfiles checksum didn't match the index file checksum")] - PackMismatch(#[source] gix_hash::verify::Error), - #[error("Failed to verify pack file checksum")] - PackVerify(#[source] crate::verify::checksum::Error), - #[error("Error verifying object at offset {offset} against checksum in the index file")] - PackObjectVerify { - offset: u64, - #[source] - source: gix_error::Error, - }, - #[error( - "The CRC32 of {kind} object at offset {offset} didn't match the checksum in the index file: expected {expected}, got {actual}" - )] - Crc32Mismatch { - expected: u32, - actual: u32, - offset: u64, - kind: gix_object::Kind, - }, - #[error("Interrupted")] - Interrupted, -} +/// Returned by [`crate::index::File::traverse_with_index()`] and [`crate::index::File::traverse_with_lookup`] +pub type Error = gix_error::Exn; diff --git a/gix-pack/src/index/traverse/mod.rs b/gix-pack/src/index/traverse/mod.rs index ad6defc0600..11c81448773 100644 --- a/gix-pack/src/index/traverse/mod.rs +++ b/gix-pack/src/index/traverse/mod.rs @@ -1,5 +1,6 @@ use std::sync::atomic::AtomicBool; +use gix_error::{ErrorExt, ResultExt, message}; use gix_features::{parallel, progress::Progress}; use crate::index; @@ -80,7 +81,7 @@ where /// /// Use [`thread_limit`][Options::thread_limit] to further control parallelism and [`check`][SafetyCheck] to define how much the passed /// objects shall be verified beforehand. - pub fn traverse( + pub fn traverse( &self, pack: &crate::data::File, progress: &mut dyn DynNestedProgress, @@ -93,11 +94,11 @@ where alloc_limit_bytes, make_pack_lookup_cache, }: Options, - ) -> Result> + ) -> Result where C: crate::cache::DecodeEntry, - E: std::error::Error + Send + Sync + 'static, - Processor: FnMut(gix_object::Kind, &[u8], &index::Entry, &dyn Progress) -> Result<(), E> + Send + Clone, + Processor: + FnMut(gix_object::Kind, &[u8], &index::Entry, &dyn Progress) -> Result<(), gix_error::Exn> + Send + Clone, F: Fn() -> C + Send + Clone, D: crate::FileData + Send + Sync, { @@ -127,35 +128,34 @@ where } } - fn possibly_verify( + fn possibly_verify( &self, pack: &crate::data::File, check: SafetyCheck, pack_progress: &mut dyn Progress, index_progress: &mut dyn Progress, should_interrupt: &AtomicBool, - ) -> Result> + ) -> Result where - E: std::error::Error + Send + Sync + 'static, D: crate::FileData + Send + Sync, { Ok(if check.file_checksum() { pack.checksum() .verify(&self.pack_checksum()) - .map_err(Error::PackMismatch)?; + .or_raise_erased(|| gix_error::CorruptionError::new("Pack checksum differs from index"))?; let (pack_res, id) = parallel::join( move || pack.verify_checksum(pack_progress, should_interrupt), move || self.verify_checksum(index_progress, should_interrupt), ); - pack_res.map_err(Error::PackVerify)?; - id.map_err(Error::IndexVerify)? + pack_res?; + id? } else { self.index_checksum() }) } #[expect(clippy::too_many_arguments)] - fn decode_and_process_entry( + fn decode_and_process_entry( &self, check: SafetyCheck, pack: &crate::data::File, @@ -164,33 +164,44 @@ where inflate: &mut gix_zlib::Inflate, progress: &mut dyn Progress, index_entry: &index::Entry, - processor: &mut impl FnMut(gix_object::Kind, &[u8], &index::Entry, &dyn Progress) -> Result<(), E>, - ) -> Result> + processor: &mut impl FnMut(gix_object::Kind, &[u8], &index::Entry, &dyn Progress) -> Result<(), gix_error::Exn>, + ) -> Result, Error> where C: crate::cache::DecodeEntry, - E: std::error::Error + Send + Sync + 'static, D: crate::FileData + Send + Sync, { - let pack_entry = pack.entry(index_entry.pack_offset)?; + let pack_entry = pack.entry(index_entry.pack_offset).map_err(ErrorExt::raise_erased)?; let pack_entry_data_offset = pack_entry.data_offset; - let entry_stats = pack - .decode_entry( - pack_entry, - buf, - inflate, - &|id, _| { - let index = self.lookup(id)?; - pack.entry(self.pack_offset_at_index(index)) - .ok() - .map(crate::data::decode::entry::ResolvedBase::InPack) - }, - cache, - ) - .map_err(|e| Error::PackDecode { - source: e, - id: index_entry.oid, - offset: index_entry.pack_offset, - })?; + let entry_stats = match pack.decode_entry( + pack_entry, + buf, + inflate, + &|id, _| { + let index = self.lookup(id)?; + pack.entry(self.pack_offset_at_index(index)) + .ok() + .map(crate::data::decode::entry::ResolvedBase::InPack) + }, + cache, + ) { + Ok(stats) => stats, + Err(err) if !check.fatal_decode_error() => { + progress.info(format!( + "Ignoring decode error for object {} at offset {}: {err}", + index_entry.oid, index_entry.pack_offset + )); + return Ok(None); + } + Err(err) => { + return Err(err + .raise(message!( + "Object {} at offset {} could not be decoded", + index_entry.oid, + index_entry.pack_offset + )) + .erased()); + } + }; let object_kind = entry_stats.kind; let header_size = (pack_entry_data_offset - index_entry.pack_offset) as usize; let entry_len = header_size + entry_stats.compressed_size; @@ -204,40 +215,38 @@ where progress, processor, )?; - Ok(entry_stats) + Ok(Some(entry_stats)) } } -fn process_entry( +fn process_entry( check: SafetyCheck, object_kind: gix_object::Kind, decompressed: &[u8], index_entry: &index::Entry, pack_entry_crc32: impl FnOnce() -> u32, progress: &dyn Progress, - processor: &mut impl FnMut(gix_object::Kind, &[u8], &index::Entry, &dyn Progress) -> Result<(), E>, -) -> Result<(), Error> -where - E: std::error::Error + Send + Sync + 'static, -{ + processor: &mut impl FnMut(gix_object::Kind, &[u8], &index::Entry, &dyn Progress) -> Result<(), gix_error::Exn>, +) -> Result<(), Error> { if check.object_checksum() { gix_object::Data::new(decompressed, object_kind, index_entry.oid.kind()) .verify_checksum(&index_entry.oid) - .map_err(|source| Error::PackObjectVerify { - offset: index_entry.pack_offset, - source: source.into_error(), + .or_raise_erased(|| { + gix_error::CorruptionError::new(format!( + "Error verifying object at offset {} against checksum in the index file", + index_entry.pack_offset + )) })?; if let Some(desired_crc32) = index_entry.crc32 { let actual_crc32 = pack_entry_crc32(); if actual_crc32 != desired_crc32 { - return Err(Error::Crc32Mismatch { - actual: actual_crc32, - expected: desired_crc32, - offset: index_entry.pack_offset, - kind: object_kind, - }); + return Err(gix_error::CorruptionError::new(format!( + "The CRC32 of {object_kind} object at offset {} didn't match the checksum in the index file: expected {desired_crc32}, got {actual_crc32}", + index_entry.pack_offset + )) + .raise_erased()); } } } - processor(object_kind, decompressed, index_entry, progress).map_err(Error::Processor) + processor(object_kind, decompressed, index_entry, progress) } diff --git a/gix-pack/src/index/traverse/reduce.rs b/gix-pack/src/index/traverse/reduce.rs index f4039cc43c0..411c95657d1 100644 --- a/gix-pack/src/index/traverse/reduce.rs +++ b/gix-pack/src/index/traverse/reduce.rs @@ -3,6 +3,7 @@ use std::{ time::Instant, }; +use gix_error::{ErrorExt, RetryableError, message}; use gix_features::{ parallel, progress::Progress, @@ -27,24 +28,21 @@ fn div_decode_result(lhs: &mut data::decode::entry::Outcome, div: usize) { } } -pub struct Reducer<'a, P, E> { +pub struct Reducer<'a, P> { progress: OwnShared>, - check: traverse::SafetyCheck, then: Instant, entries_seen: usize, stats: traverse::Statistics, should_interrupt: &'a AtomicBool, - _error: std::marker::PhantomData, } -impl<'a, P, E> Reducer<'a, P, E> +impl<'a, P> Reducer<'a, P> where P: Progress, { pub fn from_progress( progress: OwnShared>, pack_data_len_in_bytes: usize, - check: traverse::SafetyCheck, should_interrupt: &'a AtomicBool, ) -> Self { let stats = traverse::Statistics { @@ -53,34 +51,25 @@ where }; Reducer { progress, - check, then: Instant::now(), entries_seen: 0, should_interrupt, stats, - _error: Default::default(), } } } -impl parallel::Reduce for Reducer<'_, P, E> +impl

parallel::Reduce for Reducer<'_, P> where P: Progress, - E: std::error::Error + Send + Sync + 'static, { - type Input = Result, traverse::Error>; + type Input = Result, traverse::Error>; type FeedProduce = (); type Output = traverse::Statistics; - type Error = traverse::Error; + type Error = traverse::Error; fn feed(&mut self, input: Self::Input) -> Result<(), Self::Error> { - let chunk_stats: Vec<_> = match input { - Err(err @ traverse::Error::PackDecode { .. }) if !self.check.fatal_decode_error() => { - lock(&self.progress).info(format!("Ignoring decode error: {err}")); - return Ok(()); - } - res => res, - }?; + let chunk_stats = input?; self.entries_seen += chunk_stats.len(); let chunk_total = chunk_stats.into_iter().fold( @@ -106,7 +95,7 @@ where lock(&self.progress).set(self.entries_seen); if self.should_interrupt.load(Ordering::SeqCst) { - return Err(Self::Error::Interrupted); + return Err(RetryableError::new(message("Interrupted")).raise_erased()); } Ok(()) } diff --git a/gix-pack/src/index/traverse/with_index.rs b/gix-pack/src/index/traverse/with_index.rs index 56360f5ddc2..9ae913e7e1c 100644 --- a/gix-pack/src/index/traverse/with_index.rs +++ b/gix-pack/src/index/traverse/with_index.rs @@ -62,7 +62,7 @@ where /// at the cost of memory. /// /// For more details, see the documentation on the [`traverse()`][index::File::traverse()] method. - pub fn traverse_with_index( + pub fn traverse_with_index( &self, pack: &crate::data::File, mut processor: Processor, @@ -73,12 +73,16 @@ where thread_limit, alloc_limit_bytes, }: Options, - ) -> Result> + ) -> Result where - Processor: FnMut(gix_object::Kind, &[u8], &index::Entry, &dyn gix_features::progress::Progress) -> Result<(), E> + Processor: FnMut( + gix_object::Kind, + &[u8], + &index::Entry, + &dyn gix_features::progress::Progress, + ) -> Result<(), gix_error::Exn> + Send + Clone, - E: std::error::Error + Send + Sync + 'static, D: crate::FileData + Send + Sync, { let (verify_result, traversal_result) = parallel::join( @@ -100,7 +104,7 @@ where res } }, - || -> Result<_, Error<_>> { + || -> Result<_, Error> { let sorted_entries = index_entries_sorted_by_offset_ascending( self, &mut progress.add_child_with_id( @@ -135,7 +139,7 @@ where data.object_kind = object_kind; data.compressed_size = entry_end - pack_entry.data_offset; data.object_size = bytes.len() as u64; - let result = index::traverse::process_entry( + index::traverse::process_entry( check, object_kind, bytes, @@ -151,14 +155,7 @@ where }, progress, &mut processor, - ); - match result { - Err(err @ Error::PackDecode { .. }) if !check.fatal_decode_error() => { - progress.info(format!("Ignoring decode error: {err}")); - Ok(()) - } - res => res, - } + ) }, traverse::Options { object_progress: Box::new( diff --git a/gix-pack/src/index/traverse/with_lookup.rs b/gix-pack/src/index/traverse/with_lookup.rs index c81ba1f006a..a2f04ce98c0 100644 --- a/gix-pack/src/index/traverse/with_lookup.rs +++ b/gix-pack/src/index/traverse/with_lookup.rs @@ -68,7 +68,7 @@ where /// waste while decoding objects. /// /// For more details, see the documentation on the [`traverse()`][index::File::traverse()] method. - pub fn traverse_with_lookup( + pub fn traverse_with_lookup( &self, mut processor: Processor, pack: &data::File, @@ -79,11 +79,11 @@ where check, make_pack_lookup_cache, }: Options, - ) -> Result> + ) -> Result where C: crate::cache::DecodeEntry, - E: std::error::Error + Send + Sync + 'static, - Processor: FnMut(gix_object::Kind, &[u8], &index::Entry, &dyn Progress) -> Result<(), E> + Send + Clone, + Processor: + FnMut(gix_object::Kind, &[u8], &index::Entry, &dyn Progress) -> Result<(), gix_error::Exn> + Send + Clone, F: Fn() -> C + Send + Clone, D: crate::FileData + Send + Sync, { @@ -144,7 +144,7 @@ where state_per_thread, move |entries: &[index::Entry], (cache, buf, inflate, progress)| - -> Result, Error<_>> { + -> Result, Error> { progress.init( Some(entries.len()), gix_features::progress::count_with_decimals("objects", 2), @@ -163,13 +163,7 @@ where &mut processor, ); progress.inc(); - let stat = match result { - Err(err @ Error::PackDecode { .. }) if !check.fatal_decode_error() => { - progress.info(format!("Ignoring decode error: {err}")); - continue; - } - res => res, - }?; + let Some(stat) = result? else { continue }; stats.push(stat); if should_interrupt.load(Ordering::Relaxed) { break; @@ -177,7 +171,7 @@ where } Ok(stats) }, - Reducer::from_progress(reduce_progress, pack.data_len(), check, should_interrupt), + Reducer::from_progress(reduce_progress, pack.data_len(), should_interrupt), ) }, ); diff --git a/gix-pack/src/index/verify.rs b/gix-pack/src/index/verify.rs index 7b907bef494..ef963809d87 100644 --- a/gix-pack/src/index/verify.rs +++ b/gix-pack/src/index/verify.rs @@ -1,7 +1,9 @@ use std::sync::atomic::AtomicBool; +use gix_error::{ErrorExt, ResultExt}; use gix_features::progress::{DynNestedProgress, Progress}; use gix_object::WriteTo; +use gix_object::bstr::ByteSlice; use crate::index; @@ -9,30 +11,8 @@ use crate::index; pub mod integrity { use std::marker::PhantomData; - use gix_object::bstr::BString; - /// Returned by [`index::File::verify_integrity()`][crate::index::File::verify_integrity()]. - #[derive(thiserror::Error, Debug)] - #[expect(missing_docs)] - pub enum Error { - #[error("Reserialization of an object failed")] - Io(#[from] std::io::Error), - #[error("The fan at index {index} is out of order as it's larger then the following value.")] - Fan { index: usize }, - #[error("{kind} object {id} could not be decoded")] - ObjectDecode { - source: gix_object::decode::Error, - kind: gix_object::Kind, - id: gix_hash::ObjectId, - }, - #[error("{kind} object {id} wasn't re-encoded without change, wanted\n{expected}\n\nGOT\n\n{actual}")] - ObjectEncodeMismatch { - kind: gix_object::Kind, - id: gix_hash::ObjectId, - expected: BString, - actual: BString, - }, - } + pub type Error = gix_error::Exn; /// Returned by [`index::File::verify_integrity()`][crate::index::File::verify_integrity()]. pub struct Outcome { @@ -174,16 +154,17 @@ where pack: Option>, progress: &mut dyn DynNestedProgress, should_interrupt: &AtomicBool, - ) -> Result> + ) -> Result where C: crate::cache::DecodeEntry, F: Fn() -> C + Send + Clone, D: crate::FileData + Send + Sync, { if let Some(first_invalid) = crate::verify::fan(&self.fan) { - return Err(index::traverse::Error::Processor(integrity::Error::Fan { - index: first_invalid, - })); + return Err(gix_error::CorruptionError::new(format!( + "The fan at index {first_invalid} is out of order as it's larger then the following value." + )) + .raise_erased()); } match pack { @@ -225,7 +206,6 @@ where .add_child_with_id("checksum of index".into(), integrity::ProgressId::ChecksumBytes.into()), should_interrupt, ) - .map_err(index::traverse::Error::IndexVerify) .map(|id| integrity::Outcome { actual_index_checksum: id, pack_traverse_statistics: None, @@ -233,6 +213,7 @@ where } } + #[allow(clippy::too_many_arguments)] fn verify_entry( verify_mode: Mode, encode_buf: &mut Vec, @@ -245,24 +226,26 @@ where use gix_object::Kind::*; match object_kind { Tree | Commit | Tag => { - let object = - gix_object::ObjectRef::from_bytes(buf, object_kind, index_entry.oid.kind()).map_err(|err| { - integrity::Error::ObjectDecode { - source: err, - kind: object_kind, - id: index_entry.oid, - } + let object = gix_object::ObjectRef::from_bytes(buf, object_kind, index_entry.oid.kind()) + .or_raise_erased(|| { + gix_error::CorruptionError::new(format!( + "{object_kind} object {} could not be decoded", + index_entry.oid + )) })?; if let Mode::HashCrc32DecodeEncode = verify_mode { encode_buf.clear(); - object.write_to(&mut *encode_buf)?; + object.write_to(&mut *encode_buf).or_raise_erased(|| { + gix_error::CorruptionError::new("Reserialization of an object failed") + })?; if encode_buf.as_slice() != buf { - return Err(integrity::Error::ObjectEncodeMismatch { - kind: object_kind, - id: index_entry.oid, - expected: buf.into(), - actual: encode_buf.clone().into(), - }); + return Err(gix_error::CorruptionError::new(format!( + "{object_kind} object {} wasn't re-encoded without change, wanted\n{}\n\nGOT\n\n{}", + index_entry.oid, + buf.as_bstr(), + encode_buf.as_bstr() + )) + .raise_erased()); } } } diff --git a/gix-pack/src/index/write/error.rs b/gix-pack/src/index/write/error.rs index 01cd286c094..2eb27b90242 100644 --- a/gix-pack/src/index/write/error.rs +++ b/gix-pack/src/index/write/error.rs @@ -1,29 +1,2 @@ /// Returned by [`crate::index::write_data_iter_to_stream()`] -#[derive(thiserror::Error, Debug)] -#[expect(missing_docs)] -pub enum Error { - #[error("An error occurred when writing the pack index file")] - Io(#[from] std::io::Error), - #[error("A pack entry could not be extracted")] - PackEntryDecode(#[from] crate::data::input::Error), - #[error("Indices of type {} cannot be written, only {} are supported", *.0 as usize, crate::index::Version::default() as usize)] - Unsupported(crate::index::Version), - #[error("Ref delta objects are not supported as there is no way to look them up. Resolve them beforehand.")] - IteratorInvariantNoRefDelta, - #[error("The iterator failed to set a trailing hash over all prior pack entries in the last provided entry")] - IteratorInvariantTrailer, - #[error("Only u32::MAX objects can be stored in a pack, found {0}")] - IteratorInvariantTooManyObjects(usize), - #[error("{pack_offset} is not a valid offset for pack offset {distance}")] - IteratorInvariantBaseOffset { pack_offset: u64, distance: u64 }, - #[error(transparent)] - Tree(#[from] crate::cache::delta::Error), - #[error(transparent)] - TreeTraversal(#[from] crate::cache::delta::traverse::Error), -} - -impl From for Error { - fn from(err: gix_hash::io::Error) -> Self { - Error::Io(std::io::Error::other(err.into_error())) - } -} +pub type Error = gix_error::Exn; diff --git a/gix-pack/src/index/write/mod.rs b/gix-pack/src/index/write/mod.rs index 638c075bd2b..69b5f69b225 100644 --- a/gix-pack/src/index/write/mod.rs +++ b/gix-pack/src/index/write/mod.rs @@ -58,6 +58,7 @@ impl From for gix_features::progress::Id { pub(super) mod function { use std::{io, sync::atomic::AtomicBool}; + use gix_error::{ErrorExt, OptionExt, ResultExt, ValidationError}; use gix_features::progress::{self, Count, Progress, prodash::DynNestedProgress}; use crate::cache::delta::{Tree, traverse}; @@ -114,7 +115,12 @@ pub(super) mod function { F2: for<'r> Fn(crate::data::EntryRange, &'r R) -> Option<&'r [u8]> + Send + Clone, { if version != crate::index::Version::default() { - return Err(Error::Unsupported(version)); + return Err(ValidationError::new(format!( + "Indices of type {} cannot be written, only {} are supported", + version as usize, + crate::index::Version::default() as usize + )) + .raise_erased()); } let mut num_objects: usize = 0; let mut last_seen_trailer = None; @@ -173,12 +179,12 @@ pub(super) mod function { } OfsDelta { base_distance } => { let base_pack_offset = - crate::data::entry::Header::verified_base_pack_offset(pack_offset, base_distance).ok_or( - Error::IteratorInvariantBaseOffset { - pack_offset, - distance: base_distance, - }, - )?; + crate::data::entry::Header::verified_base_pack_offset(pack_offset, base_distance) + .ok_or_raise_erased(|| { + ValidationError::new(format!( + "{pack_offset} is not a valid offset for pack offset {base_distance}" + )) + })?; tree.add_child( base_pack_offset, pack_offset, @@ -193,9 +199,11 @@ pub(super) mod function { num_objects += 1; objects_progress.inc(); } - let num_objects: u32 = num_objects - .try_into() - .map_err(|_| Error::IteratorInvariantTooManyObjects(num_objects))?; + let num_objects = u32::try_from(num_objects).or_raise_erased(|| { + ValidationError::new(format!( + "Only u32::MAX objects can be stored in a pack, found {num_objects}" + )) + })?; objects_progress.show_throughput(indexing_start); decompressed_progress.show_throughput(indexing_start); @@ -251,7 +259,12 @@ pub(super) mod function { hasher.update(&header); hasher.try_finalize().map_err(gix_hash::io::from_hasher)? } - None => return Err(Error::IteratorInvariantTrailer), + None => { + return Err(ValidationError::new( + "The iterator failed to set a trailing hash over all prior pack entries in the last provided entry", + ) + .raise_erased()); + } }; let index_hash = crate::index::encode::write_to( out, @@ -281,9 +294,9 @@ fn modify_base( pack_entry: &crate::data::Entry, decompressed: &[u8], hash: gix_hash::Kind, -) -> Result<(), gix_hash::hasher::Error> { +) -> Result<(), gix_error::Exn> { let object_kind = pack_entry.header.as_kind().expect("base object as source of iteration"); - let id = gix_object::compute_hash(hash, object_kind, decompressed)?; + let id = gix_object::compute_hash(hash, object_kind, decompressed).map_err(gix_error::ErrorExt::raise_erased)?; entry.id = id; Ok(()) } diff --git a/gix-pack/src/multi_index/chunk.rs b/gix-pack/src/multi_index/chunk.rs index 27e9a9b5bc1..42c9deaa4e4 100644 --- a/gix-pack/src/multi_index/chunk.rs +++ b/gix-pack/src/multi_index/chunk.rs @@ -2,39 +2,15 @@ pub mod index_names { use std::path::{Path, PathBuf}; - use gix_object::bstr::{BString, ByteSlice}; + use gix_object::bstr::ByteSlice; /// The ID used for the index-names chunk. pub const ID: gix_chunk::Id = *b"PNAM"; /// pub mod decode { - use std::collections::TryReserveError; - - use gix_object::bstr::BString; - /// The error returned by [`from_bytes()`][super::from_bytes()]. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error("The pack names were not ordered alphabetically.")] - NotOrderedAlphabetically, - #[error("Each pack path name must be terminated with a null byte")] - MissingNullByte, - #[error("Entry too large to fit in memory")] - OutOfMemory, - #[error("Couldn't turn path '{path}' into OS path due to encoding issues")] - PathEncoding { path: BString }, - #[error("non-padding bytes found after all paths were read.")] - UnknownTrailerBytes, - } - - impl From for Error { - #[cold] - fn from(_: TryReserveError) -> Self { - Self::OutOfMemory - } - } + pub type Error = gix_error::Exn; } /// Parse null-separated index names from the given `chunk` of bytes. @@ -55,33 +31,65 @@ pub mod index_names { num_packs: u32, alloc_limit_bytes: Option, ) -> Result, decode::Error> { + use gix_error::{ + CorruptionError, ErrorExt, OptionExt, ResourceExhaustionError, ResourceExhaustionKind, ResultExt, + }; + let mut out = Vec::new(); - let num_packs = usize::try_from(num_packs).map_err(|_| decode::Error::OutOfMemory)?; + let num_packs = usize::try_from(num_packs).map_err(|_| { + ResourceExhaustionError::new( + ResourceExhaustionKind::AllocationFailure, + "Pack count does not fit into memory", + ) + .raise_erased() + })?; + if num_packs > chunk.len() { + return Err(CorruptionError::new("Pack count exceeds the available pack-name data").raise_erased()); + } let vec_allocation = num_packs .checked_mul(std::mem::size_of::()) - .ok_or(decode::Error::OutOfMemory)?; + .ok_or_raise_erased(|| { + ResourceExhaustionError::new( + ResourceExhaustionKind::AllocationFailure, + "Pack names require more memory than can be represented", + ) + })?; if alloc_limit_bytes.is_some_and(|limit| vec_allocation > limit) { - return Err(decode::Error::OutOfMemory); + return Err(ResourceExhaustionError::new( + ResourceExhaustionKind::AllocationLimit, + "Pack names require more memory than allowed", + ) + .raise_erased()); } - out.try_reserve(num_packs)?; + out.try_reserve(num_packs) + .or_raise_erased(|| gix_error::message("Pack names require more memory than can be reserved"))?; for _ in 0..num_packs { - let null_byte_pos = chunk.find_byte(b'\0').ok_or(decode::Error::MissingNullByte)?; + let null_byte_pos = chunk.find_byte(b'\0').ok_or_raise_erased(|| { + CorruptionError::new("Each pack path name must be terminated with a null byte") + })?; let path = &chunk[..null_byte_pos]; if alloc_limit_bytes.is_some_and(|limit| path.len() > limit) { - return Err(decode::Error::OutOfMemory); + return Err(ResourceExhaustionError::new( + ResourceExhaustionKind::AllocationLimit, + "Pack path requires more memory than allowed", + ) + .raise_erased()); } let path = gix_path::try_from_byte_slice(path) - .map_err(|_| decode::Error::PathEncoding { - path: BString::from(path), + .or_raise_erased(|| { + CorruptionError::new(format!( + "Couldn't turn path '{}' into OS path due to encoding issues", + path.as_bstr() + )) })? .to_owned(); if let Some(previous) = out.last() && previous >= &path { - return Err(decode::Error::NotOrderedAlphabetically); + return Err(CorruptionError::new("The pack names were not ordered alphabetically").raise_erased()); } out.push(path); @@ -89,7 +97,7 @@ pub mod index_names { } if !chunk.is_empty() && !chunk.iter().all(|b| *b == 0) { - return Err(decode::Error::UnknownTrailerBytes); + return Err(CorruptionError::new("Non-padding bytes found after all paths were read").raise_erased()); } // NOTE: git writes garbage into this chunk, usually extra \0 bytes, which we simply ignore. If we were strict // about it we couldn't read this chunk data at all. diff --git a/gix-pack/src/multi_index/init.rs b/gix-pack/src/multi_index/init.rs index ecea678aedd..b22b938a450 100644 --- a/gix-pack/src/multi_index/init.rs +++ b/gix-pack/src/multi_index/init.rs @@ -1,39 +1,18 @@ -use std::path::{Path, PathBuf}; +use std::{ + borrow::Cow, + path::{Path, PathBuf}, +}; + +use gix_error::{CorruptionError, ErrorExt, ResultExt, ValidationError, message}; use crate::multi_index::{File, Version, chunk}; -mod error { - use crate::multi_index::chunk; - - /// The error returned by [File::at()][super::File::at()]. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error("Could not open multi-index file at '{path}'")] - Io { - source: std::io::Error, - path: std::path::PathBuf, - }, - #[error("{message}")] - Corrupt { message: &'static str }, - #[error("Unsupported multi-index version: {version})")] - UnsupportedVersion { version: u8 }, - #[error("Unsupported hash kind: {kind})")] - UnsupportedObjectHash { kind: u8 }, - #[error(transparent)] - ChunkFileQuery(#[from] gix_error::Message), - #[error(transparent)] - ChunkFileDecode(#[from] gix_error::ValidationError), - #[error("The multi-pack fan doesn't have the correct size of 256 * 4 bytes")] - MultiPackFanSize, - #[error(transparent)] - PackNames(#[from] chunk::index_names::decode::Error), - #[error("multi-index chunk {:?} has invalid size: {message}", String::from_utf8_lossy(.id))] - InvalidChunkSize { id: gix_chunk::Id, message: &'static str }, - } -} +/// The error returned by [File::at()]. +pub type Error = gix_error::Exn; -pub use error::Error; +fn corrupt(message: impl Into>) -> Error { + CorruptionError::new(message).raise_erased() +} /// Initialization impl File { @@ -46,10 +25,8 @@ impl File { } fn at_inner(path: &Path, alloc_limit_bytes: Option) -> Result { - let data = crate::mmap::read_only(path).map_err(|source| Error::Io { - source, - path: path.to_owned(), - })?; + let data = crate::mmap::read_only(path) + .or_raise_erased(|| message!("Could not open multi-index file at '{}'", path.display()))?; Self::from_data(data, path.to_owned(), alloc_limit_bytes) } } @@ -73,27 +50,27 @@ where + chunk::fanout::SIZE + TRAILER_LEN { - return Err(Error::Corrupt { - message: "multi-index file is truncated and too short", - }); + return Err(corrupt("multi-index file is truncated and too short")); } let (version, object_hash, num_chunks, num_indices) = { let (signature, data) = data.split_at(4); if signature != Self::SIGNATURE { - return Err(Error::Corrupt { - message: "Invalid signature", - }); + return Err(corrupt("Invalid signature")); } let (version, data) = data.split_at(1); let version = match version[0] { 1 => Version::V1, - version => return Err(Error::UnsupportedVersion { version }), + version => { + return Err( + ValidationError::new(format!("Unsupported multi-index version: {version}")).raise_erased(), + ); + } }; let (object_hash, data) = data.split_at(1); let object_hash = gix_hash::Kind::try_from(object_hash[0]) - .map_err(|unknown| Error::UnsupportedObjectHash { kind: unknown })?; + .map_err(|unknown| ValidationError::new(format!("Unsupported hash kind: {unknown}")).raise_erased())?; let (num_chunks, data) = data.split_at(1); let num_chunks = num_chunks[0]; @@ -105,40 +82,43 @@ where (version, object_hash, num_chunks, num_indices) }; - let chunks = gix_chunk::file::Index::from_bytes(&data, Self::HEADER_LEN, u32::from(num_chunks))?; + let chunks = gix_chunk::file::Index::from_bytes(&data, Self::HEADER_LEN, u32::from(num_chunks)) + .or_raise_erased(|| CorruptionError::new("Could not decode multi-index chunk table"))?; - let index_names = chunks.data_by_id(&data, chunk::index_names::ID)?; - let index_names = chunk::index_names::from_bytes(index_names, num_indices, alloc_limit_bytes)?; + let index_names = chunks + .data_by_id(&data, chunk::index_names::ID) + .or_raise_erased(|| CorruptionError::new("Could not read multi-index pack names"))?; + let index_names = chunk::index_names::from_bytes(index_names, num_indices, alloc_limit_bytes).or_erased()?; - let fan = chunks.data_by_id(&data, chunk::fanout::ID)?; - let fan = chunk::fanout::from_bytes(fan).ok_or(Error::MultiPackFanSize)?; + let fan = chunks + .data_by_id(&data, chunk::fanout::ID) + .or_raise_erased(|| CorruptionError::new("Could not read multi-index fan"))?; + let fan = chunk::fanout::from_bytes(fan) + .ok_or_else(|| corrupt("The multi-index fan doesn't have the correct size of 256 * 4 bytes"))?; let num_objects = fan[255]; validate_fan(&fan)?; - let lookup = chunks.validated_usize_offset_by_id(chunk::lookup::ID, |offset| { - chunk::lookup::is_valid(&offset, object_hash, num_objects) - .then_some(offset) - .ok_or(Error::InvalidChunkSize { - id: chunk::lookup::ID, - message: "The chunk with alphabetically ordered object ids doesn't have the correct size", - }) - })??; - let offsets = chunks.validated_usize_offset_by_id(chunk::offsets::ID, |offset| { - chunk::offsets::is_valid(&offset, num_objects) - .then_some(offset) - .ok_or(Error::InvalidChunkSize { - id: chunk::offsets::ID, - message: "The chunk with offsets into the pack doesn't have the correct size", - }) - })??; + let lookup = chunks + .validated_usize_offset_by_id(chunk::lookup::ID, |offset| { + chunk::lookup::is_valid(&offset, object_hash, num_objects) + .then_some(offset) + .ok_or_else(|| { + corrupt("The chunk with alphabetically ordered object ids doesn't have the correct size") + }) + }) + .or_raise_erased(|| CorruptionError::new("Could not find the multi-index object-id lookup chunk"))??; + let offsets = chunks + .validated_usize_offset_by_id(chunk::offsets::ID, |offset| { + chunk::offsets::is_valid(&offset, num_objects) + .then_some(offset) + .ok_or_else(|| corrupt("The chunk with offsets into the pack doesn't have the correct size")) + }) + .or_raise_erased(|| CorruptionError::new("Could not find the multi-index pack-offset chunk"))??; let large_offsets = chunks .validated_usize_offset_by_id(chunk::large_offsets::ID, |offset| { chunk::large_offsets::is_valid(&offset) .then_some(offset) - .ok_or(Error::InvalidChunkSize { - id: chunk::large_offsets::ID, - message: "The chunk with large offsets into the pack doesn't have the correct size", - }) + .ok_or_else(|| corrupt("The chunk with large offsets into the pack doesn't have the correct size")) }) .ok() .transpose()?; @@ -146,9 +126,9 @@ where let checksum_offset = chunks.highest_offset() as usize; let trailer = &data[checksum_offset..]; if trailer.len() != object_hash.len_in_bytes() { - return Err(Error::Corrupt { - message: "Trailing checksum didn't have the expected size or there were unknown bytes after the checksum.", - }); + return Err(corrupt( + "Trailing checksum didn't have the expected size or there were unknown bytes after the checksum.", + )); } Ok(File { @@ -171,9 +151,7 @@ where fn validate_fan(fan: &[u32; 256]) -> Result<(), Error> { if !crate::fan_is_monotonically_increasing(fan) { - return Err(Error::Corrupt { - message: "multi-index fan-out table must be monotonically increasing", - }); + return Err(corrupt("multi-index fan-out table must be monotonically increasing")); } Ok(()) } diff --git a/gix-pack/src/multi_index/verify.rs b/gix-pack/src/multi_index/verify.rs index 5052d1127cd..88ed108d6e5 100644 --- a/gix-pack/src/multi_index/verify.rs +++ b/gix-pack/src/multi_index/verify.rs @@ -1,44 +1,14 @@ use std::{cmp::Ordering, sync::atomic::AtomicBool, time::Instant}; +use gix_error::{CorruptionError, ErrorExt, RetryableError, ValidationError, message}; use gix_features::progress::{Count, DynNestedProgress, Progress}; use crate::{exact_vec, index, multi_index::File}; /// pub mod integrity { - use crate::multi_index::EntryIndex; - /// Returned by [`multi_index::File::verify_integrity()`][crate::multi_index::File::verify_integrity()]. - #[derive(thiserror::Error, Debug)] - #[expect(missing_docs)] - pub enum Error { - #[error("Object {id} should be at pack-offset {expected_pack_offset} but was found at {actual_pack_offset}")] - PackOffsetMismatch { - id: gix_hash::ObjectId, - expected_pack_offset: u64, - actual_pack_offset: u64, - }, - #[error(transparent)] - MultiIndexChecksum(#[from] crate::multi_index::verify::checksum::Error), - #[error(transparent)] - IndexIntegrity(#[from] crate::index::verify::integrity::Error), - #[error(transparent)] - BundleInit(#[from] crate::bundle::init::Error), - #[error("Counted {actual} objects, but expected {expected} as per multi-index")] - UnexpectedObjectCount { actual: usize, expected: usize }, - #[error("{id} wasn't found in the index referenced in the multi-pack index")] - OidNotFound { id: gix_hash::ObjectId }, - #[error("The object id at multi-index entry {index} wasn't in order")] - OutOfOrder { index: EntryIndex }, - #[error("The fan at index {index} is out of order as it's larger then the following value.")] - Fan { index: usize }, - #[error("The multi-index claims to have no objects")] - Empty, - #[error("The multi-index path '{path}' has no parent directory")] - InvalidPath { path: std::path::PathBuf }, - #[error("Interrupted")] - Interrupted, - } + pub type Error = gix_error::Exn; /// Returned by [`multi_index::File::verify_integrity()`][crate::multi_index::File::verify_integrity()]. pub struct Outcome { @@ -110,10 +80,6 @@ where false, index::verify::integrity::Options::default(), ) - .map_err(|err| match err { - index::traverse::Error::Processor(err) => err, - _ => unreachable!("BUG: no other error type is possible"), - }) .map(|o| o.actual_index_checksum) } @@ -125,7 +91,7 @@ where progress: &mut dyn DynNestedProgress, should_interrupt: &AtomicBool, options: index::verify::integrity::Options, - ) -> Result> + ) -> Result where C: crate::cache::DecodeEntry, F: Fn() -> C + Send + Clone, @@ -139,36 +105,36 @@ where should_interrupt: &AtomicBool, deep_check: bool, options: index::verify::integrity::Options, - ) -> Result> + ) -> Result where C: crate::cache::DecodeEntry, F: Fn() -> C + Send + Clone, { let parent = self.path.parent().ok_or_else(|| { - index::traverse::Error::Processor(integrity::Error::InvalidPath { - path: self.path.clone(), - }) + ValidationError::new(format!( + "The multi-index path '{}' has no parent directory", + self.path.display() + )) + .raise_erased() })?; - let actual_index_checksum = self - .verify_checksum( - &mut progress.add_child_with_id( - format!("{}: checksum", self.path.display()), - integrity::ProgressId::ChecksumBytes.into(), - ), - should_interrupt, - ) - .map_err(integrity::Error::from) - .map_err(index::traverse::Error::Processor)?; + let actual_index_checksum = self.verify_checksum( + &mut progress.add_child_with_id( + format!("{}: checksum", self.path.display()), + integrity::ProgressId::ChecksumBytes.into(), + ), + should_interrupt, + )?; if let Some(first_invalid) = crate::verify::fan(&self.fan) { - return Err(index::traverse::Error::Processor(integrity::Error::Fan { - index: first_invalid, - })); + return Err(CorruptionError::new(format!( + "The fan at index {first_invalid} is out of order as it's larger then the following value." + )) + .raise_erased()); } if self.num_objects == 0 { - return Err(index::traverse::Error::Processor(integrity::Error::Empty)); + return Err(CorruptionError::new("The multi-index claims to have no objects").raise_erased()); } let mut pack_traverse_statistics = Vec::new(); @@ -189,9 +155,10 @@ where let rhs = self.oid_at_index(entry_index + 1); if rhs.cmp(lhs) != Ordering::Greater { - return Err(index::traverse::Error::Processor(integrity::Error::OutOfOrder { - index: entry_index, - })); + return Err(CorruptionError::new(format!( + "The object id at multi-index entry {entry_index} wasn't in order" + )) + .raise_erased()); } let (pack_id, _) = self.pack_id_and_pack_offset_at_index(entry_index); pack_ids_and_offsets.push((pack_id, entry_index)); @@ -222,18 +189,12 @@ where let index; let index_path = parent.join(index_file_name); let index = if deep_check { - let mut opened_bundle = crate::Bundle::at(index_path, self.object_hash) - .map_err(integrity::Error::from) - .map_err(index::traverse::Error::Processor)?; + let mut opened_bundle = crate::Bundle::at(index_path, self.object_hash)?; opened_bundle.pack.alloc_limit_bytes = self.alloc_limit_bytes; bundle = Some(opened_bundle); bundle.as_ref().map(|b| &b.index).expect("just set") } else { - index = Some( - index::File::at(index_path, self.object_hash) - .map_err(|err| integrity::Error::BundleInit(crate::bundle::init::Error::Index(err))) - .map_err(index::traverse::Error::Processor)?, - ); + index = Some(index::File::at(index_path, self.object_hash)?); index.as_ref().expect("just set") }; @@ -255,23 +216,23 @@ where let oid = self.oid_at_index(entry_id); let (_, expected_pack_offset) = self.pack_id_and_pack_offset_at_index(entry_id); let entry_in_bundle_index = index.lookup(oid).ok_or_else(|| { - index::traverse::Error::Processor(integrity::Error::OidNotFound { id: oid.to_owned() }) + CorruptionError::new(format!( + "{oid} wasn't found in the index referenced in the multi-pack index" + )) + .raise_erased() })?; let actual_pack_offset = index.pack_offset_at_index(entry_in_bundle_index); if actual_pack_offset != expected_pack_offset { - return Err(index::traverse::Error::Processor( - integrity::Error::PackOffsetMismatch { - id: oid.to_owned(), - expected_pack_offset, - actual_pack_offset, - }, - )); + return Err(CorruptionError::new(format!( + "Object {oid} should be at pack-offset {expected_pack_offset} but was found at {actual_pack_offset}" + )) + .raise_erased()); } offsets_progress.inc(); } if should_interrupt.load(std::sync::atomic::Ordering::Relaxed) { - return Err(index::traverse::Error::Processor(integrity::Error::Interrupted)); + return Err(RetryableError::new(message("Interrupted")).raise_erased()); } offsets_progress.show_throughput(offset_start); } @@ -283,34 +244,7 @@ where let crate::bundle::verify::integrity::Outcome { actual_index_checksum: _, pack_traverse_outcome, - } = bundle - .verify_integrity(progress, should_interrupt, options.clone()) - .map_err(|err| { - use index::traverse::Error::*; - match err { - Processor(err) => Processor(integrity::Error::IndexIntegrity(err)), - IndexVerify(err) => IndexVerify(err), - Tree(err) => Tree(err), - TreeTraversal(err) => TreeTraversal(err), - PackVerify(err) => PackVerify(err), - PackDecode { id, offset, source } => PackDecode { id, offset, source }, - PackMismatch(err) => PackMismatch(err), - EntryType(err) => EntryType(err), - PackObjectVerify { offset, source } => PackObjectVerify { offset, source }, - Crc32Mismatch { - expected, - actual, - offset, - kind, - } => Crc32Mismatch { - expected, - actual, - offset, - kind, - }, - Interrupted => Interrupted, - } - })?; + } = bundle.verify_integrity(progress, should_interrupt, options.clone())?; pack_traverse_statistics.push(pack_traverse_outcome); } } diff --git a/gix-pack/src/multi_index/write.rs b/gix-pack/src/multi_index/write.rs index 1d5d9c71c4e..569c42af037 100644 --- a/gix-pack/src/multi_index/write.rs +++ b/gix-pack/src/multi_index/write.rs @@ -2,28 +2,8 @@ use std::time::SystemTime; use crate::multi_index; -mod error { - /// The error returned by [`crate::multi_index::write_from_index_paths()`]. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - Io(#[from] std::io::Error), - #[error("Interrupted")] - Interrupted, - #[error(transparent)] - OpenIndex(#[from] crate::index::init::Error), - #[error("Too many index entries to fit in memory")] - OutOfMemory, - } - - impl From for Error { - fn from(err: gix_hash::io::Error) -> Self { - Error::Io(std::io::Error::other(err.into_error())) - } - } -} -pub use error::Error; +/// The error returned by [`crate::multi_index::write_from_index_paths()`]. +pub type Error = gix_error::Exn; /// An entry suitable for sorting and writing pub(crate) struct Entry { @@ -83,6 +63,7 @@ pub(super) mod function { time::{Instant, SystemTime}, }; + use gix_error::{ErrorExt, ResourceExhaustionError, ResourceExhaustionKind, ResultExt, RetryableError, message}; use gix_features::progress::{Count, DynNestedProgress, Progress}; use crate::{MMap, multi_index}; @@ -126,9 +107,12 @@ pub(super) mod function { .unwrap_or(SystemTime::UNIX_EPOCH); let index = crate::index::File::at(index, object_hash)?; - entries - .try_reserve(index.num_objects() as usize) - .map_err(|_| Error::OutOfMemory)?; + entries.try_reserve(index.num_objects() as usize).or_raise_erased(|| { + ResourceExhaustionError::new( + ResourceExhaustionKind::AllocationFailure, + "Too many index entries to fit in memory", + ) + })?; entries.extend(index.iter().map(|e| Entry { id: e.oid, pack_index: index_id as u32, @@ -137,7 +121,7 @@ pub(super) mod function { })); progress.inc(); if should_interrupt.load(Ordering::Relaxed) { - return Err(Error::Interrupted); + return Err(RetryableError::new(message("Interrupted")).raise_erased()); } } progress.show_throughput(start); @@ -154,7 +138,7 @@ pub(super) mod function { progress.inc_by(entries.len()); progress.show_throughput(start); if should_interrupt.load(Ordering::Relaxed) { - return Err(Error::Interrupted); + return Err(RetryableError::new(message("Interrupted")).raise_erased()); } entries }; @@ -229,7 +213,7 @@ pub(super) mod function { .map_err(gix_hash::io::from_std_io)?; progress.inc(); if should_interrupt.load(Ordering::Relaxed) { - return Err(Error::Interrupted); + return Err(RetryableError::new(message("Interrupted")).raise_erased()); } } } diff --git a/gix-pack/src/verify.rs b/gix-pack/src/verify.rs index 4bdca84de36..ec69ce1a766 100644 --- a/gix-pack/src/verify.rs +++ b/gix-pack/src/verify.rs @@ -1,20 +1,12 @@ use std::{path::Path, sync::atomic::AtomicBool}; +use gix_error::{ErrorExt, ResultExt, RetryableError, message}; use gix_features::progress::Progress; /// pub mod checksum { /// Returned by various methods to verify the checksum of a memory mapped file that might also exist on disk. - #[derive(thiserror::Error, Debug)] - #[expect(missing_docs)] - pub enum Error { - #[error("Interrupted by user")] - Interrupted, - #[error("Failed to hash data")] - Hasher(#[source] gix_error::Error), - #[error(transparent)] - Verify(#[from] gix_hash::verify::Error), - } + pub type Error = gix_error::Exn; } /// Returns the `index` at which the following `index + 1` value is not an increment over the value at `index`. @@ -25,8 +17,7 @@ pub fn fan(data: &[u32]) -> Option { } /// Calculate the hash of the given kind by trying to read the file from disk at `data_path` or falling back on the mapped content in `data`. -/// `Ok(expected)` or [`checksum::Error::Verify`] is returned if the hash matches or mismatches. -/// If the [`checksum::Error::Interrupted`] is returned, the operation was interrupted. +/// `Ok(expected)` is returned if the hash matches, otherwise the error is classified as corruption. pub fn checksum_on_disk_or_mmap( data_path: &Path, data: &[u8], @@ -45,7 +36,9 @@ pub fn checksum_on_disk_or_mmap( ) { Ok(id) => id, Err(err) => match err.downcast_any_ref::().map(std::io::Error::kind) { - Some(std::io::ErrorKind::Interrupted) => return Err(checksum::Error::Interrupted), + Some(std::io::ErrorKind::Interrupted) => { + return Err(RetryableError::new(err.into_error()).raise_erased()); + } Some(_) => { let start = std::time::Instant::now(); let mut hasher = gix_hash::hasher(object_hash); @@ -54,13 +47,14 @@ pub fn checksum_on_disk_or_mmap( progress.show_throughput(start); hasher .try_finalize() - .map_err(gix_hash::io::from_hasher) - .map_err(|err| checksum::Error::Hasher(err.into_error()))? + .or_raise_erased(|| message("Failed to hash data"))? } - None => return Err(checksum::Error::Hasher(err.into_error())), + None => return Err(err), }, }; - actual.verify(&expected)?; + actual + .verify(&expected) + .or_raise_erased(|| gix_error::CorruptionError::new("Failed to verify pack file checksum"))?; Ok(actual) } diff --git a/gix-pack/tests/pack/bundle.rs b/gix-pack/tests/pack/bundle.rs index 56a0b3246b6..445affea129 100644 --- a/gix-pack/tests/pack/bundle.rs +++ b/gix-pack/tests/pack/bundle.rs @@ -25,7 +25,7 @@ mod locate { use crate::{PACKS_AND_INDICES, fixture_path}; #[test] - fn all() -> Result<(), Box> { + fn all() -> crate::Result { for (index_path, data_path) in PACKS_AND_INDICES { // both paths are equivalent pack::Bundle::at(fixture_path(index_path), gix_hash::Kind::Sha1)?; @@ -49,7 +49,7 @@ mod locate { } #[test] - fn blob() -> Result<(), Box> { + fn blob() -> crate::Result { let mut out = Vec::new(); let obj = locate("bd46bb3f5bb4ca5431770c4fde0735fb89d382f3", &mut out); @@ -65,7 +65,7 @@ mod locate { } #[test] - fn tree() -> Result<(), Box> { + fn tree() -> crate::Result { let mut out = Vec::new(); let obj = locate("e90926b07092bccb7bf7da445fae6ffdfacf3eae", &mut out); @@ -75,7 +75,7 @@ mod locate { } #[test] - fn commit() -> Result<(), Box> { + fn commit() -> crate::Result { let mut out = Vec::new(); let obj = locate("779c5451ba9fe210ffd1f55db202e55f51acecac", &mut out); @@ -100,7 +100,7 @@ mod write_to_directory { use crate::{SMALL_PACK, SMALL_PACK_INDEX, error_chain_contains_message, fixture_path}; - fn expected_outcome() -> Result> { + fn expected_outcome() -> crate::Result { Ok(pack::bundle::write::Outcome { index: pack::index::write::Outcome { index_version: pack::index::Version::V2, @@ -117,7 +117,7 @@ mod write_to_directory { } #[test] - fn without_providing_one() -> Result<(), Box> { + fn without_providing_one() -> crate::Result { let res = write_pack(None::<&Path>, SMALL_PACK)?; assert_eq!(res, expected_outcome()?); assert_eq!( @@ -129,7 +129,7 @@ mod write_to_directory { } #[test] - fn given_a_directory() -> Result<(), Box> { + fn given_a_directory() -> crate::Result { let dir = TempDir::new()?; let mut res = write_pack(Some(&dir), SMALL_PACK)?; let (index_path, data_path, keep_path) = (res.index_path.take(), res.data_path.take(), res.keep_path.take()); @@ -162,7 +162,7 @@ mod write_to_directory { /// already has. `index-pack --fix-thin` makes such packs self-contained by appending /// those bases, leaving the original deltas as forward references. #[test] - fn in_pack_ref_deltas_with_forward_references() -> Result<(), Box> { + fn in_pack_ref_deltas_with_forward_references() -> crate::Result { for object_hash in [gix_hash::Kind::Sha1, gix_hash::Kind::Sha256] { for objects in [ &[b"A".as_slice(), b"B".as_slice()][..], @@ -219,7 +219,7 @@ mod write_to_directory { } #[test] - fn version_3_with_thin_pack_lookup() -> Result<(), Box> { + fn version_3_with_thin_pack_lookup() -> crate::Result { let object_hash = gix_hash::Kind::Sha1; let pack_data = ref_delta_pack( object_hash, @@ -247,7 +247,7 @@ mod write_to_directory { } #[test] - fn unresolved_ref_delta_base_is_reported() -> Result<(), Box> { + fn unresolved_ref_delta_base_is_reported() -> crate::Result { let object_hash = gix_hash::Kind::Sha1; let base_id = object_hash.null(); let delta = [0, 0]; @@ -274,6 +274,11 @@ mod write_to_directory { }, ) .expect_err("a ref-delta without an in-pack or external base cannot be indexed"); + assert!( + err.downcast_any_ref::().is_some(), + "an unresolved base is classified as not found" + ); + let err = err.into_error(); let expected = format!("The ref-delta base object {base_id} could not be found"); assert!( error_chain_contains_message(&err, &expected), @@ -283,7 +288,7 @@ mod write_to_directory { } #[test] - fn respects_alloc_limit_bytes() -> Result<(), Box> { + fn respects_alloc_limit_bytes() -> crate::Result { let pack_file = fs::File::open(fixture_path(SMALL_PACK))?; static SHOULD_INTERRUPT: AtomicBool = AtomicBool::new(false); @@ -305,6 +310,7 @@ mod write_to_directory { }, ) .expect_err("a zero allocation limit rejects non-empty delta-tree storage"); + let err = err.into_error(); assert!( error_chain_contains_message(&err, "The pack delta tree is too large to fit in memory"), @@ -317,13 +323,10 @@ mod write_to_directory { entry.path().file_name().unwrap().to_str().unwrap().to_owned() } - fn write_pack( - directory: Option>, - pack_file: &str, - ) -> Result> { + fn write_pack(directory: Option>, pack_file: &str) -> crate::Result { let pack_file = fs::File::open(fixture_path(pack_file))?; static SHOULD_INTERRUPT: AtomicBool = AtomicBool::new(false); - pack::Bundle::write_to_directory_eagerly( + Ok(pack::Bundle::write_to_directory_eagerly( Box::new(pack_file), None, directory, @@ -338,8 +341,7 @@ mod write_to_directory { alloc_limit_bytes: None, compression: gix_zlib::Compression::BEST_SPEED, }, - ) - .map_err(Into::into) + )?) } /// Build a complete pack whose one-byte blobs form a forward `REF_DELTA` chain. @@ -358,7 +360,7 @@ mod write_to_directory { object_hash: gix_hash::Kind, objects: &[&'static [u8]], version: pack::data::Version, - ) -> Result, Box> { + ) -> crate::Result> { let mut pack_data = pack::data::header::encode(version, objects.len() as u32).to_vec(); for pair in objects.windows(2).rev() { let (base, resolved) = (pair[0], pair[1]); @@ -377,7 +379,7 @@ mod write_to_directory { Ok(pack_data) } - fn deflate(input: &[u8]) -> Result, Box> { + fn deflate(input: &[u8]) -> crate::Result> { let mut out = gix_zlib::stream::deflate::Write::new(Vec::new(), gix_zlib::Compression::BEST_SPEED); out.write_all(input)?; out.flush()?; diff --git a/gix-pack/tests/pack/data/file.rs b/gix-pack/tests/pack/data/file.rs index 6929497dc58..9b22b6941c1 100644 --- a/gix-pack/tests/pack/data/file.rs +++ b/gix-pack/tests/pack/data/file.rs @@ -6,6 +6,23 @@ fn pack_at(at: &str) -> pack::data::File { pack::data::File::at(fixture_path(at).as_path(), gix_hash::Kind::Sha1).expect("valid pack file") } +#[test] +fn unresolved_delta_base_is_not_found() { + use gix_error::{ErrorExt, message}; + + let base_id = gix_hash::ObjectId::empty_blob(gix_hash::Kind::Sha1); + let err = pack::data::decode::DeltaBaseUnresolved(base_id).and_raise(message("Could not decode object")); + assert!(err.is_not_found(), "an unresolved delta base is a missing object"); + let err = err.into_error(); + assert!(err.is_not_found(), "conversion preserves the classification"); + assert_eq!( + err.downcast_any_ref::() + .expect("retain the custom error and missing object ID") + .0, + base_id + ); +} + mod method { use std::sync::atomic::AtomicBool; @@ -20,7 +37,7 @@ mod method { } #[test] - fn verify_checksum() -> Result<(), Box> { + fn verify_checksum() -> crate::Result { let p = pack_at(SMALL_PACK); assert_eq!( p.verify_checksum(&mut progress::Discard, &AtomicBool::new(false))?, @@ -30,7 +47,7 @@ mod method { } #[test] - fn verify_checksum_from_memory() -> Result<(), Box> { + fn verify_checksum_from_memory() -> crate::Result { let p = pack_from_memory_at(SMALL_PACK); assert_eq!( p.verify_checksum(&mut progress::Discard, &AtomicBool::new(false))?, @@ -62,23 +79,25 @@ mod method { buf.clear(); let pack = pack_at(SMALL_PACK).with_alloc_limit_bytes(Some(0)); let entry = pack.entry(entry_offset).expect("valid object type"); - assert!( - matches!( - pack.decode_entry( - entry, - &mut buf, - &mut inflate, - &|_, _| None, - &mut gix_odb::pack::cache::Never - ), - Err(gix_odb::pack::data::decode::Error::OutOfMemory) - ), - "pack-controlled allocations larger than the configured limit are rejected" + let err = pack + .decode_entry( + entry, + &mut buf, + &mut inflate, + &|_, _| None, + &mut gix_odb::pack::cache::Never, + ) + .expect_err("pack-controlled allocations larger than the configured limit are rejected"); + assert_eq!(err, "Entry too large to fit in memory"); + assert_eq!( + err.downcast_any_ref::() + .map(gix_error::ResourceExhaustionError::kind), + Some(gix_error::ResourceExhaustionKind::AllocationLimit) ); } #[test] - fn iter() -> Result<(), Box> { + fn iter() -> crate::Result { let pack = pack_at(SMALL_PACK); let it = pack.streaming_iter()?; assert_eq!(it.count(), pack.num_objects() as usize); @@ -263,6 +282,23 @@ mod decompress_entry { ); } + #[test] + fn caller_provided_buffer_must_be_large_enough() { + let p = pack_at(SMALL_PACK); + let entry = p.entry(1968).expect("valid object type"); + let mut buf = vec![0; entry.decompressed_size as usize - 1]; + + let err = p + .decompress_entry(&entry, &mut Default::default(), &mut buf) + .expect_err("an undersized caller-provided buffer is invalid input"); + assert!(err.downcast_any_ref::().is_some()); + assert!( + !err.into_error() + .classify() + .any(|classification| matches!(classification.class(), gix_error::Class::ResourceExhaustion(_))) + ); + } + fn decompress_entry_at_offset(offset: u64) -> Vec { let p = pack_at(SMALL_PACK); let entry = p.entry(offset).expect("valid object type"); diff --git a/gix-pack/tests/pack/data/fuzzed.rs b/gix-pack/tests/pack/data/fuzzed.rs index 62a4a80bbf2..91026fb2b5b 100644 --- a/gix-pack/tests/pack/data/fuzzed.rs +++ b/gix-pack/tests/pack/data/fuzzed.rs @@ -175,12 +175,14 @@ fn declared_object_size_over_alloc_limit_bytes_is_reported_as_out_of_memory() { ) })); - assert!( - matches!( - result.expect("configured allocation limits must not cause panics"), - Err(gix_pack::data::decode::Error::OutOfMemory) - ), - "pack-controlled allocations larger than the configured limit must be rejected" + let err = result + .expect("configured allocation limits must not cause panics") + .expect_err("pack-controlled allocations larger than the configured limit must be rejected"); + assert_eq!(err, "Entry too large to fit in memory"); + assert_eq!( + err.downcast_any_ref::() + .map(gix_error::ResourceExhaustionError::kind), + Some(gix_error::ResourceExhaustionKind::AllocationLimit) ); } diff --git a/gix-pack/tests/pack/data/input.rs b/gix-pack/tests/pack/data/input.rs index 7eb9f034f38..ede69fdd5b4 100644 --- a/gix-pack/tests/pack/data/input.rs +++ b/gix-pack/tests/pack/data/input.rs @@ -195,16 +195,15 @@ mod lookup_ref_delta_objects { LookupRefDeltaObjectsIter::new(into_results_iter(input), &db, gix_zlib::Compression::BEST_SPEED) .collect::, _>>(); + let err = result.expect_err("zero and out-of-bounds base distances must be rejected"); assert!( - matches!( - result, - Err(input::Error::InvalidBaseDistance { - distance: actual, - .. - }) if actual == distance - ), + err.downcast_any_ref::().is_some(), "zero and out-of-bounds base distances are rejected as corrupt pack data" ); + assert!( + err.to_string().contains(&format!("base distance {distance}")), + "the invalid distance is retained in the error: {err}" + ); } } @@ -226,18 +225,15 @@ mod lookup_ref_delta_objects { #[test] fn inner_errors_are_passed_on() { + use gix_error::ErrorExt; + + let object_id = gix_hash::Kind::Sha1.null(); let input = vec![ Ok(entry(base(), D_A)), - Err(input::Error::NotFound { - object_id: gix_hash::Kind::Sha1.null(), - }), - Ok(entry(base(), D_B)), - ]; - let expected = vec![ - Ok(entry(base(), D_A)), - Err(input::Error::NotFound { - object_id: gix_hash::Kind::Sha1.null(), - }), + Err( + gix_error::NotFoundError::new(format!("The object {object_id} could not be decoded or wasn't found")) + .raise_erased(), + ), Ok(entry(base(), D_B)), ]; let actual = LookupRefDeltaObjectsIter::new( @@ -246,9 +242,21 @@ mod lookup_ref_delta_objects { gix_zlib::Compression::BEST_SPEED, ) .collect::>(); - for (actual, expected) in actual.into_iter().zip(expected) { - assert_eq!(format!("{actual:?}"), format!("{expected:?}")); - } + assert_eq!( + actual[0].as_ref().expect("first entry passes through"), + &entry(base(), D_A) + ); + assert_eq!( + actual[1] + .as_ref() + .expect_err("the inner error passes through") + .to_string(), + format!("The object {object_id} could not be decoded or wasn't found") + ); + assert_eq!( + actual[2].as_ref().expect("last entry passes through"), + &entry(base(), D_B) + ); } #[test] 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 5f8a6ffe566..6fae6e91868 100644 --- a/gix-pack/tests/pack/data/output/count_and_entries.rs +++ b/gix-pack/tests/pack/data/output/count_and_entries.rs @@ -42,13 +42,11 @@ fn invalid_ofs_delta_base_distance_is_an_error() -> crate::Result { gix_pack::data::Version::V2, ); + let err = result + .and_then(Result::err) + .expect("an invalid OFS_DELTA base distance must fail"); assert!( - matches!( - result, - Some(Err(entry::Error::EntryType( - gix_pack::data::entry::decode::Error::Corrupt { .. } - ))) - ), + err.downcast_any_ref::().is_some(), "an invalid packed delta is reported as corrupt" ); } @@ -440,7 +438,7 @@ fn tree_additions_from_each_merge_parent_are_kept() -> crate::Result { .take_object_memory() .expect("in-memory object storage is still enabled"); let db = gix_pack::testing::Memory::new(objects.drain()); - let mut input = std::iter::once(Ok::<_, Box>(merge_commit_id)); + let mut input = std::iter::once(Ok::<_, gix_error::Exn>(merge_commit_id)); let (counts, stats) = output::count::objects_unthreaded( &db, diff --git a/gix-pack/tests/pack/index.rs b/gix-pack/tests/pack/index.rs index 91bd112b7df..a8242b56327 100644 --- a/gix-pack/tests/pack/index.rs +++ b/gix-pack/tests/pack/index.rs @@ -23,7 +23,7 @@ mod version { use crate::{INDEX_V1, fixture_path}; #[test] - fn lookup() -> Result<(), Box> { + fn lookup() -> crate::Result { let object_hash = gix_hash::Kind::Sha1; let file = index::File::at(fixture_path(INDEX_V1), object_hash)?; for (id, desired_index, assertion) in &[ @@ -67,7 +67,7 @@ mod version { use crate::{INDEX_V2, fixture_path}; #[test] - fn lookup() -> Result<(), Box> { + fn lookup() -> crate::Result { let object_hash = gix_hash::Kind::Sha1; let file = index::File::at(fixture_path(INDEX_V2), object_hash)?; for (id, expected, assertion_message, hex_len) in [ @@ -129,13 +129,13 @@ mod version { } #[test] - fn write_to_stream() -> Result<(), Box> { + fn write_to_stream() -> crate::Result { fn assert_index_write( mode: &input::Mode, compressed: &input::EntryDataMode, index_path: &&str, data_path: &&str, - ) -> Result<(), Box> { + ) -> crate::Result { let mut pack_iter = pack::data::input::BytesToEntriesIter::new_from_header( io::BufReader::new(fs::File::open(fixture_path(data_path))?), *mode, @@ -229,7 +229,7 @@ mod version { } #[test] - fn write_to_stream_respects_alloc_limit_bytes() -> Result<(), Box> { + fn write_to_stream_respects_alloc_limit_bytes() -> crate::Result { let data_path = SMALL_PACK; let mut pack_iter = pack::data::input::BytesToEntriesIter::new_from_header( io::BufReader::new(fs::File::open(fixture_path(data_path))?), @@ -257,6 +257,7 @@ mod version { pack_version, ) .expect_err("a zero allocation limit rejects non-empty delta-tree storage"); + let err = err.into_error(); assert!( crate::error_chain_contains_message(&err, "The pack delta tree is too large to fit in memory"), @@ -292,7 +293,7 @@ fn traverse_with_index_and_forward_ref_deltas() { &data, |_, _, _, _| { count.fetch_add(1, Ordering::SeqCst); - Ok::<_, std::io::Error>(()) + Ok::<_, gix_error::Exn>(()) }, &mut progress::Discard, &AtomicBool::new(false), @@ -303,14 +304,14 @@ fn traverse_with_index_and_forward_ref_deltas() { } #[test] -fn traverse_with_index_respects_alloc_limit_bytes() -> Result<(), Box> { +fn traverse_with_index_respects_alloc_limit_bytes() -> crate::Result { let index = index::File::at(fixture_path(SMALL_PACK_INDEX), gix_hash::Kind::Sha1)?; let data = pack::data::File::at(fixture_path(SMALL_PACK), gix_hash::Kind::Sha1)?; let prevent_allocation = Some(0); let err = match index.traverse_with_index( &data, - |_, _, _, _| Ok::<_, std::io::Error>(()), + |_, _, _, _| Ok::<_, gix_error::Exn>(()), &mut progress::Discard, &AtomicBool::new(false), index::traverse::with_index::Options { @@ -322,6 +323,7 @@ fn traverse_with_index_respects_alloc_limit_bytes() -> Result<(), Box panic!("a zero allocation limit rejects the first non-empty decoded object"), Err(err) => err, }; + let err = err.into_error(); assert!( crate::error_chain_contains_message(&err, "Entry too large to fit in memory"), @@ -354,7 +356,7 @@ fn from_memory_backing_supports_verification_and_traversal() { &data, |_, _, _, _| { count.fetch_add(1, Ordering::SeqCst); - Ok::<_, std::io::Error>(()) + Ok::<_, gix_error::Exn>(()) }, &mut progress::Discard, &AtomicBool::new(false), @@ -382,7 +384,7 @@ static MODES: &[index::verify::Mode] = &[ ]; #[test] -fn pack_lookup() -> Result<(), Box> { +fn pack_lookup() -> crate::Result { for (index_path, pack_path, stats) in &[ ( INDEX_V2, @@ -550,7 +552,7 @@ fn pack_lookup() -> Result<(), Box> { } #[test] -fn verify_integrity_respects_pack_alloc_limit_bytes() -> Result<(), Box> { +fn verify_integrity_respects_pack_alloc_limit_bytes() -> crate::Result { let prevent_allocation = Some(0); let idx = index::File::at(fixture_path(SMALL_PACK_INDEX), gix_hash::Kind::Sha1)?; let pack = pack::data::File::at(fixture_path(SMALL_PACK), gix_hash::Kind::Sha1)? @@ -575,6 +577,7 @@ fn verify_integrity_respects_pack_alloc_limit_bytes() -> Result<(), Box panic!("{traversal:?}: a zero allocation limit rejects the first non-empty decoded object"), Err(err) => err, }; + let err = err.into_error(); assert!( crate::error_chain_contains_message(&err, "Entry too large to fit in memory"), @@ -587,7 +590,7 @@ fn verify_integrity_respects_pack_alloc_limit_bytes() -> Result<(), Box Result<(), Box> { +fn iter() -> crate::Result { for (path, kind, num_objects, index_checksum, pack_checksum) in [ ( INDEX_V1, diff --git a/gix-pack/tests/pack/iter.rs b/gix-pack/tests/pack/iter.rs index cea09477767..7e02f20c8d7 100644 --- a/gix-pack/tests/pack/iter.rs +++ b/gix-pack/tests/pack/iter.rs @@ -24,7 +24,7 @@ mod new_from_header { use crate::{SMALL_PACK, V2_PACKS_AND_INDICES, fixture_path}; #[test] - fn header_encode() -> Result<(), Box> { + fn header_encode() -> crate::Result { for (_, data_file) in V2_PACKS_AND_INDICES { let data = fs::read(fixture_path(data_file))?; for entry in pack::data::input::BytesToEntriesIter::new_from_header( @@ -55,7 +55,7 @@ mod new_from_header { } #[test] - fn generic_iteration() -> Result<(), Box> { + fn generic_iteration() -> crate::Result { for compression_mode in &[ EntryDataMode::Ignore, EntryDataMode::Keep, @@ -103,7 +103,7 @@ mod new_from_header { } #[test] - fn version_3_is_accepted() -> Result<(), Box> { + fn version_3_is_accepted() -> crate::Result { let mut data = fs::read(fixture_path(SMALL_PACK))?; data[4..8].copy_from_slice(&3u32.to_be_bytes()); @@ -127,7 +127,7 @@ mod new_from_header { } #[test] - fn restore_missing_trailer() -> Result<(), Box> { + fn restore_missing_trailer() -> crate::Result { let pack = fs::read(fixture_path(SMALL_PACK))?; let mut iter = pack::data::input::BytesToEntriesIter::new_from_header( std::io::BufReader::new(&pack[..pack.len() - 20]), @@ -146,7 +146,7 @@ mod new_from_header { } #[test] - fn restore_partial_pack() -> Result<(), Box> { + fn restore_partial_pack() -> crate::Result { let pack = fs::read(fixture_path(SMALL_PACK))?; let mut iter = pack::data::input::BytesToEntriesIter::new_from_header( std::io::BufReader::new(&pack[..pack.len() / 2]), diff --git a/gix-pack/tests/pack/multi_index/fuzzed.rs b/gix-pack/tests/pack/multi_index/fuzzed.rs index f52e5d9333c..cd5f99c7e68 100644 --- a/gix-pack/tests/pack/multi_index/fuzzed.rs +++ b/gix-pack/tests/pack/multi_index/fuzzed.rs @@ -37,9 +37,9 @@ fn malformed_fanout_is_reported_without_panicking() { } /// Reproducer for the allocation-limit fuzz case: long user-controlled pack names in the `PNAM` -/// chunk must be rejected as `OutOfMemory` when they exceed the configured allocation limit. +/// chunk must be rejected as resource exhaustion when they exceed the configured allocation limit. #[test] -fn long_pack_names_over_alloc_limit_bytes_are_rejected_as_out_of_memory() { +fn long_pack_names_over_alloc_limit_bytes_are_resource_exhaustion() { let long_name = format!("{}.idx", "a".repeat(65)); let index = gix_pack::multi_index::File::from_data( valid_multi_index_with_index_name(long_name.as_bytes()), @@ -50,37 +50,44 @@ fn long_pack_names_over_alloc_limit_bytes_are_rejected_as_out_of_memory() { assert_eq!(index.index_names(), [PathBuf::from(&long_name)]); + let err = gix_pack::multi_index::File::from_data( + valid_multi_index_with_index_name(long_name.as_bytes()), + PathBuf::from("fuzzed-long-name.midx"), + Some(0), + ) + .err() + .expect("multi-index pack names larger than a zero allocation limit must be rejected"); + assert_eq!( + err.downcast_any_ref::() + .map(gix_error::ResourceExhaustionError::kind), + Some(gix_error::ResourceExhaustionKind::AllocationLimit), + "an application limit doesn't make otherwise valid input corrupt" + ); assert!( - matches!( - gix_pack::multi_index::File::from_data( - valid_multi_index_with_index_name(long_name.as_bytes()), - PathBuf::from("fuzzed-long-name.midx"), - Some(0) - ), - Err(gix_pack::multi_index::init::Error::PackNames( - gix_pack::multi_index::chunk::index_names::decode::Error::OutOfMemory - )) - ), - "multi-index pack names larger than the configured limit must be rejected, including with a zero limit" + err.to_string().contains("more memory than allowed"), + "the error explains the allocation-limit failure: {err}" ); + assert!(!err.into_error().is_corrupted()); } /// Reproducer for the fuzz target OOM case: the harness uses an allocation cap so attacker-controlled /// index counts must fail deterministically before reserving absurd `Vec` capacities. #[test] fn absurd_pack_count_is_rejected_with_fuzz_alloc_limit() { + let err = gix_pack::multi_index::File::from_data( + multi_index_with_absurd_pack_count(), + PathBuf::from("fuzzed-absurd-pack-count.midx"), + Some(0), + ) + .err() + .expect("an absurd pack count must be rejected under a zero allocation limit"); + assert!( + err.downcast_any_ref::().is_some(), + "a pack count which cannot fit in the available name data is corrupt input" + ); assert!( - matches!( - gix_pack::multi_index::File::from_data( - multi_index_with_absurd_pack_count(), - PathBuf::from("fuzzed-absurd-pack-count.midx"), - Some(0) - ), - Err(gix_pack::multi_index::init::Error::PackNames( - gix_pack::multi_index::chunk::index_names::decode::Error::OutOfMemory - )) - ), - "multi-index files advertising absurd pack counts must be rejected under the allocation cap, including with a zero limit" + err.to_string().contains("Pack count exceeds"), + "the error explains the structural failure: {err}" ); } diff --git a/gix-transport/tests/async-transport.rs b/gix-transport/tests/async-transport.rs index fdb06208b5c..2d08a223823 100644 --- a/gix-transport/tests/async-transport.rs +++ b/gix-transport/tests/async-transport.rs @@ -4,7 +4,7 @@ pub mod bisync { use std::path::PathBuf; -pub type Error = Box; +pub type Error = Box; pub type Result = std::result::Result; pub fn fixture_bytes(path: &str) -> Vec { diff --git a/gix-transport/tests/blocking-transport-http.rs b/gix-transport/tests/blocking-transport-http.rs index 58b67a7011d..77bfda1cc5a 100644 --- a/gix-transport/tests/blocking-transport-http.rs +++ b/gix-transport/tests/blocking-transport-http.rs @@ -6,7 +6,7 @@ pub mod bisync { use std::path::PathBuf; -pub type Error = Box; +pub type Error = Box; pub type Result = std::result::Result; pub fn fixture_bytes(path: &str) -> Vec { diff --git a/gix-transport/tests/blocking-transport.rs b/gix-transport/tests/blocking-transport.rs index a7b9846aaf1..920c4f2791a 100644 --- a/gix-transport/tests/blocking-transport.rs +++ b/gix-transport/tests/blocking-transport.rs @@ -4,7 +4,7 @@ pub mod bisync { use std::path::PathBuf; -pub type Error = Box; +pub type Error = Box; pub type Result = std::result::Result; pub fn fixture_bytes(path: &str) -> Vec { From f52b5291510f814ec19f3fbae19896db8c1fd6a1 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 20 Aug 2026 17:23:56 +0200 Subject: [PATCH 25/51] change(gix-odb)!: migrate errors to gix-error rubberstamp, but looked at it more to understand why it's more code. Answer: downstream relies on better error classification. However, I think this can also be reduced a bit. --- Cargo.lock | 1 - gix-odb/Cargo.toml | 1 - gix-odb/src/alternate/mod.rs | 63 ++++++-- gix-odb/src/alternate/parse.rs | 23 ++- gix-odb/src/lib.rs | 18 +++ gix-odb/src/store_impls/dynamic/find.rs | 94 ++++++++++-- gix-odb/src/store_impls/dynamic/load_index.rs | 83 +++++++++-- gix-odb/src/store_impls/dynamic/prefix.rs | 82 +++++++++-- gix-odb/src/store_impls/dynamic/verify.rs | 62 ++++++-- gix-odb/src/store_impls/dynamic/write.rs | 51 ++++++- gix-odb/src/store_impls/loose/find.rs | 93 +++++++++--- gix-odb/src/store_impls/loose/verify.rs | 38 +++-- gix-odb/src/store_impls/loose/write.rs | 41 +++++- gix-odb/tests/odb/error.rs | 139 ++++++++++++++++++ gix-odb/tests/odb/main.rs | 1 + gix-odb/tests/odb/store/loose.rs | 50 ++++++- gix/tests/gix/revision/spec/from_bytes/mod.rs | 24 +-- 17 files changed, 722 insertions(+), 142 deletions(-) create mode 100644 gix-odb/tests/odb/error.rs diff --git a/Cargo.lock b/Cargo.lock index 8dfbe1bca2a..a51f16e3eda 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2214,7 +2214,6 @@ dependencies = [ "pretty_assertions", "serde", "tempfile", - "thiserror 2.0.18", ] [[package]] diff --git a/gix-odb/Cargo.toml b/gix-odb/Cargo.toml index 179395cf791..929dd189ae9 100644 --- a/gix-odb/Cargo.toml +++ b/gix-odb/Cargo.toml @@ -39,7 +39,6 @@ gix-fs = { version = "^0.22.1", path = "../gix-fs" } serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"] } tempfile = "3.26.0" -thiserror = "2.0.18" parking_lot = { version = "0.12.4" } arc-swap = "1.9.0" memmap2 = "0.9.11" diff --git a/gix-odb/src/alternate/mod.rs b/gix-odb/src/alternate/mod.rs index d10086b06c0..7d22a2df25f 100644 --- a/gix-odb/src/alternate/mod.rs +++ b/gix-odb/src/alternate/mod.rs @@ -25,19 +25,64 @@ pub mod parse; pub use parse::function::parse; /// Returned by [`resolve()`] -#[derive(thiserror::Error, Debug)] -#[expect(missing_docs)] +#[derive(Debug)] +#[allow(missing_docs)] pub enum Error { - #[error(transparent)] - Io(#[from] io::Error), - #[error(transparent)] - Realpath(#[from] gix_path::realpath::Error), - #[error(transparent)] - Parse(#[from] parse::Error), - #[error("Alternates form a cycle: {} -> {}", .0.iter().map(|p| format!("'{}'", p.display())).collect::>().join(" -> "), .0.first().expect("more than one directories").display())] + Io(io::Error), + Realpath(gix_error::Error), + Parse(parse::Error), Cycle(Vec), } +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Io(err) => std::fmt::Display::fmt(err, f), + Error::Realpath(err) => std::fmt::Display::fmt(err, f), + Error::Parse(err) => std::fmt::Display::fmt(err, f), + Error::Cycle(paths) => write!( + f, + "Alternates form a cycle: {} -> {}", + paths + .iter() + .map(|p| format!("'{}'", p.display())) + .collect::>() + .join(" -> "), + paths.first().expect("more than one directories").display() + ), + } + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::Io(err) => Some(err), + Error::Realpath(err) => Some(err), + Error::Parse(err) => Some(err), + Error::Cycle(_) => Some(&crate::CORRUPTION), + } + } +} + +impl From for Error { + fn from(err: io::Error) -> Self { + Error::Io(err) + } +} + +impl From for Error { + fn from(err: gix_path::realpath::Error) -> Self { + Error::Realpath(err.into_error()) + } +} + +impl From for Error { + fn from(err: parse::Error) -> Self { + Error::Parse(err) + } +} + /// Given an `objects_directory`, try to resolve alternate object directories possibly located in the /// `./info/alternates` file into canonical paths and resolve relative paths with the help of the `current_dir`. /// If no alternate object database was resolved, the resulting `Vec` is empty (it is not an error diff --git a/gix-odb/src/alternate/parse.rs b/gix-odb/src/alternate/parse.rs index b710a0b22ab..3c83cc119f2 100644 --- a/gix-odb/src/alternate/parse.rs +++ b/gix-odb/src/alternate/parse.rs @@ -1,11 +1,28 @@ /// Returned as part of [`crate::alternate::Error::Parse`] -#[derive(thiserror::Error, Debug)] -#[expect(missing_docs)] +#[derive(Debug)] +#[allow(missing_docs)] pub enum Error { - #[error("Could not obtain an object path for the alternate directory '{}'", String::from_utf8_lossy(.0))] PathConversion(Vec), } +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::PathConversion(bytes) => write!( + f, + "Could not obtain an object path for the alternate directory '{}'", + String::from_utf8_lossy(bytes) + ), + } + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + Some(&crate::INVALID_INPUT) + } +} + pub(super) mod function { use super::Error; use std::{borrow::Cow, path::PathBuf}; diff --git a/gix-odb/src/lib.rs b/gix-odb/src/lib.rs index 88b27d9296f..a58649d93c7 100644 --- a/gix-odb/src/lib.rs +++ b/gix-odb/src/lib.rs @@ -8,6 +8,9 @@ //! * access to packed objects //! * multiple loose objects and pack locations as gathered from `alternates` files. //! +//! Custom error sources retain missing-object, corruption, validation, resource-exhaustion and retry +//! classifications when raised or converted to [`gix_error::Error`]. +//! //! ## Write And Read Loose Objects //! //! ``` @@ -60,6 +63,21 @@ use gix_features::threading::OwnShared; pub use gix_pack as pack; use gix_zlib::stream::deflate; +static NOT_FOUND: gix_error::NotFoundError = gix_error::NotFoundError { + message: std::borrow::Cow::Borrowed("Object not found"), +}; +static CORRUPTION: gix_error::CorruptionError = gix_error::CorruptionError { + message: std::borrow::Cow::Borrowed("Object database is malformed or inconsistent"), +}; +static INVALID_INPUT: gix_error::ValidationError = gix_error::ValidationError { + message: std::borrow::Cow::Borrowed("Invalid object database input"), + input: None, +}; +// Global sources must be synchronized even when the `parallel` feature is disabled. +static RETRYABLE: std::sync::LazyLock = std::sync::LazyLock::new(|| { + gix_error::RetryableError::new(gix_error::message("Object database operation can be retried")) +}); + mod store_impls; pub use store_impls::{dynamic as store, loose}; diff --git a/gix-odb/src/store_impls/dynamic/find.rs b/gix-odb/src/store_impls/dynamic/find.rs index 97aab6e0636..2a51a52a226 100644 --- a/gix-odb/src/store_impls/dynamic/find.rs +++ b/gix-odb/src/store_impls/dynamic/find.rs @@ -11,36 +11,27 @@ pub(crate) mod error { use crate::{loose, pack}; /// Returned by [`Handle::try_find()`][gix_pack::Find::try_find()] - #[derive(thiserror::Error, Debug)] - #[expect(missing_docs)] + #[derive(Debug)] + #[allow(missing_docs)] pub enum Error { - #[error("An error occurred while obtaining an object from the loose object store")] - Loose(#[from] loose::find::Error), - #[error("An error occurred while obtaining an object from the packed object store")] - Pack(#[from] GixError), - #[error(transparent)] - LoadIndex(#[from] crate::store::load_index::Error), - #[error(transparent)] - LoadPack(#[from] std::io::Error), - #[error(transparent)] - EntryType(#[from] gix_pack::data::entry::decode::Error), - #[error("Reached recursion limit of {} while resolving ref delta bases for {}", .max_depth, .id)] + Loose(loose::find::Error), + Pack(GixError), + LoadIndex(crate::store::load_index::Error), + LoadPack(std::io::Error), + EntryType(gix_pack::data::entry::decode::Error), DeltaBaseRecursionLimit { /// the maximum recursion depth we encountered. max_depth: usize, /// The original object to lookup id: gix_hash::ObjectId, }, - #[error("The base object {} could not be found but is required to decode {}", .base_id, .id)] DeltaBaseMissing { /// the id of the base object which failed to lookup base_id: gix_hash::ObjectId, /// The original object to lookup id: gix_hash::ObjectId, }, - #[error("An error occurred when looking up a ref delta base object {} to decode {}", .base_id, .id)] DeltaBaseLookup { - #[source] err: Box, /// the id of the base object which failed to lookup base_id: gix_hash::ObjectId, @@ -49,12 +40,83 @@ pub(crate) mod error { }, } + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Loose(_) => { + f.write_str("An error occurred while obtaining an object from the loose object store") + } + Error::Pack(_) => { + f.write_str("An error occurred while obtaining an object from the packed object store") + } + Error::LoadIndex(err) => std::fmt::Display::fmt(err, f), + Error::LoadPack(err) => std::fmt::Display::fmt(err, f), + Error::EntryType(err) => std::fmt::Display::fmt(err, f), + Error::DeltaBaseRecursionLimit { max_depth, id } => { + write!( + f, + "Reached recursion limit of {max_depth} while resolving ref delta bases for {id}" + ) + } + Error::DeltaBaseMissing { base_id, id } => { + write!( + f, + "The base object {base_id} could not be found but is required to decode {id}" + ) + } + Error::DeltaBaseLookup { base_id, id, .. } => write!( + f, + "An error occurred when looking up a ref delta base object {base_id} to decode {id}" + ), + } + } + } + + impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::Loose(err) => Some(err), + Error::Pack(err) => Some(err), + Error::LoadIndex(err) => Some(err), + Error::LoadPack(err) => Some(err), + Error::EntryType(err) => Some(err), + Error::DeltaBaseLookup { err, .. } => Some(&**err), + Error::DeltaBaseMissing { .. } => Some(&crate::NOT_FOUND), + Error::DeltaBaseRecursionLimit { .. } => None, + } + } + } + + impl From for Error { + fn from(err: loose::find::Error) -> Self { + Error::Loose(err) + } + } + impl From for Error { fn from(err: pack::data::decode::Error) -> Self { Error::Pack(err.into_error()) } } + impl From for Error { + fn from(err: crate::store::load_index::Error) -> Self { + Error::LoadIndex(err) + } + } + + impl From for Error { + fn from(err: std::io::Error) -> Self { + Error::LoadPack(err) + } + } + + impl From for Error { + fn from(err: gix_pack::data::entry::decode::Error) -> Self { + Error::EntryType(err) + } + } + #[derive(Copy, Clone)] pub(crate) struct DeltaBaseRecursion<'a> { pub depth: usize, diff --git a/gix-odb/src/store_impls/dynamic/load_index.rs b/gix-odb/src/store_impls/dynamic/load_index.rs index bf26c1e85fb..812b5dacb57 100644 --- a/gix-odb/src/store_impls/dynamic/load_index.rs +++ b/gix-odb/src/store_impls/dynamic/load_index.rs @@ -27,34 +27,85 @@ mod error { use gix_pack::multi_index::PackIndex; /// Returned by [`crate::at_opts()`] - #[derive(thiserror::Error, Debug)] - #[expect(missing_docs)] + #[derive(Debug)] + #[allow(missing_docs)] pub enum Error { - #[error("The objects directory at '{0}' is not an accessible directory")] Inaccessible(PathBuf), - #[error(transparent)] - Io(#[from] std::io::Error), - #[error(transparent)] - Alternate(#[from] crate::alternate::Error), - #[error("The slotmap turned out to be too small with {} entries, would need {} more", .current, .needed)] - InsufficientSlots { current: usize, needed: usize }, + Io(std::io::Error), + Alternate(crate::alternate::Error), + InsufficientSlots { + current: usize, + needed: usize, + }, /// The problem here is that some logic assumes that more recent generations are higher than previous ones. If we would overflow, /// we would break that invariant which can lead to the wrong object from being returned. It would probably be super rare, but… /// let's not risk it. - #[error( - "Would have overflown amount of max possible generations of {}", - super::Generation::MAX - )] GenerationOverflow, - #[error( - "Cannot numerically handle more than {limit} packs in a single multi-pack index, got {actual} in file {index_path:?}" - )] TooManyPacksInMultiIndex { actual: PackIndex, limit: PackIndex, index_path: PathBuf, }, } + + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Inaccessible(path) => { + write!( + f, + "The objects directory at '{}' is not an accessible directory", + path.display() + ) + } + Error::Io(err) => std::fmt::Display::fmt(err, f), + Error::Alternate(err) => std::fmt::Display::fmt(err, f), + Error::InsufficientSlots { current, needed } => write!( + f, + "The slotmap turned out to be too small with {current} entries, would need {needed} more" + ), + Error::GenerationOverflow => write!( + f, + "Would have overflown amount of max possible generations of {}", + super::Generation::MAX + ), + #[allow(clippy::unnecessary_debug_formatting)] + Error::TooManyPacksInMultiIndex { + actual, + limit, + index_path, + } => write!( + f, + "Cannot numerically handle more than {limit} packs in a single multi-pack index, got {actual} in file {index_path:?}" + ), + } + } + } + + impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::Io(err) => Some(err), + Error::Alternate(err) => Some(err), + Error::Inaccessible(_) + | Error::InsufficientSlots { .. } + | Error::GenerationOverflow + | Error::TooManyPacksInMultiIndex { .. } => None, + } + } + } + + impl From for Error { + fn from(err: std::io::Error) -> Self { + Error::Io(err) + } + } + + impl From for Error { + fn from(err: crate::alternate::Error) -> Self { + Error::Alternate(err) + } + } } pub use error::Error; diff --git a/gix-odb/src/store_impls/dynamic/prefix.rs b/gix-odb/src/store_impls/dynamic/prefix.rs index 4c2172530fa..0b43d133b55 100644 --- a/gix-odb/src/store_impls/dynamic/prefix.rs +++ b/gix-odb/src/store_impls/dynamic/prefix.rs @@ -9,13 +9,41 @@ pub mod lookup { use crate::loose; /// Returned by [`Handle::lookup_prefix()`][crate::store::Handle::lookup_prefix()] - #[derive(thiserror::Error, Debug)] - #[expect(missing_docs)] + #[derive(Debug)] + #[allow(missing_docs)] pub enum Error { - #[error("An error occurred looking up a prefix which requires iteration")] - LooseWalkDir(#[from] loose::iter::Error), - #[error(transparent)] - LoadIndex(#[from] crate::store::load_index::Error), + LooseWalkDir(loose::iter::Error), + LoadIndex(crate::store::load_index::Error), + } + + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::LooseWalkDir(_) => f.write_str("An error occurred looking up a prefix which requires iteration"), + Error::LoadIndex(err) => std::fmt::Display::fmt(err, f), + } + } + } + + impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::LooseWalkDir(err) => Some(err), + Error::LoadIndex(err) => Some(err), + } + } + } + + impl From for Error { + fn from(err: loose::iter::Error) -> Self { + Error::LooseWalkDir(err) + } + } + + impl From for Error { + fn from(err: crate::store::load_index::Error) -> Self { + Error::LoadIndex(err) + } } /// A way to indicate if a lookup, despite successful, was ambiguous or yielded exactly @@ -63,13 +91,43 @@ pub mod disambiguate { } /// Returned by [`Handle::disambiguate_prefix()`][crate::store::Handle::disambiguate_prefix()] - #[derive(thiserror::Error, Debug)] - #[expect(missing_docs)] + #[derive(Debug)] + #[allow(missing_docs)] pub enum Error { - #[error("An error occurred while trying to determine if a full hash contained in the object database")] - Contains(#[from] crate::store::find::Error), - #[error(transparent)] - Lookup(#[from] super::lookup::Error), + Contains(crate::store::find::Error), + Lookup(super::lookup::Error), + } + + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Contains(_) => f.write_str( + "An error occurred while trying to determine if a full hash contained in the object database", + ), + Error::Lookup(err) => std::fmt::Display::fmt(err, f), + } + } + } + + impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::Contains(err) => Some(err), + Error::Lookup(err) => Some(err), + } + } + } + + impl From for Error { + fn from(err: crate::store::find::Error) -> Self { + Error::Contains(err) + } + } + + impl From for Error { + fn from(err: super::lookup::Error) -> Self { + Error::Lookup(err) + } } } diff --git a/gix-odb/src/store_impls/dynamic/verify.rs b/gix-odb/src/store_impls/dynamic/verify.rs index 896d29e367f..b29f40b8181 100644 --- a/gix-odb/src/store_impls/dynamic/verify.rs +++ b/gix-odb/src/store_impls/dynamic/verify.rs @@ -22,27 +22,63 @@ pub mod integrity { pub type Options = pack::index::verify::integrity::Options; /// Returned by [`Store::verify_integrity()`][crate::Store::verify_integrity()]. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] + #[derive(Debug)] + #[allow(missing_docs)] pub enum Error { - #[error(transparent)] MultiIndexIntegrity(gix_error::Error), - #[error(transparent)] IndexIntegrity(gix_error::Error), - #[error(transparent)] IndexOpen(gix_error::Error), - #[error(transparent)] - LooseObjectStoreIntegrity(#[from] crate::loose::verify::integrity::Error), - #[error(transparent)] + LooseObjectStoreIntegrity(crate::loose::verify::integrity::Error), MultiIndexOpen(gix_error::Error), - #[error(transparent)] PackOpen(gix_error::Error), - #[error(transparent)] - InitializeODB(#[from] crate::store::load_index::Error), - #[error("The disk on state changed while performing the operation, and we observed the change.")] + InitializeODB(crate::store::load_index::Error), NeedsRetryDueToChangeOnDisk, } + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::MultiIndexIntegrity(err) => std::fmt::Display::fmt(err, f), + Error::IndexIntegrity(err) => std::fmt::Display::fmt(err, f), + Error::IndexOpen(err) => std::fmt::Display::fmt(err, f), + Error::LooseObjectStoreIntegrity(err) => std::fmt::Display::fmt(err, f), + Error::MultiIndexOpen(err) => std::fmt::Display::fmt(err, f), + Error::PackOpen(err) => std::fmt::Display::fmt(err, f), + Error::InitializeODB(err) => std::fmt::Display::fmt(err, f), + Error::NeedsRetryDueToChangeOnDisk => { + f.write_str("The disk on state changed while performing the operation, and we observed the change.") + } + } + } + } + + impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::MultiIndexIntegrity(err) => Some(err), + Error::IndexIntegrity(err) => Some(err), + Error::IndexOpen(err) => Some(err), + Error::LooseObjectStoreIntegrity(err) => Some(err), + Error::MultiIndexOpen(err) => Some(err), + Error::PackOpen(err) => Some(err), + Error::InitializeODB(err) => Some(err), + Error::NeedsRetryDueToChangeOnDisk => Some(&*crate::RETRYABLE), + } + } + } + + impl From for Error { + fn from(err: crate::loose::verify::integrity::Error) -> Self { + Error::LooseObjectStoreIntegrity(err) + } + } + + impl From for Error { + fn from(err: crate::store::load_index::Error) -> Self { + Error::InitializeODB(err) + } + } + #[derive(Debug, PartialEq, Eq, Hash, Ord, PartialOrd, Clone)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] /// Integrity information about loose object databases @@ -56,7 +92,7 @@ pub mod integrity { #[derive(Debug, PartialEq, Eq, Hash, Ord, PartialOrd, Clone)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] /// Traversal statistics of packs governed by single indices or multi-pack indices. - #[expect(missing_docs)] + #[allow(missing_docs)] pub enum SingleOrMultiStatistics { Single(pack::index::traverse::Statistics), Multi(Vec<(PathBuf, pack::index::traverse::Statistics)>), diff --git a/gix-odb/src/store_impls/dynamic/write.rs b/gix-odb/src/store_impls/dynamic/write.rs index 313ac4e5e9e..e1ff8a33c2e 100644 --- a/gix-odb/src/store_impls/dynamic/write.rs +++ b/gix-odb/src/store_impls/dynamic/write.rs @@ -10,15 +10,50 @@ mod error { use crate::{loose, store}; /// The error returned by the [dynamic Store's][crate::Store] [`Write`](gix_object::Write) implementation. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] + #[derive(Debug)] + #[allow(missing_docs)] pub enum Error { - #[error(transparent)] - LoadIndex(#[from] store::load_index::Error), - #[error(transparent)] - LooseWrite(#[from] loose::write::Error), - #[error(transparent)] - Io(#[from] std::io::Error), + LoadIndex(store::load_index::Error), + LooseWrite(loose::write::Error), + Io(std::io::Error), + } + + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::LoadIndex(err) => std::fmt::Display::fmt(err, f), + Error::LooseWrite(err) => std::fmt::Display::fmt(err, f), + Error::Io(err) => std::fmt::Display::fmt(err, f), + } + } + } + + impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::LoadIndex(err) => Some(err), + Error::LooseWrite(err) => Some(err), + Error::Io(err) => Some(err), + } + } + } + + impl From for Error { + fn from(err: store::load_index::Error) -> Self { + Error::LoadIndex(err) + } + } + + impl From for Error { + fn from(err: loose::write::Error) -> Self { + Error::LooseWrite(err) + } + } + + impl From for Error { + fn from(err: std::io::Error) -> Self { + Error::Io(err) + } } } pub use error::Error; diff --git a/gix-odb/src/store_impls/loose/find.rs b/gix-odb/src/store_impls/loose/find.rs index 5a014f98740..e2bac290088 100644 --- a/gix-odb/src/store_impls/loose/find.rs +++ b/gix-odb/src/store_impls/loose/find.rs @@ -1,22 +1,27 @@ use std::{cmp::Ordering, collections::HashSet, io, path::PathBuf}; -use gix_error::{CorruptionError, Error as GixError}; +use gix_error::{CorruptionError, Error as GixError, ResourceExhaustionError, ResourceExhaustionKind}; use crate::store_impls::loose::{HEADER_MAX_SIZE, Store, hash_path}; /// Returned by [`Store::try_find()`] -#[derive(thiserror::Error, Debug)] -#[expect(missing_docs)] +#[derive(Debug)] +#[allow(missing_docs)] pub enum Error { - #[error("decompression of loose object at '{path}' failed")] - DecompressFile { source: GixError, path: PathBuf }, - #[error("file at '{path}' showed invalid size of inflated data, expected {expected}, got {actual}")] - SizeMismatch { actual: u64, expected: u64, path: PathBuf }, - #[error(transparent)] + DecompressFile { + source: GixError, + path: PathBuf, + }, + SizeMismatch { + actual: u64, + expected: u64, + path: PathBuf, + }, Decode(GixError), - #[error("Cannot store {size} in memory as it's not representable")] - OutOfMemory { size: u64 }, - #[error("Could not {action} data at '{path}'")] + OutOfMemory { + size: u64, + source: GixError, + }, Io { source: std::io::Error, action: &'static str, @@ -24,6 +29,36 @@ pub enum Error { }, } +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::DecompressFile { path, .. } => { + write!(f, "decompression of loose object at '{}' failed", path.display()) + } + Error::SizeMismatch { actual, expected, path } => write!( + f, + "file at '{}' showed invalid size of inflated data, expected {expected}, got {actual}", + path.display() + ), + Error::Decode(err) => std::fmt::Display::fmt(err, f), + Error::OutOfMemory { size, .. } => write!(f, "Cannot store {size} bytes in memory"), + Error::Io { action, path, .. } => write!(f, "Could not {action} data at '{}'", path.display()), + } + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::DecompressFile { source, .. } => Some(source), + Error::Decode(err) => Some(err), + Error::Io { source, .. } => Some(source), + Error::OutOfMemory { source, .. } => Some(source), + Error::SizeMismatch { .. } => Some(&crate::CORRUPTION), + } + } +} + impl From for Error { fn from(err: gix_object::decode::LooseHeaderDecodeError) -> Self { Error::Decode(err.into_error()) @@ -170,14 +205,22 @@ impl Store { let (kind, size, header_size) = gix_object::decode::loose_header(&header[..consumed_out])?; self.ensure_in_alloc_limit(size)?; - let size_usize = usize::try_from(size).map_err(|_| Error::OutOfMemory { size })?; + let size_usize = usize::try_from(size).map_err(|_| Error::OutOfMemory { + size, + source: GixError::from_error(ResourceExhaustionError::new( + ResourceExhaustionKind::AllocationFailure, + "The object size cannot be represented in memory", + )), + })?; let decompressed_body_prefix_len = consumed_out.checked_sub(header_size).ok_or(Error::SizeMismatch { actual: consumed_out as u64, expected: header_size as u64, path: path.clone(), })?; - if decompressed_body_prefix_len > size_usize { + if decompressed_body_prefix_len > size_usize + || (status == gix_zlib::Status::StreamEnd && decompressed_body_prefix_len != size_usize) + { return Err(Error::SizeMismatch { expected: size, actual: decompressed_body_prefix_len as u64, @@ -186,17 +229,13 @@ impl Store { } // If the first inflate already reached the end of the stream, the fixed-size `header` buffer - // contains the complete decompressed object. In that case we can avoid allocating the full - // output buffer and a second streaming inflate pass. + // contains the complete decompressed object, so we can skip a second streaming inflate pass. out.clear(); + out.try_reserve(size_usize).map_err(|err| Error::OutOfMemory { + size, + source: GixError::from_error(err), + })?; if status == gix_zlib::Status::StreamEnd { - if consumed_out as u64 != size + header_size as u64 { - return Err(Error::SizeMismatch { - expected: size + header_size as u64, - actual: consumed_out as u64, - path, - }); - } out.extend_from_slice(&header[header_size..consumed_out]); } else { out.resize(size_usize, 0); @@ -231,7 +270,13 @@ impl Store { fn ensure_in_alloc_limit(&self, size: u64) -> Result<(), Error> { if self.alloc_limit_bytes.is_some_and(|limit| size > limit as u64) { - return Err(Error::OutOfMemory { size }); + return Err(Error::OutOfMemory { + size, + source: GixError::from_error(ResourceExhaustionError::new( + ResourceExhaustionKind::AllocationLimit, + "The object exceeds the configured allocation limit", + )), + }); } Ok(()) } @@ -266,7 +311,7 @@ mod mmap { pub fn read_only(path: &Path) -> std::io::Result { let file = std::fs::File::open(path)?; // SAFETY: we have to take the risk of somebody changing the file underneath. Git never writes into the same file. - #[expect(unsafe_code)] + #[allow(unsafe_code)] unsafe { memmap2::MmapOptions::new().map_copy_read_only(&file) } diff --git a/gix-odb/src/store_impls/loose/verify.rs b/gix-odb/src/store_impls/loose/verify.rs index 924fb863526..8c08c01cecb 100644 --- a/gix-odb/src/store_impls/loose/verify.rs +++ b/gix-odb/src/store_impls/loose/verify.rs @@ -10,34 +10,54 @@ use crate::loose::Store; /// pub mod integrity { /// The error returned by [`verify_integrity()`][super::Store::verify_integrity()]. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] + #[derive(Debug)] + #[allow(missing_docs)] pub enum Error { - #[error("{kind} object {id} could not be decoded")] ObjectDecode { source: gix_object::decode::Error, kind: gix_object::Kind, id: gix_hash::ObjectId, }, - #[error("{kind} object {expected} could not be hashed")] ObjectHasher { - #[source] source: gix_hash::hasher::Error, kind: gix_object::Kind, expected: gix_hash::ObjectId, }, - #[error("{kind} object wasn't re-encoded without change")] ObjectEncodeMismatch { - #[source] source: gix_hash::verify::Error, kind: gix_object::Kind, }, - #[error("Objects were deleted during iteration - try again")] Retry, - #[error("Interrupted")] Interrupted, } + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::ObjectDecode { kind, id, .. } => write!(f, "{kind} object {id} could not be decoded"), + Error::ObjectHasher { kind, expected, .. } => { + write!(f, "{kind} object {expected} could not be hashed") + } + Error::ObjectEncodeMismatch { kind, .. } => { + write!(f, "{kind} object wasn't re-encoded without change") + } + Error::Retry => f.write_str("Objects were deleted during iteration - try again"), + Error::Interrupted => f.write_str("Interrupted"), + } + } + } + + impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::ObjectDecode { source, .. } => Some(source), + Error::ObjectHasher { source, .. } => Some(source), + Error::ObjectEncodeMismatch { source, .. } => Some(source), + Error::Retry | Error::Interrupted => Some(&*crate::RETRYABLE), + } + } + } + /// The outcome returned by [`verify_integrity()`][super::Store::verify_integrity()]. #[derive(Debug, PartialEq, Eq, Hash, Ord, PartialOrd, Clone)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] diff --git a/gix-odb/src/store_impls/loose/write.rs b/gix-odb/src/store_impls/loose/write.rs index 39922054152..e3d0fbcc859 100644 --- a/gix-odb/src/store_impls/loose/write.rs +++ b/gix-odb/src/store_impls/loose/write.rs @@ -9,24 +9,53 @@ use super::Store; use crate::store_impls::loose; /// Returned by the [`gix_object::Write`] trait implementation of [`Store`] -#[derive(thiserror::Error, Debug)] -#[expect(missing_docs)] +#[derive(Debug)] +#[allow(missing_docs)] pub enum Error { - #[error("Could not {message} '{path}'")] Io { source: std::io::Error, message: &'static str, path: PathBuf, }, - #[error("An IO error occurred while writing an object")] - IoRaw(#[from] io::Error), - #[error("Could not turn temporary file into persisted file at '{target}'")] + IoRaw(io::Error), Persist { source: tempfile::PersistError, target: PathBuf, }, } +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Io { message, path, .. } => write!(f, "Could not {message} '{}'", path.display()), + Error::IoRaw(_) => f.write_str("An IO error occurred while writing an object"), + Error::Persist { target, .. } => { + write!( + f, + "Could not turn temporary file into persisted file at '{}'", + target.display() + ) + } + } + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::Io { source, .. } => Some(source), + Error::IoRaw(err) => Some(err), + Error::Persist { source, .. } => Some(source), + } + } +} + +impl From for Error { + fn from(err: io::Error) -> Self { + Error::IoRaw(err) + } +} + impl gix_object::Write for Store { fn write(&self, object: &dyn WriteTo) -> Result { let mut to = self.dest().or_erased()?; diff --git a/gix-odb/tests/odb/error.rs b/gix-odb/tests/odb/error.rs new file mode 100644 index 00000000000..c3578a9c2f0 --- /dev/null +++ b/gix-odb/tests/odb/error.rs @@ -0,0 +1,139 @@ +use gix_error::{CorruptionError, Error, ErrorExt, message}; +use gix_odb::{alternate, loose, store}; + +#[test] +fn io_classification_survives_custom_wrappers() { + use std::io::ErrorKind; + + for kind in [ + ErrorKind::NotFound, + ErrorKind::Interrupted, + ErrorKind::OutOfMemory, + ErrorKind::PermissionDenied, + ] { + let io = || std::io::Error::from(kind); + let load_index = || store::load_index::Error::Io(io()); + for err in [ + load_index().raise_erased(), + store::load_index::Error::Alternate(alternate::Error::Io(io())).raise_erased(), + alternate::Error::from(io().raise_erased()).raise_erased(), + store::find::Error::LoadIndex(load_index()).raise_erased(), + store::find::Error::LoadPack(io()).raise_erased(), + store::write::Error::Io(io()).raise_erased(), + store::write::Error::LoadIndex(load_index()).raise_erased(), + store::write::Error::LooseWrite(loose::write::Error::IoRaw(io())).raise_erased(), + store::prefix::disambiguate::Error::Lookup(store::prefix::lookup::Error::LoadIndex(load_index())) + .raise_erased(), + store::verify::integrity::Error::InitializeODB(load_index()).raise_erased(), + ] { + assert_eq!( + err.can_retry(), + gix_error::can_retry(&io()), + "custom wrappers retain the retry policy for {kind:?}" + ); + assert_eq!( + err.is_not_found(), + kind == ErrorKind::NotFound, + "custom wrappers retain missing-resource failures" + ); + let err = err.into_error(); + assert_eq!( + err.can_retry(), + gix_error::can_retry(&io()), + "conversion retains the retry policy for {kind:?}" + ); + assert_eq!( + err.is_not_found(), + kind == ErrorKind::NotFound, + "conversion retains missing-resource failures" + ); + assert_eq!( + err.is_resource_exhausted(), + kind == ErrorKind::OutOfMemory, + "conversion retains allocation failures" + ); + assert_eq!( + err.downcast_any_ref::() + .expect("retain the immediate I/O cause") + .kind(), + kind + ); + } + } +} + +#[test] +fn custom_wrappers_retain_classified_errors_and_branches() { + let corrupt = || Error::from_error(CorruptionError::new("malformed object")); + for err in [ + loose::find::Error::Decode(corrupt()).raise_erased(), + store::find::Error::EntryType(CorruptionError::new("invalid pack entry type")).raise_erased(), + store::verify::integrity::Error::MultiIndexIntegrity(corrupt()).raise_erased(), + store::verify::integrity::Error::IndexIntegrity(corrupt()).raise_erased(), + store::verify::integrity::Error::IndexOpen(corrupt()).raise_erased(), + store::verify::integrity::Error::MultiIndexOpen(corrupt()).raise_erased(), + store::verify::integrity::Error::PackOpen( + message("pack failed") + .raise() + .chain(message("unrelated cause")) + .chain(CorruptionError::new("malformed pack")) + .into_error(), + ) + .raise_erased(), + ] { + assert!( + err.is_corrupted(), + "custom wrappers retain classified causes, including branches" + ); + assert!(err.into_error().is_corrupted(), "conversion retains classified causes"); + } +} + +#[test] +fn custom_leaf_errors_expose_their_classification() { + let blob_id = gix_hash::ObjectId::empty_blob(gix_hash::Kind::Sha1); + let err = store::find::Error::DeltaBaseMissing { + base_id: blob_id, + id: blob_id, + } + .raise(); + assert!(err.is_not_found(), "a missing delta base is a missing object"); + assert!( + err.into_error().is_not_found(), + "conversion retains the missing-object classification" + ); + + for err in [ + loose::find::Error::SizeMismatch { + actual: 0, + expected: 1, + path: "object".into(), + } + .raise_erased(), + alternate::Error::Cycle(vec!["objects".into()]).raise_erased(), + ] { + assert!( + err.is_corrupted(), + "inconsistent object sizes and alternate cycles are corruption" + ); + assert!(err.into_error().is_corrupted()); + } + let err = alternate::Error::Parse(alternate::parse::Error::PathConversion(vec![0xff])).raise(); + assert!(err.is_validation(), "unrepresentable alternate paths are invalid input"); + assert!(err.into_error().is_validation()); + + for err in [ + loose::verify::integrity::Error::Retry.raise_erased(), + loose::verify::integrity::Error::Interrupted.raise_erased(), + store::verify::integrity::Error::NeedsRetryDueToChangeOnDisk.raise_erased(), + store::verify::integrity::Error::LooseObjectStoreIntegrity(loose::verify::integrity::Error::Retry) + .raise_erased(), + ] { + assert!( + err.can_retry(), + "verification can retry after interruption or concurrent changes" + ); + assert!(err.is_retryable(), "custom errors explicitly advertise retryability"); + assert!(err.into_error().can_retry(), "conversion retains retryability"); + } +} diff --git a/gix-odb/tests/odb/main.rs b/gix-odb/tests/odb/main.rs index 2cb674ff1d1..1642d7738b7 100644 --- a/gix-odb/tests/odb/main.rs +++ b/gix-odb/tests/odb/main.rs @@ -31,6 +31,7 @@ fn db_small_packs() -> gix_odb::Handle { } pub mod alternate; +mod error; pub mod find; pub mod header; pub mod memory; diff --git a/gix-odb/tests/odb/store/loose.rs b/gix-odb/tests/odb/store/loose.rs index 36065f3744d..0802c4b8875 100644 --- a/gix-odb/tests/odb/store/loose.rs +++ b/gix-odb/tests/odb/store/loose.rs @@ -325,6 +325,36 @@ mod find { Ok(()) } + #[test] + fn completed_object_size_is_validated_before_allocation() -> crate::Result { + use std::io::Write; + + let tmp = gix_testtools::tempfile::tempdir()?; + let object_hash = gix_testtools::object_hash(); + let db = ldb_at_opts(tmp.path(), object_hash); + let blob_id = object_hash.empty_blob(); + let path = db.object_path(&blob_id); + std::fs::create_dir(path.parent().expect("loose objects have a parent directory"))?; + + for size in [1048576, usize::MAX] { + let mut writer = gix_zlib::stream::deflate::Write::new(Vec::new(), gix_zlib::Compression::DEFAULT); + write!(writer, "blob {size}\0")?; + writer.flush()?; + std::fs::write(&path, writer.into_inner())?; + + let mut buf = Vec::new(); + let err = db + .try_find(&blob_id, &mut buf) + .expect_err("the completed stream contains no body despite its advertised size"); + assert!( + matches!(&err, loose::find::Error::SizeMismatch { .. }), + "a completed object with an oversized header is corrupt: {err}" + ); + assert_eq!(buf.capacity(), 0, "invalid sizes must be rejected before allocation"); + } + Ok(()) + } + #[test] fn tag() -> Result<(), Box> { let mut buf = Vec::new(); @@ -434,10 +464,22 @@ cjHJZXWmV4CcRfmLsXzU8s2cR9A0DBvOxhPD1TlKC2JhBFXigjuL9U4Rbq9tdegB (56915, Kind::Blob), "header-only reads remain available" ); - assert!(matches!( - db.try_find(&id, &mut buf), - Err(loose::find::Error::OutOfMemory { size: 56915 }) - )); + let err = db + .try_find(&id, &mut buf) + .expect_err("the object exceeds the configured allocation limit"); + assert!(matches!(&err, loose::find::Error::OutOfMemory { size: 56915, .. })); + let err = gix_error::Error::from_error(err); + assert_eq!( + err.classify() + .map(|classification| classification.class()) + .collect::>(), + [gix_error::Class::ResourceExhaustion( + gix_error::ResourceExhaustionKind::AllocationLimit + )], + "configured limits are classified only as resource exhaustion" + ); + assert!(!err.is_corrupted()); + assert!(!err.can_retry()); Ok(()) } diff --git a/gix/tests/gix/revision/spec/from_bytes/mod.rs b/gix/tests/gix/revision/spec/from_bytes/mod.rs index 99136709d8e..b7d420bea4d 100644 --- a/gix/tests/gix/revision/spec/from_bytes/mod.rs +++ b/gix/tests/gix/revision/spec/from_bytes/mod.rs @@ -158,15 +158,11 @@ fn bad_objects_are_valid_until_they_are_actually_read_from_the_odb() { insta::assert_snapshot!(format!("{err:#?}").replace('\\', "/"), @r#" delegate.peel_until(ValidObject) failed: "{object}" | - └─ Loose(Decode(The object header contained an unknown object kind., at gix-object/src/lib.rs:344 + └─ An error occurred while obtaining an object from the loose object store | - └─ Unknown object kind: "bad", at gix-object/src/lib.rs:344)) + └─ ValidationError { message: "The object header contained an unknown object kind.", input: None } | - └─ Decode(The object header contained an unknown object kind., at gix-object/src/lib.rs:344 - | - └─ Unknown object kind: "bad", at gix-object/src/lib.rs:344) - | - └─ ValidationError { message: "Unknown object kind", input: Some("bad") } + └─ ValidationError { message: "Unknown object kind", input: Some("bad") } | └─ The object header contained an unknown object kind. | @@ -201,19 +197,7 @@ fn bad_objects_are_valid_until_they_are_actually_read_from_the_odb() { insta::assert_snapshot!(actual, @r#" delegate.peel_until(ValidObject) failed: "{object}" | - └─ Loose(DecompressFile { source: Could not decode zip stream, at gix-zlib/src/inflate.rs:16 - | - └─ Invalid input data, at gix-zlib/src/decompress.rs:67, path: "tests/fixtures/generated-do-not-edit/make_rev_spec_parse_repos/$HASH/$SEED-unix/blob.corrupt/objects/ca/fea31147e840161a1860c50af999917ae1536b" }) - | - └─ DecompressFile { source: Could not decode zip stream, at gix-zlib/src/inflate.rs:16 - | - └─ Invalid input data, at gix-zlib/src/decompress.rs:67, path: "tests/fixtures/generated-do-not-edit/make_rev_spec_parse_repos/$HASH/$SEED-unix/blob.corrupt/objects/ca/fea31147e840161a1860c50af999917ae1536b" } - | - └─ Could not decode zip stream, at gix-zlib/src/inflate.rs:16 - | - └─ Invalid input data, at gix-zlib/src/decompress.rs:67 - | - └─ Invalid input data + └─ An error occurred while obtaining an object from the loose object store | └─ decompression of loose object at 'tests/fixtures/generated-do-not-edit/make_rev_spec_parse_repos/$HASH/$SEED-unix/blob.corrupt/objects/ca/fea31147e840161a1860c50af999917ae1536b' failed | From 1487c6fc27c6fc1d59e24187eaaedf31431be490 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 20 Aug 2026 17:24:35 +0200 Subject: [PATCH 26/51] change(gix-ref)!: migrate errors to gix-error rubberstamp But looked into it more to see why it has so much more code. The simple answer is that it basically doesn't use `gix-error`, but writes error types manually. This needs rework. --- Cargo.lock | 1 - gix-ref/Cargo.toml | 2 - gix-ref/src/lib.rs | 16 ++ gix-ref/src/peel.rs | 89 ++++++- gix-ref/src/store/file/find.rs | 104 ++++++-- gix-ref/src/store/file/log/iter.rs | 44 +++- gix-ref/src/store/file/log/line.rs | 23 +- .../src/store/file/loose/reference/decode.rs | 36 ++- gix-ref/src/store/file/loose/reflog.rs | 74 +++++- gix-ref/src/store/file/overlay_iter.rs | 53 +++- gix-ref/src/store/file/packed.rs | 34 ++- gix-ref/src/store/file/raw_ext.rs | 22 +- gix-ref/src/store/file/transaction/commit.rs | 53 +++- gix-ref/src/store/file/transaction/prepare.rs | 134 ++++++++-- gix-ref/src/store/general/handle/find.rs | 72 +++++- gix-ref/src/store/general/init.rs | 27 +- gix-ref/src/store/packed/buffer.rs | 45 +++- gix-ref/src/store/packed/find.rs | 60 ++++- gix-ref/src/store/packed/iter.rs | 24 +- gix-ref/src/store/packed/transaction.rs | 87 ++++++- gix-ref/tests/refs/error.rs | 230 ++++++++++++++++++ gix-ref/tests/refs/file/reference.rs | 21 ++ gix-ref/tests/refs/main.rs | 1 + gix/tests/gix/revision/spec/from_bytes/mod.rs | 2 + .../gix/revision/spec/from_bytes/traverse.rs | 7 +- .../invalid-branch-name-failure | 3 +- 26 files changed, 1110 insertions(+), 154 deletions(-) create mode 100644 gix-ref/tests/refs/error.rs diff --git a/Cargo.lock b/Cargo.lock index a51f16e3eda..f10c10791c8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2380,7 +2380,6 @@ dependencies = [ "libc", "memmap2", "serde", - "thiserror 2.0.18", ] [[package]] diff --git a/gix-ref/Cargo.toml b/gix-ref/Cargo.toml index 2100883c537..565a6c216ab 100644 --- a/gix-ref/Cargo.toml +++ b/gix-ref/Cargo.toml @@ -37,8 +37,6 @@ gix-validate = { version = "^0.11.4", path = "../gix-validate" } gix-actor = { version = "^0.42.0", path = "../gix-actor" } gix-lock = { version = "^24.0.0", path = "../gix-lock" } gix-tempfile = { version = "^24.0.0", default-features = false, path = "../gix-tempfile" } - -thiserror = "2.0.18" serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"] } # packed refs diff --git a/gix-ref/src/lib.rs b/gix-ref/src/lib.rs index 046d23d8f42..9b6ddd99e2f 100644 --- a/gix-ref/src/lib.rs +++ b/gix-ref/src/lib.rs @@ -15,6 +15,10 @@ //! * **packed** //! * references are stored in a single human-readable file, along with their targets if they are symbolic. //! +//! Missing references and objects expose [`gix_error::NotFoundError`] through their error sources, so +//! [`gix_error::Error::is_not_found()`] works after conversion. Malformed reference data and invalid reflog +//! input similarly expose corruption and validation classifications, while retaining their concrete errors. +//! //! ## Feature Flags #![cfg_attr( all(doc, feature = "document-features"), @@ -27,6 +31,18 @@ use gix_hash::{ObjectId, oid}; pub use gix_object::bstr; use gix_object::bstr::{BStr, BString}; +// Static causes classify leaf errors without changing their public variants or allocating a new error. +static NOT_FOUND: gix_error::NotFoundError = gix_error::NotFoundError { + message: std::borrow::Cow::Borrowed("Reference or object not found"), +}; +static CORRUPTION: gix_error::CorruptionError = gix_error::CorruptionError { + message: std::borrow::Cow::Borrowed("Reference data is malformed or inconsistent"), +}; +static INVALID_REFLOG: gix_error::ValidationError = gix_error::ValidationError { + message: std::borrow::Cow::Borrowed("Invalid reflog input"), + input: None, +}; + #[path = "store/mod.rs"] mod store_impl; pub use store_impl::{file, packed}; diff --git a/gix-ref/src/peel.rs b/gix-ref/src/peel.rs index b4b48102cf3..8e3b82294c1 100644 --- a/gix-ref/src/peel.rs +++ b/gix-ref/src/peel.rs @@ -3,16 +3,53 @@ pub mod to_id { use gix_object::bstr::BString; /// The error returned by [`crate::file::ReferenceExt::peel_to_id()`]. - #[derive(Debug, thiserror::Error)] + // TODO(review): this implementation hand-preserves `#[error(transparent)]` semantics for + // `FollowToObject`: `Display` passes the formatter through and `source()` + // forwards to the inner error's source, exactly like the `thiserror`-generated + // code did. + #[derive(Debug)] #[expect(missing_docs)] pub enum Error { - #[error(transparent)] - FollowToObject(#[from] super::to_object::Error), - #[error("An error occurred when trying to resolve an object a reference points to")] - Find(#[from] gix_object::find::Error), - #[error("Object {oid} as referred to by {name:?} could not be found")] + FollowToObject(super::to_object::Error), + Find(gix_error::Error), NotFound { oid: gix_hash::ObjectId, name: BString }, } + + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::FollowToObject(err) => std::fmt::Display::fmt(err, f), + Error::Find(_) => { + f.write_str("An error occurred when trying to resolve an object a reference points to") + } + Error::NotFound { oid, name } => { + write!(f, "Object {oid} as referred to by {name:?} could not be found") + } + } + } + } + + impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::FollowToObject(err) => err.source(), + Error::Find(err) => Some(err), + Error::NotFound { .. } => Some(&crate::NOT_FOUND), + } + } + } + + impl From for Error { + fn from(err: super::to_object::Error) -> Self { + Error::FollowToObject(err) + } + } + + impl From for Error { + fn from(err: gix_object::find::Error) -> Self { + Error::Find(err.into_error()) + } + } } /// @@ -22,14 +59,44 @@ pub mod to_object { use crate::file; /// The error returned by [`file::ReferenceExt::follow_to_object_packed()`]. - #[derive(Debug, thiserror::Error)] + #[derive(Debug)] #[expect(missing_docs)] pub enum Error { - #[error("Could not follow a single level of a symbolic reference")] - Follow(#[from] file::find::existing::Error), - #[error("Aborting due to reference cycle with first seen path being {start_absolute:?}")] + Follow(file::find::existing::Error), Cycle { start_absolute: PathBuf }, - #[error("Refusing to follow more than {max_depth} levels of indirection")] DepthLimitExceeded { max_depth: usize }, } + + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Follow(_) => f.write_str("Could not follow a single level of a symbolic reference"), + #[allow(clippy::unnecessary_debug_formatting)] + // `{:?}` of a `Path` is what `thiserror` generated; keep the rendered text identical. + Error::Cycle { start_absolute } => write!( + f, + "Aborting due to reference cycle with first seen path being {start_absolute:?}" + ), + Error::DepthLimitExceeded { max_depth } => { + write!(f, "Refusing to follow more than {max_depth} levels of indirection") + } + } + } + } + + impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::Follow(err) => Some(err), + Error::Cycle { .. } => Some(&crate::CORRUPTION), + Error::DepthLimitExceeded { .. } => None, + } + } + } + + impl From for Error { + fn from(err: file::find::existing::Error) -> Self { + Error::Follow(err) + } + } } diff --git a/gix-ref/src/store/file/find.rs b/gix-ref/src/store/file/find.rs index 82d657bc66f..ff1b6822298 100644 --- a/gix-ref/src/store/file/find.rs +++ b/gix-ref/src/store/file/find.rs @@ -424,14 +424,38 @@ pub mod existing { use crate::store_impl::file::find; /// The error returned by [file::Store::find_existing()][crate::file::Store::find()]. - #[derive(Debug, thiserror::Error)] + #[derive(Debug)] #[expect(missing_docs)] pub enum Error { - #[error("An error occurred while trying to find a reference")] - Find(#[from] find::Error), - #[error("The ref partially named {name:?} could not be found")] + Find(find::Error), NotFound { name: PathBuf }, } + + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Find(_) => f.write_str("An error occurred while trying to find a reference"), + #[allow(clippy::unnecessary_debug_formatting)] + // `{:?}` of a `Path` is what `thiserror` generated; keep the rendered text identical. + Error::NotFound { name } => write!(f, "The ref partially named {name:?} could not be found"), + } + } + } + + impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::Find(err) => Some(err), + Error::NotFound { .. } => Some(&crate::NOT_FOUND), + } + } + } + + impl From for Error { + fn from(err: find::Error) -> Self { + Error::Find(err) + } + } } } @@ -441,22 +465,74 @@ mod error { use crate::{file, store_impl::packed}; /// The error returned by [file::Store::find()]. - #[derive(Debug, thiserror::Error)] + #[derive(Debug)] #[expect(missing_docs)] pub enum Error { - #[error("The ref name or path is not a valid ref name")] - RefnameValidation(#[from] crate::name::Error), - #[error("The ref file {path:?} could not be read in full")] - ReadFileContents { source: io::Error, path: PathBuf }, - #[error("The reference at \"{relative_path}\" could not be instantiated")] + RefnameValidation(crate::name::Error), + ReadFileContents { + source: io::Error, + path: PathBuf, + }, ReferenceCreation { source: file::loose::reference::decode::Error, relative_path: PathBuf, }, - #[error("A packed ref lookup failed")] - PackedRef(#[from] packed::find::Error), - #[error("Could not open the packed refs buffer when trying to find references.")] - PackedOpen(#[from] packed::buffer::open::Error), + PackedRef(packed::find::Error), + PackedOpen(packed::buffer::open::Error), + } + + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::RefnameValidation(_) => f.write_str("The ref name or path is not a valid ref name"), + #[allow(clippy::unnecessary_debug_formatting)] + // `{:?}` of a `Path` is what `thiserror` generated; keep the rendered text identical. + Error::ReadFileContents { path, .. } => { + write!(f, "The ref file {path:?} could not be read in full") + } + Error::ReferenceCreation { relative_path, .. } => { + write!( + f, + "The reference at \"{}\" could not be instantiated", + relative_path.display() + ) + } + Error::PackedRef(_) => f.write_str("A packed ref lookup failed"), + Error::PackedOpen(_) => { + f.write_str("Could not open the packed refs buffer when trying to find references.") + } + } + } + } + + impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::RefnameValidation(err) => Some(err), + Error::ReadFileContents { source, .. } => Some(source), + Error::ReferenceCreation { source, .. } => Some(source), + Error::PackedRef(err) => Some(err), + Error::PackedOpen(err) => Some(err), + } + } + } + + impl From for Error { + fn from(err: crate::name::Error) -> Self { + Error::RefnameValidation(err) + } + } + + impl From for Error { + fn from(err: packed::find::Error) -> Self { + Error::PackedRef(err) + } + } + + impl From for Error { + fn from(err: packed::buffer::open::Error) -> Self { + Error::PackedOpen(err) + } } impl From for Error { diff --git a/gix-ref/src/store/file/log/iter.rs b/gix-ref/src/store/file/log/iter.rs index 5231bb0efb3..5d40f0ccf67 100644 --- a/gix-ref/src/store/file/log/iter.rs +++ b/gix-ref/src/store/file/log/iter.rs @@ -23,7 +23,11 @@ pub mod decode { } } - impl std::error::Error for Error {} + impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + Some(&self.inner) + } + } impl Error { pub(crate) fn new(err: log::line::decode::Error, line: LineNumber) -> Self { @@ -146,13 +150,41 @@ pub mod reverse { use super::decode; /// The error returned by the [`Reverse`][super::Reverse] iterator - #[derive(Debug, thiserror::Error)] + #[derive(Debug)] #[expect(missing_docs)] pub enum Error { - #[error("The buffer could not be filled to make more lines available")] - Io(#[from] std::io::Error), - #[error("Could not decode log line")] - Decode(#[from] decode::Error), + Io(std::io::Error), + Decode(decode::Error), + } + + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Io(_) => f.write_str("The buffer could not be filled to make more lines available"), + Error::Decode(_) => f.write_str("Could not decode log line"), + } + } + } + + impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::Io(err) => Some(err), + Error::Decode(err) => Some(err), + } + } + } + + impl From for Error { + fn from(err: std::io::Error) -> Self { + Error::Io(err) + } + } + + impl From for Error { + fn from(err: decode::Error) -> Self { + Error::Decode(err) + } } } diff --git a/gix-ref/src/store/file/log/line.rs b/gix-ref/src/store/file/log/line.rs index 43fa2b53a1f..8c32123da12 100644 --- a/gix-ref/src/store/file/log/line.rs +++ b/gix-ref/src/store/file/log/line.rs @@ -17,12 +17,25 @@ mod write { use crate::log::Line; /// The Error produced by [`Line::write_to()`] (but wrapped in an io error). - #[derive(Debug, thiserror::Error)] + #[derive(Debug)] enum Error { - #[error(r"Messages must not contain newlines (\n)")] IllegalCharacter, } + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::IllegalCharacter => f.write_str(r"Messages must not contain newlines (\n)"), + } + } + } + + impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + Some(&crate::INVALID_REFLOG) + } + } + impl From for io::Error { fn from(err: Error) -> Self { io::Error::other(err) @@ -96,7 +109,11 @@ pub mod decode { } } - impl std::error::Error for Error {} + impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + Some(&crate::CORRUPTION) + } + } impl Error { pub(crate) fn new(input: &[u8]) -> Self { diff --git a/gix-ref/src/store/file/loose/reference/decode.rs b/gix-ref/src/store/file/loose/reference/decode.rs index ddad5ecb87c..f499d29c5fd 100644 --- a/gix-ref/src/store/file/loose/reference/decode.rs +++ b/gix-ref/src/store/file/loose/reference/decode.rs @@ -9,21 +9,47 @@ enum MaybeUnsafeState { } /// The error returned by [`Reference::try_from_path()`]. -#[derive(Debug, thiserror::Error)] +#[derive(Debug)] #[expect(missing_docs)] pub enum Error { /// Git's placeholder for an unsupported backend, such as reftable, was encountered. - #[error("This reference uses an unsupported storage backend, such as reftable")] UnsupportedStorage, - #[error("{content:?} could not be parsed")] - Parse { content: BString }, - #[error("The path {path:?} to a symbolic reference within a ref file is invalid")] + Parse { + content: BString, + }, RefnameValidation { source: gix_validate::reference::name::Error, path: BString, }, } +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::UnsupportedStorage => { + f.write_str("This reference uses an unsupported storage backend, such as reftable") + } + Error::Parse { content } => write!(f, "{content:?} could not be parsed"), + Error::RefnameValidation { path, .. } => { + write!( + f, + "The path {path:?} to a symbolic reference within a ref file is invalid" + ) + } + } + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::UnsupportedStorage => None, + Error::Parse { .. } => Some(&crate::CORRUPTION), + Error::RefnameValidation { source, .. } => Some(source), + } + } +} + impl TryFrom for Target { type Error = Error; diff --git a/gix-ref/src/store/file/loose/reflog.rs b/gix-ref/src/store/file/loose/reflog.rs index afc70fc23f2..9bab96a756c 100644 --- a/gix-ref/src/store/file/loose/reflog.rs +++ b/gix-ref/src/store/file/loose/reflog.rs @@ -205,24 +205,50 @@ pub mod create_or_update { use std::path::PathBuf; /// The error returned when creating or appending to a reflog - #[derive(Debug, thiserror::Error)] + #[derive(Debug)] #[expect(missing_docs)] pub enum Error { - #[error("Could create one or more directories in {reflog_directory:?} to contain reflog file")] CreateLeadingDirectories { source: std::io::Error, reflog_directory: PathBuf, }, - #[error("Could not open reflog file at {reflog_path:?} for appending")] Append { source: std::io::Error, reflog_path: PathBuf, }, - #[error("reflog message must not contain newlines")] MessageWithNewlines, - #[error("reflog messages need a committer which isn't set")] MissingCommitter, } + + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + #[allow(clippy::unnecessary_debug_formatting)] + // `{:?}` of a `Path` is what `thiserror` generated; keep the rendered text identical. + Error::CreateLeadingDirectories { reflog_directory, .. } => write!( + f, + "Could create one or more directories in {reflog_directory:?} to contain reflog file" + ), + #[allow(clippy::unnecessary_debug_formatting)] + // `{:?}` of a `Path` is what `thiserror` generated; keep the rendered text identical. + Error::Append { reflog_path, .. } => { + write!(f, "Could not open reflog file at {reflog_path:?} for appending") + } + Error::MessageWithNewlines => f.write_str("reflog message must not contain newlines"), + Error::MissingCommitter => f.write_str("reflog messages need a committer which isn't set"), + } + } + } + + impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::CreateLeadingDirectories { source, .. } => Some(source), + Error::Append { source, .. } => Some(source), + Error::MessageWithNewlines | Error::MissingCommitter => Some(&crate::INVALID_REFLOG), + } + } + } } pub use error::Error; @@ -231,13 +257,41 @@ pub mod create_or_update { mod error { /// The error returned by [`crate::file::Store::reflog_iter()`]. - #[derive(Debug, thiserror::Error)] + #[derive(Debug)] #[expect(missing_docs)] pub enum Error { - #[error("The reflog name or path is not a valid ref name")] - RefnameValidation(#[from] crate::name::Error), - #[error("The reflog file could not read")] - Io(#[from] std::io::Error), + RefnameValidation(crate::name::Error), + Io(std::io::Error), + } + + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::RefnameValidation(_) => f.write_str("The reflog name or path is not a valid ref name"), + Error::Io(_) => f.write_str("The reflog file could not read"), + } + } + } + + impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::RefnameValidation(err) => Some(err), + Error::Io(err) => Some(err), + } + } + } + + impl From for Error { + fn from(err: crate::name::Error) -> Self { + Error::RefnameValidation(err) + } + } + + impl From for Error { + fn from(err: std::io::Error) -> Self { + Error::Io(err) + } } } pub use error::Error; diff --git a/gix-ref/src/store/file/overlay_iter.rs b/gix-ref/src/store/file/overlay_iter.rs index b368079907e..22c1600132b 100644 --- a/gix-ref/src/store/file/overlay_iter.rs +++ b/gix-ref/src/store/file/overlay_iter.rs @@ -463,20 +463,57 @@ mod error { use crate::store_impl::file; /// The error returned by the [`LooseThenPacked`][super::LooseThenPacked] iterator. - #[derive(Debug, thiserror::Error)] + #[derive(Debug)] #[expect(missing_docs)] pub enum Error { - #[error("The file system could not be traversed")] - Traversal(#[source] io::Error), - #[error("The ref file {path:?} could not be read in full")] - ReadFileContents { source: io::Error, path: PathBuf }, - #[error("The reference at \"{relative_path}\" could not be instantiated")] + Traversal(io::Error), + ReadFileContents { + source: io::Error, + path: PathBuf, + }, ReferenceCreation { source: file::loose::reference::decode::Error, relative_path: PathBuf, }, - #[error("Invalid reference in line {line_number}: {invalid_line:?}")] - PackedReference { invalid_line: BString, line_number: usize }, + PackedReference { + invalid_line: BString, + line_number: usize, + }, + } + + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Traversal(_) => f.write_str("The file system could not be traversed"), + #[allow(clippy::unnecessary_debug_formatting)] + // `{:?}` of a `Path` is what `thiserror` generated; keep the rendered text identical. + Error::ReadFileContents { path, .. } => { + write!(f, "The ref file {path:?} could not be read in full") + } + Error::ReferenceCreation { relative_path, .. } => { + write!( + f, + "The reference at \"{}\" could not be instantiated", + relative_path.display() + ) + } + Error::PackedReference { + invalid_line, + line_number, + } => write!(f, "Invalid reference in line {line_number}: {invalid_line:?}"), + } + } + } + + impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::Traversal(err) => Some(err), + Error::ReadFileContents { source, .. } => Some(source), + Error::ReferenceCreation { source, .. } => Some(source), + Error::PackedReference { .. } => Some(&crate::CORRUPTION), + } + } } } pub use error::Error; diff --git a/gix-ref/src/store/file/packed.rs b/gix-ref/src/store/file/packed.rs index 1905304ee26..5192236ceb4 100644 --- a/gix-ref/src/store/file/packed.rs +++ b/gix-ref/src/store/file/packed.rs @@ -67,13 +67,37 @@ pub mod transaction { use crate::store_impl::packed; /// The error returned by [`file::Transaction::prepare()`][crate::file::Transaction::prepare()]. - #[derive(Debug, thiserror::Error)] + #[derive(Debug)] #[expect(missing_docs)] pub enum Error { - #[error("An existing pack couldn't be opened or read when preparing a transaction")] - BufferOpen(#[from] packed::buffer::open::Error), - #[error("The lock for a packed transaction could not be obtained")] - TransactionLock(#[source] std::io::Error), + BufferOpen(packed::buffer::open::Error), + TransactionLock(std::io::Error), + } + + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::BufferOpen(_) => { + f.write_str("An existing pack couldn't be opened or read when preparing a transaction") + } + Error::TransactionLock(_) => f.write_str("The lock for a packed transaction could not be obtained"), + } + } + } + + impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::BufferOpen(err) => Some(err), + Error::TransactionLock(err) => Some(err), + } + } + } + + impl From for Error { + fn from(err: packed::buffer::open::Error) -> Self { + Error::BufferOpen(err) + } } } diff --git a/gix-ref/src/store/file/raw_ext.rs b/gix-ref/src/store/file/raw_ext.rs index f14e54abc24..d492f6cafe8 100644 --- a/gix-ref/src/store/file/raw_ext.rs +++ b/gix-ref/src/store/file/raw_ext.rs @@ -1,5 +1,6 @@ use std::collections::BTreeSet; +use gix_error::{CorruptionError, ResultExt, message}; use gix_hash::ObjectId; use crate::{ @@ -156,7 +157,7 @@ impl ReferenceExt for Reference { Ok(peeled) } None => { - let mut oid = self.follow_to_object_packed(store, packed)?; + let mut object_id = self.follow_to_object_packed(store, packed)?; let mut buf = Vec::new(); let peeled_id = loop { let gix_object::Data { @@ -164,21 +165,24 @@ impl ReferenceExt for Reference { data, object_hash: hash_kind, } = objects - .try_find(&oid, &mut buf)? + .try_find(&object_id, &mut buf)? .ok_or_else(|| peel::to_id::Error::NotFound { - oid, + oid: object_id, name: self.name.0.clone(), })?; match kind { gix_object::Kind::Tag => { - oid = gix_object::TagRefIter::from_bytes(data, hash_kind) + object_id = gix_object::TagRefIter::from_bytes(data, hash_kind) .target_id() - .map_err(|_err| peel::to_id::Error::NotFound { - oid, - name: self.name.0.clone(), - })?; + .or_raise(|| { + CorruptionError::from(message!( + "Could not decode tag {object_id} as referred to by {:?}", + self.name.0 + )) + }) + .or_erased()?; } - _ => break oid, + _ => break object_id, } }; self.peeled = Some(peeled_id); diff --git a/gix-ref/src/store/file/transaction/commit.rs b/gix-ref/src/store/file/transaction/commit.rs index 1d87caa7292..53c586eb0fa 100644 --- a/gix-ref/src/store/file/transaction/commit.rs +++ b/gix-ref/src/store/file/transaction/commit.rs @@ -180,21 +180,56 @@ mod error { use crate::store_impl::{file, packed}; /// The error returned by various [`Transaction`][super::Transaction] methods. - #[derive(Debug, thiserror::Error)] + // TODO(review): `DeleteReference` stores its io error in a field named `err`, which `thiserror` + // did NOT treat as a source — `source()` returning `None` for it is preserved + // behavior, not an omission. + #[derive(Debug)] #[expect(missing_docs)] pub enum Error { - #[error("The packed-ref transaction could not be committed")] - PackedTransactionCommit(#[source] packed::transaction::commit::Error), - #[error("Edit preprocessing failed with error")] + PackedTransactionCommit(packed::transaction::commit::Error), PreprocessingFailed { source: std::io::Error }, - #[error("The change for reference {full_name:?} could not be committed")] LockCommit { source: std::io::Error, full_name: BString }, - #[error("The reference {full_name} could not be deleted")] DeleteReference { full_name: BString, err: std::io::Error }, - #[error("The reflog of reference {full_name:?} could not be deleted")] DeleteReflog { full_name: BString, source: std::io::Error }, - #[error("The reflog could not be created or updated")] - CreateOrUpdateRefLog(#[from] file::log::create_or_update::Error), + CreateOrUpdateRefLog(file::log::create_or_update::Error), + } + + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::PackedTransactionCommit(_) => f.write_str("The packed-ref transaction could not be committed"), + Error::PreprocessingFailed { .. } => f.write_str("Edit preprocessing failed with error"), + Error::LockCommit { full_name, .. } => { + write!(f, "The change for reference {full_name:?} could not be committed") + } + Error::DeleteReference { full_name, .. } => { + write!(f, "The reference {full_name} could not be deleted") + } + Error::DeleteReflog { full_name, .. } => { + write!(f, "The reflog of reference {full_name:?} could not be deleted") + } + Error::CreateOrUpdateRefLog(_) => f.write_str("The reflog could not be created or updated"), + } + } + } + + impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::PackedTransactionCommit(err) => Some(err), + Error::PreprocessingFailed { source } => Some(source), + Error::LockCommit { source, .. } => Some(source), + Error::DeleteReference { .. } => None, + Error::DeleteReflog { source, .. } => Some(source), + Error::CreateOrUpdateRefLog(err) => Some(err), + } + } + } + + impl From for Error { + fn from(err: file::log::create_or_update::Error) -> Self { + Error::CreateOrUpdateRefLog(err) + } } } pub use error::Error; diff --git a/gix-ref/src/store/file/transaction/prepare.rs b/gix-ref/src/store/file/transaction/prepare.rs index 666a5962eef..76c764afe66 100644 --- a/gix-ref/src/store/file/transaction/prepare.rs +++ b/gix-ref/src/store/file/transaction/prepare.rs @@ -473,43 +473,127 @@ mod error { }; /// The error returned by various [`Transaction`][super::Transaction] methods. - #[derive(Debug, thiserror::Error)] + #[derive(Debug)] #[expect(missing_docs)] pub enum Error { - #[error("The packed ref buffer could not be loaded")] - Packed(#[from] packed::buffer::open::Error), - #[error("The lock for the packed-ref file could not be obtained")] - PackedTransactionAcquire(#[source] std::io::Error), - #[error("The packed transaction could not be prepared")] - PackedTransactionPrepare(#[from] packed::transaction::prepare::Error), - #[error("The packed ref file could not be parsed")] - PackedFind(#[from] packed::find::Error), - #[error("Edit preprocessing failed with an error")] - PreprocessingFailed(#[source] std::io::Error), - #[error("A lock could not be obtained for reference {full_name:?}")] - LockAcquire { source: std::io::Error, full_name: BString }, - #[error("An IO error occurred while applying an edit")] - Io(#[from] std::io::Error), - #[error("The reference {full_name:?} for deletion did not exist or could not be parsed")] - DeleteReferenceMustExist { full_name: BString }, - #[error( - "Reference {full_name:?} was not supposed to exist when writing it with value {new:?}, but actual content was {actual:?}" - )] + Packed(packed::buffer::open::Error), + PackedTransactionAcquire(std::io::Error), + PackedTransactionPrepare(packed::transaction::prepare::Error), + PackedFind(packed::find::Error), + PreprocessingFailed(std::io::Error), + LockAcquire { + source: std::io::Error, + full_name: BString, + }, + Io(std::io::Error), + DeleteReferenceMustExist { + full_name: BString, + }, MustNotExist { full_name: BString, actual: Target, new: Target, }, - #[error("Reference {full_name:?} was supposed to exist with value {expected}, but didn't.")] - MustExist { full_name: BString, expected: Target }, - #[error("The reference {full_name:?} should have content {expected}, actual content was {actual}")] + MustExist { + full_name: BString, + expected: Target, + }, ReferenceOutOfDate { full_name: BString, expected: Target, actual: Target, }, - #[error("Could not read reference")] - ReferenceDecode(#[from] file::loose::reference::decode::Error), + ReferenceDecode(file::loose::reference::decode::Error), + } + + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Packed(_) => f.write_str("The packed ref buffer could not be loaded"), + Error::PackedTransactionAcquire(_) => { + f.write_str("The lock for the packed-ref file could not be obtained") + } + Error::PackedTransactionPrepare(_) => f.write_str("The packed transaction could not be prepared"), + Error::PackedFind(_) => f.write_str("The packed ref file could not be parsed"), + Error::PreprocessingFailed(_) => f.write_str("Edit preprocessing failed with an error"), + Error::LockAcquire { full_name, .. } => { + write!(f, "A lock could not be obtained for reference {full_name:?}") + } + Error::Io(_) => f.write_str("An IO error occurred while applying an edit"), + Error::DeleteReferenceMustExist { full_name } => { + write!( + f, + "The reference {full_name:?} for deletion did not exist or could not be parsed" + ) + } + Error::MustNotExist { full_name, actual, new } => write!( + f, + "Reference {full_name:?} was not supposed to exist when writing it with value {new:?}, but actual content was {actual:?}" + ), + Error::MustExist { full_name, expected } => { + write!( + f, + "Reference {full_name:?} was supposed to exist with value {expected}, but didn't." + ) + } + Error::ReferenceOutOfDate { + full_name, + expected, + actual, + } => write!( + f, + "The reference {full_name:?} should have content {expected}, actual content was {actual}" + ), + Error::ReferenceDecode(_) => f.write_str("Could not read reference"), + } + } + } + + impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::Packed(err) => Some(err), + Error::PackedTransactionAcquire(err) => Some(err), + Error::PackedTransactionPrepare(err) => Some(err), + Error::PackedFind(err) => Some(err), + Error::PreprocessingFailed(err) => Some(err), + Error::LockAcquire { source, .. } => Some(source), + Error::Io(err) => Some(err), + Error::DeleteReferenceMustExist { .. } | Error::MustExist { .. } => Some(&crate::NOT_FOUND), + Error::MustNotExist { .. } | Error::ReferenceOutOfDate { .. } => None, + Error::ReferenceDecode(err) => Some(err), + } + } + } + + impl From for Error { + fn from(err: packed::buffer::open::Error) -> Self { + Error::Packed(err) + } + } + + impl From for Error { + fn from(err: packed::transaction::prepare::Error) -> Self { + Error::PackedTransactionPrepare(err) + } + } + + impl From for Error { + fn from(err: packed::find::Error) -> Self { + Error::PackedFind(err) + } + } + + impl From for Error { + fn from(err: std::io::Error) -> Self { + Error::Io(err) + } + } + + impl From for Error { + fn from(err: file::loose::reference::decode::Error) -> Self { + Error::ReferenceDecode(err) + } } } diff --git a/gix-ref/src/store/general/handle/find.rs b/gix-ref/src/store/general/handle/find.rs index a259da3f5d9..560592dcbc4 100644 --- a/gix-ref/src/store/general/handle/find.rs +++ b/gix-ref/src/store/general/handle/find.rs @@ -4,13 +4,43 @@ mod error { use std::convert::Infallible; /// The error returned by [`crate::file::Store::find_loose()`]. - #[derive(Debug, thiserror::Error)] + #[derive(Debug)] #[expect(missing_docs)] pub enum Error { - #[error("An error occurred while finding a reference in the loose file database")] - Loose(#[from] crate::file::find::Error), - #[error("The ref name or path is not a valid ref name")] - RefnameValidation(#[from] crate::name::Error), + Loose(crate::file::find::Error), + RefnameValidation(crate::name::Error), + } + + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Loose(_) => { + f.write_str("An error occurred while finding a reference in the loose file database") + } + Error::RefnameValidation(_) => f.write_str("The ref name or path is not a valid ref name"), + } + } + } + + impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::Loose(err) => Some(err), + Error::RefnameValidation(err) => Some(err), + } + } + } + + impl From for Error { + fn from(err: crate::file::find::Error) -> Self { + Error::Loose(err) + } + } + + impl From for Error { + fn from(err: crate::name::Error) -> Self { + Error::RefnameValidation(err) + } } impl From for Error { @@ -45,13 +75,37 @@ mod existing { use std::path::PathBuf; /// The error returned by [file::Store::find_existing()][crate::file::Store::find_existing()]. - #[derive(Debug, thiserror::Error)] + #[derive(Debug)] pub enum Error { - #[error("An error occurred while finding a reference in the database")] - Find(#[from] crate::store::find::Error), - #[error("The ref partially named {name:?} could not be found")] + Find(crate::store::find::Error), NotFound { name: PathBuf }, } + + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Find(_) => f.write_str("An error occurred while finding a reference in the database"), + #[allow(clippy::unnecessary_debug_formatting)] + // `{:?}` of a `Path` is what `thiserror` generated; keep the rendered text identical. + Error::NotFound { name } => write!(f, "The ref partially named {name:?} could not be found"), + } + } + } + + impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::Find(err) => Some(err), + Error::NotFound { .. } => Some(&crate::NOT_FOUND), + } + } + } + + impl From for Error { + fn from(err: crate::store::find::Error) -> Self { + Error::Find(err) + } + } } pub use error::Error; diff --git a/gix-ref/src/store/general/init.rs b/gix-ref/src/store/general/init.rs index 5c1e5f7bbe6..48d985b47e4 100644 --- a/gix-ref/src/store/general/init.rs +++ b/gix-ref/src/store/general/init.rs @@ -2,10 +2,31 @@ use std::path::PathBuf; mod error { /// The error returned by [`crate::Store::at()`]. - #[derive(Debug, thiserror::Error)] + #[derive(Debug)] pub enum Error { - #[error("There was an error accessing the store's directory")] - Io(#[from] std::io::Error), + Io(std::io::Error), + } + + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Io(_) => f.write_str("There was an error accessing the store's directory"), + } + } + } + + impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::Io(err) => Some(err), + } + } + } + + impl From for Error { + fn from(err: std::io::Error) -> Self { + Error::Io(err) + } } } diff --git a/gix-ref/src/store/packed/buffer.rs b/gix-ref/src/store/packed/buffer.rs index f02ae3ca337..45252401ff6 100644 --- a/gix-ref/src/store/packed/buffer.rs +++ b/gix-ref/src/store/packed/buffer.rs @@ -109,15 +109,48 @@ pub mod open { use crate::packed; /// The error returned by [`open()`][super::packed::Buffer::open()]. - #[derive(Debug, thiserror::Error)] + #[derive(Debug)] #[expect(missing_docs)] pub enum Error { - #[error("The packed-refs file did not have a header or wasn't sorted and could not be iterated")] - Iter(#[from] packed::iter::Error), - #[error("The header could not be parsed, even though first line started with '#'")] + Iter(packed::iter::Error), HeaderParsing, - #[error("The buffer could not be opened or read")] - Io(#[from] std::io::Error), + Io(std::io::Error), + } + + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Iter(_) => f.write_str( + "The packed-refs file did not have a header or wasn't sorted and could not be iterated", + ), + Error::HeaderParsing => { + f.write_str("The header could not be parsed, even though first line started with '#'") + } + Error::Io(_) => f.write_str("The buffer could not be opened or read"), + } + } + } + + impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::Iter(err) => Some(err), + Error::HeaderParsing => Some(&crate::CORRUPTION), + Error::Io(err) => Some(err), + } + } + } + + impl From for Error { + fn from(err: packed::iter::Error) -> Self { + Error::Iter(err) + } + } + + impl From for Error { + fn from(err: std::io::Error) -> Self { + Error::Io(err) + } } } pub use error::Error; diff --git a/gix-ref/src/store/packed/find.rs b/gix-ref/src/store/packed/find.rs index 005f87f4357..0a5f0c5f3c4 100644 --- a/gix-ref/src/store/packed/find.rs +++ b/gix-ref/src/store/packed/find.rs @@ -101,15 +101,37 @@ mod error { use std::convert::Infallible; /// The error returned by [`find()`][super::packed::Buffer::find()] - #[derive(Debug, thiserror::Error)] + #[derive(Debug)] #[expect(missing_docs)] pub enum Error { - #[error("The ref name or path is not a valid ref name")] - RefnameValidation(#[from] crate::name::Error), - #[error("The reference could not be parsed")] + RefnameValidation(crate::name::Error), Parse, } + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::RefnameValidation(_) => f.write_str("The ref name or path is not a valid ref name"), + Error::Parse => f.write_str("The reference could not be parsed"), + } + } + } + + impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::RefnameValidation(err) => Some(err), + Error::Parse => Some(&crate::CORRUPTION), + } + } + } + + impl From for Error { + fn from(err: crate::name::Error) -> Self { + Error::RefnameValidation(err) + } + } + impl From for Error { fn from(_: Infallible) -> Self { unreachable!("this impl is needed to allow passing a known valid partial path as parameter") @@ -122,14 +144,36 @@ pub use error::Error; pub mod existing { /// The error returned by [`find_existing()`][super::packed::Buffer::find()] - #[derive(Debug, thiserror::Error)] + #[derive(Debug)] #[expect(missing_docs)] pub enum Error { - #[error("The find operation failed")] - Find(#[from] super::Error), - #[error("The reference did not exist even though that was expected")] + Find(super::Error), NotFound, } + + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Find(_) => f.write_str("The find operation failed"), + Error::NotFound => f.write_str("The reference did not exist even though that was expected"), + } + } + } + + impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::Find(err) => Some(err), + Error::NotFound => Some(&crate::NOT_FOUND), + } + } + } + + impl From for Error { + fn from(err: super::Error) -> Self { + Error::Find(err) + } + } } pub(crate) fn transform_full_name_for_lookup(name: &FullNameRef) -> Option<&FullNameRef> { diff --git a/gix-ref/src/store/packed/iter.rs b/gix-ref/src/store/packed/iter.rs index aa2ef319f2e..0ac9642d031 100644 --- a/gix-ref/src/store/packed/iter.rs +++ b/gix-ref/src/store/packed/iter.rs @@ -115,14 +115,32 @@ mod error { use gix_object::bstr::BString; /// The error returned by [`Iter`][super::packed::Iter], - #[derive(Debug, thiserror::Error)] + #[derive(Debug)] #[expect(missing_docs)] pub enum Error { - #[error("The header existed but could not be parsed: {invalid_first_line:?}")] Header { invalid_first_line: BString }, - #[error("Invalid reference in line {line_number}: {invalid_line:?}")] Reference { invalid_line: BString, line_number: usize }, } + + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Header { invalid_first_line } => { + write!(f, "The header existed but could not be parsed: {invalid_first_line:?}") + } + Error::Reference { + invalid_line, + line_number, + } => write!(f, "Invalid reference in line {line_number}: {invalid_line:?}"), + } + } + } + + impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + Some(&crate::CORRUPTION) + } + } } pub use error::Error; diff --git a/gix-ref/src/store/packed/transaction.rs b/gix-ref/src/store/packed/transaction.rs index 07a6cfc550a..352c59b761c 100644 --- a/gix-ref/src/store/packed/transaction.rs +++ b/gix-ref/src/store/packed/transaction.rs @@ -281,13 +281,41 @@ pub(crate) fn buffer_into_transaction( /// pub mod prepare { /// The error used in [`Transaction::prepare(…)`][crate::file::Transaction::prepare()]. - #[derive(Debug, thiserror::Error)] + #[derive(Debug)] #[expect(missing_docs)] pub enum Error { - #[error("Could not close a lock which won't ever be committed")] - CloseLock(#[from] std::io::Error), - #[error("The lookup of an object failed while peeling it")] - Resolve(#[from] Box), + CloseLock(std::io::Error), + Resolve(Box), + } + + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::CloseLock(_) => f.write_str("Could not close a lock which won't ever be committed"), + Error::Resolve(_) => f.write_str("The lookup of an object failed while peeling it"), + } + } + } + + impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::CloseLock(err) => Some(err), + Error::Resolve(err) => Some(&**err), + } + } + } + + impl From for Error { + fn from(err: std::io::Error) -> Self { + Error::CloseLock(err) + } + } + + impl From> for Error { + fn from(err: Box) -> Self { + Error::Resolve(err) + } } } @@ -296,14 +324,49 @@ pub mod commit { use crate::store_impl::packed; /// The error used in [`Transaction::commit(…)`][crate::file::Transaction::commit()]. - #[derive(Debug, thiserror::Error)] + #[derive(Debug)] #[expect(missing_docs)] pub enum Error { - #[error("Changes to the resource could not be committed")] - Commit(#[from] gix_lock::commit::Error), - #[error("Some references in the packed refs buffer could not be parsed")] - Iteration(#[from] packed::iter::Error), - #[error("Failed to write a ref line to the packed ref file")] - Io(#[from] std::io::Error), + Commit(gix_lock::commit::Error), + Iteration(packed::iter::Error), + Io(std::io::Error), + } + + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Commit(_) => f.write_str("Changes to the resource could not be committed"), + Error::Iteration(_) => f.write_str("Some references in the packed refs buffer could not be parsed"), + Error::Io(_) => f.write_str("Failed to write a ref line to the packed ref file"), + } + } + } + + impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::Commit(err) => Some(err), + Error::Iteration(err) => Some(err), + Error::Io(err) => Some(err), + } + } + } + + impl From> for Error { + fn from(err: gix_lock::commit::Error) -> Self { + Error::Commit(err) + } + } + + impl From for Error { + fn from(err: packed::iter::Error) -> Self { + Error::Iteration(err) + } + } + + impl From for Error { + fn from(err: std::io::Error) -> Self { + Error::Io(err) + } } } diff --git a/gix-ref/tests/refs/error.rs b/gix-ref/tests/refs/error.rs new file mode 100644 index 00000000000..7c6050139a5 --- /dev/null +++ b/gix-ref/tests/refs/error.rs @@ -0,0 +1,230 @@ +use gix_error::{Error, ErrorExt}; +use gix_ref::{file::ReferenceExt, packed, transaction::PreviousValue}; + +#[test] +fn missing_references_remain_classified_after_erasure() -> crate::Result { + let store = crate::file::store_with_packed_refs()?; + let packed = store.open_packed_buffer()?.expect("the fixture has packed refs"); + for err in [ + Error::from_error(store.find("missing").expect_err("the reference is absent")), + Error::from_error(store.find_loose("missing").expect_err("the reference is absent")), + Error::from_error( + store + .find_packed("missing", Some(&packed)) + .expect_err("the reference is absent"), + ), + Error::from_error(packed.find("missing").expect_err("the reference is absent")), + ] { + assert!( + err.is_not_found(), + "missing loose and packed refs are classified: {err}" + ); + assert!( + err.and_raise(gix_error::message("reference lookup failed")) + .into_error() + .is_not_found(), + "additional context preserves the classification" + ); + } + Ok(()) +} + +#[test] +fn peeling_missing_targets_is_classified() -> crate::Result { + let store = crate::file::store_at("make_ref_repository.sh")?; + let mut symbolic = store.find("HEAD")?; + symbolic.target = gix_ref::Target::Symbolic("refs/heads/missing".try_into()?); + let mut direct = store.find("main")?; + for err in [ + Error::from_error( + symbolic + .follow(&store) + .expect("HEAD is symbolic") + .expect_err("the referent is absent"), + ), + Error::from_error( + symbolic + .peel_to_id(&store, &gix_object::find::Never) + .expect_err("the symbolic target is absent"), + ), + Error::from_error( + direct + .peel_to_id(&store, &gix_object::find::Never) + .expect_err("the object database is empty"), + ), + ] { + assert!( + err.is_not_found(), + "missing referents and objects are classified: {err}" + ); + assert!(!err.is_corrupted(), "absence alone does not imply corruption"); + } + Ok(()) +} + +#[test] +fn malformed_tags_are_corruption_instead_of_missing_objects() -> crate::Result { + struct MalformedTag; + impl gix_object::Find for MalformedTag { + fn try_find<'a>( + &self, + object_id: &gix_hash::oid, + _buffer: &'a mut Vec, + ) -> Result>, gix_error::Exn> { + Ok(Some(gix_object::Data { + kind: gix_object::Kind::Tag, + object_hash: object_id.kind(), + data: b"object invalid\n", + })) + } + } + + let store = crate::file::store_at("make_ref_repository.sh")?; + let err = Error::from_error( + store + .find("main")? + .peel_to_id(&store, &MalformedTag) + .expect_err("the tag target cannot be decoded"), + ); + assert!(err.is_corrupted(), "malformed stored tag data is corruption"); + assert!( + !err.is_not_found(), + "the malformed tag was found in the object database" + ); + assert!( + err.downcast_any_ref::().is_some(), + "the tag parser's original error remains available" + ); + Ok(()) +} + +#[test] +fn malformed_reference_data_is_classified() -> crate::Result { + let store = crate::file::store_at("make_ref_repository.sh")?; + let hash = crate::fixture_hash_kind(); + let packed = packed::Buffer::from_bytes( + b"# pack-refs with: peeled fully-peeled sorted\nbogus refs/heads/main\n", + hash, + )?; + for err in [ + Error::from_error( + gix_ref::file::loose::Reference::try_from_path("HEAD".try_into()?, b"invalid", hash) + .expect_err("the loose ref is malformed"), + ), + Error::from_error(packed::Buffer::from_bytes(b"# invalid\n", hash).expect_err("the header is malformed")), + Error::from_error(packed.find("main").expect_err("the packed ref is malformed")), + Error::from_error( + packed + .iter()? + .next() + .expect("one packed ref") + .expect_err("the ref is malformed"), + ), + Error::from_error( + store + .iter_packed(Some(&packed))? + .find_map(Result::err) + .expect("the overlay encounters the malformed packed ref"), + ), + Error::from_error( + store + .find("loop-a")? + .peel_to_id(&store, &gix_object::find::Never) + .expect_err("the symbolic refs form a cycle"), + ), + Error::from_error( + gix_ref::file::log::LineRef::from_bytes(b"invalid").expect_err("the reflog line is malformed"), + ), + Error::from_error( + gix_ref::file::log::iter::forward(b"invalid\n") + .next() + .expect("one reflog line") + .expect_err("the reflog line is malformed"), + ), + ] { + assert!(err.is_corrupted(), "malformed stored data is classified: {err}"); + assert!(!err.is_not_found(), "malformed stored data is present"); + } + assert!( + !Error::from_error(gix_ref::peel::to_object::Error::DepthLimitExceeded { max_depth: 5 }).is_corrupted(), + "a depth limit can also be reached by a valid symbolic reference chain" + ); + Ok(()) +} + +#[test] +fn missing_transaction_targets_are_classified() -> crate::Result { + use gix_lock::acquire::Fail; + use gix_ref::transaction::RefEdit; + + let (_keep, store) = crate::file::transaction::prepare_and_commit::empty_store()?; + for edit in [ + RefEdit::delete("refs/heads/missing".try_into()?, PreviousValue::MustExist), + RefEdit::update( + "refs/heads/missing".try_into()?, + gix_ref::Target::Object(crate::fixture_hash_kind().null()), + PreviousValue::MustExist, + "update", + ), + ] { + let err = Error::from_error( + store + .transaction() + .prepare([edit], Fail::Immediately, Fail::Immediately) + .expect_err("the edit requires an existing reference"), + ); + assert!(err.is_not_found(), "updates and deletions classify missing refs: {err}"); + } + Ok(()) +} + +#[test] +fn invalid_reflog_input_is_classified() -> crate::Result { + use crate::file::transaction::prepare_and_commit::{committer, create_at, empty_store}; + use gix_lock::acquire::Fail; + + let (_keep, store) = empty_store()?; + let line = gix_ref::log::Line { + previous_oid: crate::fixture_hash_kind().null(), + new_oid: crate::fixture_hash_kind().null(), + signature: committer(), + message: "invalid\nmessage".into(), + }; + for err in [ + Error::from_error(line.write_to(&mut Vec::new()).expect_err("newlines are forbidden")), + Error::from_error( + store + .transaction() + .prepare([create_at("refs/heads/new")], Fail::Immediately, Fail::Immediately)? + .commit(None) + .expect_err("writing a reflog requires a committer"), + ), + ] { + assert!(err.is_validation(), "invalid reflog input is classified: {err}"); + assert!(!err.is_corrupted(), "invalid input does not imply corrupt stored data"); + } + Ok(()) +} + +#[test] +fn existing_sources_and_retry_classifications_are_preserved() { + for kind in [std::io::ErrorKind::PermissionDenied, std::io::ErrorKind::TimedOut] { + let err = Error::from_error(gix_ref::file::find::existing::Error::Find( + gix_ref::file::find::Error::ReadFileContents { + source: std::io::Error::from(kind), + path: "refs/heads/main".into(), + }, + )); + assert_eq!( + err.downcast_any_ref::().map(std::io::Error::kind), + Some(kind), + "the concrete I/O cause remains accessible" + ); + assert_eq!( + err.can_retry(), + kind == std::io::ErrorKind::TimedOut, + "I/O retry policy is retained" + ); + assert!(!err.is_not_found(), "a failed lookup does not imply absence"); + } +} diff --git a/gix-ref/tests/refs/file/reference.rs b/gix-ref/tests/refs/file/reference.rs index d5892c2bb3d..f90b15843f8 100644 --- a/gix-ref/tests/refs/file/reference.rs +++ b/gix-ref/tests/refs/file/reference.rs @@ -56,6 +56,27 @@ mod peel { hex_to_id, }; + #[test] + fn object_lookup_error_preserves_causes() { + use gix_error::ErrorExt; + + let lookup_error = std::io::Error::from(std::io::ErrorKind::PermissionDenied) + .and_raise(gix_error::message("Could not read the referenced object")) + .erased(); + let err = gix_error::Error::from_error(gix_ref::peel::to_id::Error::from(lookup_error)); + + assert_eq!( + err.downcast_any_ref::().map(std::io::Error::kind), + Some(std::io::ErrorKind::PermissionDenied), + "the peeling error retains the I/O cause beneath the lookup context" + ); + assert!( + err.iter_errors() + .any(|cause| cause.to_string() == "Could not read the referenced object"), + "the lookup context remains part of the error chain" + ); + } + #[test] fn one_level() -> crate::Result { let store = file::store()?; diff --git a/gix-ref/tests/refs/main.rs b/gix-ref/tests/refs/main.rs index c723d12e57d..9bb448a5e27 100644 --- a/gix-ref/tests/refs/main.rs +++ b/gix-ref/tests/refs/main.rs @@ -64,6 +64,7 @@ fn translate_sha1_to_fixture_sha256(hex: &str) -> String { pub use gix_testtools::Result; mod equality; +mod error; mod file; mod fullname; mod partialname { diff --git a/gix/tests/gix/revision/spec/from_bytes/mod.rs b/gix/tests/gix/revision/spec/from_bytes/mod.rs index b7d420bea4d..df45ab5a1af 100644 --- a/gix/tests/gix/revision/spec/from_bytes/mod.rs +++ b/gix/tests/gix/revision/spec/from_bytes/mod.rs @@ -246,6 +246,8 @@ fn invalid_head() { └─ Could not follow a single level of a symbolic reference | | | └─ The ref partially named "refs/heads/main" could not be found + | | + | └─ Reference or object not found | └─ Couldn't get object at internal index 0 "#); diff --git a/gix/tests/gix/revision/spec/from_bytes/traverse.rs b/gix/tests/gix/revision/spec/from_bytes/traverse.rs index e8e097bc9ac..7b48c999d25 100644 --- a/gix/tests/gix/revision/spec/from_bytes/traverse.rs +++ b/gix/tests/gix/revision/spec/from_bytes/traverse.rs @@ -39,11 +39,10 @@ fn freestanding_negation_yields_descriptive_error() -> crate::Result { couldn't parse revision: "!" | └─ The ref partially named "!" could not be found + | + └─ Reference or object not found "#); - assert_eq!( - err.probable_cause().to_string(), - "The ref partially named \"!\" could not be found" - ); + assert!(err.is_not_found(), "the missing anchor reference remains classified"); Ok(()) } #[test] diff --git a/tests/snapshots/porcelain/estimate-hours/invalid-branch-name-failure b/tests/snapshots/porcelain/estimate-hours/invalid-branch-name-failure index fec486aaac0..938ed91fa4a 100644 --- a/tests/snapshots/porcelain/estimate-hours/invalid-branch-name-failure +++ b/tests/snapshots/porcelain/estimate-hours/invalid-branch-name-failure @@ -1,4 +1,5 @@ Error: couldn't parse revision: "foobar" Caused by: - The ref partially named "foobar" could not be found \ No newline at end of file + 0: The ref partially named "foobar" could not be found + 1: Reference or object not found \ No newline at end of file From 2c373b2e1789fa3ffb4140f0978080152028a520 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 20 Aug 2026 17:26:57 +0200 Subject: [PATCH 27/51] change(gix-transport)!: migrate errors to gix-error rubberstamp, but it needs some more work to not repeat `can_retry()` --- Cargo.lock | 1 - gix-protocol/src/fetch/function.rs | 30 +- gix-protocol/src/fetch/response/io.rs | 33 +-- gix-protocol/src/handshake/function.rs | 10 +- gix-protocol/src/ls_refs.rs | 8 +- .../tests/protocol/fetch/arguments.rs | 10 +- gix-protocol/tests/protocol/fetch/response.rs | 6 +- gix-transport/Cargo.toml | 1 - gix-transport/src/client/async_io/connect.rs | 17 +- .../src/client/blocking_io/connect.rs | 39 ++- gix-transport/src/client/blocking_io/file.rs | 3 +- .../src/client/blocking_io/http/curl/mod.rs | 42 +-- .../client/blocking_io/http/curl/remote.rs | 121 ++++---- .../src/client/blocking_io/http/mod.rs | 95 +++++- .../src/client/blocking_io/http/redirect.rs | 27 +- .../client/blocking_io/http/reqwest/mod.rs | 6 +- .../client/blocking_io/http/reqwest/remote.rs | 50 ++-- .../src/client/blocking_io/http/traits.rs | 39 +-- .../src/client/blocking_io/ssh/mod.rs | 66 ++++- gix-transport/src/client/capabilities.rs | 40 +-- gix-transport/src/client/git/async_io.rs | 4 +- gix-transport/src/client/git/blocking_io.rs | 37 +-- gix-transport/src/client/non_io_types.rs | 279 +++++++++++++----- gix-transport/src/client/traits.rs | 6 +- gix-transport/src/lib.rs | 37 +-- gix-transport/tests/client/capabilities.rs | 3 +- gix-transport/tests/client/git.rs | 30 ++ gix/src/env.rs | 11 +- gix/src/remote/connect.rs | 6 +- gix/src/remote/connection/fetch/error.rs | 8 +- gix/src/remote/connection/fetch/mod.rs | 6 +- gix/src/remote/connection/ref_map.rs | 15 +- .../no-repo/pack/clone/fail-ambiguous-host | 4 +- .../no-repo/pack/clone/fail-ambiguous-path | 3 +- .../fail-ambiguous-username/explicit-ssh | 4 +- .../fail-ambiguous-username/implicit-ssh | 4 +- .../no-repo/pack/receive/fail-ambiguous-host | 4 +- .../no-repo/pack/receive/fail-ambiguous-path | 3 +- .../fail-ambiguous-username/explicit-ssh | 4 +- .../fail-ambiguous-username/implicit-ssh | 4 +- 40 files changed, 641 insertions(+), 475 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f10c10791c8..ae79b121feb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2612,7 +2612,6 @@ dependencies = [ "pin-project-lite", "reqwest", "serde", - "thiserror 2.0.18", ] [[package]] diff --git a/gix-protocol/src/fetch/function.rs b/gix-protocol/src/fetch/function.rs index 72c0ddcc22b..cd80b4541cf 100644 --- a/gix-protocol/src/fetch/function.rs +++ b/gix-protocol/src/fetch/function.rs @@ -127,7 +127,7 @@ where let mut reader = arguments .send(transport, is_done) .await - .map_err(|err| transport_error(err, "Failed to send fetch arguments"))?; + .or_raise_erased(|| message("Failed to send fetch arguments"))?; if sideband_all { setup_remote_progress(&mut progress, &mut reader, should_interrupt); } @@ -286,16 +286,6 @@ fn add_shallow_args( Ok((shallow_commits, shallow_lock)) } -pub(crate) fn transport_error(err: crate::transport::client::Error, context: &'static str) -> Error { - use crate::transport::IsSpuriousError; - - if err.is_spurious() { - RetryableError::new(err).and_raise(message(context)).erased() - } else { - err.and_raise(message(context)).erased() - } -} - fn setup_remote_progress<'a>( progress: &mut dyn gix_features::progress::DynNestedProgress, reader: &mut Box + Unpin + 'a>, @@ -313,21 +303,3 @@ fn setup_remote_progress<'a>( } }) as HandleProgress<'a>)); } - -#[cfg(test)] -mod tests { - #[test] - fn transport_errors_keep_retryability() { - let err = super::transport_error( - crate::transport::client::Error::Io(std::io::ErrorKind::ConnectionReset.into()), - "transport failed", - ) - .into_error(); - - assert!(err.can_retry(), "the transport classifier remains visible"); - assert!( - err.downcast_any_ref::().is_some(), - "the original I/O error remains in the chain" - ); - } -} diff --git a/gix-protocol/src/fetch/response/io.rs b/gix-protocol/src/fetch/response/io.rs index f58046cb369..c9e83776ef3 100644 --- a/gix-protocol/src/fetch/response/io.rs +++ b/gix-protocol/src/fetch/response/io.rs @@ -4,7 +4,7 @@ use std::io; use crate::transport::client::async_io::ExtendedBufRead; #[crate::bisync::only_sync] use crate::transport::client::blocking_io::ExtendedBufRead; -use gix_error::{CorruptionError, ErrorExt, RetryableError, message}; +use gix_error::{CorruptionError, ErrorExt, message}; use gix_transport::{Protocol, client, client::MessageKind}; use crate::fetch::{ @@ -185,36 +185,33 @@ fn read_error(err: io::Error) -> response::Error { } fn transport_error(err: client::Error) -> response::Error { - use crate::transport::IsSpuriousError; - - let context = message("Failed to read from line reader"); - let can_retry = - !matches!(&err, client::Error::Io(err) if err.kind() == io::ErrorKind::OutOfMemory) && err.is_spurious(); - if can_retry { - RetryableError::new(err).and_raise(context).erased() - } else { - err.and_raise(context).erased() - } + err.and_raise(message("Failed to read from line reader")).erased() } #[cfg(test)] mod tests { #[test] - fn line_reader_io_uses_transport_retry_policy_except_for_memory_exhaustion() { + fn line_reader_io_preserves_classification() { let err = super::read_error(std::io::ErrorKind::ConnectionAborted.into()).into_error(); assert!( - err.can_retry(), + err.can_retry_lenient(), "connection failures remain retryable while reading packet lines" ); + assert!(!err.can_retry(), "connection failures require the lenient policy"); + assert!( + !err.is_retryable(), + "wrapping I/O does not add an explicit retry marker" + ); let err = super::read_error(std::io::ErrorKind::OutOfMemory.into()).into_error(); assert!( !err.can_retry(), - "memory exhaustion isn't retryable by transport policy" + "memory exhaustion isn't retryable by the conservative policy" + ); + assert!(err.can_retry_lenient(), "the lenient policy includes memory exhaustion"); + assert!( + err.is_resource_exhausted(), + "the allocation failure remains recognizable" ); - assert!(err.classify().any(|classification| { - classification.class() - == gix_error::Class::ResourceExhaustion(gix_error::ResourceExhaustionKind::AllocationFailure) - })); } } diff --git a/gix-protocol/src/handshake/function.rs b/gix-protocol/src/handshake/function.rs index 9c65947b077..29a74315528 100644 --- a/gix-protocol/src/handshake/function.rs +++ b/gix-protocol/src/handshake/function.rs @@ -1,7 +1,6 @@ use crate::bisync::bisync; use gix_error::{ErrorExt, ResultExt, ValidationError, message}; use gix_features::{progress, progress::Progress}; -use gix_transport::IsSpuriousError; use gix_transport::{Service, client}; use super::Error; @@ -97,14 +96,7 @@ where } Err(err) => Err(err), } - .map_err(|err| { - let context = message("Transport handshake failed"); - if err.is_spurious() { - gix_error::RetryableError::new(err).and_raise(context).erased() - } else { - err.and_raise(context).erased() - } - })?; + .or_raise_erased(|| message("Transport handshake failed"))?; if !supported_versions.is_empty() && !supported_versions.contains(&actual_protocol) { return Err(ValidationError::new(format!( diff --git a/gix-protocol/src/ls_refs.rs b/gix-protocol/src/ls_refs.rs index 1fed1a0f95d..01ee3320905 100644 --- a/gix-protocol/src/ls_refs.rs +++ b/gix-protocol/src/ls_refs.rs @@ -144,7 +144,7 @@ pub(crate) mod function { trace, ) .await - .map_err(|err| crate::fetch::function::transport_error(err, "Could not invoke ls-refs"))?; + .or_raise_erased(|| message("Could not invoke ls-refs"))?; $from_v2_refs(&mut remote_refs).await } }; @@ -244,7 +244,11 @@ pub(crate) mod function { .invoke_blocking(transport, &mut gix_features::progress::Discard, false) .expect_err("the transport write fails") .into_error(); - assert_eq!(err.can_retry(), retryable, "preserve retry policy for {kind:?}"); + assert_eq!(err.can_retry_lenient(), retryable, "preserve retry policy for {kind:?}"); + assert!( + !err.is_retryable(), + "I/O failures are classified without extra retry markers" + ); assert_eq!( err.downcast_any_ref::() .expect("retain the I/O cause") diff --git a/gix-protocol/tests/protocol/fetch/arguments.rs b/gix-protocol/tests/protocol/fetch/arguments.rs index eaec7e1ef9e..99a70d0af45 100644 --- a/gix-protocol/tests/protocol/fetch/arguments.rs +++ b/gix-protocol/tests/protocol/fetch/arguments.rs @@ -52,10 +52,7 @@ mod impls { self.stateful } - fn configure( - &mut self, - config: &dyn std::any::Any, - ) -> Result<(), Box> { + fn configure(&mut self, config: &dyn std::any::Any) -> Result<(), gix_error::Exn> { self.inner.configure(config) } } @@ -113,10 +110,7 @@ mod impls { self.stateful } - fn configure( - &mut self, - config: &dyn std::any::Any, - ) -> Result<(), Box> { + fn configure(&mut self, config: &dyn std::any::Any) -> Result<(), gix_error::Exn> { self.inner.configure(config) } } diff --git a/gix-protocol/tests/protocol/fetch/response.rs b/gix-protocol/tests/protocol/fetch/response.rs index 299a6fe510a..502fe48420c 100644 --- a/gix-protocol/tests/protocol/fetch/response.rs +++ b/gix-protocol/tests/protocol/fetch/response.rs @@ -167,7 +167,8 @@ mod v1 { #[cfg_attr(feature = "blocking-client", test)] #[cfg_attr(all(feature = "async-client", not(feature = "blocking-client")), async_std::test)] async fn all() -> crate::Result { - let (caps, _) = Capabilities::from_bytes(&b"7814e8a05a59c0cf5fb186661d1551c75d1299b5 HEAD\0multi_ack thin-pack filter side-band side-band-64k ofs-delta shallow deepen-since deepen-not deepen-relative no-progress include-tag multi_ack_detailed symref=HEAD:refs/heads/master object-format=sha1 agent=git/2.28.0"[..])?; + let (caps, _) = Capabilities::from_bytes(&b"7814e8a05a59c0cf5fb186661d1551c75d1299b5 HEAD\0multi_ack thin-pack filter side-band side-band-64k ofs-delta shallow deepen-since deepen-not deepen-relative no-progress include-tag multi_ack_detailed symref=HEAD:refs/heads/master object-format=sha1 agent=git/2.28.0"[..]) + ?; let mut args = fetch::Arguments::new( Protocol::V1, Command::Fetch.default_features(Protocol::V1, &caps), @@ -428,7 +429,8 @@ mod v2 { #[cfg_attr(feature = "blocking-client", test)] #[cfg_attr(all(feature = "async-client", not(feature = "blocking-client")), async_std::test)] async fn all() -> crate::Result { - let (caps, _) = Capabilities::from_bytes(&b"7814e8a05a59c0cf5fb186661d1551c75d1299b5 HEAD\0multi_ack thin-pack filter side-band side-band-64k ofs-delta shallow deepen-since deepen-not deepen-relative no-progress include-tag multi_ack_detailed symref=HEAD:refs/heads/master object-format=sha1 agent=git/2.28.0"[..])?; + let (caps, _) = Capabilities::from_bytes(&b"7814e8a05a59c0cf5fb186661d1551c75d1299b5 HEAD\0multi_ack thin-pack filter side-band side-band-64k ofs-delta shallow deepen-since deepen-not deepen-relative no-progress include-tag multi_ack_detailed symref=HEAD:refs/heads/master object-format=sha1 agent=git/2.28.0"[..]) + ?; let mut args = fetch::Arguments::new( Protocol::V2, Command::Fetch.default_features(Protocol::V1, &caps), diff --git a/gix-transport/Cargo.toml b/gix-transport/Cargo.toml index a8eae901043..ad7d603a3d5 100644 --- a/gix-transport/Cargo.toml +++ b/gix-transport/Cargo.toml @@ -109,7 +109,6 @@ bstr = { version = "1.12.0", default-features = false, features = [ "std", "unicode", ] } -thiserror = "2.0.18" parking_lot = { version = "0.12.4", optional = true } # for async-client diff --git a/gix-transport/src/client/async_io/connect.rs b/gix-transport/src/client/async_io/connect.rs index 01669388672..ca3e371b5c4 100644 --- a/gix-transport/src/client/async_io/connect.rs +++ b/gix-transport/src/client/async_io/connect.rs @@ -3,6 +3,7 @@ pub use crate::client::non_io_types::connect::{Error, Options}; #[cfg(feature = "async-std")] pub(crate) mod function { use crate::client::{async_io::Transport, git::async_io::Connection, non_io_types::connect::Error}; + use gix_error::{ErrorExt, ResultExt, message}; /// A general purpose connector connecting to a repository identified by the given `url`. /// @@ -15,14 +16,16 @@ pub(crate) mod function { Url: TryInto, E: std::error::Error + Send + Sync + 'static, { - let mut url = url.try_into().map_err(gix_error::Error::from_error)?; + let mut url = url.try_into().or_raise(|| message("Could not parse URL"))?; Ok(match url.scheme { gix_url::Scheme::Git => { if url.user().is_some() { - return Err(Error::UnsupportedUrlTokens { - url: url.to_bstring(), - scheme: url.scheme, - }); + return Err(message!( + "The url {:?} contains information that would not be used by the {} protocol", + url.to_bstring(), + url.scheme + ) + .raise()); } let path = std::mem::take(&mut url.path); Box::new( @@ -34,10 +37,10 @@ pub(crate) mod function { options.trace, ) .await - .map_err(|e| Box::new(e) as Box)?, + .or_raise(|| message("connection failed"))?, ) } - scheme => return Err(Error::UnsupportedScheme(scheme)), + scheme => return Err(message!("The '{scheme}' protocol is currently unsupported").raise()), }) } } diff --git a/gix-transport/src/client/blocking_io/connect.rs b/gix-transport/src/client/blocking_io/connect.rs index 4e8ab1a569c..eadf0f70ff2 100644 --- a/gix-transport/src/client/blocking_io/connect.rs +++ b/gix-transport/src/client/blocking_io/connect.rs @@ -6,6 +6,7 @@ pub(crate) mod function { #[cfg(all(feature = "http-client-reqwest", not(feature = "http-client-curl")))] use crate::client::blocking_io::http::reqwest::Remote as Reqwest; use crate::client::{blocking_io::Transport, non_io_types::connect::Error}; + use gix_error::{ErrorExt, ResultExt, message}; /// A general purpose connector connecting to a repository identified by the given `url`. /// @@ -21,33 +22,37 @@ pub(crate) mod function { Url: TryInto, E: std::error::Error + Send + Sync + 'static, { - let mut url = url.try_into().map_err(gix_error::Error::from_error)?; + let mut url = url.try_into().or_raise(|| message("Could not parse URL"))?; Ok(match url.scheme { gix_url::Scheme::Ext | gix_url::Scheme::Helper(_) | gix_url::Scheme::HelperUrl(_) => { - return Err(Error::UnsupportedScheme(url.scheme)); + return Err(message!("The '{}' protocol is currently unsupported", url.scheme).raise()); } gix_url::Scheme::File => { if url.user().is_some() || url.password().is_some() || url.host().is_some() || url.port.is_some() { - return Err(Error::UnsupportedUrlTokens { - url: url.to_bstring(), - scheme: url.scheme, - }); + return Err(message!( + "The url {:?} contains information that would not be used by the {} protocol", + url.to_bstring(), + url.scheme + ) + .raise()); } Box::new( crate::client::blocking_io::file::connect(url.path, options.version, options.trace) - .map_err(|e| Box::new(e) as Box)?, + .or_raise(|| message("connection failed"))?, ) } gix_url::Scheme::Ssh => Box::new({ crate::client::blocking_io::ssh::connect(url, options.version, options.ssh, options.trace) - .map_err(|e| Box::new(e) as Box)? + .or_raise(|| message("connection failed"))? }), gix_url::Scheme::Git => { if url.user().is_some() { - return Err(Error::UnsupportedUrlTokens { - url: url.to_bstring(), - scheme: url.scheme, - }); + return Err(message!( + "The url {:?} contains information that would not be used by the {} protocol", + url.to_bstring(), + url.scheme + ) + .raise()); } Box::new({ let path = std::mem::take(&mut url.path); @@ -58,11 +63,17 @@ pub(crate) mod function { url.port, options.trace, ) - .map_err(|e| Box::new(e) as Box)? + .or_raise(|| message("connection failed"))? }) } #[cfg(not(any(feature = "http-client-curl", feature = "http-client-reqwest")))] - gix_url::Scheme::Https | gix_url::Scheme::Http => return Err(Error::CompiledWithoutHttp(url.scheme)), + gix_url::Scheme::Https | gix_url::Scheme::Http => { + return Err(message!( + "'{}' is not compiled in. Compile with the 'http-client-curl' or 'http-client-reqwest' cargo feature", + url.scheme + ) + .raise()); + } #[cfg(feature = "http-client-curl")] gix_url::Scheme::Https | gix_url::Scheme::Http => Box::new( crate::client::blocking_io::http::connect::(url, options.version, options.trace), diff --git a/gix-transport/src/client/blocking_io/file.rs b/gix-transport/src/client/blocking_io/file.rs index 159e2053dfd..496fb8ea718 100644 --- a/gix-transport/src/client/blocking_io/file.rs +++ b/gix-transport/src/client/blocking_io/file.rs @@ -1,7 +1,6 @@ use std::{ any::Any, borrow::Cow, - error::Error, ffi::OsString, io::Write, process::{self, Stdio}, @@ -177,7 +176,7 @@ impl client::TransportWithoutIO for SpawnProcessOnDemand { true } - fn configure(&mut self, _config: &dyn Any) -> Result<(), Box> { + fn configure(&mut self, _config: &dyn Any) -> Result<(), gix_error::Exn> { Ok(()) } } diff --git a/gix-transport/src/client/blocking_io/http/curl/mod.rs b/gix-transport/src/client/blocking_io/http/curl/mod.rs index 45f19739ab8..e870cae893b 100644 --- a/gix-transport/src/client/blocking_io/http/curl/mod.rs +++ b/gix-transport/src/client/blocking_io/http/curl/mod.rs @@ -6,6 +6,7 @@ use std::{ thread, }; +use gix_error::{ResultExt, message}; use gix_features::io; use parking_lot::Mutex; @@ -25,29 +26,9 @@ pub struct Options { /// The error returned by the 'remote' helper, a purely internal construct to perform http requests. /// /// It can be used for downcasting errors, which are boxed to hide the actual implementation. -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] -pub enum Error { - #[error(transparent)] - Curl(#[from] curl::Error), - #[error(transparent)] - Redirect(#[from] http::redirect::Error), - #[error("Could not finish reading all data to post to the remote")] - ReadPostBody(#[from] std::io::Error), - #[error(transparent)] - Authenticate(gix_error::Error), -} - -impl crate::IsSpuriousError for Error { - fn is_spurious(&self) -> bool { - match self { - Error::Curl(err) => curl_is_spurious(err), - _ => false, - } - } -} +pub type Error = gix_error::Exn; -pub(crate) fn curl_is_spurious(err: &curl::Error) -> bool { +pub(crate) fn curl_is_retryable(err: &curl::Error) -> bool { err.is_couldnt_connect() || err.is_couldnt_resolve_proxy() || err.is_couldnt_resolve_host() @@ -83,7 +64,7 @@ impl Curl { self.req = req; self.res = res; self.redirected_base_url = redirected_base_url; - err_that_brought_thread_down.into() + err_that_brought_thread_down } fn make_request( @@ -95,7 +76,15 @@ impl Curl { ) -> Result, http::Error> { let mut list = curl::easy::List::new(); for header in headers { - list.append(header.as_ref())?; + list.append(header.as_ref()) + .map_err(|err| { + if curl_is_retryable(&err) { + gix_error::Error::from_error(gix_error::RetryableError::new(err)) + } else { + gix_error::Error::from_error(err) + } + }) + .or_raise(|| message("Could not add HTTP header"))?; } if self .req @@ -163,10 +152,7 @@ impl http::Http for Curl { self.make_request(url, base_url, headers, Some(body)) } - fn configure( - &mut self, - config: &dyn std::any::Any, - ) -> Result<(), Box> { + fn configure(&mut self, config: &dyn std::any::Any) -> Result<(), gix_error::Exn> { if let Some(config) = config.downcast_ref::() { self.config = config.clone(); } diff --git a/gix-transport/src/client/blocking_io/http/curl/remote.rs b/gix-transport/src/client/blocking_io/http/curl/remote.rs index 34a32e11854..a68fb5bf61a 100644 --- a/gix-transport/src/client/blocking_io/http/curl/remote.rs +++ b/gix-transport/src/client/blocking_io/http/curl/remote.rs @@ -11,18 +11,35 @@ use std::{ use bstr::ByteSlice; use curl::easy::{Auth, Easy2}; +use gix_error::{ErrorExt, OptionExt, ResultExt, message}; use gix_features::io::pipe; use parking_lot::Mutex; use crate::client::blocking_io::http::{ self, curl::Error, - curl::curl_is_spurious, + curl::curl_is_retryable, options::{FollowRedirects, HttpVersion, ProxyAuthMethod, SslVersion}, redirect::{self, Action as RedirectAction}, traits::PostBodyDataKind, }; +fn classify_curl(err: curl::Error) -> gix_error::Error { + if curl_is_retryable(&err) { + gix_error::Error::from_error(gix_error::RetryableError::new(err)) + } else { + gix_error::Error::from_error(err) + } +} + +macro_rules! curl { + ($expr:expr) => { + $expr + .map_err(classify_curl) + .or_raise(|| message("Curl operation failed"))? + }; +} + enum StreamOrBuffer { Stream(pipe::Reader), Buffer(std::io::Cursor>), @@ -89,22 +106,22 @@ impl Handler { self.redirect_action = redirect_action; } - fn parse_status_inner(data: &[u8]) -> Result> { + fn parse_status_inner(data: &[u8]) -> Result { let code = data .split(|b| *b == b' ') .nth(1) - .ok_or("Expected HTTP/ STATUS")?; - let code = std::str::from_utf8(code)?; - code.parse().map_err(Into::into) + .ok_or_raise_erased(|| message("Expected HTTP/ STATUS"))?; + let code = std::str::from_utf8(code).or_erased()?; + code.parse::().or_erased() } - fn parse_status(data: &[u8], follow: FollowRedirects) -> Option<(usize, Box)> { + fn parse_status(data: &[u8], follow: FollowRedirects) -> Option<(usize, gix_error::Exn)> { let valid_end = match follow { FollowRedirects::Initial | FollowRedirects::All => 308, FollowRedirects::None => 299, }; match Self::parse_status_inner(data) { Ok(status) if !(200..=valid_end).contains(&status) => { - Some((status, format!("Received HTTP status {status}").into())) + Some((status, message!("Received HTTP status {status}").raise_erased())) } Ok(_) => None, Err(err) => Some((500, err)), @@ -311,7 +328,7 @@ impl curl::easy::Handler for Handler { if self.redirect_action == RedirectAction::RejectConfiguredHeaders && is_redirect_status(status) { Some(( status, - "refusing to follow redirect after request headers were configured".into(), + message("refusing to follow redirect after request headers were configured").raise_erased(), )) } else { Handler::parse_status(data, self.follow) @@ -331,7 +348,7 @@ impl curl::easy::Handler for Handler { } else { io::ErrorKind::Other }, - err, + err.into_error(), ))) .ok(); } @@ -370,8 +387,8 @@ pub fn new() -> Worker { let handle = std::thread::spawn(move || -> Result<(), Error> { let mut handle = Easy2::new(Handler::default()); // We don't wait for the possibility for pipelining to become clear, and curl tries to reuse connections by default anyway. - handle.pipewait(false)?; - handle.tcp_keepalive(true)?; + curl!(handle.pipewait(false)); + curl!(handle.tcp_keepalive(true)); let mut follow = None; @@ -403,39 +420,39 @@ pub fn new() -> Worker { { let redirected_base_url = redirected_base_url_shared.lock().clone(); let effective_url = redirect::swap_tails(redirected_base_url.as_deref(), &base_url, url.clone()); - handle.url(&effective_url)?; + curl!(handle.url(&effective_url)); - handle.post(upload_body_kind.is_some())?; + curl!(handle.post(upload_body_kind.is_some())); let has_extra_headers = !extra_headers.is_empty(); for header in extra_headers { - headers.append(&header)?; + curl!(headers.append(&header)); } // needed to avoid sending Expect: 100-continue, which adds another response and only CURL wants that - headers.append("Expect:")?; - handle.verbose(verbose)?; + curl!(headers.append("Expect:")); + curl!(handle.verbose(verbose)); if let Some(ca_info) = ssl_ca_info { - handle.cainfo(ca_info)?; + curl!(handle.cainfo(ca_info)); } if let Some(ref mut curl_options) = backend.as_ref().and_then(|backend| backend.lock().ok()) && let Some(opts) = curl_options.downcast_mut::() && let Some(enabled) = opts.schannel_check_revoke { - handle.ssl_options(curl::easy::SslOpt::new().no_revoke(!enabled))?; + curl!(handle.ssl_options(curl::easy::SslOpt::new().no_revoke(!enabled))); } if let Some(ssl_version) = ssl_version { let (min, max) = ssl_version.min_max(); if min == max { - handle.ssl_version(to_curl_ssl_version(min))?; + curl!(handle.ssl_version(to_curl_ssl_version(min))); } else { - handle.ssl_min_max_version(to_curl_ssl_version(min), to_curl_ssl_version(max))?; + curl!(handle.ssl_min_max_version(to_curl_ssl_version(min), to_curl_ssl_version(max))); } } - handle.ssl_verify_peer(ssl_verify)?; - handle.ssl_verify_host(ssl_verify)?; + curl!(handle.ssl_verify_peer(ssl_verify)); + curl!(handle.ssl_verify_host(ssl_verify)); if let Some(http_version) = http_version { let version = match http_version { @@ -451,7 +468,7 @@ pub fn new() -> Worker { let mut proxy_auth_action = None; if let Some(proxy) = proxy { - handle.proxy(&proxy)?; + curl!(handle.proxy(&proxy)); let proxy_type = if proxy.starts_with("socks5h") { curl::easy::ProxyType::Socks5Hostname } else if proxy.starts_with("socks5") { @@ -463,26 +480,26 @@ pub fn new() -> Worker { } else { curl::easy::ProxyType::Http }; - handle.proxy_type(proxy_type)?; + curl!(handle.proxy_type(proxy_type)); if let Some((obtain_creds_action, authenticate)) = proxy_authenticate { let creds = authenticate.lock().expect("no panics in other threads")(obtain_creds_action) - .map_err(|err| Error::Authenticate(err.into_error()))? + .or_raise(|| message("Could not obtain proxy credentials"))? .expect("action to fetch credentials"); - handle.proxy_username(&creds.identity.username)?; - handle.proxy_password(&creds.identity.password)?; + curl!(handle.proxy_username(&creds.identity.username)); + curl!(handle.proxy_password(&creds.identity.password)); proxy_auth_action = Some((creds.next, authenticate)); } } if let Some(no_proxy) = no_proxy { - handle.noproxy(&no_proxy)?; + curl!(handle.noproxy(&no_proxy)); } if let Some(user_agent) = user_agent { - handle.useragent(&user_agent)?; + curl!(handle.useragent(&user_agent)); } - handle.transfer_encoding(false)?; + curl!(handle.transfer_encoding(false)); if let Some(timeout) = connect_timeout { - handle.connect_timeout(timeout)?; + curl!(handle.connect_timeout(timeout)); } { let mut auth = Auth::new(); @@ -499,13 +516,13 @@ pub fn new() -> Worker { ProxyAuthMethod::Negotiate => auth.digest_ie(true), ProxyAuthMethod::Ntlm => auth.ntlm(true), }; - handle.proxy_auth(&auth)?; + curl!(handle.proxy_auth(&auth)); } - handle.tcp_keepalive(true)?; + curl!(handle.tcp_keepalive(true)); if low_speed_time_seconds > 0 && low_speed_limit_bytes_per_second > 0 { - handle.low_speed_limit(low_speed_limit_bytes_per_second)?; - handle.low_speed_time(Duration::from_secs(low_speed_time_seconds))?; + curl!(handle.low_speed_limit(low_speed_limit_bytes_per_second)); + curl!(handle.low_speed_time(Duration::from_secs(low_speed_time_seconds))); } let (receive_data, receive_headers, send_body, mut receive_body) = { let handler = handle.get_mut(); @@ -530,7 +547,7 @@ pub fn new() -> Worker { redirect_action, ); } - handle.follow_location(redirect_action == RedirectAction::Follow)?; + curl!(handle.follow_location(redirect_action == RedirectAction::Follow)); if *follow == FollowRedirects::Initial { *follow = FollowRedirects::None; @@ -551,13 +568,15 @@ pub fn new() -> Worker { Some(PostBodyDataKind::Unbounded) | None => StreamOrBuffer::Stream(receive_body), Some(PostBodyDataKind::BoundedAndFitsIntoMemory) => { let mut buf = Vec::::with_capacity(512); - receive_body.read_to_end(&mut buf)?; - handle.post_field_size(buf.len() as u64)?; + receive_body + .read_to_end(&mut buf) + .or_raise(|| message("Could not finish reading all data to post to the remote"))?; + curl!(handle.post_field_size(buf.len() as u64)); drop(receive_body); StreamOrBuffer::Buffer(std::io::Cursor::new(buf)) } }); - handle.http_headers(headers)?; + curl!(handle.http_headers(headers)); if let Err(err) = handle.perform() { let handler = handle.get_mut(); @@ -567,7 +586,7 @@ pub fn new() -> Worker { authenticate.lock().expect("no panics in other threads")(action.erase()).ok(); } let err = Err(io::Error::new( - if curl_is_spurious(&err) { + if curl_is_retryable(&err) { std::io::ErrorKind::ConnectionReset } else { std::io::ErrorKind::Other @@ -590,9 +609,7 @@ pub fn new() -> Worker { (None, None) => {} } } else { - let actual_url = handle - .effective_url()? - .expect("effective url is present and valid UTF-8"); + let actual_url = curl!(handle.effective_url()).expect("effective url is present and valid UTF-8"); if actual_url != effective_url { let new_base_url = redirect::base_url(actual_url, &base_url, url)?; *redirected_base_url_shared.lock() = Some(new_base_url); @@ -605,7 +622,7 @@ pub fn new() -> Worker { } else { action.erase() }) - .map_err(|err| Error::Authenticate(err.into_error()))?; + .or_raise(|| message("Could not update proxy credentials"))?; } handler.reset(); handler.receive_body.take(); @@ -636,22 +653,6 @@ fn is_redirect_status(status: usize) -> bool { (300..=308).contains(&status) } -impl From for http::Error { - fn from(err: Error) -> Self { - http::Error::Detail { - description: err.to_string(), - } - } -} - -impl From for http::Error { - fn from(err: curl::Error) -> Self { - http::Error::Detail { - description: err.to_string(), - } - } -} - #[cfg(test)] mod authentication_tests { use curl::easy::Handler as _; diff --git a/gix-transport/src/client/blocking_io/http/mod.rs b/gix-transport/src/client/blocking_io/http/mod.rs index fdcb04fe825..f9586b46ebc 100644 --- a/gix-transport/src/client/blocking_io/http/mod.rs +++ b/gix-transport/src/client/blocking_io/http/mod.rs @@ -8,6 +8,7 @@ use std::{ use base64::Engine; use bstr::BStr; +use gix_error::{ErrorExt, message}; pub use traits::{Error, GetResponse, Http, PostBodyDataKind, PostResponse}; use crate::{ @@ -294,11 +295,13 @@ impl Transport { name.eq_ignore_ascii_case("content-type") && value.trim() == wanted_content_type }) }) { - return Err(client::Error::Http(Error::Detail { - description: format!( + return Err(client::Error::Http( + message!( "Didn't find '{wanted_content_type}' header to indicate 'smart' protocol, and 'dumb' protocol is not supported." - ), - })); + ) + .raise() + .into_error(), + )); } Ok(()) } @@ -348,7 +351,7 @@ impl client::TransportWithoutIO for Transport { false } - fn configure(&mut self, config: &dyn Any) -> Result<(), Box> { + fn configure(&mut self, config: &dyn Any) -> Result<(), gix_error::Exn> { self.http.configure(config) } } @@ -390,7 +393,7 @@ impl blocking_io::Transport for Transport { .get(url.as_ref(), &self.url, static_headers.iter().chain(&dynamic_headers)) .map_err(|err| { self.sync_redirected_base_url(); - client::Error::from(err) + client::Error::Http(err.into_error()) })?; if let Err(err) = >::check_content_type(service, "advertisement", headers) { const MAX_ERROR_BODY_DRAIN_BYTES: u64 = 1024 * 1024; @@ -418,13 +421,15 @@ impl blocking_io::Transport for Transport { if let Some(announced_service) = line.as_bstr().strip_prefix(b"# service=") { if announced_service != service.as_str().as_bytes() { - return Err(client::Error::Http(Error::Detail { - description: format!( + return Err(client::Error::Http( + message!( "Expected to see service {:?}, but got {:?}", service.as_str(), announced_service - ), - })); + ) + .raise() + .into_error(), + )); } line_reader.as_read().read_to_end(&mut Vec::new())?; @@ -476,7 +481,7 @@ impl blocking_io::Transport for Transport { .post(&url, &self.url, all_headers, write_mode.into()) .map_err(|err| { self.sync_redirected_base_url(); - client::Error::from(err) + client::Error::Http(err.into_error()) })?; self.sync_redirected_base_url(); let line_provider = self @@ -582,3 +587,71 @@ pub fn connect(url: gix_url::Url, desired_version: Protocol, /// pub mod redirect; + +#[cfg(test)] +mod tests { + use super::*; + use crate::client::blocking_io::Transport as _; + + struct FailingHttp; + + impl Http for FailingHttp { + type Headers = std::io::Empty; + type ResponseBody = std::io::Empty; + type PostBody = std::io::Sink; + + fn get( + &mut self, + _url: &str, + _base_url: &str, + _headers: impl IntoIterator>, + ) -> Result, gix_error::Exn> { + Err(gix_error::RetryableError::new(message("temporary backend failure")).and_raise(message("GET failed"))) + } + + fn post( + &mut self, + _url: &str, + _base_url: &str, + _headers: impl IntoIterator>, + _body: PostBodyDataKind, + ) -> Result, gix_error::Exn> + { + Err(std::io::Error::from(std::io::ErrorKind::ConnectionRefused).and_raise(message("POST failed"))) + } + + fn configure(&mut self, _config: &dyn Any) -> Result<(), gix_error::Exn> { + Ok(()) + } + } + + #[test] + fn backend_failures_remain_http_errors() { + let mut transport = Transport::new_http( + FailingHttp, + "http://localhost/repo".try_into().expect("valid URL"), + Protocol::V2, + false, + ); + let get_error = transport + .handshake(Service::UploadPack, &[]) + .err() + .expect("GET fails in the backend"); + transport.service = Some(Service::UploadPack); + let post_error = transport + .request(client::WriteMode::Binary, MessageKind::Flush, false) + .err() + .expect("POST fails in the backend"); + + for err in [get_error, post_error] { + assert!( + matches!(err, client::Error::Http(_)), + "HTTP backend failures retain their transport error variant: {err:?}" + ); + assert!( + gix_error::can_retry_lenient(&err), + "retryable backend causes survive conversion: {err:?}" + ); + } + } +} diff --git a/gix-transport/src/client/blocking_io/http/redirect.rs b/gix-transport/src/client/blocking_io/http/redirect.rs index 106bd3898fb..754894a9283 100644 --- a/gix-transport/src/client/blocking_io/http/redirect.rs +++ b/gix-transport/src/client/blocking_io/http/redirect.rs @@ -1,12 +1,7 @@ +use gix_error::{ErrorExt, message}; + /// The error provided when redirection went beyond what we deem acceptable. -#[derive(Debug, thiserror::Error)] -#[error( - "Redirect url {redirect_url:?} could not be reconciled with original url {expected_url} as the scheme is insecure or they don't share the same suffix" -)] -pub struct Error { - redirect_url: String, - expected_url: String, -} +pub type Error = gix_error::Exn; #[derive(Default, Clone, Copy, Debug, Eq, PartialEq)] pub(crate) enum Action { @@ -75,16 +70,18 @@ pub(crate) fn base_url(redirect_url: &str, base_url: &str, url: String) -> Resul .strip_prefix(base_url) .expect("BUG: caller assures `base_url` is subset of `url`"); if !scheme_is_safe(redirect_url, base_url) { - return Err(Error { - redirect_url: redirect_url.into(), - expected_url: url, - }); + return Err(message!( + "Redirect url {redirect_url:?} could not be reconciled with original url {url} as the scheme is insecure or they don't share the same suffix" + ) + .raise()); } redirect_url .strip_suffix(tail) - .ok_or_else(|| Error { - redirect_url: redirect_url.into(), - expected_url: url, + .ok_or_else(|| { + message!( + "Redirect url {redirect_url:?} could not be reconciled with original url {url} as the scheme is insecure or they don't share the same suffix" + ) + .raise() }) .map(ToOwned::to_owned) } diff --git a/gix-transport/src/client/blocking_io/http/reqwest/mod.rs b/gix-transport/src/client/blocking_io/http/reqwest/mod.rs index 718975d33a8..1398faeea4a 100644 --- a/gix-transport/src/client/blocking_io/http/reqwest/mod.rs +++ b/gix-transport/src/client/blocking_io/http/reqwest/mod.rs @@ -14,10 +14,8 @@ pub struct Remote { /// A function to configure a single request prior to sending it, support most complex configuration beyond what's possible with /// basic `git` http configuration. -pub type ConfigureRequestFn = dyn FnMut(&mut reqwest::blocking::Request) -> Result<(), Box> - + Send - + Sync - + 'static; +pub type ConfigureRequestFn = + dyn FnMut(&mut reqwest::blocking::Request) -> Result<(), gix_error::Exn> + Send + Sync + 'static; /// Options to configure the reqwest HTTP handler. #[derive(Default)] diff --git a/gix-transport/src/client/blocking_io/http/reqwest/remote.rs b/gix-transport/src/client/blocking_io/http/reqwest/remote.rs index 70eb70035e5..706a6c21e2c 100644 --- a/gix-transport/src/client/blocking_io/http/reqwest/remote.rs +++ b/gix-transport/src/client/blocking_io/http/reqwest/remote.rs @@ -5,6 +5,7 @@ use std::{ sync::Arc, }; +use gix_error::{ResultExt, message}; use gix_features::io::pipe; use parking_lot::Mutex; @@ -17,27 +18,13 @@ use crate::client::blocking_io::http::{ }; /// The error returned by the 'remote' helper, a purely internal construct to perform http requests. -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] -pub enum Error { - #[error(transparent)] - Reqwest(#[from] reqwest::Error), - #[error("Could not finish reading all data to post to the remote")] - ReadPostBody(#[from] std::io::Error), - #[error("Request configuration failed")] - ConfigureRequest(#[from] Box), - #[error(transparent)] - Redirect(#[from] redirect::Error), -} +pub type Error = gix_error::Exn; -impl crate::IsSpuriousError for Error { - fn is_spurious(&self) -> bool { - match self { - Error::Reqwest(err) => { - err.is_timeout() || err.is_connect() || err.status().is_some_and(|status| status.is_server_error()) - } - _ => false, - } +fn classify_reqwest(err: reqwest::Error) -> gix_error::Error { + if err.is_timeout() || err.is_connect() || err.status().is_some_and(|status| status.is_server_error()) { + gix_error::Error::from_error(gix_error::RetryableError::new(err)) + } else { + gix_error::Error::from_error(err) } } @@ -105,7 +92,9 @@ impl Default for Remote { } } })) - .build()?; + .build() + .map_err(classify_reqwest) + .or_raise(|| message("Could not initialize HTTP client"))?; for Request { url, @@ -142,20 +131,25 @@ impl Default for Remote { req_builder = match upload_body_kind { Some(PostBodyDataKind::BoundedAndFitsIntoMemory) => { let mut buf = Vec::::with_capacity(512); - post_body_rx.read_to_end(&mut buf)?; + post_body_rx + .read_to_end(&mut buf) + .or_raise(|| message("Could not finish reading all data to post to the remote"))?; req_builder.body(buf) } Some(PostBodyDataKind::Unbounded) => req_builder.body(reqwest::blocking::Body::new(post_body_rx)), None => req_builder, }; - let mut req = req_builder.build()?; + let mut req = req_builder + .build() + .map_err(classify_reqwest) + .or_raise(|| message("Could not build HTTP request"))?; let mut has_configure_request = false; if let Some(ref mut request_options) = config.backend.as_ref().and_then(|backend| backend.lock().ok()) && let Some(options) = request_options.downcast_mut::() && let Some(configure_request) = &mut options.configure_request { has_configure_request = true; - configure_request(&mut req)?; + configure_request(&mut req).or_raise(|| message("Request configuration failed"))?; } let follow = follow.get_or_insert(config.follow_redirects); @@ -209,7 +203,7 @@ impl Default for Remote { // Preserve the `reqwest::Error` as the source so the underlying cause -- e.g. a // connection or TLS failure -- isn't lost. It was previously stringified, which // dead-ended `source()` and hid the real reason a request failed. See #2140. - None => std::io::Error::other(err), + None => std::io::Error::other(classify_reqwest(err)), }; headers_tx.channel.send(Err(err)).ok(); continue; @@ -271,9 +265,7 @@ impl Remote { .expect("handler thread should never panic") .expect_err("something should have gone wrong with curl (we join on error only)"); *self = Remote::default(); - http::Error::InitHttpClient { - source: Box::new(err_that_brought_thread_down), - } + err_that_brought_thread_down.raise(message("Could not initialize the http client")) } fn make_request( @@ -366,7 +358,7 @@ impl http::Http for Remote { self.make_request(url, base_url, headers, Some(post_body_kind)) } - fn configure(&mut self, config: &dyn Any) -> Result<(), Box> { + fn configure(&mut self, config: &dyn Any) -> Result<(), gix_error::Exn> { if let Some(config) = config.downcast_ref::() { self.config = config.clone(); } diff --git a/gix-transport/src/client/blocking_io/http/traits.rs b/gix-transport/src/client/blocking_io/http/traits.rs index 5c5009ec407..4075dc28532 100644 --- a/gix-transport/src/client/blocking_io/http/traits.rs +++ b/gix-transport/src/client/blocking_io/http/traits.rs @@ -1,39 +1,7 @@ use crate::client::WriteMode; /// The error used by the [Http] trait. -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] -pub enum Error { - #[error("Could not initialize the http client")] - InitHttpClient { - source: Box, - }, - #[error("{description}")] - Detail { description: String }, - #[error("An IO error occurred while uploading the body of a POST request")] - PostBody(#[from] std::io::Error), -} - -impl crate::IsSpuriousError for Error { - fn is_spurious(&self) -> bool { - match self { - Error::PostBody(err) => err.is_spurious(), - #[cfg(any(feature = "http-client-reqwest", feature = "http-client-curl"))] - Error::InitHttpClient { source } => { - #[cfg(feature = "http-client-curl")] - if let Some(err) = source.downcast_ref::() { - return err.is_spurious(); - } - #[cfg(feature = "http-client-reqwest")] - if let Some(err) = source.downcast_ref::() { - return err.is_spurious(); - } - false - } - _ => false, - } - } -} +pub type Error = gix_error::Exn; /// The return value of [`Http::get()`]. pub struct GetResponse { @@ -128,10 +96,7 @@ pub trait Http { /// Pass `config` which can deserialize in the implementation's configuration, as documented separately. /// /// The caller must know how that `config` data looks like for the intended implementation. - fn configure( - &mut self, - config: &dyn std::any::Any, - ) -> Result<(), Box>; + fn configure(&mut self, config: &dyn std::any::Any) -> Result<(), gix_error::Exn>; /// Return the effective base URL after a backend accepted a redirect, if available. fn redirected_base_url(&self) -> Option { diff --git a/gix-transport/src/client/blocking_io/ssh/mod.rs b/gix-transport/src/client/blocking_io/ssh/mod.rs index ce9b45e0cff..8e14954da6e 100644 --- a/gix-transport/src/client/blocking_io/ssh/mod.rs +++ b/gix-transport/src/client/blocking_io/ssh/mod.rs @@ -8,16 +8,36 @@ use gix_url::{ArgumentSafety::*, Url}; use crate::{Protocol, client::blocking_io::file::SpawnProcessOnDemand}; /// The error used in [`connect()`]. -#[derive(Debug, thiserror::Error)] +#[derive(Debug)] #[expect(missing_docs)] pub enum Error { - #[error("The scheme in \"{}\" is not usable for an ssh connection", .0.to_bstring())] UnsupportedScheme(gix_url::Url), - #[error("Host name '{host}' could be mistaken for a command-line argument")] AmbiguousHostName { host: String }, } -impl crate::IsSpuriousError for Error {} +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::UnsupportedScheme(url) => write!( + f, + "The scheme in \"{}\" is not usable for an ssh connection", + url.to_bstring() + ), + Error::AmbiguousHostName { host } => { + write!(f, "Host name '{host}' could be mistaken for a command-line argument") + } + } + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::AmbiguousHostName { .. } => Some(&crate::INVALID_INPUT), + Error::UnsupportedScheme(_) => None, + } + } +} /// The kind of SSH programs we have built-in support for. /// @@ -43,14 +63,15 @@ pub mod invocation { use std::ffi::OsString; /// The error returned when producing ssh invocation arguments based on a selected invocation kind. - #[derive(Debug, thiserror::Error)] + #[derive(Debug)] #[expect(missing_docs)] pub enum Error { - #[error("Username '{user}' could be mistaken for a command-line argument")] - AmbiguousUserName { user: String }, - #[error("Host name '{host}' could be mistaken for a command-line argument")] - AmbiguousHostName { host: String }, - #[error("The 'Simple' ssh variant doesn't support {function}")] + AmbiguousUserName { + user: String, + }, + AmbiguousHostName { + host: String, + }, Unsupported { /// The simple command that should have been invoked. command: OsString, @@ -58,6 +79,31 @@ pub mod invocation { function: &'static str, }, } + + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::AmbiguousUserName { user } => { + write!(f, "Username '{user}' could be mistaken for a command-line argument") + } + Error::AmbiguousHostName { host } => { + write!(f, "Host name '{host}' could be mistaken for a command-line argument") + } + Error::Unsupported { function, .. } => { + write!(f, "The 'Simple' ssh variant doesn't support {function}") + } + } + } + } + + impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::AmbiguousUserName { .. } | Error::AmbiguousHostName { .. } => Some(&crate::INVALID_INPUT), + Error::Unsupported { .. } => None, + } + } + } } /// diff --git a/gix-transport/src/client/capabilities.rs b/gix-transport/src/client/capabilities.rs index 0e1a005e195..06dbf2f981b 100644 --- a/gix-transport/src/client/capabilities.rs +++ b/gix-transport/src/client/capabilities.rs @@ -1,26 +1,11 @@ use bstr::{BStr, BString, ByteSlice}; +use gix_error::{ErrorExt, message}; -use crate::Protocol; #[cfg(any(feature = "blocking-client", feature = "async-client"))] -use crate::client; +use crate::{Protocol, client}; /// The error used in [`Capabilities::from_bytes()`] and [`Capabilities::from_lines()`]. -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] -pub enum Error { - #[error("Capabilities were missing entirely as there was no 0 byte")] - MissingDelimitingNullByte, - #[error("there was not a single capability behind the delimiter")] - NoCapabilities, - #[error("a version line was expected, but none was retrieved")] - MissingVersionLine, - #[error("expected 'version X', got {0:?}")] - MalformattedVersionLine(BString), - #[error("Got unsupported version {actual:?}, expected {}", *desired as u8)] - UnsupportedVersion { desired: Protocol, actual: BString }, - #[error("An IO error occurred while reading V2 lines")] - Io(#[from] std::io::Error), -} +pub type Error = gix_error::Exn; /// A structure to represent multiple [capabilities](Capability) or features supported by the server. /// @@ -83,9 +68,11 @@ impl Capabilities { /// /// Useful in case they are encoded within a `ref` behind a null byte. pub fn from_bytes(bytes: &[u8]) -> Result<(Capabilities, usize), Error> { - let delimiter_pos = bytes.find_byte(0).ok_or(Error::MissingDelimitingNullByte)?; + let delimiter_pos = bytes + .find_byte(0) + .ok_or_else(|| message("Capabilities were missing entirely as there was no 0 byte").raise())?; if delimiter_pos + 1 == bytes.len() { - return Err(Error::NoCapabilities); + return Err(message("there was not a single capability behind the delimiter").raise()); } let capabilities = &bytes[delimiter_pos + 1..]; Ok(( @@ -105,20 +92,19 @@ impl Capabilities { /// in a non-blocking fashion. pub fn from_lines(lines_buf: BString) -> Result { let mut lines = <_ as bstr::ByteSlice>::lines(lines_buf.as_slice().trim()); - let version_line = lines.next().ok_or(Error::MissingVersionLine)?; + let version_line = lines + .next() + .ok_or_else(|| message("a version line was expected, but none was retrieved").raise())?; let (name, value) = version_line.split_at( version_line .find(b" ") - .ok_or_else(|| Error::MalformattedVersionLine(version_line.to_owned().into()))?, + .ok_or_else(|| message!("expected 'version X', got {version_line:?}").raise())?, ); if name != b"version" { - return Err(Error::MalformattedVersionLine(version_line.to_owned().into())); + return Err(message!("expected 'version X', got {version_line:?}").raise()); } if value != b" 2" { - return Err(Error::UnsupportedVersion { - desired: Protocol::V2, - actual: value.to_owned().into(), - }); + return Err(message!("Got unsupported version {value:?}, expected 2").raise()); } Ok(Capabilities { value_sep: b'\n', diff --git a/gix-transport/src/client/git/async_io.rs b/gix-transport/src/client/git/async_io.rs index 53fbfd58dd8..2d8c6f33812 100644 --- a/gix-transport/src/client/git/async_io.rs +++ b/gix-transport/src/client/git/async_io.rs @@ -1,4 +1,4 @@ -use std::{borrow::Cow, error::Error}; +use std::borrow::Cow; use async_trait::async_trait; use bstr::{BStr, BString, ByteVec}; @@ -65,7 +65,7 @@ where true } - fn configure(&mut self, _config: &dyn std::any::Any) -> Result<(), Box> { + fn configure(&mut self, _config: &dyn std::any::Any) -> Result<(), gix_error::Exn> { Ok(()) } } diff --git a/gix-transport/src/client/git/blocking_io.rs b/gix-transport/src/client/git/blocking_io.rs index 28045697863..5de2db7aa42 100644 --- a/gix-transport/src/client/git/blocking_io.rs +++ b/gix-transport/src/client/git/blocking_io.rs @@ -1,4 +1,4 @@ -use std::{any::Any, borrow::Cow, error::Error, io::Write}; +use std::{any::Any, borrow::Cow, io::Write}; use bstr::{BStr, BString, ByteVec}; @@ -63,7 +63,7 @@ where true } - fn configure(&mut self, _config: &dyn Any) -> Result<(), Box> { + fn configure(&mut self, _config: &dyn Any) -> Result<(), gix_error::Exn> { Ok(()) } } @@ -174,28 +174,13 @@ pub mod connect { use std::net::{TcpStream, ToSocketAddrs}; use bstr::BString; + use gix_error::{ErrorExt, ResultExt, message}; use super::Connection; use crate::client::git; /// The error used in [`connect()`]. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error("An IO error occurred when connecting to the server")] - Io(#[from] std::io::Error), - #[error("Could not parse {host:?} as virtual host with format [:port]")] - VirtualHostInvalid { host: String }, - } - - impl crate::IsSpuriousError for Error { - fn is_spurious(&self) -> bool { - match self { - Error::Io(err) => err.is_spurious(), - _ => false, - } - } - } + pub type Error = gix_error::Exn; fn parse_host(input: String) -> Result<(String, Option), Error> { let mut tokens = input.splitn(2, ':'); @@ -203,7 +188,9 @@ pub mod connect { (Some(host), None) => (host.to_owned(), None), (Some(host), Some(port)) => ( host.to_owned(), - Some(port.parse().map_err(|_| Error::VirtualHostInvalid { host: input })?), + Some(port.parse().map_err(|_| { + gix_error::message!("Could not parse {input:?} as virtual host with format [:port]").raise() + })?), ), _ => unreachable!("we expect at least one token, the original string"), }) @@ -222,12 +209,16 @@ pub mod connect { ) -> Result, Error> { let read = TcpStream::connect_timeout( &(host, port.unwrap_or(9418)) - .to_socket_addrs()? + .to_socket_addrs() + .or_raise(|| message("Could not resolve git server"))? .next() .expect("after successful resolution there is an IP address"), std::time::Duration::from_secs(5), - )?; - let write = read.try_clone()?; + ) + .or_raise(|| message("Could not connect to git server"))?; + let write = read + .try_clone() + .or_raise(|| message("Could not clone git server connection"))?; let vhost = std::env::var("GIT_OVERRIDE_VIRTUAL_HOST") .ok() .map(parse_host) diff --git a/gix-transport/src/client/non_io_types.rs b/gix-transport/src/client/non_io_types.rs index fe391b7e064..e86c91475f7 100644 --- a/gix-transport/src/client/non_io_types.rs +++ b/gix-transport/src/client/non_io_types.rs @@ -48,45 +48,7 @@ pub(crate) mod connect { /// The error used in `connect()`. /// /// (Both blocking and async I/O use the same error type.) - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - Url(#[from] gix_error::Error), - #[error("The git repository path could not be converted to UTF8")] - PathConversion(#[from] bstr::Utf8Error), - #[error("connection failed")] - Connection(#[from] Box), - #[error("The url {url:?} contains information that would not be used by the {scheme} protocol")] - UnsupportedUrlTokens { - url: bstr::BString, - scheme: gix_url::Scheme, - }, - #[error("The '{0}' protocol is currently unsupported")] - UnsupportedScheme(gix_url::Scheme), - #[cfg(not(any(feature = "http-client-curl", feature = "http-client-reqwest")))] - #[error("'{0}' is not compiled in. Compile with the 'http-client-curl' or 'http-client-reqwest' cargo feature")] - CompiledWithoutHttp(gix_url::Scheme), - } - - // TODO: maybe fix this workaround: want `IsSpuriousError` in `Connection(…)` - impl crate::IsSpuriousError for Error { - fn is_spurious(&self) -> bool { - match self { - Error::Connection(err) => { - #[cfg(feature = "blocking-client")] - if let Some(err) = err.downcast_ref::() { - return err.is_spurious(); - } - if let Some(err) = err.downcast_ref::() { - return err.is_spurious(); - } - false - } - _ => false, - } - } - } + pub type Error = gix_error::Exn; } mod error { @@ -94,14 +56,12 @@ mod error { use bstr::BString; - #[cfg(feature = "http-client")] - use crate::client::blocking_io::http; #[cfg(feature = "blocking-client")] use crate::client::blocking_io::ssh; use crate::client::capabilities; #[cfg(feature = "http-client")] - type HttpError = http::Error; + type HttpError = gix_error::Error; #[cfg(feature = "blocking-client")] type SshInvocationError = ssh::invocation::Error; #[cfg(not(feature = "http-client"))] @@ -114,57 +74,228 @@ mod error { /// /// Callers can downcast [`std::io::Error::get_ref()`] to this type and forward the challenges /// to credential helpers as `wwwauth[]` attributes. - #[derive(Debug, Default, thiserror::Error)] - #[error("Received HTTP status 401")] + #[derive(Debug, Default)] pub struct AuthenticationRequired { /// HTTP `WWW-Authenticate` header values in the order supplied by the server. pub www_authenticate: Vec, } - /// The error used in most methods of the [`client`][crate::client] module - #[derive(thiserror::Error, Debug)] + impl std::fmt::Display for AuthenticationRequired { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("Received HTTP status 401") + } + } + + impl std::error::Error for AuthenticationRequired {} + + /// The error used in most methods of the [`client`][crate::client] module. + /// + /// Sources preserve classifications when raised or converted to [`gix_error::Error`]. Use + /// [`gix_error::can_retry()`] or [`gix_error::can_retry_lenient()`] to inspect this error directly. + #[derive(Debug)] #[expect(missing_docs)] pub enum Error { - #[error("A request was performed without performing the handshake first")] MissingHandshake, - #[error("An IO error occurred when talking to the server")] - Io(#[from] std::io::Error), - #[error("Capabilities could not be parsed")] - Capabilities { - #[from] - err: capabilities::Error, - }, - #[error("A packet line could not be decoded")] - LineDecode { - #[from] - err: gix_packetline::decode::Error, - }, - #[error("A {0} line was expected, but there was none")] + Io(std::io::Error), + Capabilities { err: gix_error::Error }, + LineDecode { err: gix_packetline::decode::Error }, ExpectedLine(&'static str), - #[error("Expected a data line, but got a delimiter")] ExpectedDataLine, - #[error("The transport layer does not support authentication")] AuthenticationUnsupported, - #[error("The transport layer refuses to use a given identity: {0}")] AuthenticationRefused(&'static str), - #[error("The protocol version indicated by {:?} is unsupported", {0})] UnsupportedProtocolVersion(BString), - #[error("Failed to invoke program {command:?}")] InvokeProgram { source: std::io::Error, command: OsString }, - #[error(transparent)] - Http(#[from] HttpError), - #[error(transparent)] + Http(HttpError), SshInvocation(SshInvocationError), - #[error("The repository path '{path}' could be mistaken for a command-line argument")] AmbiguousPath { path: BString }, } - impl crate::IsSpuriousError for Error { - fn is_spurious(&self) -> bool { + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Error::Io(err) => err.is_spurious(), - Error::Http(err) => err.is_spurious(), - _ => false, + Error::MissingHandshake => { + f.write_str("A request was performed without performing the handshake first") + } + Error::Io(_) => f.write_str("An IO error occurred when talking to the server"), + Error::Capabilities { .. } => f.write_str("Capabilities could not be parsed"), + Error::LineDecode { .. } => f.write_str("A packet line could not be decoded"), + Error::ExpectedLine(line) => write!(f, "A {line} line was expected, but there was none"), + Error::ExpectedDataLine => f.write_str("Expected a data line, but got a delimiter"), + Error::AuthenticationUnsupported => f.write_str("The transport layer does not support authentication"), + Error::AuthenticationRefused(reason) => { + write!(f, "The transport layer refuses to use a given identity: {reason}") + } + Error::UnsupportedProtocolVersion(version) => { + write!(f, "The protocol version indicated by {version:?} is unsupported") + } + Error::InvokeProgram { command, .. } => write!(f, "Failed to invoke program {}", command.display()), + Error::Http(err) => std::fmt::Display::fmt(err, f), + Error::SshInvocation(err) => std::fmt::Display::fmt(err, f), + Error::AmbiguousPath { path } => { + write!( + f, + "The repository path '{path}' could be mistaken for a command-line argument" + ) + } + } + } + } + + impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::Io(err) => Some(err), + Error::LineDecode { err } => Some(err), + Error::InvokeProgram { source, .. } => Some(source), + Error::Capabilities { err } => Some(err), + Error::Http(err) => Some(err), + Error::SshInvocation(err) => Some(err), + Error::AmbiguousPath { .. } => Some(&crate::INVALID_INPUT), + Error::ExpectedLine(_) | Error::ExpectedDataLine => Some(&crate::CORRUPTION), + _ => None, + } + } + } + + impl From for Error { + fn from(err: std::io::Error) -> Self { + Error::Io(err) + } + } + + impl From for Error { + fn from(err: capabilities::Error) -> Self { + Error::Capabilities { err: err.into_error() } + } + } + + impl From for Error { + fn from(err: gix_packetline::decode::Error) -> Self { + Error::LineDecode { err } + } + } + + impl From for gix_error::Error { + fn from(err: Error) -> Self { + Self::from_error(err) + } + } + + #[cfg(test)] + mod tests { + use gix_error::ErrorExt; + #[cfg(feature = "http-client")] + use gix_error::{RetryableError, message}; + + #[test] + fn io_classification_is_independent_of_conversion() { + for kind in [ + std::io::ErrorKind::Interrupted, + std::io::ErrorKind::UnexpectedEof, + std::io::ErrorKind::TimedOut, + std::io::ErrorKind::BrokenPipe, + std::io::ErrorKind::AddrInUse, + std::io::ErrorKind::ConnectionAborted, + std::io::ErrorKind::ConnectionReset, + std::io::ErrorKind::ConnectionRefused, + std::io::ErrorKind::OutOfMemory, + std::io::ErrorKind::NotFound, + std::io::ErrorKind::PermissionDenied, + ] { + let make_error = || super::Error::Io(kind.into()); + let can_retry = gix_error::can_retry(&make_error()); + let can_retry_lenient = gix_error::can_retry_lenient(&make_error()); + for err in [ + gix_error::Error::from(make_error()), + gix_error::Error::from_error(make_error()), + make_error().raise().into_error(), + ] { + assert_eq!( + err.can_retry(), + can_retry, + "conversion preserves the conservative policy for {kind:?}" + ); + assert_eq!( + err.can_retry_lenient(), + can_retry_lenient, + "conversion preserves the lenient policy for {kind:?}" + ); + assert!( + !err.is_retryable(), + "conversion does not add an explicit retry marker for {kind:?}" + ); + assert_eq!(err.is_not_found(), kind == std::io::ErrorKind::NotFound); + assert_eq!(err.is_resource_exhausted(), kind == std::io::ErrorKind::OutOfMemory); + } + } + } + + #[cfg(feature = "http-client")] + #[test] + fn http_keeps_retryable_sources() { + let err = super::Error::Http( + std::io::Error::new(std::io::ErrorKind::BrokenPipe, "retry me") + .and_raise(message("HTTP failed")) + .into_error(), + ); + + assert!(gix_error::can_retry_lenient(&err)); + assert!(!gix_error::can_retry(&err)); + let source = std::error::Error::source(&err) + .and_then(|err| err.downcast_ref::()) + .expect("HTTP errors retain their gix-error wrapper"); + assert!( + source + .iter_errors() + .any(::is::) + ); + + let explicit = super::Error::Http( + RetryableError::new(message("retry me")) + .and_raise(message("HTTP failed")) + .into_error(), + ); + assert!(gix_error::can_retry(&explicit)); + assert!(gix_error::Error::from(explicit).is_retryable()); + + let out_of_memory = super::Error::Http( + std::io::Error::from(std::io::ErrorKind::OutOfMemory) + .and_raise(message("HTTP failed")) + .into_error(), + ); + assert!(!gix_error::can_retry(&out_of_memory)); + assert!(gix_error::can_retry_lenient(&out_of_memory)); + assert!(gix_error::Error::from(out_of_memory).is_resource_exhausted()); + } + + #[test] + fn custom_errors_expose_classifications() { + let err = gix_error::Error::from(super::Error::AmbiguousPath { path: "-arg".into() }); + assert!(err.is_validation(), "unsafe transport arguments are invalid input"); + for err in [super::Error::ExpectedLine("version"), super::Error::ExpectedDataLine] { + assert!( + gix_error::Error::from(err).is_corrupted(), + "unexpected packet lines are malformed responses" + ); + } + + #[cfg(feature = "blocking-client")] + { + use crate::client::blocking_io::ssh; + + for err in [ + ssh::invocation::Error::AmbiguousUserName { user: "-arg".into() }, + ssh::invocation::Error::AmbiguousHostName { host: "-arg".into() }, + ] { + let err = gix_error::Error::from(super::Error::SshInvocation(err)); + assert!(err.is_validation(), "unsafe SSH arguments are invalid input"); + assert!( + err.downcast_any_ref::().is_some(), + "the concrete SSH error remains available" + ); + } + let err = gix_error::Error::from_error(ssh::Error::AmbiguousHostName { host: "-arg".into() }); + assert!(err.is_validation(), "SSH connection errors expose invalid input too"); } } } diff --git a/gix-transport/src/client/traits.rs b/gix-transport/src/client/traits.rs index 4a54fa15ee9..e8606d922aa 100644 --- a/gix-transport/src/client/traits.rs +++ b/gix-transport/src/client/traits.rs @@ -48,7 +48,7 @@ pub trait TransportWithoutIO { /// Pass `config` can be cast and interpreted by the implementation, as documented separately. /// /// The caller must know how that `config` data looks like for the intended implementation. - fn configure(&mut self, config: &dyn Any) -> Result<(), Box>; + fn configure(&mut self, config: &dyn Any) -> Result<(), gix_error::Exn>; } // Would be nice if the box implementation could auto-forward to all implemented traits. @@ -69,7 +69,7 @@ impl TransportWithoutIO for Box { self.deref().connection_persists_across_multiple_requests() } - fn configure(&mut self, config: &dyn Any) -> Result<(), Box> { + fn configure(&mut self, config: &dyn Any) -> Result<(), gix_error::Exn> { self.deref_mut().configure(config) } } @@ -91,7 +91,7 @@ impl TransportWithoutIO for &mut T { self.deref().connection_persists_across_multiple_requests() } - fn configure(&mut self, config: &dyn Any) -> Result<(), Box> { + fn configure(&mut self, config: &dyn Any) -> Result<(), gix_error::Exn> { self.deref_mut().configure(config) } } diff --git a/gix-transport/src/lib.rs b/gix-transport/src/lib.rs index a580448e685..0ee1f6aab26 100644 --- a/gix-transport/src/lib.rs +++ b/gix-transport/src/lib.rs @@ -19,6 +19,14 @@ pub use bstr; pub use futures_io; pub use gix_packetline as packetline; +static INVALID_INPUT: gix_error::ValidationError = gix_error::ValidationError { + message: std::borrow::Cow::Borrowed("Invalid transport input"), + input: None, +}; +static CORRUPTION: gix_error::CorruptionError = gix_error::CorruptionError { + message: std::borrow::Cow::Borrowed("Malformed server response"), +}; + /// The version of the way client and server communicate. #[derive(Default, PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] @@ -60,34 +68,5 @@ impl Service { } } -mod traits { - use std::convert::Infallible; - - /// An error which can tell whether it's worth retrying to maybe succeed next time. - pub trait IsSpuriousError: std::error::Error { - /// Return `true` if retrying might result in a different outcome due to IO working out differently. - fn is_spurious(&self) -> bool { - false - } - } - - impl IsSpuriousError for Infallible {} - - impl IsSpuriousError for std::io::Error { - fn is_spurious(&self) -> bool { - // TODO: also include the new special Kinds (currently unstable) - use std::io::ErrorKind::*; - match self.kind() { - Unsupported | WriteZero | InvalidInput | InvalidData | WouldBlock | AlreadyExists - | AddrNotAvailable | NotConnected | Other | PermissionDenied | NotFound => false, - Interrupted | UnexpectedEof | OutOfMemory | TimedOut | BrokenPipe | AddrInUse | ConnectionAborted - | ConnectionReset | ConnectionRefused => true, - _ => false, - } - } - } -} -pub use traits::IsSpuriousError; - /// pub mod client; diff --git a/gix-transport/tests/client/capabilities.rs b/gix-transport/tests/client/capabilities.rs index 50276389b1c..c1c70fb0fb4 100644 --- a/gix-transport/tests/client/capabilities.rs +++ b/gix-transport/tests/client/capabilities.rs @@ -11,7 +11,8 @@ use gix_transport::client::capabilities::blocking_recv::Handshake; #[test] fn from_bytes() -> crate::Result { - let (caps, delim_pos) = Capabilities::from_bytes(&b"7814e8a05a59c0cf5fb186661d1551c75d1299b5 HEAD\0multi_ack thin-pack side-band side-band-64k ofs-delta shallow deepen-since deepen-not deepen-relative no-progress include-tag multi_ack_detailed symref=HEAD:refs/heads/master object-format=sha1 agent=git/2.28.0"[..])?; + let (caps, delim_pos) = Capabilities::from_bytes(&b"7814e8a05a59c0cf5fb186661d1551c75d1299b5 HEAD\0multi_ack thin-pack side-band side-band-64k ofs-delta shallow deepen-since deepen-not deepen-relative no-progress include-tag multi_ack_detailed symref=HEAD:refs/heads/master object-format=sha1 agent=git/2.28.0"[..]) + ?; assert_eq!(delim_pos, 45); assert_eq!( caps.iter().map(|c| c.name().to_owned()).collect::>(), diff --git a/gix-transport/tests/client/git.rs b/gix-transport/tests/client/git.rs index 3539dd1f72d..489a817f09a 100644 --- a/gix-transport/tests/client/git.rs +++ b/gix-transport/tests/client/git.rs @@ -23,6 +23,36 @@ use parking_lot::Mutex; use crate::fixture_bytes; +#[cfg(any(feature = "blocking-client", feature = "async-std"))] +#[crate::bisync::bisync] +#[cfg_attr(feature = "blocking-client", test)] +#[cfg_attr(all(feature = "async-client", not(feature = "blocking-client")), async_std::test)] +async fn refused_connections_remain_retryable() -> crate::Result { + #[cfg(all(feature = "async-client", not(feature = "blocking-client")))] + use client::async_io::connect::connect; + #[cfg(feature = "blocking-client")] + use client::blocking_io::connect::connect; + + let listener = std::net::TcpListener::bind(("127.0.0.1", 0))?; + let port = listener.local_addr()?.port(); + drop(listener); + let url = format!("git://127.0.0.1:{port}/repo.git"); + let err = connect(url.as_str(), Default::default()) + .await + .err() + .expect("the local port has no listener") + .into_error(); + assert_eq!( + err.downcast_any_ref::() + .expect("retain the connection failure") + .kind(), + std::io::ErrorKind::ConnectionRefused, + "the test must exercise a refused connection" + ); + assert!(err.can_retry_lenient(), "a refused connection can succeed on retry"); + Ok(()) +} + #[crate::bisync::bisync] #[cfg_attr(feature = "blocking-client", test)] #[cfg_attr(all(feature = "async-client", not(feature = "blocking-client")), async_std::test)] diff --git a/gix/src/env.rs b/gix/src/env.rs index 00f53b5c9d0..a02ac94e68b 100644 --- a/gix/src/env.rs +++ b/gix/src/env.rs @@ -83,11 +83,12 @@ pub mod collate { } #[cfg(any(feature = "async-network-client", feature = "blocking-network-client"))] - impl crate::protocol::transport::IsSpuriousError for Error + impl Error where E: std::error::Error + Send + Sync + 'static, { - fn is_spurious(&self) -> bool { + /// Return `true` if retrying the failed operation might succeed. + pub fn can_retry(&self) -> bool { match self { Error::Open(_) | Error::CredentialHelperConfig(_) @@ -95,9 +96,9 @@ pub mod collate { | Error::FindExistingReference(_) | Error::FindExistingRemote(_) | Error::Other(_) => false, - Error::Connect(err) => err.is_spurious(), - Error::PrepareFetch(err) => err.is_spurious(), - Error::Fetch(err) => err.is_spurious(), + Error::Connect(err) => err.can_retry(), + Error::PrepareFetch(err) => err.can_retry(), + Error::Fetch(err) => err.can_retry(), } } } diff --git a/gix/src/remote/connect.rs b/gix/src/remote/connect.rs index 8083c0a4841..c73f4ec1ef5 100644 --- a/gix/src/remote/connect.rs +++ b/gix/src/remote/connect.rs @@ -40,11 +40,11 @@ mod error { }, } - impl gix_protocol::transport::IsSpuriousError for Error { + impl Error { /// Return `true` if retrying might result in a different outcome due to IO working out differently. - fn is_spurious(&self) -> bool { + pub fn can_retry(&self) -> bool { match self { - Error::Connect(err) => err.is_spurious(), + Error::Connect(err) => err.can_retry_lenient(), _ => false, } } diff --git a/gix/src/remote/connection/fetch/error.rs b/gix/src/remote/connection/fetch/error.rs index 4d7fff1585d..eb5cf4ec8c1 100644 --- a/gix/src/remote/connection/fetch/error.rs +++ b/gix/src/remote/connection/fetch/error.rs @@ -38,11 +38,11 @@ pub enum Error { NegotiationAlgorithmConfig(#[from] config::key::GenericErrorWithValue), } -impl gix_protocol::transport::IsSpuriousError for Error { - fn is_spurious(&self) -> bool { +impl Error { + pub(crate) fn can_retry(&self) -> bool { match self { - Error::Fetch(err) => err.can_retry(), - Error::Client(err) => err.is_spurious(), + Error::Fetch(err) => err.can_retry_lenient(), + Error::Client(err) => gix_error::can_retry_lenient(err), _ => false, } } diff --git a/gix/src/remote/connection/fetch/mod.rs b/gix/src/remote/connection/fetch/mod.rs index ace1a506cff..4991b1894d6 100644 --- a/gix/src/remote/connection/fetch/mod.rs +++ b/gix/src/remote/connection/fetch/mod.rs @@ -111,10 +111,10 @@ pub mod prepare { RefMap(#[from] crate::remote::ref_map::Error), } - impl gix_protocol::transport::IsSpuriousError for Error { - fn is_spurious(&self) -> bool { + impl Error { + pub(crate) fn can_retry(&self) -> bool { match self { - Error::RefMap(err) => err.is_spurious(), + Error::RefMap(err) => err.can_retry(), _ => false, } } diff --git a/gix/src/remote/connection/ref_map.rs b/gix/src/remote/connection/ref_map.rs index 97b5b818206..ba7a2713337 100644 --- a/gix/src/remote/connection/ref_map.rs +++ b/gix/src/remote/connection/ref_map.rs @@ -21,7 +21,7 @@ pub enum Error { source: crate::config::transport::Error, }, #[error("Failed to configure the transport layer")] - ConfigureTransport(#[from] Box), + ConfigureTransport(#[from] gix_error::Error), #[error(transparent)] Handshake(gix_error::Error), #[error(transparent)] @@ -30,11 +30,11 @@ pub enum Error { ConfigureCredentials(#[from] crate::config::credential_helpers::Error), } -impl gix_protocol::transport::IsSpuriousError for Error { - fn is_spurious(&self) -> bool { +impl Error { + pub(crate) fn can_retry(&self) -> bool { match self { - Error::Transport(err) => err.is_spurious(), - Error::Handshake(err) => err.can_retry(), + Error::Transport(err) => gix_error::can_retry_lenient(err), + Error::Handshake(err) => err.can_retry_lenient(), _ => false, } } @@ -151,7 +151,10 @@ where })?; } if let Some(config) = self.transport_options.as_ref() { - self.transport.inner.configure(&**config)?; + self.transport + .inner + .configure(&**config) + .map_err(gix_error::Exn::into_error)?; } let mut handshake = gix_protocol::handshake( &mut self.transport.inner, diff --git a/tests/snapshots/plumbing/no-repo/pack/clone/fail-ambiguous-host b/tests/snapshots/plumbing/no-repo/pack/clone/fail-ambiguous-host index 0a99e122c4c..b70d817d256 100644 --- a/tests/snapshots/plumbing/no-repo/pack/clone/fail-ambiguous-host +++ b/tests/snapshots/plumbing/no-repo/pack/clone/fail-ambiguous-host @@ -1,4 +1,6 @@  Error: Transport handshake failed Caused by: - Host name '-oProxyCommand=open$IFS-aCalculator' could be mistaken for a command-line argument \ No newline at end of file + 0: Host name '-oProxyCommand=open$IFS-aCalculator' could be mistaken for a command-line argument + 1: Host name '-oProxyCommand=open$IFS-aCalculator' could be mistaken for a command-line argument + 2: Invalid transport input \ No newline at end of file diff --git a/tests/snapshots/plumbing/no-repo/pack/clone/fail-ambiguous-path b/tests/snapshots/plumbing/no-repo/pack/clone/fail-ambiguous-path index 62ed36c1588..f1c684fe12a 100644 --- a/tests/snapshots/plumbing/no-repo/pack/clone/fail-ambiguous-path +++ b/tests/snapshots/plumbing/no-repo/pack/clone/fail-ambiguous-path @@ -1,4 +1,5 @@  Error: Transport handshake failed Caused by: - The repository path '-oProxyCommand=open$IFS-aCalculator/bar' could be mistaken for a command-line argument \ No newline at end of file + 0: The repository path '-oProxyCommand=open$IFS-aCalculator/bar' could be mistaken for a command-line argument + 1: Invalid transport input \ No newline at end of file diff --git a/tests/snapshots/plumbing/no-repo/pack/clone/fail-ambiguous-username/explicit-ssh b/tests/snapshots/plumbing/no-repo/pack/clone/fail-ambiguous-username/explicit-ssh index d4c35412b29..bd621b82a22 100644 --- a/tests/snapshots/plumbing/no-repo/pack/clone/fail-ambiguous-username/explicit-ssh +++ b/tests/snapshots/plumbing/no-repo/pack/clone/fail-ambiguous-username/explicit-ssh @@ -1,4 +1,6 @@  Error: Transport handshake failed Caused by: - Username '-Fconfigfile' could be mistaken for a command-line argument \ No newline at end of file + 0: Username '-Fconfigfile' could be mistaken for a command-line argument + 1: Username '-Fconfigfile' could be mistaken for a command-line argument + 2: Invalid transport input \ No newline at end of file diff --git a/tests/snapshots/plumbing/no-repo/pack/clone/fail-ambiguous-username/implicit-ssh b/tests/snapshots/plumbing/no-repo/pack/clone/fail-ambiguous-username/implicit-ssh index d4c35412b29..bd621b82a22 100644 --- a/tests/snapshots/plumbing/no-repo/pack/clone/fail-ambiguous-username/implicit-ssh +++ b/tests/snapshots/plumbing/no-repo/pack/clone/fail-ambiguous-username/implicit-ssh @@ -1,4 +1,6 @@  Error: Transport handshake failed Caused by: - Username '-Fconfigfile' could be mistaken for a command-line argument \ No newline at end of file + 0: Username '-Fconfigfile' could be mistaken for a command-line argument + 1: Username '-Fconfigfile' could be mistaken for a command-line argument + 2: Invalid transport input \ No newline at end of file diff --git a/tests/snapshots/plumbing/no-repo/pack/receive/fail-ambiguous-host b/tests/snapshots/plumbing/no-repo/pack/receive/fail-ambiguous-host index 0935826475c..0c052f7a29b 100644 --- a/tests/snapshots/plumbing/no-repo/pack/receive/fail-ambiguous-host +++ b/tests/snapshots/plumbing/no-repo/pack/receive/fail-ambiguous-host @@ -1,4 +1,6 @@ Error: Transport handshake failed Caused by: - Host name '-oProxyCommand=open$IFS-aCalculator' could be mistaken for a command-line argument \ No newline at end of file + 0: Host name '-oProxyCommand=open$IFS-aCalculator' could be mistaken for a command-line argument + 1: Host name '-oProxyCommand=open$IFS-aCalculator' could be mistaken for a command-line argument + 2: Invalid transport input \ No newline at end of file diff --git a/tests/snapshots/plumbing/no-repo/pack/receive/fail-ambiguous-path b/tests/snapshots/plumbing/no-repo/pack/receive/fail-ambiguous-path index fb4123a2d29..295e17e757f 100644 --- a/tests/snapshots/plumbing/no-repo/pack/receive/fail-ambiguous-path +++ b/tests/snapshots/plumbing/no-repo/pack/receive/fail-ambiguous-path @@ -1,4 +1,5 @@ Error: Transport handshake failed Caused by: - The repository path '-oProxyCommand=open$IFS-aCalculator/bar' could be mistaken for a command-line argument \ No newline at end of file + 0: The repository path '-oProxyCommand=open$IFS-aCalculator/bar' could be mistaken for a command-line argument + 1: Invalid transport input \ No newline at end of file diff --git a/tests/snapshots/plumbing/no-repo/pack/receive/fail-ambiguous-username/explicit-ssh b/tests/snapshots/plumbing/no-repo/pack/receive/fail-ambiguous-username/explicit-ssh index a3798370070..354a85cc561 100644 --- a/tests/snapshots/plumbing/no-repo/pack/receive/fail-ambiguous-username/explicit-ssh +++ b/tests/snapshots/plumbing/no-repo/pack/receive/fail-ambiguous-username/explicit-ssh @@ -1,4 +1,6 @@ Error: Transport handshake failed Caused by: - Username '-Fconfigfile' could be mistaken for a command-line argument \ No newline at end of file + 0: Username '-Fconfigfile' could be mistaken for a command-line argument + 1: Username '-Fconfigfile' could be mistaken for a command-line argument + 2: Invalid transport input \ No newline at end of file diff --git a/tests/snapshots/plumbing/no-repo/pack/receive/fail-ambiguous-username/implicit-ssh b/tests/snapshots/plumbing/no-repo/pack/receive/fail-ambiguous-username/implicit-ssh index a3798370070..354a85cc561 100644 --- a/tests/snapshots/plumbing/no-repo/pack/receive/fail-ambiguous-username/implicit-ssh +++ b/tests/snapshots/plumbing/no-repo/pack/receive/fail-ambiguous-username/implicit-ssh @@ -1,4 +1,6 @@ Error: Transport handshake failed Caused by: - Username '-Fconfigfile' could be mistaken for a command-line argument \ No newline at end of file + 0: Username '-Fconfigfile' could be mistaken for a command-line argument + 1: Username '-Fconfigfile' could be mistaken for a command-line argument + 2: Invalid transport input \ No newline at end of file From b1e31eb8990f9baf3263d97e1b3b3a6fd88f215e Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 20 Aug 2026 17:28:35 +0200 Subject: [PATCH 28/51] change(gix)!: consolidate public API failures under gix::Error rubberstamp --- Cargo.lock | 1 - gitoxide-core/Cargo.toml | 2 - gitoxide-core/src/pack/receive.rs | 17 +- gix-tix/spec.md | 2 + gix-tix/src/edit/rebase.rs | 19 +- gix-tix/src/edit/undo.rs | 2 +- gix-tix/src/history.rs | 30 +- gix-tix/src/lib.rs | 3 +- gix-tix/src/ref_tree.rs | 2 +- gix/Cargo.toml | 4 +- gix/src/clone/access.rs | 5 +- gix/src/clone/checkout.rs | 83 +- gix/src/clone/fetch/mod.rs | 176 ++--- gix/src/clone/fetch/util.rs | 120 +-- gix/src/clone/mod.rs | 93 ++- gix/src/commit/mod.rs | 60 +- gix/src/commit/sign.rs | 110 ++- gix/src/commit/verify.rs | 49 +- gix/src/config/cache/access.rs | 91 ++- gix/src/config/cache/incubate.rs | 40 +- gix/src/config/cache/init.rs | 41 +- gix/src/config/file_mut.rs | 79 +- gix/src/config/mod.rs | 341 ++------- gix/src/config/overrides.rs | 27 +- gix/src/config/snapshot/access.rs | 19 +- gix/src/config/snapshot/credential_helpers.rs | 31 +- gix/src/config/tree/keys.rs | 101 +-- gix/src/config/tree/mod.rs | 16 +- gix/src/config/tree/sections/checkout.rs | 22 +- gix/src/config/tree/sections/core.rs | 82 +- gix/src/config/tree/sections/diff.rs | 18 +- gix/src/config/tree/sections/extensions.rs | 7 +- gix/src/config/tree/sections/fetch.rs | 14 +- gix/src/config/tree/sections/gitoxide.rs | 11 +- gix/src/config/tree/sections/gpg.rs | 10 +- gix/src/config/tree/sections/http.rs | 25 +- gix/src/config/tree/sections/index.rs | 5 +- gix/src/config/tree/sections/merge.rs | 6 +- gix/src/config/tree/sections/notes.rs | 6 +- gix/src/config/tree/sections/pack.rs | 22 +- gix/src/config/tree/sections/protocol.rs | 22 +- gix/src/config/tree/sections/push.rs | 6 +- gix/src/config/tree/sections/remote.rs | 6 +- gix/src/config/tree/sections/ssh.rs | 6 +- gix/src/config/tree/sections/status.rs | 7 +- gix/src/config/tree/traits.rs | 4 +- gix/src/create.rs | 93 ++- gix/src/diff.rs | 41 +- gix/src/dirwalk/iter.rs | 34 +- gix/src/dirwalk/mod.rs | 21 +- gix/src/discover.rs | 22 +- gix/src/env.rs | 80 +- gix/src/filter.rs | 140 ++-- gix/src/head/peel.rs | 62 +- gix/src/id.rs | 36 +- gix/src/init.rs | 49 +- gix/src/lib.rs | 38 +- gix/src/mailmap.rs | 13 +- gix/src/note.rs | 4 +- gix/src/object/blob.rs | 19 +- gix/src/object/commit.rs | 32 +- gix/src/object/errors.rs | 33 +- gix/src/object/mod.rs | 81 +- gix/src/object/peel.rs | 26 +- gix/src/object/tree/diff/for_each.rs | 13 +- gix/src/object/tree/diff/mod.rs | 9 +- gix/src/object/tree/editor.rs | 62 +- gix/src/object/tree/mod.rs | 15 +- gix/src/open/mod.rs | 19 +- gix/src/open/repository.rs | 118 +-- gix/src/pathspec.rs | 31 +- gix/src/reference/edits.rs | 17 +- gix/src/reference/errors.rs | 104 +-- gix/src/reference/iter.rs | 39 +- gix/src/reference/mod.rs | 32 +- gix/src/remote/build.rs | 10 +- gix/src/remote/connect.rs | 97 +-- gix/src/remote/connection/access.rs | 4 +- gix/src/remote/connection/fetch/config.rs | 17 +- gix/src/remote/connection/fetch/error.rs | 49 +- gix/src/remote/connection/fetch/mod.rs | 22 +- .../remote/connection/fetch/receive_pack.rs | 30 +- .../connection/fetch/update_refs/mod.rs | 270 ++++--- .../connection/fetch/update_refs/tests.rs | 52 ++ .../connection/fetch/update_refs/update.rs | 25 +- gix/src/remote/connection/ref_map.rs | 57 +- gix/src/remote/errors.rs | 51 +- gix/src/remote/init.rs | 39 +- gix/src/remote/name.rs | 18 +- gix/src/remote/save.rs | 72 +- gix/src/remote/url/scheme_permission.rs | 16 +- gix/src/repository/attributes.rs | 31 +- gix/src/repository/blame.rs | 6 +- gix/src/repository/branch.rs | 147 ++-- gix/src/repository/config/branch.rs | 89 ++- gix/src/repository/config/mod.rs | 35 +- gix/src/repository/config/transport.rs | 711 +++++++++--------- gix/src/repository/diff.rs | 11 +- gix/src/repository/dirwalk.rs | 21 +- gix/src/repository/filter.rs | 51 +- gix/src/repository/graph.rs | 5 +- gix/src/repository/identity.rs | 6 +- gix/src/repository/index.rs | 47 +- gix/src/repository/location.rs | 37 +- gix/src/repository/mailmap.rs | 57 +- gix/src/repository/merge.rs | 32 +- gix/src/repository/mod.rs | 347 +-------- gix/src/repository/object.rs | 76 +- gix/src/repository/pathspec.rs | 10 +- gix/src/repository/reference.rs | 81 +- gix/src/repository/remote.rs | 81 +- gix/src/repository/revision.rs | 48 +- gix/src/repository/submodule.rs | 41 +- gix/src/repository/worktree.rs | 40 +- .../revision/spec/parse/delegate/revision.rs | 20 +- gix/src/revision/spec/parse/mod.rs | 11 +- gix/src/revision/walk.rs | 43 +- gix/src/status/index_worktree.rs | 87 +-- gix/src/status/iter/mod.rs | 52 +- gix/src/status/mod.rs | 51 +- gix/src/status/tree_index.rs | 34 +- gix/src/submodule/errors.rs | 114 +-- gix/src/submodule/mod.rs | 83 +- gix/src/tag.rs | 13 +- gix/src/worktree/mod.rs | 62 +- gix/src/worktree/proxy.rs | 22 +- gix/tests/gix-init.rs | 120 ++- gix/tests/gix/clone.rs | 76 +- gix/tests/gix/commit.rs | 17 +- gix/tests/gix/config/file_mut.rs | 41 +- gix/tests/gix/config/tree.rs | 37 +- gix/tests/gix/head.rs | 14 + gix/tests/gix/id.rs | 25 +- gix/tests/gix/init.rs | 22 +- gix/tests/gix/reference/mod.rs | 9 + gix/tests/gix/reference/remote.rs | 11 +- gix/tests/gix/remote/connect.rs | 19 +- gix/tests/gix/remote/fetch.rs | 14 +- gix/tests/gix/remote/save.rs | 11 +- gix/tests/gix/repository/branch.rs | 34 +- gix/tests/gix/repository/config/mod.rs | 21 + gix/tests/gix/repository/mod.rs | 4 + gix/tests/gix/repository/object.rs | 11 +- gix/tests/gix/repository/open.rs | 59 +- gix/tests/gix/repository/remote.rs | 18 +- gix/tests/gix/repository/worktree.rs | 8 +- .../gix/revision/spec/from_bytes/ambiguous.rs | 2 +- gix/tests/gix/submodule.rs | 164 ++-- gix/tests/gix/util.rs | 2 +- gix/tests/repository.rs | 56 +- tests/snapshots/porcelain/init/fail | 2 +- .../init/fail-with-multi-element-directory | 2 +- 152 files changed, 3195 insertions(+), 4161 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ae79b121feb..3c467279596 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1606,7 +1606,6 @@ dependencies = [ "signal-hook 0.4.4", "smallvec", "termtree", - "thiserror 2.0.18", ] [[package]] diff --git a/gitoxide-core/Cargo.toml b/gitoxide-core/Cargo.toml index baddde6b516..f3a166ed9ee 100644 --- a/gitoxide-core/Cargo.toml +++ b/gitoxide-core/Cargo.toml @@ -130,6 +130,4 @@ ignored = [ "gix-transport-configuration-only", # Imported as crate `layout`, which does not match the package name `layout-rs`. "layout-rs", - # Enabled through `estimate-hours` feature wiring only. - "smallvec", ] diff --git a/gitoxide-core/src/pack/receive.rs b/gitoxide-core/src/pack/receive.rs index 811e2f729cc..29e68643efc 100644 --- a/gitoxide-core/src/pack/receive.rs +++ b/gitoxide-core/src/pack/receive.rs @@ -10,7 +10,7 @@ use gix::error::{ResultExt, message}; use gix::protocol::transport::client::async_io::connect; #[cfg(feature = "blocking-client")] use gix::protocol::transport::client::blocking_io::connect; -use gix::{DynNestedProgress, config::tree::Key, protocol::bisync, remote::fetch::Error}; +use gix::{DynNestedProgress, config::tree::Key, protocol::bisync}; pub use gix::{ NestedProgress, Progress, hash::ObjectId, @@ -108,11 +108,16 @@ where .map_err(gix::Exn::into_error)?; if refmap.is_missing_required_mapping() { - return Err(Error::NoMapping { - refspecs: refmap.refspecs.clone(), - num_remote_refs: refmap.remote_refs.len(), - } - .into()); + anyhow::bail!( + "None of the refspec(s) {} matched any of the {} refs on the remote", + refmap + .refspecs + .iter() + .map(|spec| spec.to_ref().instruction().to_bstring().to_string()) + .collect::>() + .join(", "), + refmap.remote_refs.len() + ); } let mut negotiate = Negotiate { refmap: &refmap }; diff --git a/gix-tix/spec.md b/gix-tix/spec.md index 6de71778e17..d2b44e9d9a3 100644 --- a/gix-tix/spec.md +++ b/gix-tix/spec.md @@ -143,6 +143,8 @@ without trading responsiveness for metadata that is not visible. - Revisions must resolve and peel to commits. Invalid or non-commit visible revisions are errors. An unavailable hidden revision emits a warning and is ignored when another hidden revision resolves; if none resolve, startup fails. +- References that disappear during enumeration are ignored. Other errors while + reading references are reported. - The interactive UI owns the alternate screen by default. `--no-alt-screen` instead draws interactively on the normal screen. Raw mode, focus reporting, mouse capture, and enhanced keyboard reporting are restored on every exit path. diff --git a/gix-tix/src/edit/rebase.rs b/gix-tix/src/edit/rebase.rs index 9d658570e31..dacbfd9b6cc 100644 --- a/gix-tix/src/edit/rebase.rs +++ b/gix-tix/src/edit/rebase.rs @@ -17,7 +17,7 @@ use gix::{ }, }; -use crate::history::HistoryGraph; +use crate::history::{HistoryGraph, is_missing_ref}; const MARKER: &[u8] = b"tix-rebase"; const ORIGINAL_PARENT: &[u8] = b"tix-rebase-parent"; @@ -408,7 +408,7 @@ pub(crate) fn capture_refs(repo: &gix::Repository, scope: &[ObjectId], tips: &[O for reference in repo.references()?.all()? { let reference = match reference { Ok(reference) => reference, - Err(err) if is_missing_ref(&*err) => continue, + Err(err) if is_missing_ref(&err) => continue, Err(err) => anyhow::bail!("could not inspect a reference before editing: {err}"), }; if matches!( @@ -2988,7 +2988,7 @@ fn update_refs( for reference in repo.references()?.all()? { let reference = match reference { Ok(reference) => reference, - Err(err) if is_missing_ref(&*err) => continue, + Err(err) if is_missing_ref(&err) => continue, Err(err) => anyhow::bail!("could not inspect a reference before rebasing: {err}"), }; if matches!( @@ -3131,19 +3131,6 @@ fn pin_name( } } -fn is_missing_ref(mut err: &(dyn std::error::Error + 'static)) -> bool { - loop { - if err - .downcast_ref::() - .is_some_and(|err| err.kind() == std::io::ErrorKind::NotFound) - { - return true; - } - let Some(source) = err.source() else { return false }; - err = source; - } -} - fn ref_edit(name: gix::refs::FullName, old: Option, new: Option) -> RefEdit { match (old, new) { (Some(old), Some(new)) => RefEdit::update_with_log( diff --git a/gix-tix/src/edit/undo.rs b/gix-tix/src/edit/undo.rs index 7e2dc02d2d6..d8fca603334 100644 --- a/gix-tix/src/edit/undo.rs +++ b/gix-tix/src/edit/undo.rs @@ -160,7 +160,7 @@ pub(crate) fn review_blocks_undo(repo: &gix::Repository) -> Result { { let reference = match reference { Ok(reference) => reference, - Err(err) if crate::history::is_missing_ref(&*err) => continue, + Err(err) if crate::history::is_missing_ref(&err) => continue, Err(err) => return Err(anyhow::anyhow!("could not read review reference: {err}")), }; if crate::history::review_number(reference.name().as_bstr()).is_some() { diff --git a/gix-tix/src/history.rs b/gix-tix/src/history.rs index 42cb03420a3..b0606af6079 100644 --- a/gix-tix/src/history.rs +++ b/gix-tix/src/history.rs @@ -957,7 +957,7 @@ fn local_refs_by_target(repo: &gix::Repository) -> Result reference, - Err(err) if is_missing_ref(&*err) => continue, + Err(err) if is_missing_ref(&err) => continue, Err(err) => return Err(anyhow::anyhow!("could not read local branch: {err}")), }; out.entry(reference.id().detach()) @@ -1274,7 +1274,7 @@ pub(crate) fn ref_tree_revisions(repo: &gix::Repository, include_tags: bool) -> { let mut reference = match reference { Ok(reference) => reference, - Err(err) if is_missing_ref(&*err) => continue, + Err(err) if is_missing_ref(&err) => continue, Err(err) => return Err(anyhow::anyhow!("could not read reference: {err}")), }; let name = reference.name().as_bstr().to_owned(); @@ -1485,7 +1485,7 @@ fn refs_with_commit_targets(repo: &gix::Repository, prefix: &[u8], label: &str) { let mut reference = match reference { Ok(reference) => reference, - Err(err) if is_missing_ref(&*err) => continue, + Err(err) if is_missing_ref(&err) => continue, Err(err) => return Err(anyhow::anyhow!("could not read tix {label}: {err}")), }; let suffix = reference.name().as_bstr().strip_prefix(prefix).unwrap_or_default(); @@ -1928,7 +1928,7 @@ pub(crate) fn decorations_excluding( { let mut reference = match reference { Ok(reference) => reference, - Err(err) if is_missing_ref(&*err) => continue, + Err(err) if is_missing_ref(&err) => continue, Err(err) => return Err(anyhow::anyhow!("could not read reference: {err}")), }; let full_name = reference.name().to_owned(); @@ -2083,17 +2083,8 @@ pub(crate) fn decorations_excluding( Ok(out) } -pub(crate) fn is_missing_ref(mut err: &(dyn std::error::Error + 'static)) -> bool { - loop { - if err - .downcast_ref::() - .is_some_and(|err| err.kind() == std::io::ErrorKind::NotFound) - { - return true; - } - let Some(source) = err.source() else { return false }; - err = source; - } +pub(crate) fn is_missing_ref(err: &gix::Error) -> bool { + err.is_not_found() } pub(crate) fn decoration_kind(name: &[u8]) -> DecorationKind { @@ -2171,9 +2162,12 @@ mod tests { #[test] fn only_missing_ref_reads_are_ignored() { - let ref_error = |kind| gix::refs::file::iter::loose_then_packed::Error::ReadFileContents { - source: std::io::Error::from(kind), - path: "refs/heads/racing".into(), + let ref_error = |kind| { + gix::Exn::new(gix::refs::file::iter::loose_then_packed::Error::ReadFileContents { + source: std::io::Error::from(kind), + path: "refs/heads/racing".into(), + }) + .into_error() }; assert!( is_missing_ref(&ref_error(std::io::ErrorKind::NotFound)), diff --git a/gix-tix/src/lib.rs b/gix-tix/src/lib.rs index 4abd34ecb52..bd30e942f8a 100644 --- a/gix-tix/src/lib.rs +++ b/gix-tix/src/lib.rs @@ -1925,7 +1925,8 @@ fn event_loop( let result = repository.delete_local_branches(names); let changed = result.is_ok() || result.as_ref().is_err_and(|err| { - matches!(err, gix::repository::branch::delete::Error::Cleanup { .. }) + err.downcast_any_ref::() + .is_some() }); if changed { let recorded = before diff --git a/gix-tix/src/ref_tree.rs b/gix-tix/src/ref_tree.rs index 0d9b9f1e059..cba8091cb5c 100644 --- a/gix-tix/src/ref_tree.rs +++ b/gix-tix/src/ref_tree.rs @@ -825,7 +825,7 @@ pub(crate) fn pin_references_reporting( { let mut reference = match reference { Ok(reference) => reference, - Err(err) if crate::history::is_missing_ref(&*err) => continue, + Err(err) if crate::history::is_missing_ref(&err) => continue, Err(err) => return Err(anyhow::anyhow!("could not read reference to pin: {err}")), }; let name = reference.name().to_owned(); diff --git a/gix/Cargo.toml b/gix/Cargo.toml index 73534a643e1..21550ddab2b 100644 --- a/gix/Cargo.toml +++ b/gix/Cargo.toml @@ -296,6 +296,9 @@ max-performance = ["max-performance-safe"] ## All [`gix::Error`](crate::Error) will produce error chains, which makes them work as expected in conjunction with `anyhow`. auto-chain-error = ["gix-error/auto-chain-error"] +## Allow [`gix::Exn`](crate::Exn) to convert directly into `anyhow::Error`. +anyhow = ["gix-error/anyhow"] + ## The opposite of `auto-chain-error`. Use it to override `auto-chain-error`. tree-error = ["gix-error/tree-error"] @@ -403,7 +406,6 @@ gix-transport = { version = "^0.59.2", path = "../gix-transport", optional = tru # Just to get the progress-tree feature prodash = { version = "31.0.0", optional = true, features = ["progress-tree"] } signal-hook = { version = "0.4.3", default-features = false, optional = true } -thiserror = "2.0.18" nonempty = "0.12.0" serde = { version = "1.0.114", optional = true, default-features = false, features = [ "derive", diff --git a/gix/src/clone/access.rs b/gix/src/clone/access.rs index 2c13d10a14b..e81078d76f4 100644 --- a/gix/src/clone/access.rs +++ b/gix/src/clone/access.rs @@ -18,7 +18,7 @@ impl PrepareFetch { /// Otherwise, a clone is configured to be complete and fetches all tags, not only those reachable from all branches. pub fn configure_remote( mut self, - f: impl FnMut(crate::Remote<'_>) -> Result, Box> + 'static, + f: impl FnMut(crate::Remote<'_>) -> Result, gix_error::Exn> + 'static, ) -> Self { self.configure_remote = Some(Box::new(f)); self @@ -81,8 +81,7 @@ impl PrepareFetch { self.revision = revision .map(|revision| { let revision = revision.into(); - let spec = gix_refspec::parse(revision.as_ref(), gix_refspec::parse::Operation::Fetch) - .map_err(gix_error::Exn::into_error)?; + let spec = gix_refspec::parse(revision.as_ref(), gix_refspec::parse::Operation::Fetch)?; let source = spec.source().expect("one-sided non-empty fetch refspec"); let is_full_ref = source.starts_with(b"refs/") && source.find_byteset(b"*?[]\\").is_none(); let is_valid = revision.as_bstr() == source diff --git a/gix/src/clone/checkout.rs b/gix/src/clone/checkout.rs index db1be304fb8..09ecf97f492 100644 --- a/gix/src/clone/checkout.rs +++ b/gix/src/clone/checkout.rs @@ -2,40 +2,14 @@ use crate::{Repository, clone::PrepareCheckout}; /// pub mod main_worktree { - use std::{path::PathBuf, sync::atomic::AtomicBool}; + use std::sync::atomic::AtomicBool; + + use gix_error::ResultExt; use crate::{Progress, Repository, clone::PrepareCheckout}; /// The error returned by [`PrepareCheckout::main_worktree()`]. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error("Repository at \"{}\" is a bare repository and cannot have a main worktree checkout", git_dir.display())] - BareRepository { git_dir: PathBuf }, - #[error("The object pointed to by HEAD is not a treeish")] - NoHeadTree(#[from] crate::object::peel::to_kind::Error), - #[error("Could not create index from tree at {id}")] - IndexFromTree { - id: gix_hash::ObjectId, - source: gix_error::Error, - }, - #[error("Couldn't obtain configuration for core.protect*")] - BooleanConfig(#[from] crate::config::boolean::Error), - #[error(transparent)] - WriteIndex(gix_error::Error), - #[error(transparent)] - CheckoutOptions(#[from] crate::config::checkout_options::Error), - #[error(transparent)] - IndexCheckout(#[from] gix_error::Error), - #[error(transparent)] - Peel(#[from] crate::reference::peel::Error), - #[error("Failed to reopen object database as Arc (only if thread-safety wasn't compiled in)")] - OpenArcOdb(#[from] std::io::Error), - #[error("The HEAD reference could not be located")] - FindHead(#[from] crate::reference::find::existing::Error), - #[error("The HEAD reference could not be located")] - PeelHeadToId(#[from] crate::head::peel::Error), - } + pub type Error = gix_error::Error; /// The progress ids used in [`PrepareCheckout::main_worktree()`]. /// @@ -92,17 +66,35 @@ pub mod main_worktree { .repo .as_ref() .expect("BUG: this method may only be called until it is successful"); - let workdir = repo.workdir().ok_or_else(|| Error::BareRepository { - git_dir: repo.git_dir().to_owned(), + let workdir = repo.workdir().ok_or_else(|| { + gix_error::Error::from_error(gix_error::message!( + "Repository at \"{}\" is a bare repository and cannot have a main worktree checkout", + repo.git_dir().display() + )) })?; let root_tree_id = match &self.ref_name { - Some(reference_val) => Some(repo.find_reference(reference_val)?.peel_to_id()?), - None => repo.head()?.try_peel_to_id()?, + Some(reference_val) => Some( + repo.find_reference(reference_val) + .or_raise(|| gix_error::message("The HEAD reference could not be located"))? + .peel_to_id() + .or_erased()?, + ), + None => repo + .head() + .or_raise(|| gix_error::message("The HEAD reference could not be located"))? + .try_peel_to_id() + .or_raise(|| gix_error::message("The HEAD reference could not be located"))?, }; let root_tree = match root_tree_id { - Some(id) => id.object().expect("downloaded from remote").peel_to_tree()?.id, + Some(id) => { + id.object() + .expect("downloaded from remote") + .peel_to_tree() + .or_raise(|| gix_error::message("The object pointed to by HEAD is not a treeish"))? + .id + } None => { return Ok(( self.repo.take().expect("still present"), @@ -111,11 +103,12 @@ pub mod main_worktree { } }; - let index = gix_index::State::from_tree(&root_tree, &repo.objects, repo.config.protect_options()?) - .map_err(|err| Error::IndexFromTree { - id: root_tree, - source: err.into_error(), - })?; + let protect_options = repo + .config + .protect_options() + .or_raise(|| gix_error::message("Couldn't obtain configuration for core.protect*"))?; + let index = gix_index::State::from_tree(&root_tree, &repo.objects, protect_options) + .or_raise(|| gix_error::message!("Could not create index from tree at {root_tree}"))?; let mut index = gix_index::File::from_state(index, repo.index_path()); let mut opts = repo.checkout_options(gix_worktree::stack::state::attributes::Source::IdMapping)?; @@ -131,7 +124,11 @@ pub mod main_worktree { let outcome = gix_worktree_state::checkout( &mut index, workdir, - repo.objects.clone().into_arc()?, + repo.objects.clone().into_arc().or_raise(|| { + gix_error::message( + "Failed to reopen object database as Arc (only if thread-safety wasn't compiled in)", + ) + })?, &files, &bytes, should_interrupt, @@ -141,9 +138,7 @@ pub mod main_worktree { files.show_throughput(start); bytes.show_throughput(start); - index - .write(Default::default()) - .map_err(|err| Error::WriteIndex(err.into_error()))?; + index.write(Default::default()).map_err(gix_error::Error::from)?; Ok((self.repo.take().expect("still present").clone(), outcome)) } } diff --git a/gix/src/clone/fetch/mod.rs b/gix/src/clone/fetch/mod.rs index 131903dd558..5f0f09ba2bf 100644 --- a/gix/src/clone/fetch/mod.rs +++ b/gix/src/clone/fetch/mod.rs @@ -1,7 +1,5 @@ -use crate::{ - bstr::{BStr, BString, ByteSlice}, - clone::PrepareFetch, -}; +use crate::{bstr::BStr, clone::PrepareFetch}; +use gix_error::ResultExt; use gix_ref::Category; use crate::config::tree::Key; @@ -11,82 +9,7 @@ fn mapping_validation(err: &gix_error::Error) -> Option<&gix_refspec::match_grou } /// The error returned by [`PrepareFetch::fetch_only()`]. -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] -pub enum Error { - #[error(transparent)] - Connect(#[from] crate::remote::connect::Error), - #[error(transparent)] - PrepareFetch(#[from] crate::remote::fetch::prepare::Error), - #[error(transparent)] - Fetch(#[from] crate::remote::fetch::Error), - #[error(transparent)] - RemoteInit(#[from] crate::remote::init::Error), - #[error("Custom configuration of remote to clone from failed")] - RemoteConfiguration(#[source] Box), - #[error("Custom configuration of connection to use when cloning failed")] - RemoteConnection(#[source] Box), - #[error(transparent)] - RemoteName(#[from] crate::config::remote::symbolic_name::Error), - #[error(transparent)] - ParseConfig(#[from] crate::config::overrides::Error), - #[error(transparent)] - ApplyConfig(#[from] crate::config::Error), - #[error(transparent)] - Config(#[from] gix_error::ValidationError), - #[error("Failed to load repo-local git configuration before writing")] - LoadConfig(#[from] gix_config::file::init::from_paths::Error), - #[error("Failed to store configured remote in memory")] - SaveConfig(#[from] crate::remote::save::AsError), - #[error("Failed to write repository configuration to disk")] - SaveConfigIo(#[from] std::io::Error), - #[error("Failed to acquire lock to write repository configuration to disk")] - SaveConfigLockAcquire(#[source] gix_error::Error), - #[error("Failed to commit lock after writing repository configuration to disk")] - SaveConfigLockCommit(#[from] gix_lock::commit::Error), - #[error("The remote HEAD points to a reference named {head_ref_name:?} which is invalid.")] - InvalidHeadRef { - source: gix_validate::reference::name::Error, - head_ref_name: crate::bstr::BString, - }, - #[error("Failed to update HEAD with values from remote")] - HeadUpdate(#[from] crate::reference::edit::Error), - #[error("The remote didn't have any ref that matched '{}'", wanted.as_ref().as_bstr())] - RefNameMissing { wanted: gix_ref::PartialName }, - #[error("The remote has {} refs for '{}', try to use a specific name: {}", candidates.len(), wanted.as_ref().as_bstr(), candidates.iter().filter_map(|n| n.to_str().ok()).collect::>().join(", "))] - RefNameAmbiguous { - wanted: gix_ref::PartialName, - candidates: Vec, - }, - #[error("The remote didn't have the requested revision {wanted:?}")] - RevisionMissing { wanted: BString }, - #[error("The requested revision could not be read")] - FindRevision(#[from] crate::object::find::existing::Error), - #[error("The requested revision did not peel to a commit")] - PeelRevision(#[from] crate::object::peel::to_kind::Error), - #[error(transparent)] - CommitterOrFallback(#[from] crate::config::commit_signature::Error), - #[error(transparent)] - RefMap(#[from] crate::remote::ref_map::Error), - #[error(transparent)] - ReferenceName(#[from] gix_validate::reference::name::Error), - #[error( - "Remote now uses {remote} even though the local repository was reconfigured to {local} \ - to match the remote's previously advertised object format. The server is advertising \ - inconsistent formats; if the correct one is known, pass `create::Options {{ \ - object_hash: Some(gix_hash::Kind::{remote:?}), .. }}` to `PrepareFetch::new` to pin it." - )] - IncompatibleObjectHash { - local: gix_hash::Kind, - remote: gix_hash::Kind, - }, - #[cfg(feature = "sha256")] - #[error("Failed to reopen the local repository after adopting the remote's object format")] - ReopenWithObjectHash(#[from] crate::open::Error), - #[cfg(feature = "sha256")] - #[error("Failed to transfer in-memory configuration after adopting the remote's object format")] - TransferInMemoryConfig(#[from] gix_config::file::init::Error), -} +pub type Error = gix_error::Error; /// Modification impl PrepareFetch { @@ -122,11 +45,13 @@ impl PrepareFetch { .expect("user error: multiple calls are allowed only until it succeeds") .clone(); - repo.committer_or_set_generic_fallback()?; + repo.committer_or_set_generic_fallback().or_erased()?; if !self.config_overrides.is_empty() { let mut snapshot = repo.config_snapshot_mut(); - snapshot.append_config(&self.config_overrides, gix_config::Source::Api)?; + snapshot + .append_config(&self.config_overrides, gix_config::Source::Api) + .or_erased()?; } let remote_name = match self.remote_name.as_ref() { @@ -153,13 +78,15 @@ impl PrepareFetch { && self.fetch_options.extra_refspecs.is_empty() && self.revision.is_none(); - let target_ref = if use_single_branch_for_shallow { + let target_ref: Option = if use_single_branch_for_shallow { // Determine target branch from user-specified ref_name or default branch if let Some(ref_name) = &self.ref_name { let prev_tags = std::mem::replace(&mut remote.fetch_tags, remote::fetch::Tags::None); let mut connection = remote.connect(remote::Direction::Fetch).await?; if let Some(f) = self.configure_connection.as_mut() { - f(&mut connection).map_err(Error::RemoteConnection)?; + f(&mut connection).or_raise(|| { + gix_error::message("Custom configuration of connection to use when cloning failed") + })?; } let (refmap, _) = connection .ref_map( @@ -176,14 +103,16 @@ impl PrepareFetch { .await?; let (_target, full_ref_name) = util::find_custom_refname(&refmap, ref_name)?; remote.fetch_tags = prev_tags; - Some(full_ref_name.try_into()?) + Some(gix_ref::FullName::try_from(full_ref_name).or_erased()?) } else { // For shallow clones without a specified ref, we need to determine the ref to clone. // Just fetch HEAD for that. let prev_tags = std::mem::replace(&mut remote.fetch_tags, remote::fetch::Tags::None); let mut connection = remote.connect(remote::Direction::Fetch).await?; if let Some(f) = self.configure_connection.as_mut() { - f(&mut connection).map_err(Error::RemoteConnection)?; + f(&mut connection).or_raise(|| { + gix_error::message("Custom configuration of connection to use when cloning failed") + })?; } let (refmap, _) = connection .ref_map( @@ -210,17 +139,20 @@ impl PrepareFetch { | gix_protocol::handshake::Ref::Unborn { full_ref_name, target, .. } if full_ref_name == "HEAD" => gix_ref::FullName::try_from(target) - .map_err(|err| Error::InvalidHeadRef { - head_ref_name: target.clone(), - source: err, + .or_raise(|| { + gix_error::ValidationError::new(format!( + "The remote HEAD points to a reference named {target:?} which is invalid." + )) }) .into(), _ => None, }) .transpose()?; - let target = target.ok_or_else(|| Error::RefNameMissing { - wanted: "HEAD".try_into().expect("valid partial name"), + let target = target.ok_or_else(|| { + gix_error::Error::from_error(gix_error::NotFoundError::new( + "The remote didn't have a ref that matched 'HEAD'", + )) })?; remote.fetch_tags = prev_tags; @@ -261,7 +193,8 @@ impl PrepareFetch { let mut clone_fetch_tags = None; if let Some(f) = self.configure_remote.as_mut() { - remote = f(remote).map_err(Error::RemoteConfiguration)?; + remote = + f(remote).or_raise(|| gix_error::message("Custom configuration of remote to clone from failed"))?; } else if self.revision.is_none() { clone_fetch_tags = remote::fetch::Tags::All.into(); } @@ -300,7 +233,8 @@ impl PrepareFetch { // For shallow clones, we already connected once, so we need to connect again let mut connection = remote.connect(remote::Direction::Fetch).await?; if let Some(f) = self.configure_connection.as_mut() { - f(&mut connection).map_err(Error::RemoteConnection)?; + f(&mut connection) + .or_raise(|| gix_error::message("Custom configuration of connection to use when cloning failed"))?; } let connection = connection.into_detached(); let mut fetch_opts = { @@ -324,18 +258,18 @@ impl PrepareFetch { }; match connection.prepare_fetch(&repo, &mut progress, fetch_opts.clone()).await { Ok(prepare) => prepare, - Err(remote::fetch::prepare::Error::RefMap(remote::ref_map::Error::InitRefMap(err))) - if mapping_validation(&err).is_some_and(|err| { - err.issues.len() == 1 - && fetch_opts.extra_refspecs.contains(&head_refspec) - && matches!( - err.issues.first(), - Some(gix_refspec::match_group::validate::Issue::Conflict { - destination_full_ref_name, - .. - }) if *destination_full_ref_name == head_local_tracking_branch - ) - }) => + Err(err) + if fetch_opts.extra_refspecs.contains(&head_refspec) + && mapping_validation(&err).is_some_and(|err| { + err.issues.len() == 1 + && matches!( + err.issues.first(), + Some(gix_refspec::match_group::validate::Issue::Conflict { + destination_full_ref_name, + .. + }) if destination_full_ref_name.as_slice() == head_local_tracking_branch.as_bytes() + ) + }) => { let head_refspec_idx = fetch_opts .extra_refspecs @@ -351,7 +285,7 @@ impl PrepareFetch { fetch_opts.extra_refspecs.remove(head_refspec_idx); connection.prepare_fetch(&repo, &mut progress, fetch_opts).await? } - Err(err) => return Err(err.into()), + Err(err) => return Err(err), } }; drop(remote); @@ -371,9 +305,12 @@ impl PrepareFetch { let remote_object_hash = pending_pack.ref_map().object_hash; if remote_object_hash != repo.object_hash() { let mut in_memory_config = Vec::new(); - repo.config.resolved.write_to_filter(&mut in_memory_config, |section| { - section.meta().source == gix_config::Source::Api - })?; + repo.config + .resolved + .write_to_filter(&mut in_memory_config, |section| { + section.meta().source == gix_config::Source::Api + }) + .or_erased()?; // Reopen the still-empty repo with the remote's format; on error the original is kept for a retry. repo = util::reinitialize_with_object_hash(&repo, remote_object_hash)?; let mut resolved_config = repo.config.resolved.as_ref().clone(); @@ -383,11 +320,20 @@ impl PrepareFetch { // TODO: make this much easier - we go from parsed-to-buffer-to-parsed. // Maybe make API changes available as overlay, just as utility over // Api sections. - resolved_config.append(gix_config::File::from_bytes_owned( - &mut in_memory_config, - gix_config::file::Metadata::api(), - Default::default(), - )?)?; + resolved_config + .append( + gix_config::File::from_bytes_owned( + &mut in_memory_config, + gix_config::file::Metadata::api(), + Default::default(), + ) + .map_err(gix_error::Exn::into_error)?, + ) + .or_raise(|| { + gix_error::message( + "Failed to transfer in-memory configuration after adopting the remote's object format", + ) + })?; repo.config .reread_values_and_clear_caches_replacing_config(resolved_config.into())?; config = None; @@ -410,7 +356,7 @@ impl PrepareFetch { // Before finalisation, the current repo handle still needs to // learn about the remote config written after it was opened. if let Some(config) = config { - util::append_config_to_repo_config(&mut repo, config)?; + util::append_config_to_repo_config(&mut repo, config).or_erased()?; } util::update_head( &mut repo, diff --git a/gix/src/clone/fetch/util.rs b/gix/src/clone/fetch/util.rs index f1a9a171eca..8d09355bc0c 100644 --- a/gix/src/clone/fetch/util.rs +++ b/gix/src/clone/fetch/util.rs @@ -1,5 +1,6 @@ use std::io::Write; +use gix_error::ResultExt; use gix_ref::{ Category, FullNameRef, PartialName, transaction::{LogChange, RefLog}, @@ -15,19 +16,17 @@ enum WriteMode { Overwrite, Append, } - -#[expect( - clippy::result_large_err, - reason = "will be removed once `gix-error` is used consistently" -)] pub fn append_remote_to_local_config_file( remote: &mut crate::Remote<'_>, remote_name: BString, ) -> Result { let mut config = gix_config::File::new(local_config_meta(remote.repo)); - remote.save_as_to(remote_name, &mut config)?; + remote + .save_as_to(remote_name, &mut config) + .or_raise(|| gix_error::message("Failed to store configured remote in memory"))?; - write_to_local_config(&config, WriteMode::Append)?; + write_to_local_config(&config, WriteMode::Append) + .or_raise(|| gix_error::message("Failed to write repository configuration to disk"))?; Ok(config) } @@ -55,29 +54,39 @@ pub(super) fn reinitialize_with_object_hash( let git_dir = repo.git_dir(); let config_path = git_dir.join("config"); - let mut config = gix_config::File::from_path_no_includes(config_path.clone(), gix_config::Source::Local)?; + let mut config = gix_config::File::from_path_no_includes(config_path.clone(), gix_config::Source::Local) + .or_raise(|| gix_error::message("Failed to load repo-local git configuration before writing"))?; // Mirror what `crate::create` writes at init time: only SHA-256 repositories get // `repositoryformatversion = 1` along with the `objectformat` extension. let is_sha256 = object_hash == gix_hash::Kind::Sha256; config .section_mut("core", None) .expect("freshly initialized repository has a core section") - .set("repositoryformatversion", if is_sha256 { "1" } else { "0" })?; + .set("repositoryformatversion", if is_sha256 { "1" } else { "0" }) + .or_erased()?; if is_sha256 { config .section_mut_or_create_new("extensions", None) .expect("valid section name") - .set("objectformat", object_hash.to_string())?; + .set("objectformat", object_hash.to_string()) + .or_erased()?; } else { // In a freshly initialized repository, this section exists solely to carry `objectformat`. config.remove_section("extensions", None); } let mut lock = gix_lock::File::acquire_to_update_resource(&config_path, gix_lock::acquire::Fail::Immediately, None) - .map_err(|err| Error::SaveConfigLockAcquire(err.into_error()))?; - config.write_to_filter(&mut lock, |section| section.meta().source == gix_config::Source::Local)?; - lock.commit()?; + .or_raise(|| gix_error::message("Failed to acquire lock to write repository configuration to disk"))?; + config + .write_to_filter(&mut lock, |section| section.meta().source == gix_config::Source::Local) + .or_raise(|| gix_error::message("Failed to write repository configuration to disk"))?; + lock.commit() + .or_raise(|| gix_error::message("Failed to commit lock after writing repository configuration to disk"))?; - Ok(crate::ThreadSafeRepository::open_opts(git_dir, repo.options.clone())?.to_thread_local()) + Ok(crate::ThreadSafeRepository::open_opts(git_dir, repo.options.clone()) + .or_raise(|| { + gix_error::message("Failed to reopen the local repository after adopting the remote's object format") + })? + .to_thread_local()) } fn local_config_meta(repo: &Repository) -> gix_config::file::Metadata { @@ -134,10 +143,13 @@ pub fn update_head( let revision_head_id = revision .map(|revision| -> Result { let mapping = find_revision(ref_map, revision)?; - let id = mapping.remote.peeled_id().ok_or_else(|| Error::RevisionMissing { - wanted: revision.to_ref().source().expect("validated revision").to_owned(), - })?; - Ok(repo.find_object(id)?.peel_to_commit()?.id) + let id = mapping.remote.peeled_id().ok_or_else(|| revision_missing(revision))?; + Ok(repo + .find_object(id) + .or_raise(|| gix_error::message("The requested revision could not be read"))? + .peel_to_commit() + .or_raise(|| gix_error::message("The requested revision did not peel to a commit"))? + .id) }) .transpose()?; let head_info = match revision_head_id.as_ref() { @@ -178,9 +190,10 @@ pub fn update_head( }; match head_ref { Some(referent) => { - let referent: gix_ref::FullName = referent.try_into().map_err(|err| Error::InvalidHeadRef { - head_ref_name: referent.to_owned(), - source: err, + let referent: gix_ref::FullName = gix_ref::FullName::try_from(referent).or_raise(|| { + gix_error::ValidationError::new(format!( + "The remote HEAD points to a reference named {referent:?} which is invalid." + )) })?; repo.refs .transaction() @@ -208,13 +221,14 @@ pub fn update_head( gix_lock::acquire::Fail::Immediately, gix_lock::acquire::Fail::Immediately, ) - .map_err(crate::reference::edit::Error::from)? + .or_raise(|| gix_error::message("Failed to update HEAD with values from remote"))? .commit( repo.committer() .transpose() - .map_err(|err| Error::HeadUpdate(crate::reference::edit::Error::ParseCommitterTime(err)))?, + .map_err(gix_error::Error::from) + .or_raise(|| gix_error::message("Failed to update HEAD with values from remote"))?, ) - .map_err(crate::reference::edit::Error::from)?; + .or_raise(|| gix_error::message("Failed to update HEAD with values from remote"))?; if let Some(head_peeled_id) = head_peeled_id { let mut log = reflog_message(); @@ -224,7 +238,8 @@ pub fn update_head( head_peeled_id.to_owned(), PreviousValue::Any, log, - ))?; + )) + .or_raise(|| gix_error::message("Failed to update HEAD with values from remote"))?; } setup_branch_config(repo, referent.as_ref(), head_peeled_id, remote_name)?; @@ -237,7 +252,8 @@ pub fn update_head( .to_owned(), PreviousValue::Any, reflog_message(), - ))?; + )) + .or_raise(|| gix_error::message("Failed to update HEAD with values from remote"))?; } } Ok(()) @@ -245,7 +261,7 @@ pub fn update_head( /// Find the mapping produced by the exact refspec used to request `revision`. /// -/// Returns [`Error::RevisionMissing`] if the remote did not map that refspec. +/// Returns an error if the remote did not map that refspec. pub(super) fn find_revision<'a>( ref_map: &'a crate::remote::fetch::RefMap, revision: &gix_refspec::RefSpec, @@ -259,9 +275,14 @@ pub(super) fn find_revision<'a>( .get(&ref_map.refspecs, &ref_map.extra_refspecs) .is_some_and(|spec| spec == revision) }) - .ok_or_else(|| Error::RevisionMissing { - wanted: revision.to_ref().source().expect("validated revision").to_owned(), - }) + .ok_or_else(|| revision_missing(revision)) +} + +fn revision_missing(revision: &gix_refspec::RefSpec) -> Error { + gix_error::Error::from_error(gix_error::NotFoundError::new(format!( + "The remote didn't have the requested revision {:?}", + revision.to_ref().source().expect("validated revision") + ))) } /// Resolve `ref_name` to its object ID and full name among the mapped remote references. @@ -294,12 +315,12 @@ pub(super) fn find_custom_refname<'a>( return Ok((item.target, item.full_ref_name)); } if !requested_name.starts_with(b"refs/") { - let branch_name = Category::LocalBranch.to_full_name(requested_name)?; + let branch_name = Category::LocalBranch.to_full_name(requested_name).or_erased()?; if let Some(item) = find_item(branch_name.as_bstr()) { return Ok((item.target, item.full_ref_name)); } - let tag_name = Category::Tag.to_full_name(requested_name)?; + let tag_name = Category::Tag.to_full_name(requested_name).or_erased()?; if let Some(item) = find_item(tag_name.as_bstr()) { return Ok((item.target, item.full_ref_name)); } @@ -307,26 +328,39 @@ pub(super) fn find_custom_refname<'a>( let res = group.match_lhs(filtered_items.iter().copied()); match res.mappings.len() { - 0 => Err(Error::RefNameMissing { - wanted: ref_name.clone(), - }), + 0 => Err(gix_error::Error::from_error(gix_error::NotFoundError::new(format!( + "The remote didn't have any ref that matched '{requested_name}'" + )))), 1 => { let item = filtered_items[res.mappings[0] .item_index .expect("we map by name only and have no object-id in refspec")]; Ok((item.target, item.full_ref_name)) } - _ => Err(Error::RefNameAmbiguous { - wanted: ref_name.clone(), - candidates: res + _ => { + let candidates = res .mappings .into_iter() .filter_map(|m| match m.lhs { gix_refspec::match_group::SourceRef::FullName(name) => Some(name.into_owned()), gix_refspec::match_group::SourceRef::ObjectId(_) => None, }) - .collect(), - }), + .collect::>(); + Err(gix_error::Error::from_error( + gix_error::ValidationError::new_with_input( + format!( + "The remote has {} refs for '{requested_name}', try to use a specific name: {}", + candidates.len(), + candidates + .iter() + .filter_map(|name| name.to_str().ok()) + .collect::>() + .join(", ") + ), + ref_name.as_ref().as_bstr().to_owned(), + ), + )) + } } } @@ -364,9 +398,9 @@ fn setup_branch_config( let mut section = config .new_section("branch", short_name) .expect("section header name is always valid per naming rules, our input branch name is valid"); - section.push("remote", remote_name)?; - section.push("merge", branch.as_bstr())?; - write_to_local_config(&config, WriteMode::Overwrite)?; + section.push("remote", remote_name).or_erased()?; + section.push("merge", branch.as_bstr()).or_erased()?; + write_to_local_config(&config, WriteMode::Overwrite).or_erased()?; config.commit().expect("configuration we set is valid"); } Ok(()) diff --git a/gix/src/clone/mod.rs b/gix/src/clone/mod.rs index da991dc75dd..4ce09d6682b 100644 --- a/gix/src/clone/mod.rs +++ b/gix/src/clone/mod.rs @@ -1,4 +1,6 @@ #![allow(clippy::result_large_err)] +use gix_error::{Exn, ResultExt}; + use crate::{bstr::BString, remote}; #[cfg(feature = "async-network-client")] @@ -6,14 +8,10 @@ use gix_transport::client::async_io::Transport; #[cfg(feature = "blocking-network-client")] use gix_transport::client::blocking_io::Transport; -type ConfigureRemoteFn = - Box) -> Result, Box>>; +type ConfigureRemoteFn = Box) -> Result, Exn>>; #[cfg(any(feature = "async-network-client", feature = "blocking-network-client"))] -type ConfigureConnectionFn = Box< - dyn FnMut( - &mut remote::Connection<'_, '_, '_, Box>, - ) -> Result<(), Box>, ->; +type ConfigureConnectionFn = + Box>) -> Result<(), Exn>>; /// A utility to collect configuration on how to fetch from a remote and initiate a fetch operation. It will delete the newly /// created repository on when dropped without successfully finishing a fetch. @@ -52,34 +50,43 @@ pub struct PrepareFetch { /// Errors returned by [`PrepareFetch::with_revision()`]. pub mod with_revision { /// An invalid revision for a single-revision clone. - #[derive(Debug, thiserror::Error)] + #[derive(Debug)] #[expect(missing_docs)] pub enum Error { - #[error(transparent)] - Parse(#[from] gix_error::Error), - #[error("A clone revision must be HEAD, a full reference name, or a full object ID, got {revision:?}")] + Parse(gix_error::Error), Invalid { revision: crate::bstr::BString }, } + + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Parse(err) => err.fmt(f), + Error::Invalid { revision } => write!( + f, + "A clone revision must be HEAD, a full reference name, or a full object ID, got {revision:?}" + ), + } + } + } + + impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::Parse(err) => Some(err), + Error::Invalid { .. } => None, + } + } + } + + impl From for Error { + fn from(err: gix_refspec::parse::Error) -> Self { + Error::Parse(err.into_error()) + } + } } /// The error returned by [`PrepareFetch::new()`]. -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] -pub enum Error { - #[error(transparent)] - Config(#[from] crate::config::Error), - #[error(transparent)] - Init(#[from] crate::init::Error), - #[error(transparent)] - CommitterOrFallback(#[from] crate::config::commit_signature::Error), - #[error(transparent)] - UrlParse(#[from] gix_error::Error), - #[error("Failed to turn a the relative file url \"{}\" into an absolute one", url.to_bstring())] - CanonicalizeUrl { - url: gix_url::Url, - source: gix_path::realpath::Error, - }, -} +pub type Error = gix_error::Error; /// Instantiation impl PrepareFetch { @@ -93,10 +100,6 @@ impl PrepareFetch { /// /// Similar to `git`, a missing user name and email configuration is not terminal and we will fill it in with dummy values. However, /// instead of deriving values from the system, ours are hardcoded to indicate what happened. - #[expect( - clippy::result_large_err, - reason = "will be removed once `gix-error` is used consistently" - )] pub fn new( url: Url, path: impl AsRef, @@ -108,19 +111,8 @@ impl PrepareFetch { Url: TryInto, E: std::error::Error + Send + Sync + 'static, { - Self::new_inner( - url.try_into().map_err(gix_error::Error::from_error)?, - path.as_ref(), - kind, - create_opts, - open_opts, - ) + Self::new_inner(url.try_into().or_erased()?, path.as_ref(), kind, create_opts, open_opts) } - - #[expect( - clippy::result_large_err, - reason = "will be removed once `gix-error` is used consistently" - )] fn new_inner( mut url: gix_url::Url, path: &std::path::Path, @@ -157,11 +149,12 @@ impl PrepareFetch { }; let mut repo = crate::ThreadSafeRepository::init_opts(path, kind, create_opts, open_opts)?.to_thread_local(); - url.canonicalize(repo.options.current_dir_or_empty()) - .map_err(|err| Error::CanonicalizeUrl { - url: url.clone(), - source: err, - })?; + url.canonicalize(repo.options.current_dir_or_empty()).map_err(|err| { + gix_error::Error::from(err.raise(gix_error::message!( + "Failed to turn the relative file url {:?} into an absolute one", + url.to_bstring() + ))) + })?; repo.committer_or_set_generic_fallback()?; Ok(PrepareFetch { url, @@ -225,7 +218,7 @@ mod access_feat { mut self, f: impl FnMut( &mut crate::remote::Connection<'_, '_, '_, Box>, - ) -> Result<(), Box> + ) -> Result<(), gix_error::Exn> + 'static, ) -> Self { self.configure_connection = Some(Box::new(f)); diff --git a/gix/src/commit/mod.rs b/gix/src/commit/mod.rs index d84f4397e19..5b457fad4e8 100644 --- a/gix/src/commit/mod.rs +++ b/gix/src/commit/mod.rs @@ -1,8 +1,6 @@ //! #![allow(clippy::empty_docs)] -use std::convert::Infallible; - /// Commit signing errors. #[cfg(feature = "command")] pub mod sign; @@ -45,33 +43,12 @@ fn signature_program( pub const NO_PARENT_IDS: [gix_hash::ObjectId; 0] = []; /// The error returned by [`commit(…)`](crate::Repository::commit()). -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] -pub enum Error { - #[error(transparent)] - ParseTime(#[from] crate::config::time::Error), - #[error("Committer identity is not configured")] - CommitterMissing, - #[error("Author identity is not configured")] - AuthorMissing, - #[error(transparent)] - ReferenceNameValidation(#[from] gix_ref::name::Error), - #[error(transparent)] - WriteObject(#[from] crate::object::write::Error), - #[error(transparent)] - ReferenceEdit(#[from] crate::reference::edit::Error), -} - -impl From for Error { - fn from(_value: Infallible) -> Self { - unreachable!("cannot be invoked") - } -} +pub type Error = gix_error::Error; /// #[cfg(feature = "revision")] pub mod describe { - use gix_error::Exn; + use gix_error::ResultExt; use gix_hash::ObjectId; use gix_hashtable::HashMap; use std::borrow::Cow; @@ -89,7 +66,10 @@ pub mod describe { impl Resolution<'_> { /// Turn this instance into something displayable. pub fn format(self) -> Result, Error> { - let prefix = self.id.shorten()?; + let prefix = self + .id + .shorten() + .or_raise(|| gix_error::message("Could not produce an unambiguous shortened id for formatting."))?; Ok(self.outcome.into_format(prefix.hex_len())) } @@ -104,7 +84,10 @@ pub mod describe { self, dirty_suffix: impl Into>, ) -> Result, Error> { - let prefix = self.id.shorten()?; + let prefix = self + .id + .shorten() + .or_raise(|| gix_error::message("Could not produce an unambiguous shortened id for formatting."))?; let mut dirty_suffix = dirty_suffix.into(); if dirty_suffix.is_some() && !self.id.repo.is_dirty()? { dirty_suffix.take(); @@ -116,23 +99,7 @@ pub mod describe { } /// The error returned by [`try_format()`][Platform::try_format()]. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - OpenCache(#[from] crate::repository::commit_graph_if_enabled::Error), - #[error(transparent)] - Describe(#[from] gix_revision::describe::Error), - #[error("Could not produce an unambiguous shortened id for formatting.")] - ShortId(#[from] crate::id::shorten::Error), - #[error(transparent)] - RefIter(#[from] crate::reference::iter::Error), - #[error(transparent)] - RefIterInit(#[from] crate::reference::iter::init::Error), - #[error(transparent)] - #[cfg(feature = "status")] - DetermineIsDirty(#[from] crate::status::is_dirty::Error), - } + pub type Error = gix_error::Error; /// A selector to choose what kind of references should contribute to names. #[derive(Default, Debug, Clone, Copy, PartialOrd, PartialEq, Ord, Eq, Hash)] @@ -148,7 +115,7 @@ pub mod describe { impl SelectRef { fn names(&self, repo: &Repository) -> Result>, Error> { - let platform = repo.references()?; + let platform = repo.references().or_erased()?; Ok(match self { SelectRef::AllTags | SelectRef::AllRefs => { @@ -282,8 +249,7 @@ pub mod describe { first_parent: self.first_parent, max_candidates: self.max_candidates, }, - ) - .map_err(Exn::into_inner)?; + )?; Ok(outcome.map(|outcome| Resolution { outcome, diff --git a/gix/src/commit/sign.rs b/gix/src/commit/sign.rs index acfa1012fbe..1a9e5243f37 100644 --- a/gix/src/commit/sign.rs +++ b/gix/src/commit/sign.rs @@ -1,69 +1,32 @@ use std::{ffi::OsString, path::PathBuf, process::Stdio}; use crate::{ - bstr::{BString, ByteSlice}, + bstr::ByteSlice, config::tree::{Gpg, Key, User, gpg}, }; +use gix_error::ResultExt; use gix_object::signature::sign::is_literal_ssh_key; pub use gix_object::signature::{Format, sign::Options}; /// Errors encountered when applying resolved signing options to a commit. -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] -pub enum Error { - #[error(transparent)] - Options(#[from] options::Error), - #[error(transparent)] - Decode(#[from] gix_object::decode::Error), - #[error(transparent)] - Sign(#[from] gix_error::Error), - #[error(transparent)] - WriteObject(#[from] crate::object::write::Error), - #[error(transparent)] - FindCommit(#[from] crate::object::find::existing::with_conversion::Error), -} +pub type Error = gix_error::Error; /// Errors encountered when resolving commit-signing options. pub mod options { - use std::ffi::OsString; - - use crate::bstr::BString; - /// The error returned by [`crate::Repository::commit_signing_options()`]. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - ConfigBoolean(#[from] crate::config::boolean::Error), - #[error(transparent)] - ParseTime(#[from] crate::config::time::Error), - #[error("Unsupported value for gpg.format: {0:?}")] - UnsupportedFormat(BString), - #[error("Committer identity is not configured and user.signingKey is unset")] - MissingCommitter, - #[error("user.signingKey or gpg.ssh.defaultKeyCommand must provide an SSH signing key")] - MissingSshSigningKey, - #[error("Could not interpolate a configured commit-signing path")] - ConfiguredPath(#[from] gix_error::Error), - #[error("Could not execute gpg.ssh.defaultKeyCommand {program:?}")] - DefaultKeyCommand { - program: OsString, - #[source] - source: std::io::Error, - }, - #[error("gpg.ssh.defaultKeyCommand failed: {0:?}")] - DefaultKeyCommandFailed(BString), - #[error("gpg.ssh.defaultKeyCommand returned an invalid key: {0:?}")] - InvalidDefaultKey(BString), - } + pub type Error = gix_error::Error; } pub(crate) fn sign<'repo>(commit: &crate::Commit<'repo>) -> Result, Error> { let options = commit.repo.commit_signing_options()?; - let signed = commit.decode()?.sign(options).map_err(gix_error::Exn::into_error)?; + let signed = commit + .decode() + .or_raise(|| gix_error::message("Could not decode the commit before signing"))? + .sign(options) + .or_raise(|| gix_error::message("Could not sign the commit"))?; let id = commit.repo.write_object(&signed)?; - Ok(commit.repo.find_commit(id)?) + commit.repo.find_commit(id) } pub(crate) fn signing_options(repo: &crate::Repository) -> Result { @@ -73,19 +36,33 @@ pub(crate) fn signing_options(repo: &crate::Repository) -> Result config - .trusted_path(User::SIGNING_KEY)? + .trusted_path(User::SIGNING_KEY) + .or_raise(|| gix_error::message("Could not interpolate the configured commit-signing key path"))? .map_or_else(|| gix_path::from_bstring(key).into_os_string(), PathBuf::into_os_string), Some(key) if !key.is_empty() => gix_path::from_bstring(key).into_os_string(), - _ if format == Format::Ssh => default_ssh_key(&config)?.ok_or(Error::MissingSshSigningKey)?, + _ if format == Format::Ssh => default_ssh_key(&config)?.ok_or_else(|| { + Error::from_error(gix_error::message( + "user.signingKey or gpg.ssh.defaultKeyCommand must provide an SSH signing key", + )) + })?, _ => { - let committer = repo.committer().ok_or(Error::MissingCommitter)??; + let committer = repo + .committer() + .ok_or_else(|| { + Error::from_error(gix_error::message( + "Committer identity is not configured and user.signingKey is unset", + )) + })? + .or_raise(|| gix_error::message("Could not parse the committer time for commit signing"))?; let mut identity = committer.name.to_owned(); identity.extend_from_slice(b" <"); identity.extend_from_slice(committer.email); @@ -103,10 +80,12 @@ pub(crate) fn signing_options(repo: &crate::Repository) -> Result Result, options::Error> { - repo.config - .may_sign_commits()? - .then(|| signing_options(repo)) - .transpose() + let enabled = repo + .config + .may_sign_commits() + .or_raise(|| gix_error::message("Could not determine whether commit signing is enabled"))? + .then(|| signing_options(repo)); + enabled.transpose() } fn parse_format(value: &[u8]) -> Option { @@ -133,21 +112,20 @@ fn default_ssh_key(config: &crate::config::Snapshot<'_>) -> Result) -> Result, Error> { - let Some((signature, signed_data)) = commit.signature()? else { + let Some((signature, signed_data)) = commit + .signature() + .or_raise(|| gix_error::message("Could not decode the commit signature"))? + else { return Ok(None); }; let config = commit.repo.config_snapshot(); let minimum_trust = config .string(Gpg::MIN_TRUST_LEVEL) .map(|value| Gpg::MIN_TRUST_LEVEL.try_into_trust_level(value)) - .transpose()? + .transpose() + .or_raise(|| gix_error::message("The configured minimum signature trust level is invalid"))? .unwrap_or_default(); - let format = Format::from_signature(&signature).ok_or_else(|| { - Error::Verify(gix_error::Error::from_error(gix_error::CorruptionError::new( - "The signature format is unsupported", - ))) - })?; - let program = super::signature_program(&config, format).map_err(Error::ConfiguredPath)?; + let format = Format::from_signature(&signature) + .ok_or_else(|| Error::from_error(gix_error::CorruptionError::new("The signature format is unsupported")))?; + let program = super::signature_program(&config, format) + .or_raise(|| gix_error::message("Could not interpolate the configured signature-verification program path"))?; let options = match format { Format::OpenPgp => gix_object::signature::verify::Options::OpenPgp { program, @@ -57,11 +45,15 @@ pub(crate) fn verify(commit: &crate::Commit<'_>) -> Result, Erro Format::Ssh => { let allowed_signers = config .trusted_path(gpg::Ssh::ALLOWED_SIGNERS_FILE) - .map_err(Error::ConfiguredPath)? - .ok_or(Error::MissingAllowedSigners)?; + .or_raise(|| gix_error::message("Could not interpolate a configured signature-verification path"))? + .ok_or_else(|| { + Error::from_error(gix_error::message( + "gpg.ssh.allowedSignersFile must be configured for SSH signature verification", + )) + })?; let revocation_file = config .trusted_path(gpg::Ssh::REVOCATION_FILE) - .map_err(Error::ConfiguredPath)? + .or_raise(|| gix_error::message("Could not interpolate a configured signature-verification path"))? .filter(|path| path.exists()); gix_object::signature::verify::Options::Ssh { program, @@ -76,8 +68,7 @@ pub(crate) fn verify(commit: &crate::Commit<'_>) -> Result, Erro }; let outcome = signed_data .verify(&signature, options) - .map_err(gix_error::Exn::into_error) - .map_err(Error::Verify)?; + .or_raise(|| gix_error::message("Could not verify the commit signature"))?; Ok(Some(outcome)) } diff --git a/gix/src/config/cache/access.rs b/gix/src/config/cache/access.rs index 266faafe973..7ecb24dc93c 100644 --- a/gix/src/config/cache/access.rs +++ b/gix/src/config/cache/access.rs @@ -2,6 +2,10 @@ use std::{path::PathBuf, time::Duration}; use gix_config::file::Metadata; +#[cfg(feature = "blob-diff")] +use gix_error::ErrorExt; +#[cfg(any(feature = "attributes", feature = "excludes"))] +use gix_error::ResultExt; use gix_lock::acquire::Fail; use crate::{ @@ -67,10 +71,11 @@ impl Cache { driver.is_binary = config::tree::Diff::DRIVER_BINARY .try_into_binary(binary) .with_leniency(self.lenient_config) - .map_err(|err| config::diff::drivers::Error { - name: driver.name.clone(), - attribute: "binary", - source: Box::new(err), + .map_err(|err| { + gix_error::Error::from(err.and_raise(gix_error::message!( + "Failed to parse value of 'diff.{}.binary'", + driver.name + ))) })?; } if let Some(command) = section.value(config::tree::Diff::DRIVER_COMMAND.name) { @@ -89,10 +94,11 @@ impl Cache { err => Err(err), }) .with_lenient_default(self.lenient_config) - .map_err(|err| config::diff::drivers::Error { - name: driver.name.clone(), - attribute: "algorithm", - source: Box::new(err), + .map_err(|err| { + gix_error::Error::from(err.and_raise(gix_error::message!( + "Failed to parse value of 'diff.{}.algorithm'", + driver.name + ))) })? .into(); } @@ -141,7 +147,7 @@ impl Cache { &self, ) -> Result { Ok(gix_merge::blob::pipeline::Options { - large_file_threshold_bytes: self.big_file_threshold()?, + large_file_threshold_bytes: self.big_file_threshold().map_err(gix_error::Error::from)?, }) } @@ -150,8 +156,8 @@ impl Cache { &self, ) -> Result { Ok(gix_diff::blob::pipeline::Options { - large_file_threshold_bytes: self.big_file_threshold()?, - fs: self.fs_capabilities()?, + large_file_threshold_bytes: self.big_file_threshold().map_err(gix_error::Error::from)?, + fs: self.fs_capabilities().map_err(gix_error::Error::from)?, }) } @@ -294,16 +300,19 @@ impl Cache { pub(crate) fn stat_options(&self) -> Result { use crate::config::tree::gitoxide; Ok(gix_index::entry::stat::Options { - trust_ctime: boolean(self, "core.trustCTime", &Core::TRUST_C_TIME, true)?, - use_nsec: boolean(self, "gitoxide.core.useNsec", &gitoxide::Core::USE_NSEC, false)?, - use_stdev: boolean(self, "gitoxide.core.useStdev", &gitoxide::Core::USE_STDEV, false)?, + trust_ctime: boolean(self, "core.trustCTime", &Core::TRUST_C_TIME, true).map_err(gix_error::Error::from)?, + use_nsec: boolean(self, "gitoxide.core.useNsec", &gitoxide::Core::USE_NSEC, false) + .map_err(gix_error::Error::from)?, + use_stdev: boolean(self, "gitoxide.core.useStdev", &gitoxide::Core::USE_STDEV, false) + .map_err(gix_error::Error::from)?, check_stat: self .apply_leniency( self.resolved .string(Core::CHECK_STAT) .map(|v| Core::CHECK_STAT.try_into_checkstat(v)) .transpose(), - )? + ) + .map_err(gix_error::Error::from)? .unwrap_or(true), }) } @@ -339,13 +348,15 @@ impl Cache { ) -> Result { use crate::config::tree::gitoxide; let git_dir = repo.git_dir(); - let thread_limit = self.apply_leniency( - crate::config::tree::Checkout::WORKERS.try_from_workers( - self.resolved - .integer_filter("checkout.workers", &mut self.filter_config_section.clone()), - ), - )?; - let capabilities = self.fs_capabilities()?; + let thread_limit = self + .apply_leniency( + crate::config::tree::Checkout::WORKERS.try_from_workers( + self.resolved + .integer_filter("checkout.workers", &mut self.filter_config_section.clone()), + ), + ) + .map_err(gix_error::Error::from)?; + let capabilities = self.fs_capabilities().map_err(gix_error::Error::from)?; let filters = { let mut filters = gix_filter::Pipeline::new(repo.command_context()?, crate::filter::Pipeline::options(repo)?); @@ -361,14 +372,16 @@ impl Cache { "gitoxide.core.filterProcessDelay", &gitoxide::Core::FILTER_PROCESS_DELAY, true, - )? { + ) + .map_err(gix_error::Error::from)? + { gix_filter::driver::apply::Delay::Allow } else { gix_filter::driver::apply::Delay::Forbid }; Ok(gix_worktree_state::checkout::Options { filter_process_delay, - validate: self.protect_options()?, + validate: self.protect_options().map_err(gix_error::Error::from)?, filters, attributes: self .assemble_attribute_globals(git_dir, attributes_source, self.attributes)? @@ -378,12 +391,7 @@ impl Cache { destination_is_initially_empty: false, overwrite_existing: false, keep_going: false, - stat_options: self.stat_options().map_err(|err| match err { - config::stat_options::Error::ConfigCheckStat(err) => { - config::checkout_options::Error::ConfigCheckStat(err) - } - config::stat_options::Error::ConfigBoolean(err) => config::checkout_options::Error::ConfigBoolean(err), - })?, + stat_options: self.stat_options()?, }) } @@ -407,14 +415,19 @@ impl Cache { source: gix_worktree::stack::state::ignore::Source, buf: &mut Vec, ) -> Result { - let excludes_file = match self.excludes_file().map_err(gix_error::Exn::into_error)? { + let excludes_file = match self.excludes_file().map_err(|err| { + gix_error::Error::from(err.raise(gix_error::message( + "The value for `core.excludesFile` could not be read from configuration", + ))) + })? { Some(user_path) => Some(user_path), - None => self.xdg_config_path("ignore")?, + None => self.xdg_config_path("ignore").or_erased()?, }; - let parse_ignore = self.ignore_pattern_parser()?; + let parse_ignore = self.ignore_pattern_parser().map_err(gix_error::Error::from)?; Ok(gix_worktree::stack::state::Ignore::new( overrides.unwrap_or_default(), - gix_ignore::Search::from_git_dir(git_dir, excludes_file, buf, parse_ignore)?, + gix_ignore::Search::from_git_dir(git_dir, excludes_file, buf, parse_ignore) + .or_raise(|| gix_error::message("Could not read repository exclude"))?, None, source, parse_ignore, @@ -429,10 +442,9 @@ impl Cache { attributes: crate::open::permissions::Attributes, ) -> Result<(gix_worktree::stack::state::Attributes, Vec), config::attribute_stack::Error> { use gix_attributes::Source; - let configured_or_user_attributes = match self - .trusted_file_path(Core::ATTRIBUTES_FILE) - .map_err(gix_error::Exn::into_error)? - { + let configured_or_user_attributes = match self.trusted_file_path(Core::ATTRIBUTES_FILE).or_raise(|| { + gix_error::message("Failed to interpolate the attribute file configured at `core.attributesFile`") + })? { Some(attributes) => Some(attributes), None => { if attributes.git { @@ -455,7 +467,8 @@ impl Cache { let mut buf = Vec::new(); let mut collection = gix_attributes::search::MetadataCollection::default(); let state = gix_worktree::stack::state::Attributes::new( - gix_attributes::Search::new_globals(attribute_files, &mut buf, &mut collection)?, + gix_attributes::Search::new_globals(attribute_files, &mut buf, &mut collection) + .or_raise(|| gix_error::message("An attribute file could not be read"))?, Some(info_attributes_path), source, collection, diff --git a/gix/src/config/cache/incubate.rs b/gix/src/config/cache/incubate.rs index aca00b906e4..af54d22dc97 100644 --- a/gix/src/config/cache/incubate.rs +++ b/gix/src/config/cache/incubate.rs @@ -5,6 +5,7 @@ use crate::config::{ cache::util::{ApplyLeniency, ApplyLeniencyDefaultValue}, tree::{Core, Extensions, gitoxide}, }; +use gix_error::{ErrorExt, ResultExt}; /// A utility to deal with the cyclic dependency between the ref store and the configuration. The ref-store needs the /// object hash kind, and the configuration needs the current branch name to resolve conditional includes with `onbranch`. @@ -46,9 +47,17 @@ impl StageOne { let object_hash = match (repo_format_version, config.string(Extensions::OBJECT_FORMAT)) { // objectFormat is a repository format version 1 extension. (1, Some(format)) => Extensions::OBJECT_FORMAT.try_into_object_format(format)?, - (0, Some(_)) => return Err(Error::ObjectFormatRequiresV1), + (0, Some(_)) => { + return Err(gix_error::Error::from_error(gix_error::ValidationError::new( + "extensions.objectFormat is a v1-only extension, but the repository format version is 0; set core.repositoryFormatVersion=1 to use it, or remove extensions.objectFormat to fall back to the default Sha1 format (if supported by this build)", + ))); + } (0 | 1, None) => legacy_object_hash()?, - (version, _) => return Err(Error::UnsupportedRepositoryFormatVersion { version }), + (version, _) => { + return Err(gix_error::Error::from_error(gix_error::ValidationError::new(format!( + "Unsupported repository format version {version}; only versions 0 and 1 are supported" + )))); + } }; let extension_worktree = util::config_bool( @@ -67,12 +76,12 @@ impl StageOne { lossy, lenient, )?; - config.append(worktree_config)?; + config.append(worktree_config).or_erased()?; } let precompose_unicode = Core::PRECOMPOSE_UNICODE .enrich_error(config.boolean(Core::PRECOMPOSE_UNICODE)) .with_leniency(lenient) - .map_err(Error::ConfigBoolean)? + .map_err(gix_error::Error::from)? .unwrap_or_default(); const IS_WINDOWS: bool = cfg!(windows); @@ -107,7 +116,9 @@ fn legacy_object_hash() -> Result { } #[cfg(not(feature = "sha1"))] { - Err(Error::UnsupportedObjectFormat { name: "sha1".into() }) + Err(gix_error::Error::from_error(gix_error::ValidationError::new( + "Cannot handle objects formatted as \"sha1\"", + ))) } } @@ -126,10 +137,10 @@ fn load_config( Ok(f) => f, Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(gix_config::File::new(metadata)), Err(err) => { - let err = Error::Io { - source: err, - path: config_path, - }; + let err = gix_error::Error::from(err.and_raise(gix_error::message!( + "Could not read configuration file at \"{}\"", + config_path.display() + ))); if lenient { gix_trace::warn!("ignoring: {err:#?}"); return Ok(gix_config::File::new(metadata)); @@ -141,10 +152,10 @@ fn load_config( buf.clear(); if let Err(err) = std::io::copy(&mut file, buf) { - let err = Error::Io { - source: err, - path: config_path, - }; + let err = gix_error::Error::from(err.and_raise(gix_error::message!( + "Could not read configuration file at \"{}\"", + config_path.display() + ))); if lenient { gix_trace::warn!("ignoring: {err:#?}"); buf.clear(); @@ -160,7 +171,8 @@ fn load_config( includes: gix_config::file::includes::Options::no_follow(), ..util::base_options(lossy, lenient) }, - )?; + ) + .map_err(gix_error::Exn::into_error)?; Ok(config) } diff --git a/gix/src/config/cache/init.rs b/gix/src/config/cache/init.rs index 056c7f198dc..642d442ec47 100644 --- a/gix/src/config/cache/init.rs +++ b/gix/src/config/cache/init.rs @@ -1,6 +1,7 @@ #![allow(clippy::result_large_err)] use std::ffi::OsString; +use gix_error::{ErrorExt, ResultExt}; use gix_sec::Permission; use super::{Error, StageOne, interpolate_context, util}; @@ -321,30 +322,40 @@ pub(crate) fn load( ..options }, ) - .map_err(Error::from)? + .map_err(gix_error::Exn::into_error)? .unwrap_or_default(); let local_meta = git_dir_config.as_ref().map(gix_config::File::meta_owned); if let Some(git_dir_config) = git_dir_config { - globals.append(git_dir_config)?; + globals.append(git_dir_config).or_erased()?; } - globals.resolve_includes(options)?; + globals.resolve_includes(options).map_err(gix_error::Exn::into_error)?; if use_env { - globals.append(gix_config::File::from_env(options)?.unwrap_or_default())?; + globals + .append( + gix_config::File::from_env(options) + .map_err(gix_error::Exn::into_error)? + .unwrap_or_default(), + ) + .or_erased()?; } if !cli_config_overrides.is_empty() { config::overrides::append(&mut globals, cli_config_overrides, gix_config::Source::Cli, |_| None).map_err( - |err| Error::ConfigOverrides { - err, - source: gix_config::Source::Cli, + |err| { + gix_error::Error::from(err.and_raise(gix_error::message!( + "{:?} configuration overrides at open or init time could not be applied.", + gix_config::Source::Cli + ))) }, )?; } if !api_config_overrides.is_empty() { config::overrides::append(&mut globals, api_config_overrides, gix_config::Source::Api, |_| None).map_err( - |err| Error::ConfigOverrides { - err, - source: gix_config::Source::Api, + |err| { + gix_error::Error::from(err.and_raise(gix_error::message!( + "{:?} configuration overrides at open or init time could not be applied.", + gix_config::Source::Api + ))) }, )?; } @@ -754,7 +765,9 @@ fn apply_environment_overrides( .expect("statically known valid section name"); for (var, key) in data { if let Some(value) = var_as_bstring(var, permission) { - section.push_with_comment(*key, value, format!("from {var}"))?; + section + .push_with_comment(*key, value, format!("from {var}")) + .or_erased()?; } } if section.num_values() == 0 { @@ -783,7 +796,9 @@ fn apply_environment_overrides( }, ] { if let Some(value) = var_as_bstring(var, permission) { - section.push_with_comment(key, value, format!("from {var}"))?; + section + .push_with_comment(key, value, format!("from {var}")) + .or_erased()?; } } @@ -794,7 +809,7 @@ fn apply_environment_overrides( } if !env_override.is_void() { - config.append(env_override)?; + config.append(env_override).or_erased()?; } Ok(()) } diff --git a/gix/src/config/file_mut.rs b/gix/src/config/file_mut.rs index a272d810a76..21993b34210 100644 --- a/gix/src/config/file_mut.rs +++ b/gix/src/config/file_mut.rs @@ -5,49 +5,12 @@ use std::{ ops::{Deref, DerefMut}, }; +use gix_error::{ErrorExt, ResultExt, message}; + use super::FileTransaction; /// The error produced by [`crate::config_path()`] or when opening or committing a [`FileTransaction`]. -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] -pub enum Error { - #[error("Configuration source {0:?} requires a repository or has no physical file")] - UnsupportedSource(gix_config::Source), - #[error("Configuration source {0:?} has no available path with these options")] - SourceUnavailable(gix_config::Source), - #[error("Could not load global configuration")] - Config(#[from] super::Error), - #[error("Could not obtain the current directory for a relative configuration path")] - CurrentDir(#[source] std::io::Error), - #[error(transparent)] - LockTimeout(#[from] super::lock_timeout::Error), - #[error(transparent)] - InvalidSharedRepository(#[from] crate::config::key::GenericErrorWithValue), - #[error("Could not acquire the lock for the configuration file")] - AcquireLock(#[from] gix_lock::acquire::Error), - #[error("Could not read metadata of the configuration file at {path:?}")] - Metadata { - source: std::io::Error, - path: std::path::PathBuf, - }, - #[error("Could not read the configuration file at {path:?}")] - Read { - source: std::io::Error, - path: std::path::PathBuf, - }, - #[error("Could not parse the configuration file at {path:?}")] - Parse { - source: gix_config::file::init::Error, - path: std::path::PathBuf, - }, - #[error("Could not write the configuration file at {path:?}")] - Write { - source: std::io::Error, - path: std::path::PathBuf, - }, - #[error("Could not commit the configuration file lock")] - CommitLock(#[from] gix_lock::commit::Error), -} +pub type Error = gix_error::Error; impl FileTransaction { pub(crate) fn open( @@ -67,41 +30,35 @@ impl FileTransaction { None, Some(&gix_lock::acquire::resolve_symlink), adjust_permissions, - )?; + ) + .or_raise(|| message("Could not acquire the lock for the configuration file"))?; let path = lock.resource_path(); let config = match std::fs::File::open(&path) { Ok(mut file) => { let permissions = file .metadata() - .map_err(|source| Error::Metadata { - source, - path: path.clone(), - })? + .or_raise(|| message!("Could not read metadata of the configuration file at {path:?}"))? .permissions(); lock.with_mut(|file| file.set_permissions(permissions)) - .map_err(|source| Error::Write { - source, - path: path.clone(), - })?; + .or_raise(|| message!("Could not write the configuration file at {path:?}"))?; let mut bytes = Vec::new(); - file.read_to_end(&mut bytes).map_err(|source| Error::Read { - source, - path: path.clone(), - })?; + file.read_to_end(&mut bytes) + .or_raise(|| message!("Could not read the configuration file at {path:?}"))?; gix_config::File::from_bytes_no_includes( &bytes, gix_config::file::Metadata::from(source).at(path.clone()).with(trust), Default::default(), ) - .map_err(|source| Error::Parse { - source, - path: path.clone(), - })? + .or_raise(|| message!("Could not parse the configuration file at {path:?}"))? } Err(err) if err.kind() == std::io::ErrorKind::NotFound => { gix_config::File::new(gix_config::file::Metadata::from(source).at(path).with(trust)) } - Err(source) => return Err(Error::Read { source, path }), + Err(err) => { + return Err(err + .and_raise(message!("Could not read the configuration file at {path:?}")) + .into()); + } }; Ok(FileTransaction { lock, config }) } @@ -111,8 +68,10 @@ impl FileTransaction { let path = self.lock.resource_path(); self.config .write_to(&mut self.lock) - .map_err(|source| Error::Write { source, path })?; - self.lock.commit()?; + .or_raise(|| message!("Could not write the configuration file at {path:?}"))?; + self.lock + .commit() + .or_raise(|| message("Could not commit the configuration file lock"))?; Ok(()) } } diff --git a/gix/src/config/mod.rs b/gix/src/config/mod.rs index 158ae53ead4..85a80897c8a 100644 --- a/gix/src/config/mod.rs +++ b/gix/src/config/mod.rs @@ -1,7 +1,7 @@ pub use gix_config::*; use gix_features::threading::OnceCell; -use crate::{Repository, bstr::BString, repository::identity}; +use crate::{Repository, repository::identity}; pub(crate) mod cache; pub mod file_mut; @@ -66,106 +66,26 @@ pub mod section { /// pub mod set_value { /// The error produced when calling [`SnapshotMut::set(_subsection)?_value()`][crate::config::SnapshotMut::set_value()] - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - SetRaw(gix_error::Error), - #[error(transparent)] - Validate(#[from] crate::config::tree::key::validate::Error), - #[error("The key needs a subsection parameter to be valid.")] - SubSectionRequired, - #[error("The key must not be used with a subsection")] - SubSectionForbidden, - } - - impl From for Error { - fn from(err: gix_config::file::set_raw_value::Error) -> Self { - Error::SetRaw(err.into_error()) - } - } + pub type Error = gix_error::Error; } /// The error returned when failing to initialize the repository configuration. /// /// This configuration is on the critical path when opening a repository. -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] -pub enum Error { - #[error(transparent)] - ConfigBoolean(#[from] boolean::Error), - #[error(transparent)] - ConfigUnsigned(#[from] unsigned_integer::Error), - #[error(transparent)] - ConfigTypedString(#[from] key::GenericErrorWithValue), - #[error(transparent)] - ConfigCompression(#[from] key::GenericError), - #[error(transparent)] - RefsNamespace(#[from] refs_namespace::Error), - #[error("Cannot handle objects formatted as {:?}", .name)] - UnsupportedObjectFormat { name: BString }, - #[error( - "extensions.objectFormat is a v1-only extension, but the repository format version is 0; \ - set core.repositoryFormatVersion=1 to use it, or remove extensions.objectFormat to fall back to the default Sha1 format (if supported by this build)" - )] - ObjectFormatRequiresV1, - #[error("Unsupported repository format version {version}; only versions 0 and 1 are supported")] - UnsupportedRepositoryFormatVersion { version: usize }, - #[error(transparent)] - CoreAbbrev(#[from] abbrev::Error), - #[error("Could not read configuration file at \"{}\"", path.display())] - Io { - source: std::io::Error, - path: std::path::PathBuf, - }, - #[error(transparent)] - Config(gix_error::Error), - #[error(transparent)] - ConfigValidation(#[from] gix_error::ValidationError), - #[error("The path {path:?} at the 'core.worktree' configuration could not be interpolated")] - PathInterpolation { path: BString, source: gix_error::Error }, - #[error("{source:?} configuration overrides at open or init time could not be applied.")] - ConfigOverrides { - #[source] - err: overrides::Error, - source: gix_config::Source, - }, -} - -impl From for Error { - fn from(err: gix_error::Exn) -> Self { - Error::Config(err.into_error()) - } -} - -impl From> for Error { - fn from(err: gix_error::Exn) -> Self { - Error::Config(err.into_error()) - } -} +pub type Error = gix_error::Error; /// pub mod merge { /// pub mod pipeline_options { /// The error produced when obtaining options needed to fill in [gix_merge::blob::pipeline::Options]. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - BigFileThreshold(#[from] crate::config::unsigned_integer::Error), - } + pub type Error = gix_error::Error; } /// pub mod drivers { /// The error produced when obtaining a list of [Drivers](gix_merge::blob::Driver). - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - ConfigBoolean(#[from] crate::config::boolean::Error), - } + pub type Error = gix_error::Error; } } @@ -176,155 +96,84 @@ pub mod diff { use crate::bstr::BString; /// The error produced when obtaining `diff.algorithm`. - #[derive(Debug, thiserror::Error)] + #[derive(Debug)] #[expect(missing_docs)] pub enum Error { - #[error("Unknown diff algorithm named '{name}'")] Unknown { name: BString }, - #[error("The '{name}' algorithm is not yet implemented")] Unimplemented { name: BString }, } + + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Unknown { name } => write!(f, "Unknown diff algorithm named '{name}'"), + Error::Unimplemented { name } => write!(f, "The '{name}' algorithm is not yet implemented"), + } + } + } + + impl std::error::Error for Error {} } /// pub mod pipeline_options { /// The error produced when obtaining options needed to fill in [gix_diff::blob::pipeline::Options]. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - FilesystemCapabilities(#[from] crate::config::boolean::Error), - #[error(transparent)] - BigFileThreshold(#[from] crate::config::unsigned_integer::Error), - } + pub type Error = gix_error::Error; } /// pub mod drivers { - use crate::bstr::BString; - /// The error produced when obtaining a list of [Drivers](gix_diff::blob::Driver). - #[derive(Debug, thiserror::Error)] - #[error("Failed to parse value of 'diff.{name}.{attribute}'")] - pub struct Error { - /// The name of the driver. - pub name: BString, - /// The name of the attribute we tried to parse. - pub attribute: &'static str, - /// The actual error that occurred. - pub source: Box, - } + pub type Error = gix_error::Error; } } /// pub mod stat_options { /// The error produced when collecting stat information, and returned by [Repository::stat_options()](crate::Repository::stat_options()). - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - ConfigCheckStat(#[from] super::key::GenericErrorWithValue), - #[error(transparent)] - ConfigBoolean(#[from] super::boolean::Error), - } + pub type Error = gix_error::Error; } /// #[cfg(feature = "attributes")] pub mod checkout_options { /// The error produced when collecting all information needed for checking out files into a worktree. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - ConfigCheckStat(#[from] super::key::GenericErrorWithValue), - #[error(transparent)] - ConfigBoolean(#[from] super::boolean::Error), - #[error(transparent)] - CheckoutWorkers(#[from] super::checkout::workers::Error), - #[error(transparent)] - Attributes(#[from] super::attribute_stack::Error), - #[error(transparent)] - FilterPipelineOptions(#[from] crate::filter::pipeline::options::Error), - #[error(transparent)] - CommandContext(#[from] crate::config::command_context::Error), - } + pub type Error = gix_error::Error; } /// #[cfg(feature = "command")] pub mod command_context { - use crate::config; - /// The error produced when collecting all information relevant to spawned commands, /// obtained via [Repository::command_context()](crate::Repository::command_context()). - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - Boolean(#[from] config::boolean::Error), - #[error(transparent)] - ParseBool(#[from] gix_error::Error), - } + pub type Error = gix_error::Error; } /// pub mod exclude_stack { - use crate::config; - use std::path::PathBuf; - /// The error produced when setting up a stack to query `gitignore` information. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error("Could not read repository exclude")] - Io(#[from] std::io::Error), - #[error(transparent)] - EnvironmentPermission(#[from] gix_sec::permission::Error), - #[error("The value for `core.excludesFile` could not be read from configuration")] - ExcludesFilePathInterpolation(#[from] gix_error::Error), - #[error(transparent)] - ParsePreciousEnabled(#[from] config::boolean::Error), - } + pub type Error = gix_error::Error; } /// pub mod attribute_stack { /// The error produced when setting up the attribute stack to query `gitattributes`. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error("An attribute file could not be read")] - Io(#[from] std::io::Error), - #[error("Failed to interpolate the attribute file configured at `core.attributesFile`")] - AttributesFileInterpolation(#[from] gix_error::Error), - } + pub type Error = gix_error::Error; } /// pub mod protocol { /// pub mod allow { - use crate::bstr::BString; - /// The error returned when obtaining the permission for a particular scheme. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - #[error("The value {value:?} must be allow|deny|user in configuration key protocol{0}.allow", scheme.as_ref().map(|s| format!(".{s}")).unwrap_or_default())] - pub struct Error { - pub scheme: Option, - pub value: BString, - } + pub type Error = gix_error::Error; } } /// pub mod ssh_connect_options { /// The error produced when obtaining ssh connection configuration. - #[derive(Debug, thiserror::Error)] - #[error(transparent)] - pub struct Error(#[from] super::key::GenericErrorWithValue); + pub type Error = gix_error::Error; } /// @@ -359,8 +208,7 @@ pub mod key { /// A generic error suitable to produce decent messages for all kinds of configuration errors with config-key granularity. /// /// This error is meant to be reusable and help produce uniform error messages related to parsing any configuration key. - #[derive(Debug, thiserror::Error)] - #[error("{} \"{key}{}\"{} {}", prefix(PREFIX), value.as_ref().map(|v| format!("={v}")).unwrap_or_default(), environment_override.as_deref().map(|var| format!(" (possibly from {var})")).unwrap_or_default(), suffix(SUFFIX))] + #[derive(Debug)] pub struct Error { /// The configuration key that contained the value. pub key: BString, @@ -372,6 +220,45 @@ pub mod key { pub source: Option, } + impl std::fmt::Display for Error + where + E: std::error::Error + Send + Sync + 'static, + { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{} \"{}{}\"{} {}", + prefix(PREFIX), + self.key, + self.value.as_ref().map(|v| format!("={v}")).unwrap_or_default(), + self.environment_override + .map(|var| format!(" (possibly from {var})")) + .unwrap_or_default(), + suffix(SUFFIX) + ) + } + } + + impl std::error::Error for Error + where + E: std::error::Error + Send + Sync + 'static, + { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + self.source.as_ref().map(|err| err as _) + } + } + + impl From> for gix_error::Error + where + E: std::error::Error + Send + Sync + 'static, + { + fn from(err: Error) -> Self { + use gix_error::ErrorExt; + let message = err.to_string(); + err.and_raise(gix_error::ValidationError::new(message)).into() + } + } + /// Initialization /// Instantiate a new error from the given `key`. /// @@ -429,19 +316,8 @@ pub mod key { /// pub mod encoding { - use crate::bstr::BString; - /// The error produced when failing to parse the `core.checkRoundTripEncoding` key. - #[derive(Debug, thiserror::Error)] - #[error("The encoding named '{encoding}' seen in key '{key}={value}' is unsupported")] - pub struct Error { - /// The configuration key that contained the value. - pub key: BString, - /// The value that was assigned to `key`. - pub value: BString, - /// The encoding that failed. - pub encoding: BString, - } + pub type Error = gix_error::Error; } /// @@ -457,17 +333,8 @@ pub mod checkout { /// pub mod abbrev { - use crate::bstr::BString; - /// The error describing an incorrect `core.abbrev` value. - #[derive(Debug, thiserror::Error)] - #[error("Invalid value for 'core.abbrev' = '{}'. It must be between 4 and {}", .value, .max)] - pub struct Error { - /// The value found in the git configuration - pub value: BString, - /// The maximum abbreviation length, the length of an object hash. - pub max: u8, - } + pub type Error = gix_error::Error; } /// @@ -488,16 +355,7 @@ pub mod time { /// pub mod commit_signature { /// The error produced when obtaining or installing a fallback commit signature. - #[derive(Debug, thiserror::Error)] - #[allow(missing_docs)] - pub enum Error { - #[error(transparent)] - Time(#[from] super::time::Error), - #[error(transparent)] - SetValue(#[from] gix_config::file::set_raw_value::Error), - #[error(transparent)] - Span(#[from] gix_config::parse::span::Error), - } + pub type Error = gix_error::Error; } /// @@ -556,70 +414,13 @@ pub mod ssl_version { /// pub mod transport { - use crate::bstr::BString; - /// The error produced when configuring a transport for a particular protocol. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error( - "Could not interpret configuration key {key:?} as {kind} integer of desired range with value: {actual}" - )] - InvalidInteger { - key: &'static str, - kind: &'static str, - actual: i64, - }, - #[error("Could not interpret configuration key {key:?}")] - ConfigValue { - source: gix_error::Error, - key: &'static str, - }, - #[error("Could not interpolate path at key {key:?}")] - InterpolatePath { - source: gix_error::Error, - key: &'static str, - }, - #[error("Could not decode value at key {key:?} as UTF-8 string")] - IllformedUtf8 { - key: BString, - source: crate::config::string::Error, - }, - #[error("Invalid URL passed for configuration")] - ParseUrl(#[from] gix_error::Error), - #[error("Could obtain configuration for an HTTP url")] - Http(#[from] http::Error), - } + pub type Error = gix_error::Error; /// pub mod http { - use crate::bstr::BString; - /// The error produced when configuring a HTTP transport. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - Boolean(#[from] crate::config::boolean::Error), - #[error(transparent)] - UnsignedInteger(#[from] crate::config::unsigned_integer::Error), - #[error(transparent)] - ConnectTimeout(#[from] crate::config::duration::Error), - #[error("The proxy authentication at key `{key}` is invalid")] - InvalidProxyAuthMethod { - source: crate::config::key::GenericErrorWithValue, - key: BString, - }, - #[error("Could not configure the credential helpers for the authenticated proxy url")] - #[cfg(feature = "credentials")] - ConfigureProxyAuthenticate(#[from] crate::config::snapshot::credential_helpers::Error), - #[error(transparent)] - InvalidSslVersion(#[from] crate::config::ssl_version::Error), - #[error("The HTTP version must be 'HTTP/2' or 'HTTP/1.1'")] - InvalidHttpVersion(#[from] crate::config::key::GenericErrorWithValue), - #[error("The follow redirects value 'initial', or boolean true or false")] - InvalidFollowRedirects(#[source] crate::config::key::GenericErrorWithValue), - } + pub type Error = gix_error::Error; } } diff --git a/gix/src/config/overrides.rs b/gix/src/config/overrides.rs index 2030156e892..5bb286c33fb 100644 --- a/gix/src/config/overrides.rs +++ b/gix/src/config/overrides.rs @@ -1,14 +1,9 @@ +use gix_error::ResultExt; + use crate::bstr::{BStr, BString, ByteSlice}; /// The error returned by [`SnapshotMut::apply_cli_overrides()`][crate::config::SnapshotMut::append_config()]. -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] -pub enum Error { - #[error("{input:?} is not a valid configuration key. Examples are 'core.abbrev' or 'remote.origin.url'")] - InvalidKey { input: BString }, - #[error(transparent)] - Config(#[from] gix_error::ValidationError), -} +pub type Error = gix_error::Error; pub(crate) fn append( config: &mut gix_config::File, @@ -22,15 +17,23 @@ pub(crate) fn append( let mut tokens = key_value.splitn(2, |b| *b == b'=').map(ByteSlice::trim); let key = tokens.next().expect("always one value").as_bstr(); let value = tokens.next(); - let key = gix_config::KeyRef::parse_unvalidated(key).ok_or_else(|| Error::InvalidKey { input: key.into() })?; - let mut section = file.section_mut_or_create_new(key.section_name, key.subsection_name)?; + let key = gix_config::KeyRef::parse_unvalidated(key).ok_or_else(|| { + let input: BString = key.into(); + gix_error::Error::from_error(gix_error::message!( + "{input:?} is not a valid configuration key. Examples are 'core.abbrev' or 'remote.origin.url'" + )) + })?; + let mut section = file + .section_mut_or_create_new(key.section_name, key.subsection_name) + .or_erased()?; let comment = make_comment(key_value); let value = value.map(ByteSlice::as_bstr); match comment { Some(comment) => section.push_with_comment(key.value_name, value, &**comment), None => section.push(key.value_name, value), - }?; + } + .or_erased()?; } - config.append(file)?; + config.append(file).or_erased()?; Ok(()) } diff --git a/gix/src/config/snapshot/access.rs b/gix/src/config/snapshot/access.rs index 47ecccb3ee1..c5eadcc7295 100644 --- a/gix/src/config/snapshot/access.rs +++ b/gix/src/config/snapshot/access.rs @@ -118,7 +118,9 @@ impl<'repo> SnapshotMut<'repo> { new_value: impl gix_utils::AsBStr, ) -> Result, crate::config::set_value::Error> { if let Some(crate::config::tree::SubSectionRequirement::Parameter(_)) = key.subsection_requirement() { - return Err(crate::config::set_value::Error::SubSectionRequired); + return Err(gix_error::Error::from_error(gix_error::ValidationError::new( + "The key needs a subsection parameter to be valid.", + ))); } let value = new_value.as_bstr(); key.validate(value)?; @@ -126,8 +128,12 @@ impl<'repo> SnapshotMut<'repo> { let current = match section.parent() { Some(parent) => self .config - .set_raw_value_by(parent.name(), section.name(), key.name(), value)?, - None => self.config.set_raw_value_by(section.name(), None, key.name(), value)?, + .set_raw_value_by(parent.name(), section.name(), key.name(), value) + .map_err(gix_error::Exn::into_error)?, + None => self + .config + .set_raw_value_by(section.name(), None, key.name(), value) + .map_err(gix_error::Exn::into_error)?, }; Ok(current) } @@ -141,7 +147,9 @@ impl<'repo> SnapshotMut<'repo> { new_value: impl gix_utils::AsBStr, ) -> Result, crate::config::set_value::Error> { if let Some(crate::config::tree::SubSectionRequirement::Never) = key.subsection_requirement() { - return Err(crate::config::set_value::Error::SubSectionForbidden); + return Err(gix_error::Error::from_error(gix_error::ValidationError::new( + "The key must not be used with a subsection", + ))); } let value = new_value.as_bstr(); key.validate(value)?; @@ -153,7 +161,8 @@ impl<'repo> SnapshotMut<'repo> { .expect("statically known keys can always be parsed"); let current = self .config - .set_raw_value_by(key.section_name, key.subsection_name, key.value_name, value)?; + .set_raw_value_by(key.section_name, key.subsection_name, key.value_name, value) + .map_err(gix_error::Exn::into_error)?; Ok(current) } diff --git a/gix/src/config/snapshot/credential_helpers.rs b/gix/src/config/snapshot/credential_helpers.rs index 6b7dda36254..b74d8604964 100644 --- a/gix/src/config/snapshot/credential_helpers.rs +++ b/gix/src/config/snapshot/credential_helpers.rs @@ -1,22 +1,7 @@ -pub use error::Error; - use crate::config::Snapshot; -mod error { - use crate::bstr::BString; - - /// The error returned by [`Snapshot::credential_helpers()`][super::Snapshot::credential_helpers()]. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error("Could not parse 'useHttpPath' key in section {section}")] - InvalidUseHttpPath { section: BString, source: gix_error::Error }, - #[error("core.askpass could not be read")] - CoreAskpass(#[from] gix_error::Error), - #[error(transparent)] - BooleanConfig(#[from] crate::config::boolean::Error), - } -} +/// The error returned by [`Snapshot::credential_helpers()`][Snapshot::credential_helpers()]. +pub type Error = gix_error::Error; impl Snapshot<'_> { /// Returns the configuration for all git-credential helpers from trusted configuration that apply @@ -46,6 +31,8 @@ impl Snapshot<'_> { } pub(super) mod function { + use gix_error::ResultExt; + use crate::{ bstr::{ByteSlice, ByteVec}, config::{ @@ -165,9 +152,11 @@ pub(super) mod function { .value(use_http_path_key.name) .map(|val| { gix_config::Boolean::try_from(val) - .map_err(|err| Error::InvalidUseHttpPath { - source: err.into_error(), - section: section.header().to_bstring(), + .map_err(|err| { + gix_error::Error::from(err.raise(gix_error::ValidationError::new(format!( + "Could not parse 'useHttpPath' key in section {}", + section.header().to_bstring() + )))) }) .map(|b| b.0) }) @@ -200,7 +189,7 @@ pub(super) mod function { true, // An empty core.askpass is allowed even with strict configuration. environment, ) - .map_err(gix_error::Exn::into_error)?, + .or_raise(|| gix_error::message("core.askpass could not be read"))?, mode: Credentials::TERMINAL_PROMPT .enrich_error(config.boolean(Credentials::TERMINAL_PROMPT)) .with_leniency(is_lenient_config)? diff --git a/gix/src/config/tree/keys.rs b/gix/src/config/tree/keys.rs index 1ae3beb7a80..2585bd5c1f8 100644 --- a/gix/src/config/tree/keys.rs +++ b/gix/src/config/tree/keys.rs @@ -1,8 +1,5 @@ #![allow(clippy::result_large_err)] -use std::{ - error::Error, - fmt::{Debug, Formatter}, -}; +use std::fmt::{Debug, Formatter}; use gix_config::KeyRef; @@ -143,7 +140,13 @@ impl Key for Any { } fn validate(&self, value: &BStr) -> Result<(), config::tree::key::validate::Error> { - Ok(self.validate.validate(value)?) + self.validate.validate(value).map_err(|err| { + err.raise(gix_error::ValidationError::new_with_input( + format!("Invalid value for configuration key '{}'", self.logical_name()), + value, + )) + .into() + }) } fn section(&self) -> &dyn Section { @@ -565,12 +568,14 @@ mod remote_name { /// Provide a way to validate a value, or decode a value from `git-config`. pub trait Validate { /// Validate `value` or return an error. - fn validate(&self, value: &BStr) -> Result<(), Box>; + fn validate(&self, value: &BStr) -> Result<(), gix_error::Exn>; } /// various implementations of the `Validate` trait. pub mod validate { - use std::{borrow::Cow, error::Error}; + use std::borrow::Cow; + + use gix_error::{ErrorExt, ResultExt, message}; use crate::{ bstr::{BStr, ByteSlice}, @@ -583,7 +588,7 @@ pub mod validate { pub struct All; impl Validate for All { - fn validate(&self, _value: &BStr) -> Result<(), Box> { + fn validate(&self, _value: &BStr) -> Result<(), gix_error::Exn> { Ok(()) } } @@ -593,8 +598,8 @@ pub mod validate { pub struct Time; impl Validate for Time { - fn validate(&self, value: &BStr) -> Result<(), Box> { - gix_date::parse(value.to_str()?, gix_date::Zoned::now().into()).map_err(gix_error::Exn::into_inner)?; + fn validate(&self, value: &BStr) -> Result<(), gix_error::Exn> { + gix_date::parse(value.to_str().or_erased()?, gix_date::Zoned::now().into()).or_erased()?; Ok(()) } } @@ -604,13 +609,14 @@ pub mod validate { pub struct UnsignedInteger; impl Validate for UnsignedInteger { - fn validate(&self, value: &BStr) -> Result<(), Box> { + fn validate(&self, value: &BStr) -> Result<(), gix_error::Exn> { usize::try_from( - gix_config::Integer::try_from(value)? + gix_config::Integer::try_from(value) + .or_erased()? .to_decimal() - .ok_or_else(|| format!("integer {value} cannot be represented as `usize`"))?, + .ok_or_else(|| message!("integer {value} cannot be represented as `usize`").raise_erased())?, ) - .map_err(|_| "cannot use sign for unsigned integer")?; + .map_err(|_| message("cannot use sign for unsigned integer").raise_erased())?; Ok(()) } } @@ -620,8 +626,8 @@ pub mod validate { pub struct Boolean; impl Validate for Boolean { - fn validate(&self, value: &BStr) -> Result<(), Box> { - gix_config::Boolean::try_from(value)?; + fn validate(&self, value: &BStr) -> Result<(), gix_error::Exn> { + gix_config::Boolean::try_from(value).or_erased()?; Ok(()) } } @@ -645,9 +651,9 @@ pub mod validate { } impl Validate for FullNameRef { - fn validate(&self, value: &BStr) -> Result<(), Box> { + fn validate(&self, value: &BStr) -> Result<(), gix_error::Exn> { if !self.allow_empty || !value.is_empty() { - gix_ref::FullName::try_from(value.to_owned())?; + gix_ref::FullName::try_from(value.to_owned()).or_erased()?; } Ok(()) } @@ -657,9 +663,9 @@ pub mod validate { #[derive(Default, Clone, Copy)] pub struct RemoteName; impl Validate for RemoteName { - fn validate(&self, value: &BStr) -> Result<(), Box> { + fn validate(&self, value: &BStr) -> Result<(), gix_error::Exn> { remote::Name::try_from(Cow::Borrowed(value)) - .map_err(|_| format!("Illformed UTF-8 in remote name: \"{}\"", value.to_str_lossy()))?; + .map_err(|_| message!("Illformed UTF-8 in remote name: \"{}\"", value.to_str_lossy()).raise_erased())?; Ok(()) } } @@ -668,7 +674,7 @@ pub mod validate { #[derive(Default, Clone, Copy)] pub struct Program; impl Validate for Program { - fn validate(&self, _value: &BStr) -> Result<(), Box> { + fn validate(&self, _value: &BStr) -> Result<(), gix_error::Exn> { Ok(()) } } @@ -677,7 +683,7 @@ pub mod validate { #[derive(Default, Clone, Copy)] pub struct Executable; impl Validate for Executable { - fn validate(&self, _value: &BStr) -> Result<(), Box> { + fn validate(&self, _value: &BStr) -> Result<(), gix_error::Exn> { Ok(()) } } @@ -686,8 +692,8 @@ pub mod validate { #[derive(Default, Clone, Copy)] pub struct Url; impl Validate for Url { - fn validate(&self, value: &BStr) -> Result<(), Box> { - gix_url::parse(value)?; + fn validate(&self, value: &BStr) -> Result<(), gix_error::Exn> { + gix_url::parse(value).or_erased()?; Ok(()) } } @@ -696,8 +702,8 @@ pub mod validate { #[derive(Default, Clone, Copy)] pub struct PushRefSpec; impl Validate for PushRefSpec { - fn validate(&self, value: &BStr) -> Result<(), Box> { - gix_refspec::parse(value, gix_refspec::parse::Operation::Push).map_err(gix_error::Exn::into_error)?; + fn validate(&self, value: &BStr) -> Result<(), gix_error::Exn> { + gix_refspec::parse(value, gix_refspec::parse::Operation::Push).or_erased()?; Ok(()) } } @@ -706,8 +712,8 @@ pub mod validate { #[derive(Default, Clone, Copy)] pub struct FetchRefSpec; impl Validate for FetchRefSpec { - fn validate(&self, value: &BStr) -> Result<(), Box> { - gix_refspec::parse(value, gix_refspec::parse::Operation::Fetch).map_err(gix_error::Exn::into_error)?; + fn validate(&self, value: &BStr) -> Result<(), gix_error::Exn> { + gix_refspec::parse(value, gix_refspec::parse::Operation::Fetch).or_erased()?; Ok(()) } } @@ -716,11 +722,14 @@ pub mod validate { #[derive(Clone, Copy)] pub struct LockTimeout; impl Validate for LockTimeout { - fn validate(&self, value: &BStr) -> Result<(), Box> { - let value = gix_config::Integer::try_from(value)? + fn validate(&self, value: &BStr) -> Result<(), gix_error::Exn> { + let value = gix_config::Integer::try_from(value) + .or_erased()? .to_decimal() - .ok_or_else(|| format!("integer {value} cannot be represented as integer")); - super::super::Core::FILES_REF_LOCK_TIMEOUT.try_into_lock_timeout(Ok(Some(value?)))?; + .ok_or_else(|| message!("integer {value} cannot be represented as integer").raise_erased())?; + super::super::Core::FILES_REF_LOCK_TIMEOUT + .try_into_lock_timeout(Ok(Some(value))) + .or_erased()?; Ok(()) } } @@ -729,11 +738,14 @@ pub mod validate { #[derive(Clone, Copy)] pub struct Compression; impl Validate for Compression { - fn validate(&self, value: &BStr) -> Result<(), Box> { - let value = gix_config::Integer::try_from(value)? + fn validate(&self, value: &BStr) -> Result<(), gix_error::Exn> { + let value = gix_config::Integer::try_from(value) + .or_erased()? .to_decimal() - .ok_or_else(|| format!("integer {value} cannot be represented as integer")); - super::super::Core::COMPRESSION.try_into_compression(Ok(Some(value?)))?; + .ok_or_else(|| message!("integer {value} cannot be represented as integer").raise_erased())?; + super::super::Core::COMPRESSION + .try_into_compression(Ok(Some(value))) + .or_erased()?; Ok(()) } } @@ -742,11 +754,14 @@ pub mod validate { #[derive(Clone, Copy)] pub struct DurationInMilliseconds; impl Validate for DurationInMilliseconds { - fn validate(&self, value: &BStr) -> Result<(), Box> { - let value = gix_config::Integer::try_from(value)? + fn validate(&self, value: &BStr) -> Result<(), gix_error::Exn> { + let value = gix_config::Integer::try_from(value) + .or_erased()? .to_decimal() - .ok_or_else(|| format!("integer {value} cannot be represented as integer")); - super::super::gitoxide::Http::CONNECT_TIMEOUT.try_into_duration(Ok(Some(value?)))?; + .ok_or_else(|| message!("integer {value} cannot be represented as integer").raise_erased())?; + super::super::gitoxide::Http::CONNECT_TIMEOUT + .try_into_duration(Ok(Some(value))) + .or_erased()?; Ok(()) } } @@ -755,8 +770,8 @@ pub mod validate { #[derive(Clone, Copy)] pub struct String; impl Validate for String { - fn validate(&self, value: &BStr) -> Result<(), Box> { - value.to_str()?; + fn validate(&self, value: &BStr) -> Result<(), gix_error::Exn> { + value.to_str().or_erased()?; Ok(()) } } @@ -765,7 +780,7 @@ pub mod validate { #[derive(Clone, Copy)] pub struct Path; impl Validate for Path { - fn validate(&self, _value: &BStr) -> Result<(), Box> { + fn validate(&self, _value: &BStr) -> Result<(), gix_error::Exn> { Ok(()) } } diff --git a/gix/src/config/tree/mod.rs b/gix/src/config/tree/mod.rs index 7472d2388c9..66c65a44746 100644 --- a/gix/src/config/tree/mod.rs +++ b/gix/src/config/tree/mod.rs @@ -132,24 +132,12 @@ pub mod key { /// pub mod validate { /// The error returned by [`Key::validate()`][crate::config::tree::Key::validate()]. - #[derive(Debug, thiserror::Error)] - #[error(transparent)] - pub struct Error { - #[from] - source: Box, - } + pub type Error = gix_error::Error; } /// pub mod validate_assignment { /// The error returned by [`Key::validated_assignment`*()][crate::config::tree::Key::validated_assignment_fmt()]. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error("Failed to validate the value to be assigned to this key")] - Validate(#[from] super::validate::Error), - #[error("{message}")] - Name { message: String }, - } + pub type Error = gix_error::Error; } } diff --git a/gix/src/config/tree/sections/checkout.rs b/gix/src/config/tree/sections/checkout.rs index 0a59d858ab0..4d83e79f030 100644 --- a/gix/src/config/tree/sections/checkout.rs +++ b/gix/src/config/tree/sections/checkout.rs @@ -47,20 +47,22 @@ mod workers { /// pub mod validate { use crate::{bstr::BStr, config::tree::keys}; - use gix_error::{ErrorExt, ValidationError}; + use gix_error::{ErrorExt, ResultExt, ValidationError}; pub struct Workers; impl keys::Validate for Workers { - fn validate(&self, value: &BStr) -> Result<(), Box> { - super::Checkout::WORKERS.try_from_workers( - gix_config::Integer::try_from(value) - .and_then(|i| { - i.to_decimal().ok_or_else(|| { - ValidationError::new_with_input("Integer overflow", value.to_owned()).raise() + fn validate(&self, value: &BStr) -> Result<(), gix_error::Exn> { + super::Checkout::WORKERS + .try_from_workers( + gix_config::Integer::try_from(value) + .and_then(|i| { + i.to_decimal().ok_or_else(|| { + ValidationError::new_with_input("Integer overflow", value.to_owned()).raise() + }) }) - }) - .map(Some), - )?; + .map(Some), + ) + .or_erased()?; Ok(()) } } diff --git a/gix/src/config/tree/sections/core.rs b/gix/src/config/tree/sections/core.rs index 20444705a6f..4a960a8c582 100644 --- a/gix/src/config/tree/sections/core.rs +++ b/gix/src/config/tree/sections/core.rs @@ -226,11 +226,11 @@ mod filter { { out.push( gix_filter::encoding::Encoding::for_label(encoding.trim()).ok_or_else(|| { - config::encoding::Error { - key: self.logical_name().into(), - value: value.into(), - encoding: encoding.into(), - } + gix_error::Error::from_error(gix_error::ValidationError::new(format!( + "The encoding named '{}' seen in key '{}={value}' is unsupported", + encoding.as_bstr(), + self.logical_name() + ))) })?, ); } @@ -455,11 +455,13 @@ mod abbrev { ) -> Result, Error> { let hex_len_str = hex_len_str.as_bstr(); let max = object_hash.len_in_hex() as u8; + let invalid = || { + gix_error::Error::from_error(gix_error::ValidationError::new(format!( + "Invalid value for 'core.abbrev' = '{hex_len_str}'. It must be between 4 and {max}" + ))) + }; if hex_len_str.trim().is_empty() { - return Err(Error { - value: hex_len_str.into(), - max, - }); + return Err(invalid()); } if hex_len_str.trim().eq_ignore_ascii_case(b"auto") { Ok(None) @@ -469,20 +471,11 @@ mod abbrev { Ok(object_hash.len_in_hex().into()) } else { let value = gix_config::Integer::try_from(value_bytes) - .map_err(|_| Error { - value: hex_len_str.into(), - max, - })? + .map_err(|_| invalid())? .to_decimal() - .ok_or_else(|| Error { - value: hex_len_str.into(), - max, - })?; + .ok_or_else(&invalid)?; if value < 4 || value as usize > object_hash.len_in_hex() { - return Err(Error { - value: hex_len_str.into(), - max, - }); + return Err(invalid()); } Ok(Some(value as usize)) } @@ -493,14 +486,16 @@ mod abbrev { mod validate { use crate::{bstr::BStr, config::tree::keys}; + use gix_error::ResultExt; #[derive(Clone, Copy)] pub struct Disambiguate; impl keys::Validate for Disambiguate { - #[cfg_attr(not(feature = "revision"), allow(unused_variables))] - fn validate(&self, value: &BStr) -> Result<(), Box> { + fn validate(&self, _value: &BStr) -> Result<(), gix_error::Exn> { #[cfg(feature = "revision")] - super::Core::DISAMBIGUATE.try_into_object_kind_hint(value)?; + super::Core::DISAMBIGUATE + .try_into_object_kind_hint(_value) + .or_erased()?; Ok(()) } } @@ -508,9 +503,10 @@ mod validate { #[derive(Clone, Copy)] pub struct LogAllRefUpdates; impl keys::Validate for LogAllRefUpdates { - fn validate(&self, value: &BStr) -> Result<(), Box> { + fn validate(&self, value: &BStr) -> Result<(), gix_error::Exn> { super::Core::LOG_ALL_REF_UPDATES - .try_into_ref_updates(gix_config::Boolean::try_from(value).map(|b| Some(b.0)))?; + .try_into_ref_updates(gix_config::Boolean::try_from(value).map(|b| Some(b.0))) + .or_erased()?; Ok(()) } } @@ -518,8 +514,8 @@ mod validate { #[derive(Clone, Copy)] pub struct CheckStat; impl keys::Validate for CheckStat { - fn validate(&self, value: &BStr) -> Result<(), Box> { - super::Core::CHECK_STAT.try_into_checkstat(value)?; + fn validate(&self, value: &BStr) -> Result<(), gix_error::Exn> { + super::Core::CHECK_STAT.try_into_checkstat(value).or_erased()?; Ok(()) } } @@ -527,12 +523,14 @@ mod validate { #[derive(Clone, Copy)] pub struct Abbrev; impl keys::Validate for Abbrev { - fn validate(&self, value: &BStr) -> Result<(), Box> { + fn validate(&self, value: &BStr) -> Result<(), gix_error::Exn> { // The keys::Validate trait API doesn't take a hash kind, and passing one through // would touch ~50 impl sites. The repo-aware check with the actual hash runs in // config::cache::util::parse_core_abbrev, so here we just use Kind::longest() // to allow the most permissive upper bound. - super::Core::ABBREV.try_into_abbreviation(value, gix_hash::Kind::longest())?; + super::Core::ABBREV + .try_into_abbreviation(value, gix_hash::Kind::longest()) + .or_erased()?; Ok(()) } } @@ -540,8 +538,10 @@ mod validate { #[derive(Clone, Copy)] pub struct SharedRepository; impl keys::Validate for SharedRepository { - fn validate(&self, value: &BStr) -> Result<(), Box> { - super::Core::SHARED_REPOSITORY.try_into_shared_repository(Some(value))?; + fn validate(&self, value: &BStr) -> Result<(), gix_error::Exn> { + super::Core::SHARED_REPOSITORY + .try_into_shared_repository(Some(value)) + .or_erased()?; Ok(()) } } @@ -551,8 +551,8 @@ mod validate { pub struct SafeCrlf; #[cfg(feature = "attributes")] impl keys::Validate for SafeCrlf { - fn validate(&self, value: &BStr) -> Result<(), Box> { - super::Core::SAFE_CRLF.try_into_safecrlf(value)?; + fn validate(&self, value: &BStr) -> Result<(), gix_error::Exn> { + super::Core::SAFE_CRLF.try_into_safecrlf(value).or_erased()?; Ok(()) } } @@ -562,8 +562,8 @@ mod validate { pub struct AutoCrlf; #[cfg(feature = "attributes")] impl keys::Validate for AutoCrlf { - fn validate(&self, value: &BStr) -> Result<(), Box> { - super::Core::AUTO_CRLF.try_into_autocrlf(value)?; + fn validate(&self, value: &BStr) -> Result<(), gix_error::Exn> { + super::Core::AUTO_CRLF.try_into_autocrlf(value).or_erased()?; Ok(()) } } @@ -573,8 +573,8 @@ mod validate { pub struct Eol; #[cfg(feature = "attributes")] impl keys::Validate for Eol { - fn validate(&self, value: &BStr) -> Result<(), Box> { - super::Core::EOL.try_into_eol(value)?; + fn validate(&self, value: &BStr) -> Result<(), gix_error::Exn> { + super::Core::EOL.try_into_eol(value).or_erased()?; Ok(()) } } @@ -584,8 +584,10 @@ mod validate { pub struct CheckRoundTripEncoding; #[cfg(feature = "attributes")] impl keys::Validate for CheckRoundTripEncoding { - fn validate(&self, value: &BStr) -> Result<(), Box> { - super::Core::CHECK_ROUND_TRIP_ENCODING.try_into_encodings(Some(value))?; + fn validate(&self, value: &BStr) -> Result<(), gix_error::Exn> { + super::Core::CHECK_ROUND_TRIP_ENCODING + .try_into_encodings(Some(value)) + .or_erased()?; Ok(()) } } diff --git a/gix/src/config/tree/sections/diff.rs b/gix/src/config/tree/sections/diff.rs index 484b54d99c5..77b05c470d6 100644 --- a/gix/src/config/tree/sections/diff.rs +++ b/gix/src/config/tree/sections/diff.rs @@ -201,6 +201,8 @@ mod renames { } pub(super) mod validate { + use gix_error::{ErrorExt, ResultExt, message}; + use crate::{ bstr::BStr, config::tree::{Diff, keys}, @@ -209,9 +211,9 @@ pub(super) mod validate { #[derive(Copy, Clone)] pub struct Ignore; impl keys::Validate for Ignore { - fn validate(&self, value: &BStr) -> Result<(), Box> { + fn validate(&self, value: &BStr) -> Result<(), gix_error::Exn> { gix_submodule::config::Ignore::try_from(value) - .map_err(|()| format!("Value '{value}' is not a valid submodule 'ignore' value"))?; + .map_err(|()| message!("Value '{value}' is not a valid submodule 'ignore' value").raise_erased())?; Ok(()) } } @@ -219,8 +221,8 @@ pub(super) mod validate { #[derive(Copy, Clone)] pub struct Algorithm; impl keys::Validate for Algorithm { - fn validate(&self, value: &BStr) -> Result<(), Box> { - Diff::ALGORITHM.try_into_algorithm(value)?; + fn validate(&self, value: &BStr) -> Result<(), gix_error::Exn> { + Diff::ALGORITHM.try_into_algorithm(value).or_erased()?; Ok(()) } } @@ -228,9 +230,9 @@ pub(super) mod validate { #[derive(Copy, Clone)] pub struct Renames; impl keys::Validate for Renames { - fn validate(&self, value: &BStr) -> Result<(), Box> { + fn validate(&self, value: &BStr) -> Result<(), gix_error::Exn> { let boolean = gix_config::Boolean::try_from(value).map(|b| Some(b.0)); - Diff::RENAMES.try_into_renames(boolean)?; + Diff::RENAMES.try_into_renames(boolean).or_erased()?; Ok(()) } } @@ -238,8 +240,8 @@ pub(super) mod validate { #[derive(Copy, Clone)] pub struct Binary; impl keys::Validate for Binary { - fn validate(&self, value: &BStr) -> Result<(), Box> { - Diff::DRIVER_BINARY.try_into_binary(Some(value))?; + fn validate(&self, value: &BStr) -> Result<(), gix_error::Exn> { + Diff::DRIVER_BINARY.try_into_binary(Some(value)).or_erased()?; Ok(()) } } diff --git a/gix/src/config/tree/sections/extensions.rs b/gix/src/config/tree/sections/extensions.rs index f163fd3a3dd..17ab767b42d 100644 --- a/gix/src/config/tree/sections/extensions.rs +++ b/gix/src/config/tree/sections/extensions.rs @@ -52,13 +52,16 @@ impl Section for Extensions { mod validate { use crate::{bstr::BStr, config::tree::keys}; + use gix_error::ResultExt; #[derive(Clone, Copy)] pub struct ObjectFormat; impl keys::Validate for ObjectFormat { - fn validate(&self, value: &BStr) -> Result<(), Box> { - super::Extensions::OBJECT_FORMAT.try_into_object_format(value)?; + fn validate(&self, value: &BStr) -> Result<(), gix_error::Exn> { + super::Extensions::OBJECT_FORMAT + .try_into_object_format(value) + .or_erased()?; Ok(()) } } diff --git a/gix/src/config/tree/sections/fetch.rs b/gix/src/config/tree/sections/fetch.rs index 72a492dbef1..ec5b64e5e11 100644 --- a/gix/src/config/tree/sections/fetch.rs +++ b/gix/src/config/tree/sections/fetch.rs @@ -74,14 +74,18 @@ mod algorithm { mod validate { use crate::{bstr::BStr, config::tree::keys}; + #[cfg(any(feature = "credentials", feature = "attributes"))] + use gix_error::ResultExt; #[derive(Clone, Copy)] pub struct NegotiationAlgorithm; impl keys::Validate for NegotiationAlgorithm { #[cfg_attr(not(feature = "credentials"), allow(unused_variables))] - fn validate(&self, value: &BStr) -> Result<(), Box> { + fn validate(&self, value: &BStr) -> Result<(), gix_error::Exn> { #[cfg(feature = "credentials")] - crate::config::tree::Fetch::NEGOTIATION_ALGORITHM.try_into_negotiation_algorithm(value)?; + crate::config::tree::Fetch::NEGOTIATION_ALGORITHM + .try_into_negotiation_algorithm(value) + .or_erased()?; Ok(()) } } @@ -91,10 +95,12 @@ mod validate { pub struct RecurseSubmodules; #[cfg(feature = "attributes")] impl keys::Validate for RecurseSubmodules { - fn validate(&self, value: &BStr) -> Result<(), Box> { + fn validate(&self, value: &BStr) -> Result<(), gix_error::Exn> { { let boolean = gix_config::Boolean::try_from(value).map(|b| Some(b.0)); - crate::config::tree::Fetch::RECURSE_SUBMODULES.try_into_recurse_submodules(boolean)?; + crate::config::tree::Fetch::RECURSE_SUBMODULES + .try_into_recurse_submodules(boolean) + .or_erased()?; } Ok(()) } diff --git a/gix/src/config/tree/sections/gitoxide.rs b/gix/src/config/tree/sections/gitoxide.rs index 5138902e96d..5a55ef1b806 100644 --- a/gix/src/config/tree/sections/gitoxide.rs +++ b/gix/src/config/tree/sections/gitoxide.rs @@ -614,16 +614,15 @@ mod subsections { pub use subsections::{Allow, Author, Commit, Committer, Core, Credentials, Http, Https, Objects, Pathspec, Ssh, User}; pub mod validate { - use gix_error::ValidationError; - use std::error::Error; + use gix_error::{ErrorExt, ResultExt, ValidationError}; use crate::{bstr::BStr, config::tree::keys::Validate}; #[derive(Clone, Copy)] pub struct RefsNamespace; impl Validate for RefsNamespace { - fn validate(&self, value: &BStr) -> Result<(), Box> { - super::Core::REFS_NAMESPACE.try_into_refs_namespace(value)?; + fn validate(&self, value: &BStr) -> Result<(), gix_error::Exn> { + super::Core::REFS_NAMESPACE.try_into_refs_namespace(value).or_erased()?; Ok(()) } } @@ -631,9 +630,9 @@ pub mod validate { #[derive(Clone, Copy)] pub struct NonEmptyPath; impl Validate for NonEmptyPath { - fn validate(&self, value: &BStr) -> Result<(), Box> { + fn validate(&self, value: &BStr) -> Result<(), gix_error::Exn> { if value.is_empty() { - return Err(ValidationError::new("index file path must not be empty").into()); + return Err(ValidationError::new("index file path must not be empty").raise_erased()); } Ok(()) } diff --git a/gix/src/config/tree/sections/gpg.rs b/gix/src/config/tree/sections/gpg.rs index f0895a5537f..c9e807f4b7d 100644 --- a/gix/src/config/tree/sections/gpg.rs +++ b/gix/src/config/tree/sections/gpg.rs @@ -138,22 +138,26 @@ mod validate { bstr::BStr, config::tree::{Gpg, keys}, }; + #[cfg(not(feature = "command"))] + use gix_error::ErrorExt; + #[cfg(feature = "command")] + use gix_error::ResultExt; #[derive(Copy, Clone)] pub struct MinTrustLevel; impl keys::Validate for MinTrustLevel { - fn validate(&self, value: &BStr) -> Result<(), Box> { + fn validate(&self, value: &BStr) -> Result<(), gix_error::Exn> { #[cfg(feature = "command")] { - Gpg::MIN_TRUST_LEVEL.try_into_trust_level(value)?; + Gpg::MIN_TRUST_LEVEL.try_into_trust_level(value).or_erased()?; Ok(()) } #[cfg(not(feature = "command"))] { let err: crate::config::key::GenericErrorWithValue = crate::config::key::GenericErrorWithValue::from_value(&Gpg::MIN_TRUST_LEVEL, value.into()); - Err(Box::new(err)) + Err(err.raise_erased()) } } } diff --git a/gix/src/config/tree/sections/http.rs b/gix/src/config/tree/sections/http.rs index c88e3b5767c..e3eecb3c5e2 100644 --- a/gix/src/config/tree/sections/http.rs +++ b/gix/src/config/tree/sections/http.rs @@ -259,7 +259,7 @@ mod key_impls { } pub mod validate { - use std::error::Error; + use gix_error::ResultExt; use crate::{ bstr::{BStr, ByteSlice}, @@ -268,12 +268,12 @@ pub mod validate { pub struct SslVersion; impl Validate for SslVersion { - fn validate(&self, _value: &BStr) -> Result<(), Box> { + fn validate(&self, _value: &BStr) -> Result<(), gix_error::Exn> { #[cfg(any( feature = "blocking-http-transport-reqwest", feature = "blocking-http-transport-curl" ))] - super::Http::SSL_VERSION.try_into_ssl_version(_value)?; + super::Http::SSL_VERSION.try_into_ssl_version(_value).or_erased()?; Ok(()) } @@ -281,12 +281,14 @@ pub mod validate { pub struct ProxyAuthMethod; impl Validate for ProxyAuthMethod { - fn validate(&self, _value: &BStr) -> Result<(), Box> { + fn validate(&self, _value: &BStr) -> Result<(), gix_error::Exn> { #[cfg(any( feature = "blocking-http-transport-reqwest", feature = "blocking-http-transport-curl" ))] - super::Http::PROXY_AUTH_METHOD.try_into_proxy_auth_method(_value)?; + super::Http::PROXY_AUTH_METHOD + .try_into_proxy_auth_method(_value) + .or_erased()?; Ok(()) } @@ -294,12 +296,12 @@ pub mod validate { pub struct Version; impl Validate for Version { - fn validate(&self, _value: &BStr) -> Result<(), Box> { + fn validate(&self, _value: &BStr) -> Result<(), gix_error::Exn> { #[cfg(any( feature = "blocking-http-transport-reqwest", feature = "blocking-http-transport-curl" ))] - super::Http::VERSION.try_into_http_version(_value)?; + super::Http::VERSION.try_into_http_version(_value).or_erased()?; Ok(()) } @@ -307,21 +309,22 @@ pub mod validate { pub struct ExtraHeader; impl Validate for ExtraHeader { - fn validate(&self, value: &BStr) -> Result<(), Box> { - value.to_str()?; + fn validate(&self, value: &BStr) -> Result<(), gix_error::Exn> { + value.to_str().or_erased()?; Ok(()) } } pub struct FollowRedirects; impl Validate for FollowRedirects { - fn validate(&self, _value: &BStr) -> Result<(), Box> { + fn validate(&self, _value: &BStr) -> Result<(), gix_error::Exn> { #[cfg(any( feature = "blocking-http-transport-reqwest", feature = "blocking-http-transport-curl" ))] super::Http::FOLLOW_REDIRECTS - .try_into_follow_redirects(_value, || gix_config::Boolean::try_from(_value).map(|b| Some(b.0)))?; + .try_into_follow_redirects(_value, || gix_config::Boolean::try_from(_value).map(|b| Some(b.0))) + .or_erased()?; Ok(()) } } diff --git a/gix/src/config/tree/sections/index.rs b/gix/src/config/tree/sections/index.rs index c567b072451..45ce589191d 100644 --- a/gix/src/config/tree/sections/index.rs +++ b/gix/src/config/tree/sections/index.rs @@ -55,12 +55,13 @@ impl Section for Index { mod validate { use crate::{bstr::BStr, config::tree::keys}; + use gix_error::ResultExt; #[derive(Clone, Copy)] pub struct IndexThreads; impl keys::Validate for IndexThreads { - fn validate(&self, value: &BStr) -> Result<(), Box> { - super::Index::THREADS.try_into_index_threads(value)?; + fn validate(&self, value: &BStr) -> Result<(), gix_error::Exn> { + super::Index::THREADS.try_into_index_threads(value).or_erased()?; Ok(()) } } diff --git a/gix/src/config/tree/sections/merge.rs b/gix/src/config/tree/sections/merge.rs index f93dc968969..27444d395b1 100644 --- a/gix/src/config/tree/sections/merge.rs +++ b/gix/src/config/tree/sections/merge.rs @@ -91,6 +91,8 @@ mod conflict_style { #[cfg(feature = "merge")] mod validate { + use gix_error::ResultExt; + use crate::{ bstr::BStr, config::tree::{Merge, keys}, @@ -99,8 +101,8 @@ mod validate { #[derive(Clone, Copy)] pub struct ConflictStyle; impl keys::Validate for ConflictStyle { - fn validate(&self, value: &BStr) -> Result<(), Box> { - Merge::CONFLICT_STYLE.try_into_conflict_style(value)?; + fn validate(&self, value: &BStr) -> Result<(), gix_error::Exn> { + Merge::CONFLICT_STYLE.try_into_conflict_style(value).or_erased()?; Ok(()) } } diff --git a/gix/src/config/tree/sections/notes.rs b/gix/src/config/tree/sections/notes.rs index 2847bd8d0d8..3bcdfd62190 100644 --- a/gix/src/config/tree/sections/notes.rs +++ b/gix/src/config/tree/sections/notes.rs @@ -52,7 +52,7 @@ impl DisplayRef { } mod validate { - use std::error::Error; + use gix_error::ResultExt; use crate::{bstr::BStr, config::tree::keys::Validate}; @@ -60,8 +60,8 @@ mod validate { pub struct DisplayRef; impl Validate for DisplayRef { - fn validate(&self, value: &BStr) -> Result<(), Box> { - super::Notes::DISPLAY_REF.try_into_display_refs(value)?; + fn validate(&self, value: &BStr) -> Result<(), gix_error::Exn> { + super::Notes::DISPLAY_REF.try_into_display_refs(value).or_erased()?; Ok(()) } } diff --git a/gix/src/config/tree/sections/pack.rs b/gix/src/config/tree/sections/pack.rs index 0845c578937..fe6029caef4 100644 --- a/gix/src/config/tree/sections/pack.rs +++ b/gix/src/config/tree/sections/pack.rs @@ -55,20 +55,22 @@ impl Section for Pack { mod validate { use crate::{bstr::BStr, config::tree::keys}; - use gix_error::{ErrorExt, ValidationError}; + use gix_error::{ErrorExt, ResultExt, ValidationError}; #[derive(Clone, Copy)] pub struct IndexVersion; impl keys::Validate for IndexVersion { - fn validate(&self, value: &BStr) -> Result<(), Box> { - super::Pack::INDEX_VERSION.try_into_index_version( - gix_config::Integer::try_from(value) - .and_then(|int| { - int.to_decimal() - .ok_or_else(|| ValidationError::new_with_input("integer out of range", value).raise()) - }) - .map(Some), - )?; + fn validate(&self, value: &BStr) -> Result<(), gix_error::Exn> { + super::Pack::INDEX_VERSION + .try_into_index_version( + gix_config::Integer::try_from(value) + .and_then(|int| { + int.to_decimal() + .ok_or_else(|| ValidationError::new_with_input("integer out of range", value).raise()) + }) + .map(Some), + ) + .or_erased()?; Ok(()) } } diff --git a/gix/src/config/tree/sections/protocol.rs b/gix/src/config/tree/sections/protocol.rs index 99def73fa8b..1c29cebfb2b 100644 --- a/gix/src/config/tree/sections/protocol.rs +++ b/gix/src/config/tree/sections/protocol.rs @@ -31,9 +31,11 @@ mod allow { scheme: Option<&str>, ) -> Result { let value = value.as_bstr(); - scheme_permission::Allow::try_from(value.as_bstr()).map_err(|value| config::protocol::allow::Error { - value, - scheme: scheme.map(ToOwned::to_owned), + scheme_permission::Allow::try_from(value.as_bstr()).map_err(|value| { + gix_error::Error::from_error(gix_error::ValidationError::new(format!( + "The value {value:?} must be allow|deny|user in configuration key protocol{}.allow", + scheme.map(|scheme| format!(".{scheme}")).unwrap_or_default() + ))) }) } } @@ -110,13 +112,14 @@ mod key_impls { mod validate { use crate::{bstr::BStr, config::tree::keys}; + use gix_error::{ErrorExt, ResultExt, message}; #[derive(Clone, Copy)] pub struct Allow; impl keys::Validate for Allow { - fn validate(&self, _value: &BStr) -> Result<(), Box> { + fn validate(&self, _value: &BStr) -> Result<(), gix_error::Exn> { #[cfg(any(feature = "blocking-network-client", feature = "async-network-client"))] - super::Protocol::ALLOW.try_into_allow(_value, None)?; + super::Protocol::ALLOW.try_into_allow(_value, None).or_erased()?; Ok(()) } } @@ -124,13 +127,14 @@ mod validate { #[derive(Clone, Copy)] pub struct Version; impl keys::Validate for Version { - fn validate(&self, value: &BStr) -> Result<(), Box> { - let value = gix_config::Integer::try_from(value)? + fn validate(&self, value: &BStr) -> Result<(), gix_error::Exn> { + let value = gix_config::Integer::try_from(value) + .or_erased()? .to_decimal() - .ok_or_else(|| format!("integer {value} cannot be represented as integer"))?; + .ok_or_else(|| message!("integer {value} cannot be represented as integer").raise_erased())?; match value { 0..=2 => Ok(()), - _ => Err(format!("protocol version {value} is unknown").into()), + _ => Err(message!("protocol version {value} is unknown").raise_erased()), } } } diff --git a/gix/src/config/tree/sections/push.rs b/gix/src/config/tree/sections/push.rs index 0c2c28ff135..98923184b5a 100644 --- a/gix/src/config/tree/sections/push.rs +++ b/gix/src/config/tree/sections/push.rs @@ -46,13 +46,13 @@ mod default { mod validate { #[derive(Clone, Copy)] pub struct Default; - use std::error::Error; + use gix_error::ResultExt; use crate::{bstr::BStr, config::tree::keys::Validate}; impl Validate for Default { - fn validate(&self, value: &BStr) -> Result<(), Box> { - super::Push::DEFAULT.try_into_default(value)?; + fn validate(&self, value: &BStr) -> Result<(), gix_error::Exn> { + super::Push::DEFAULT.try_into_default(value).or_erased()?; Ok(()) } } diff --git a/gix/src/config/tree/sections/remote.rs b/gix/src/config/tree/sections/remote.rs index f419af1bb4b..b9ee5de574e 100644 --- a/gix/src/config/tree/sections/remote.rs +++ b/gix/src/config/tree/sections/remote.rs @@ -81,14 +81,14 @@ mod tag_opts { } pub mod validate { - use std::error::Error; + use gix_error::ResultExt; use crate::{bstr::BStr, config::tree::keys::Validate}; pub struct TagOpt; impl Validate for TagOpt { - fn validate(&self, value: &BStr) -> Result<(), Box> { - super::Remote::TAG_OPT.try_into_tag_opt(value)?; + fn validate(&self, value: &BStr) -> Result<(), gix_error::Exn> { + super::Remote::TAG_OPT.try_into_tag_opt(value).or_erased()?; Ok(()) } } diff --git a/gix/src/config/tree/sections/ssh.rs b/gix/src/config/tree/sections/ssh.rs index c65262c1d7e..6729c1f81d7 100644 --- a/gix/src/config/tree/sections/ssh.rs +++ b/gix/src/config/tree/sections/ssh.rs @@ -53,12 +53,14 @@ impl Section for Ssh { mod validate { use crate::{bstr::BStr, config::tree::keys}; + #[cfg(feature = "blocking-network-client")] + use gix_error::ResultExt; pub struct Variant; impl keys::Validate for Variant { - fn validate(&self, _value: &BStr) -> Result<(), Box> { + fn validate(&self, _value: &BStr) -> Result<(), gix_error::Exn> { #[cfg(feature = "blocking-network-client")] - super::Ssh::VARIANT.try_into_variant(_value)?; + super::Ssh::VARIANT.try_into_variant(_value).or_erased()?; Ok(()) } } diff --git a/gix/src/config/tree/sections/status.rs b/gix/src/config/tree/sections/status.rs index 6a8680bb711..92263c60329 100644 --- a/gix/src/config/tree/sections/status.rs +++ b/gix/src/config/tree/sections/status.rs @@ -56,12 +56,15 @@ impl Section for Status { mod validate { use crate::{bstr::BStr, config::tree::keys}; + use gix_error::ResultExt; #[derive(Clone, Copy)] pub struct ShowUntrackedFiles; impl keys::Validate for ShowUntrackedFiles { - fn validate(&self, value: &BStr) -> Result<(), Box> { - super::Status::SHOW_UNTRACKED_FILES.try_into_show_untracked_files(value)?; + fn validate(&self, value: &BStr) -> Result<(), gix_error::Exn> { + super::Status::SHOW_UNTRACKED_FILES + .try_into_show_untracked_files(value) + .or_erased()?; Ok(()) } } diff --git a/gix/src/config/tree/traits.rs b/gix/src/config/tree/traits.rs index 9c076f5da1f..d01d783e881 100644 --- a/gix/src/config/tree/traits.rs +++ b/gix/src/config/tree/traits.rs @@ -179,7 +179,7 @@ pub trait Key: std::fmt::Debug { self.validate(value)?; let mut key = self .full_name(None) - .map_err(|message| validate_assignment::Error::Name { message })?; + .map_err(|message| gix_error::Error::from_error(gix_error::ValidationError::new(message)))?; key.push(b'='); key.push_str(value); Ok(key) @@ -205,7 +205,7 @@ pub trait Key: std::fmt::Debug { self.validate(value)?; let mut key = self .full_name(Some(subsection)) - .map_err(|message| validate_assignment::Error::Name { message })?; + .map_err(|message| gix_error::Error::from_error(gix_error::ValidationError::new(message)))?; key.push(b'='); key.push_str(value); Ok(key) diff --git a/gix/src/create.rs b/gix/src/create.rs index b7e9b049f00..358038d1c03 100644 --- a/gix/src/create.rs +++ b/gix/src/create.rs @@ -5,25 +5,15 @@ use std::{ }; use gix_discover::DOT_GIT_DIR; +use gix_error::{ErrorExt, ResultExt}; /// The error used in [`into()`]. -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] -pub enum Error { - #[error("Could not obtain the current directory")] - CurrentDir(#[from] std::io::Error), - #[error("Could not open data at '{}'", .path.display())] - IoOpen { source: std::io::Error, path: PathBuf }, - #[error("Could not write data at '{}'", .path.display())] - IoWrite { source: std::io::Error, path: PathBuf }, - #[error("Refusing to initialize the existing '{}' directory", .path.display())] - DirectoryExists { path: PathBuf }, - #[error("Refusing to initialize the non-empty directory as '{}'", .path.display())] - DirectoryNotEmpty { path: PathBuf }, - #[error("Could not create directory at '{}'", .path.display())] - CreateDirectory { source: std::io::Error, path: PathBuf }, - #[error(transparent)] - Config(#[from] gix_error::ValidationError), +pub type Error = gix_error::Error; + +fn io_error(source: std::io::Error, action: &str, path: &Path) -> Error { + source + .and_raise(gix_error::message!("{action} at '{}'", path.display())) + .into() } /// The kind of repository to create. @@ -91,21 +81,13 @@ fn write_file(data: &[u8], path: &Path) -> Result<(), Error> { .truncate(true) .append(false) .open(path) - .map_err(|e| Error::IoOpen { - source: e, - path: path.to_owned(), - })?; - file.write_all(data).map_err(|e| Error::IoWrite { - source: e, - path: path.to_owned(), - }) + .map_err(|err| io_error(err, "Could not open data", path))?; + file.write_all(data) + .map_err(|err| io_error(err, "Could not write data", path)) } fn create_dir(p: &Path) -> Result<(), Error> { - fs::create_dir_all(p).map_err(|e| Error::CreateDirectory { - source: e, - path: p.to_owned(), - }) + fs::create_dir_all(p).map_err(|err| io_error(err, "Could not create directory", p)) } /// Options for use in [`into()`]; @@ -194,13 +176,15 @@ pub(crate) fn into_with_capabilities( Err(err) } }) - .map_err(|err| Error::IoOpen { - source: err, - path: dot_git.clone(), - })? + .map_err(|err| io_error(err, "Could not open data", &dot_git))? .count(); if num_entries_in_dot_git != 0 { - return Err(Error::DirectoryNotEmpty { path: dot_git }); + return Err(gix_error::Error::from_error( + gix_error::ValidationError::new_with_input( + "Refusing to initialize the non-empty directory as", + dot_git.display().to_string(), + ), + )); } } @@ -208,7 +192,12 @@ pub(crate) fn into_with_capabilities( dot_git.push(DOT_GIT_DIR); if dot_git.is_dir() { - return Err(Error::DirectoryExists { path: dot_git }); + return Err(gix_error::Error::from_error( + gix_error::ValidationError::new_with_input( + "Refusing to initialize an existing directory", + dot_git.display().to_string(), + ), + )); } } create_dir(&dot_git)?; @@ -257,32 +246,38 @@ pub(crate) fn into_with_capabilities( let (mut config_file, config_path) = { let mut cursor = PathCursor(&mut dot_git); let config_path = cursor.at("config"); - (fs::File::create(config_path)?, config_path.to_owned()) + ( + fs::File::create(config_path).map_err(|err| io_error(err, "Could not create data", config_path))?, + config_path.to_owned(), + ) }; let mut config = gix_config::File::default(); let caps = { let caps = fs_capabilities.unwrap_or_else(|| gix_fs::Capabilities::probe(&dot_git)); let mut core = config.new_section("core", None).expect("valid section name"); - core.push("filemode", bool(caps.executable_bit))?; - core.push("bare", bool(bare))?; - core.push("logallrefupdates", bool(!bare))?; + core.push("filemode", bool(caps.executable_bit)).or_erased()?; + core.push("bare", bool(bare)).or_erased()?; + core.push("logallrefupdates", bool(!bare)).or_erased()?; if !caps.symlink { - core.push("symlinks", bool(false))?; + core.push("symlinks", bool(false)).or_erased()?; } - core.push("ignorecase", bool(caps.ignore_case))?; - core.push("precomposeunicode", bool(caps.precompose_unicode))?; + core.push("ignorecase", bool(caps.ignore_case)).or_erased()?; + core.push("precomposeunicode", bool(caps.precompose_unicode)) + .or_erased()?; match object_hash { #[cfg(feature = "sha256")] Some(gix_hash::Kind::Sha256) => { - core.push("repositoryformatversion", "1")?; + core.push("repositoryformatversion", "1").or_erased()?; let mut extensions = config.new_section("extensions", None).expect("valid section name"); - extensions.push("objectformat", gix_hash::Kind::Sha256.to_string())?; + extensions + .push("objectformat", gix_hash::Kind::Sha256.to_string()) + .or_erased()?; } _ => { - core.push("repositoryformatversion", "0")?; + core.push("repositoryformatversion", "0").or_erased()?; } } @@ -290,10 +285,7 @@ pub(crate) fn into_with_capabilities( }; config_file .write_all(&config.to_bstring()) - .map_err(|err| Error::IoWrite { - source: err, - path: config_path, - })?; + .map_err(|err| io_error(err, "Could not write data", &config_path))?; caps }; @@ -305,7 +297,8 @@ pub(crate) fn into_with_capabilities( } else { gix_discover::repository::Kind::WorkTree { linked_git_dir: None } }, - &gix_fs::current_dir(caps.precompose_unicode)?, + &gix_fs::current_dir(caps.precompose_unicode) + .or_raise(|| gix_error::message("Could not obtain the current directory"))?, ) .expect("by now the `dot_git` dir is valid as we have accessed it"), caps, diff --git a/gix/src/diff.rs b/gix/src/diff.rs index 5c4cc72379f..355eb237b88 100644 --- a/gix/src/diff.rs +++ b/gix/src/diff.rs @@ -6,13 +6,7 @@ pub mod options { /// pub mod init { /// The error returned when instantiating [diff options](crate::diff::Options). - #[derive(Debug, thiserror::Error)] - #[cfg_attr(feature = "blob-diff", expect(missing_docs))] - pub enum Error { - #[cfg(feature = "blob-diff")] - #[error(transparent)] - RewritesConfiguration(#[from] crate::diff::new_rewrites::Error), - } + pub type Error = gix_error::Error; } } @@ -131,6 +125,7 @@ pub mod rename { #[cfg(feature = "blob-diff")] pub(crate) mod utils { use gix_diff::{Rewrites, rewrites::Copies}; + use gix_error::ResultExt; use crate::{ Repository, @@ -141,33 +136,13 @@ pub(crate) mod utils { /// pub mod new_rewrites { /// The error returned by [`new_rewrites()`](super::new_rewrites()). - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - ConfigDiffRenames(#[from] crate::config::key::GenericError), - #[error(transparent)] - ConfigDiffRenameLimit(#[from] crate::config::unsigned_integer::Error), - } + pub type Error = gix_error::Error; } /// pub mod resource_cache { /// The error returned by [`resource_cache()`](super::resource_cache()). - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - DiffAlgorithm(#[from] crate::config::diff::algorithm::Error), - #[error(transparent)] - WorktreeFilterOptions(#[from] crate::filter::pipeline::options::Error), - #[error(transparent)] - DiffDrivers(#[from] crate::config::diff::drivers::Error), - #[error(transparent)] - DiffPipelineOptions(#[from] crate::config::diff::pipeline_options::Error), - #[error(transparent)] - CommandContext(#[from] crate::config::command_context::Error), - } + pub type Error = gix_error::Error; } /// Create an instance by reading all relevant information from the `config`uration, while being `lenient` or not. @@ -189,7 +164,8 @@ pub(crate) mod utils { ) -> Result<(Option, bool), new_rewrites::Error> { let copies = match renames .try_into_renames(config.boolean(renames)) - .with_leniency(lenient)? + .with_leniency(lenient) + .or_erased()? { Some(renames) => match renames { Tracking::Disabled => return Ok((None, true)), @@ -205,7 +181,8 @@ pub(crate) mod utils { copies, limit: rename_limit .try_into_usize(config.integer(rename_limit)) - .with_leniency(lenient)? + .with_leniency(lenient) + .or_erased()? .unwrap_or(default.limit), ..default } @@ -229,7 +206,7 @@ pub(crate) mod utils { attr_stack: gix_worktree::Stack, roots: gix_diff::blob::pipeline::WorktreeRoots, ) -> Result { - let diff_algo = repo.config.diff_algorithm()?; + let diff_algo = repo.config.diff_algorithm().or_erased()?; let diff_cache = gix_diff::blob::Platform::new( gix_diff::blob::platform::Options { algorithm: Some(diff_algo), diff --git a/gix/src/dirwalk/iter.rs b/gix/src/dirwalk/iter.rs index 92e54241cc5..1d09bbedebf 100644 --- a/gix/src/dirwalk/iter.rs +++ b/gix/src/dirwalk/iter.rs @@ -1,7 +1,6 @@ -use std::path::PathBuf; +use gix_error::ResultExt; -#[cfg(feature = "parallel")] -use gix_error::ErrorExt; +use std::path::PathBuf; use super::Iter; use crate::{ @@ -45,19 +44,7 @@ pub struct Outcome { } /// The error returned by [Repository::dirwalk_iter()]. -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] -pub enum Error { - #[error("Failed to spawn producer thread")] - #[cfg(feature = "parallel")] - SpawnThread(#[from] std::io::Error), - #[error(transparent)] - #[cfg(not(feature = "parallel"))] - Dirwalk(#[from] dirwalk::Error), - #[error(transparent)] - #[cfg(not(feature = "parallel"))] - DetachPathSpec(#[from] std::io::Error), -} +pub type Error = gix_error::Error; /// Lifecycle impl Iter { @@ -83,20 +70,13 @@ impl Iter { Ok(Outcome { index, excludes: out.excludes.detach(), - pathspec: out.pathspec.detach().map_err(|err| { - dirwalk::Error::Walk( - err.and_raise(gix_error::message!( - "Could not detach the pathspec at '{}'", - repo.git_dir().display() - )) - .into_error(), - ) - })?, + pathspec: out.pathspec.detach().or_erased()?, traversal_root: out.traversal_root, dirwalk: out.dirwalk, }) } - })?; + }) + .or_erased()?; Ok(Iter { rx_and_join: Some((rx, handle)), @@ -111,7 +91,7 @@ impl Iter { let out = Outcome { index, excludes: out.excludes.detach(), - pathspec: out.pathspec.detach()?, + pathspec: out.pathspec.detach().or_erased()?, traversal_root: out.traversal_root, dirwalk: out.dirwalk, }; diff --git a/gix/src/dirwalk/mod.rs b/gix/src/dirwalk/mod.rs index bd3c83dccbd..33c6cc99f70 100644 --- a/gix/src/dirwalk/mod.rs +++ b/gix/src/dirwalk/mod.rs @@ -2,7 +2,7 @@ use std::path::PathBuf; use gix_dir::walk::{CollapsedEntriesEmissionMode, EmissionMode, ForDeletionMode}; -use crate::{AttributeStack, Pathspec, config}; +use crate::{AttributeStack, Pathspec}; mod options; @@ -39,24 +39,7 @@ pub struct Iter { } /// The error returned by [dirwalk()](crate::Repository::dirwalk()). -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] -pub enum Error { - #[error(transparent)] - Walk(gix_error::Error), - #[error("A working tree is required to perform a directory walk")] - MissingWorkDir, - #[error(transparent)] - Excludes(#[from] config::exclude_stack::Error), - #[error(transparent)] - Pathspec(#[from] crate::pathspec::init::Error), - #[error(transparent)] - Prefix(#[from] gix_path::realpath::Error), - #[error(transparent)] - FilesystemOptions(#[from] config::boolean::Error), - #[error("Could not list worktrees to assure they are no candidates for deletion")] - ListWorktrees(#[from] std::io::Error), -} +pub type Error = gix_error::Error; /// The outcome of the [dirwalk()](crate::Repository::dirwalk). pub struct Outcome<'repo> { diff --git a/gix/src/discover.rs b/gix/src/discover.rs index f529e2e1d24..d7f53643671 100644 --- a/gix/src/discover.rs +++ b/gix/src/discover.rs @@ -8,14 +8,7 @@ pub use gix_discover::*; use crate::{ThreadSafeRepository, bstr::BString}; /// The error returned by [`crate::discover()`]. -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] -pub enum Error { - #[error(transparent)] - Discover(#[from] gix_error::Error), - #[error(transparent)] - Open(#[from] crate::open::Error), -} +pub type Error = gix_error::Error; impl ThreadSafeRepository { /// Try to open a git repository in `directory` and search upwards through its parents until one is found, @@ -38,19 +31,16 @@ impl ThreadSafeRepository { trust_map: gix_sec::trust::Mapping, ) -> Result { let _span = gix_trace::coarse!("ThreadSafeRepository::discover()"); - let (path, trust) = - upwards_opts(directory.as_ref(), options).map_err(|err| Error::Discover(err.into_error()))?; + let (path, trust) = upwards_opts(directory.as_ref(), options).map_err(gix_error::Exn::into_error)?; let (git_dir, worktree_dir) = path.into_repository_and_work_tree_directories(); let mut options = trust_map.into_value_by_level(trust); options.git_dir_trust = trust.into(); // Note that we will adjust the `current_dir` later so it matches the value of `core.precomposeUnicode`. options.current_dir = Some(gix_fs::current_dir(false).map_err(|err| { - Error::Discover( - err.and_raise(message("Could not obtain the current working directory")) - .into_error(), - ) + err.and_raise(message("Could not obtain the current working directory")) + .into_error() })?); - Self::open_from_paths(git_dir, worktree_dir, options, None).map_err(Into::into) + Self::open_from_paths(git_dir, worktree_dir, options, None) } /// Try to open a git repository directly from the environment. @@ -107,7 +97,7 @@ impl ThreadSafeRepository { } if std::env::var_os("GIT_DIR").is_some() { - return Self::open_with_environment_overrides(directory.as_ref(), trust_map).map_err(Error::Open); + return Self::open_with_environment_overrides(directory.as_ref(), trust_map); } options = apply_additional_environment(options.apply_environment()); diff --git a/gix/src/env.rs b/gix/src/env.rs index a02ac94e68b..62358707d39 100644 --- a/gix/src/env.rs +++ b/gix/src/env.rs @@ -52,84 +52,6 @@ pub mod collate { /// pub mod fetch { /// An error which combines all possible errors when opening a repository, finding remotes and using them to fetch. - /// - /// It can be used to detect if the repository is likely be corrupted in some way, or if the fetch failed spuriously - /// and thus can be retried. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - Open(#[from] crate::open::Error), - #[error(transparent)] - FindExistingReference(#[from] crate::reference::find::existing::Error), - #[error(transparent)] - RemoteInit(#[from] crate::remote::init::Error), - #[error(transparent)] - FindExistingRemote(#[from] crate::remote::find::existing::Error), - #[error(transparent)] - #[cfg(feature = "credentials")] - CredentialHelperConfig(#[from] crate::config::credential_helpers::Error), - #[cfg(any(feature = "blocking-network-client", feature = "async-network-client"))] - #[error(transparent)] - Connect(#[from] crate::remote::connect::Error), - #[cfg(any(feature = "blocking-network-client", feature = "async-network-client"))] - #[error(transparent)] - PrepareFetch(#[from] crate::remote::fetch::prepare::Error), - #[cfg(any(feature = "blocking-network-client", feature = "async-network-client"))] - #[error(transparent)] - Fetch(#[from] crate::remote::fetch::Error), - #[error(transparent)] - Other(E), - } - - #[cfg(any(feature = "async-network-client", feature = "blocking-network-client"))] - impl Error - where - E: std::error::Error + Send + Sync + 'static, - { - /// Return `true` if retrying the failed operation might succeed. - pub fn can_retry(&self) -> bool { - match self { - Error::Open(_) - | Error::CredentialHelperConfig(_) - | Error::RemoteInit(_) - | Error::FindExistingReference(_) - | Error::FindExistingRemote(_) - | Error::Other(_) => false, - Error::Connect(err) => err.can_retry(), - Error::PrepareFetch(err) => err.can_retry(), - Error::Fetch(err) => err.can_retry(), - } - } - } - - /// Queries - impl Error - where - E: std::error::Error + Send + Sync + 'static, - { - /// Return true if repository corruption caused the failure. - pub fn is_corrupted(&self) -> bool { - match self { - Error::Open(crate::open::Error::NotARepository { .. } | crate::open::Error::Config(_)) => true, - #[cfg(any(feature = "async-network-client", feature = "blocking-network-client"))] - Error::PrepareFetch(crate::remote::fetch::prepare::Error::RefMap( - // Configuration couldn't be accessed or was incomplete. - crate::remote::ref_map::Error::GatherTransportConfig { .. } - | crate::remote::ref_map::Error::ConfigureCredentials(_), - )) => true, - // Maybe the value of the configuration was corrupted, or a file couldn't be removed. - #[cfg(any(feature = "async-network-client", feature = "blocking-network-client"))] - Error::Fetch( - crate::remote::fetch::Error::PackThreads(_) - | crate::remote::fetch::Error::PackIndexVersion(_) - | crate::remote::fetch::Error::RemovePackKeepFile { .. }, - ) => true, - #[cfg(any(feature = "async-network-client", feature = "blocking-network-client"))] - Error::Fetch(crate::remote::fetch::Error::Fetch(err)) => err.is_corrupted(), - _ => false, - } - } - } + pub type Error = gix_error::Error; } } diff --git a/gix/src/filter.rs b/gix/src/filter.rs index b3d0c520be4..0cb0e26deae 100644 --- a/gix/src/filter.rs +++ b/gix/src/filter.rs @@ -1,4 +1,5 @@ //! lower-level access to filters which are applied to create working tree checkouts or to 'clean' working tree contents for storage in git. +use gix_error::{ErrorExt, ResultExt}; pub use gix_filter as plumbing; use gix_object::Find; @@ -16,66 +17,26 @@ use crate::{ pub mod pipeline { /// pub mod options { - use crate::{bstr::BString, config}; - /// The error returned by [Pipeline::options()](crate::filter::Pipeline::options()). - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - CheckRoundTripEncodings(#[from] config::encoding::Error), - #[error(transparent)] - SafeCrlf(#[from] config::key::GenericErrorWithValue), - #[error("Could not interpret 'filter.{name}.required' configuration")] - Driver { name: BString, source: gix_error::Error }, - #[error(transparent)] - CommandContext(#[from] config::command_context::Error), - } + pub type Error = gix_error::Error; } /// pub mod convert_to_git { /// The error returned by [Pipeline::convert_to_git()](crate::filter::Pipeline::convert_to_git()). - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error("Failed to prime attributes to the path at which the data resides")] - WorktreeCacheAtPath(#[from] std::io::Error), - #[error(transparent)] - Convert(#[from] gix_filter::pipeline::convert::to_git::Error), - } + pub type Error = gix_error::Error; } /// pub mod convert_to_worktree { /// The error returned by [Pipeline::convert_to_worktree()](crate::filter::Pipeline::convert_to_worktree()). - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error("Failed to prime attributes to the path at which the data resides")] - WorktreeCacheAtPath(#[from] std::io::Error), - #[error(transparent)] - Convert(#[from] gix_filter::pipeline::convert::to_worktree::Error), - } + pub type Error = gix_error::Error; } /// pub mod worktree_file_to_object { - use std::path::PathBuf; - /// The error returned by [Pipeline::worktree_file_to_object()](crate::filter::Pipeline::worktree_file_to_object()). - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error("Cannot add worktree files in bare repositories")] - MissingWorktree, - #[error("Failed to perform IO for object creation for '{}'", path.display())] - IO { source: std::io::Error, path: PathBuf }, - #[error(transparent)] - WriteBlob(#[from] crate::object::write::Error), - #[error(transparent)] - ConvertToGit(#[from] crate::filter::pipeline::convert_to_git::Error), - } + pub type Error = gix_error::Error; } } @@ -94,8 +55,9 @@ impl<'repo> Pipeline<'repo> { /// Extract options from `repo` that are needed to properly drive a standard git filter pipeline. pub fn options(repo: &'repo Repository) -> Result { let config = &repo.config.resolved; - let encodings = - Core::CHECK_ROUND_TRIP_ENCODING.try_into_encodings(config.string("core.checkRoundtripEncoding"))?; + let encodings = Core::CHECK_ROUND_TRIP_ENCODING + .try_into_encodings(config.string("core.checkRoundtripEncoding")) + .or_erased()?; let safe_crlf = config .string("core.safecrlf") .map(|value| Core::SAFE_CRLF.try_into_safecrlf(value)) @@ -105,17 +67,20 @@ impl<'repo> Pipeline<'repo> { repo.config.lenient_config, // in lenient mode, we prefer the safe option, instead of just (trying) to output warnings. gix_filter::pipeline::CrlfRoundTripCheck::Fail, - )?; + ) + .or_erased()?; let auto_crlf = config .string("core.autocrlf") .map(|value| Core::AUTO_CRLF.try_into_autocrlf(value)) .transpose() - .with_leniency(repo.config.lenient_config)? + .with_leniency(repo.config.lenient_config) + .or_erased()? .unwrap_or_default(); let eol = config .string("core.eol") .map(|value| Core::EOL.try_into_eol(value)) - .transpose()?; + .transpose() + .or_erased()?; let drivers = extract_drivers(repo)?; Ok(gix_filter::pipeline::Options { drivers, @@ -159,24 +124,29 @@ impl Pipeline<'_> { where R: std::io::Read, { - let entry = self.cache.at_path(rela_path, None, &self.repo.objects)?; - Ok(self.inner.convert_to_git( - src, - rela_path, - &mut |_, attrs| { - entry.matching_attributes(attrs); - }, - &mut |buf| -> Result<_, gix_object::find::Error> { - let entry = match index - .entry_by_path(gix_path::to_unix_separators_on_windows(gix_path::into_bstr(rela_path)).as_ref()) - { - None => return Ok(None), - Some(entry) => entry, - }; - let obj = self.repo.objects.try_find(&entry.id, buf)?; - Ok(obj.filter(|obj| obj.kind == gix_object::Kind::Blob).map(|_| ())) - }, - )?) + let entry = self + .cache + .at_path(rela_path, None, &self.repo.objects) + .or_raise(|| gix_error::message("Failed to prime attributes to the path at which the data resides"))?; + self.inner + .convert_to_git( + src, + rela_path, + &mut |_, attrs| { + entry.matching_attributes(attrs); + }, + &mut |buf| -> Result<_, gix_object::find::Error> { + let entry = match index + .entry_by_path(gix_path::to_unix_separators_on_windows(gix_path::into_bstr(rela_path)).as_ref()) + { + None => return Ok(None), + Some(entry) => entry, + }; + let obj = self.repo.objects.try_find(&entry.id, buf)?; + Ok(obj.filter(|obj| obj.kind == gix_object::Kind::Blob).map(|_| ())) + }, + ) + .map_err(gix_error::Error::from) } /// Convert a `src` buffer located at `rela_path` (in the index) from what's in `git` to the worktree representation. @@ -194,7 +164,7 @@ impl Pipeline<'_> { options: gix_filter::pipeline::convert::to_worktree::Options, ) -> Result, pipeline::convert_to_worktree::Error> { - let entry = self.cache.at_entry(rela_path, None, &self.repo.objects)?; + let entry = self.cache.at_entry(rela_path, None, &self.repo.objects).or_erased()?; Ok(self.inner.convert_to_worktree( src, rela_path, @@ -222,11 +192,11 @@ impl Pipeline<'_> { Option<(gix_hash::ObjectId, gix_object::tree::EntryKind, std::fs::Metadata)>, pipeline::worktree_file_to_object::Error, > { - use pipeline::worktree_file_to_object::Error; - let rela_path_as_path = gix_path::from_bstr(rela_path); let repo = self.repo; - let worktree_dir = repo.workdir().ok_or(Error::MissingWorktree)?; + let worktree_dir = repo.workdir().ok_or_else(|| { + gix_error::Error::from_error(gix_error::message("Cannot add worktree files in bare repositories")) + })?; let path = worktree_dir.join(&rela_path_as_path); let md = match std::fs::symlink_metadata(&path) { Ok(md) => md, @@ -234,18 +204,31 @@ impl Pipeline<'_> { if gix_fs::io_err::is_not_found(err.kind(), err.raw_os_error()) { return Ok(None); } else { - return Err(Error::IO { source: err, path }); + return Err(gix_error::Error::from(err.and_raise(gix_error::message!( + "Failed to perform IO for object creation for '{}'", + path.display() + )))); } } }; let (id, kind) = if md.is_symlink() { - let target = std::fs::read_link(&path).map_err(|source| Error::IO { source, path })?; - let id = repo.write_blob(gix_path::into_bstr(target).as_ref())?; + let target = std::fs::read_link(&path).map_err(|source| { + gix_error::Error::from(source.and_raise(gix_error::message!( + "Failed to perform IO for object creation for '{}'", + path.display() + ))) + })?; + let id = repo.write_blob(gix_path::into_bstr(target).as_ref()).or_erased()?; (id, gix_object::tree::EntryKind::Link) } else if md.is_file() { use gix_filter::pipeline::convert::ToGitOutcome; - let file = std::fs::File::open(&path).map_err(|source| Error::IO { source, path })?; + let file = std::fs::File::open(&path).map_err(|source| { + gix_error::Error::from(source.and_raise(gix_error::message!( + "Failed to perform IO for object creation for '{}'", + path.display() + ))) + })?; let file_for_git = self.convert_to_git(file, rela_path_as_path.as_ref(), index)?; let id = match file_for_git { ToGitOutcome::Unchanged(mut file) => repo.write_blob_stream(&mut file)?, @@ -322,9 +305,10 @@ fn extract_drivers(repo: &Repository) -> Result, pipelin } if let Some(value) = section.value("required") { driver.required = gix_config::Boolean::try_from(BStr::new(&value)) - .map_err(|source| pipeline::options::Error::Driver { - name: name.to_owned(), - source: source.into_error(), + .map_err(|err| { + gix_error::Error::from(err.raise(gix_error::message!( + "Could not interpret 'filter.{name}.required' configuration" + ))) })? .into(); } diff --git a/gix/src/head/peel.rs b/gix/src/head/peel.rs index b8d416b6d51..fb4a3115a11 100644 --- a/gix/src/head/peel.rs +++ b/gix/src/head/peel.rs @@ -1,3 +1,5 @@ +use gix_error::ResultExt; + use crate::{ Head, ext::{ObjectIdExt, ReferenceExt}, @@ -5,65 +7,29 @@ use crate::{ }; mod error { - use crate::{object, reference}; - /// The error returned by [`Head::peel_to_id()`](super::Head::try_peel_to_id()) and /// [`Head::into_fully_peeled_id()`](super::Head::try_into_peeled_id()). - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - FindExistingObject(#[from] object::find::existing::Error), - #[error(transparent)] - PeelReference(#[from] reference::peel::Error), - } + pub type Error = gix_error::Error; } pub use error::Error; /// pub mod into_id { - use crate::object; - /// The error returned by [`Head::into_peeled_id()`](super::Head::into_peeled_id()). - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - Peel(#[from] super::Error), - #[error("Branch '{name}' does not have any commits")] - Unborn { name: gix_ref::FullName }, - #[error(transparent)] - ObjectKind(#[from] object::try_into::Error), - } + pub type Error = gix_error::Error; } /// pub mod to_commit { - use crate::object; - /// The error returned by [`Head::peel_to_commit()`](super::Head::peel_to_commit()). - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - PeelToObject(#[from] super::to_object::Error), - #[error(transparent)] - ObjectKind(#[from] object::try_into::Error), - } + pub type Error = gix_error::Error; } /// pub mod to_object { /// The error returned by [`Head::peel_to_object()`](super::Head::peel_to_object()). - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - Peel(#[from] super::Error), - #[error("Branch '{name}' does not have any commits")] - Unborn { name: gix_ref::FullName }, - } + pub type Error = gix_error::Error; } impl<'repo> Head<'repo> { @@ -74,7 +40,9 @@ impl<'repo> Head<'repo> { pub fn into_peeled_id(mut self) -> Result, into_id::Error> { self.try_peel_to_id()?; self.id().ok_or_else(|| match self.kind { - Kind::Symbolic(gix_ref::Reference { name, .. }) | Kind::Unborn(name) => into_id::Error::Unborn { name }, + Kind::Symbolic(gix_ref::Reference { name, .. }) | Kind::Unborn(name) => gix_error::Error::from_error( + gix_error::NotFoundError::new(format!("Branch '{name}' does not have any commits")), + ), Kind::Detached { .. } => unreachable!("id can be returned after peeling"), }) } @@ -142,7 +110,7 @@ impl<'repo> Head<'repo> { let mut nr = r.clone().attach(self.repo); let peeled = nr.peel_to_id(); *r = nr.detach(); - peeled? + peeled.or_erased()? } })) } @@ -163,11 +131,13 @@ impl<'repo> Head<'repo> { /// /// Note that this method mutates `self` in place. pub fn peel_to_object(&mut self) -> Result, to_object::Error> { - let id = self.try_peel_to_id()?.ok_or_else(|| to_object::Error::Unborn { - name: self.referent_name().expect("unborn").to_owned(), + let id = self.try_peel_to_id()?.ok_or_else(|| { + gix_error::Error::from_error(gix_error::NotFoundError::new(format!( + "Branch '{}' does not have any commits", + self.referent_name().expect("unborn") + ))) })?; id.object() - .map_err(|err| to_object::Error::Peel(Error::FindExistingObject(err))) } /// Follow the symbolic reference of this head until its target object and peel it by following tag objects until there is no @@ -186,6 +156,6 @@ impl<'repo> Head<'repo> { /// /// Note that this method mutates `self` in place. pub fn peel_to_commit(&mut self) -> Result, to_commit::Error> { - Ok(self.peel_to_object()?.try_into_commit()?) + Ok(self.peel_to_object()?.try_into_commit().or_erased()?) } } diff --git a/gix/src/id.rs b/gix/src/id.rs index 90a9bb86566..646ddd1a3d0 100644 --- a/gix/src/id.rs +++ b/gix/src/id.rs @@ -1,5 +1,7 @@ //! #![allow(clippy::empty_docs)] +use gix_error::ResultExt; + use std::ops::Deref; use gix_hash::{ObjectId, oid}; @@ -42,17 +44,28 @@ impl<'repo> Id<'repo> { /// Turn this object id into a shortened id with a length in hex as configured by `core.abbrev`. pub fn shorten(&self) -> Result { - let hex_len = self.repo.config.hex_len.map_or_else( - || self.repo.objects.packed_object_count().map(calculate_auto_hex_len), - Ok, - )?; + let hex_len = self + .repo + .config + .hex_len + .map_or_else( + || self.repo.objects.packed_object_count().map(calculate_auto_hex_len), + Ok, + ) + .or_erased()?; let prefix = gix_odb::store::prefix::disambiguate::Candidate::new(self.inner, hex_len) .expect("BUG: internal hex-len must always be valid"); self.repo .objects - .disambiguate_prefix(prefix)? - .ok_or(shorten::Error::NotFound { oid: self.inner }) + .disambiguate_prefix(prefix) + .or_erased()? + .ok_or_else(|| { + gix_error::Error::from_error(gix_error::message!( + "Id could not be shortened as the object with id {} could not be found", + self.inner + )) + }) } /// Turn this object id into a shortened id with a length in hex as configured by `core.abbrev`, or default @@ -71,16 +84,7 @@ fn calculate_auto_hex_len(num_packed_objects: u64) -> usize { /// pub mod shorten { /// Returned by [`Id::prefix()`][super::Id::shorten()]. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - PackedObjectsCount(#[from] gix_odb::store::load_index::Error), - #[error(transparent)] - DisambiguatePrefix(#[from] gix_odb::store::prefix::disambiguate::Error), - #[error("Id could not be shortened as the object with id {} could not be found", .oid)] - NotFound { oid: gix_hash::ObjectId }, - } + pub type Error = gix_error::Error; } impl Deref for Id<'_> { diff --git a/gix/src/init.rs b/gix/src/init.rs index 5c1992c110f..2b18988dcfd 100644 --- a/gix/src/init.rs +++ b/gix/src/init.rs @@ -1,17 +1,15 @@ #![allow(clippy::result_large_err)] use std::path::Path; +use gix_error::ErrorExt; use gix_ref::{ Category, FullName, store::WriteReflog, transaction::{PreviousValue, RefEdit}, }; -use crate::{ - ThreadSafeRepository, - bstr::{BString, ByteSlice}, - config::tree::Init, -}; +use crate::{ThreadSafeRepository, bstr::ByteSlice, config::tree::Init}; +use gix_error::ResultExt; /// The name of the branch to use if non is configured via git configuration. /// @@ -21,23 +19,7 @@ use crate::{ pub const DEFAULT_BRANCH_NAME: &str = "main"; /// The error returned by [`crate::init()`]. -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] -pub enum Error { - #[error("Could not obtain the current directory")] - CurrentDir(#[from] std::io::Error), - #[error(transparent)] - Init(#[from] crate::create::Error), - #[error(transparent)] - Open(#[from] crate::open::Error), - #[error("Invalid default branch name: {name:?}")] - InvalidBranchName { - name: BString, - source: gix_validate::reference::name::Error, - }, - #[error("Could not edit HEAD reference with new default name")] - EditHeadForDefaultBranch(#[from] crate::reference::edit::Error), -} +pub type Error = gix_error::Error; impl ThreadSafeRepository { /// Create a repository with work-tree within `directory`, creating intermediate directories as needed. @@ -75,7 +57,9 @@ impl ThreadSafeRepository { let (git_dir, worktree_dir) = path.into_repository_and_work_tree_directories(); open_options.git_dir_trust = Some(gix_sec::Trust::Full); // The repo will use `core.precomposeUnicode` to adjust the value as needed. - open_options.current_dir = gix_fs::current_dir(false)?.into(); + open_options.current_dir = gix_fs::current_dir(false) + .or_raise(|| gix_error::message("Could not obtain the current directory"))? + .into(); let repo = ThreadSafeRepository::open_from_paths(git_dir, worktree_dir, open_options, None)?; let branch_name = repo @@ -87,13 +71,17 @@ impl ThreadSafeRepository { let configured_branch_name = branch_name; let sym_ref: FullName = Category::LocalBranch .to_full_name(configured_branch_name.as_bstr()) - .map_err(|err| Error::InvalidBranchName { - name: configured_branch_name.clone(), - source: err, + .map_err(|err| { + gix_error::Error::from(err.and_raise(gix_error::ValidationError::new_with_input( + "Invalid default branch name", + configured_branch_name.clone(), + ))) })?; - gix_validate::reference::branch_name(sym_ref.as_bstr()).map_err(|err| Error::InvalidBranchName { - name: configured_branch_name, - source: err, + gix_validate::reference::branch_name(sym_ref.as_bstr()).map_err(|err| { + gix_error::Error::from(err.and_raise(gix_error::ValidationError::new_with_input( + "Invalid default branch name", + configured_branch_name, + ))) })?; let mut repo = repo.to_thread_local(); let prev_write_reflog = repo.refs.write_reflog; @@ -103,7 +91,8 @@ impl ThreadSafeRepository { sym_ref, PreviousValue::Any, "", - ))?; + )) + .or_raise(|| gix_error::message("Could not edit HEAD reference with new default name"))?; repo.refs.write_reflog = prev_write_reflog; } diff --git a/gix/src/lib.rs b/gix/src/lib.rs index e0c759c4869..163beaa23fb 100644 --- a/gix/src/lib.rs +++ b/gix/src/lib.rs @@ -322,10 +322,6 @@ pub fn init_bare(directory: impl AsRef) -> Result( url: Url, path: impl AsRef, @@ -347,10 +343,6 @@ where /// (but amended with using configuration from the git installation to ensure all authentication options are honored). /// /// See [`clone::PrepareFetch::new()`] for a function to take full control over all options. -#[expect( - clippy::result_large_err, - reason = "will be removed once `gix-error` is used consistently" -)] pub fn prepare_clone(url: Url, path: impl AsRef) -> Result where Url: std::convert::TryInto, @@ -401,7 +393,7 @@ pub fn open_opts(directory: impl Into, options: open::Option /// /// `git_dir` supplies context for `includeIf.gitdir` conditions and does not have to exist. Without it, these /// conditions aren't matched. Repository-local and branch-dependent configuration isn't available at this stage. -pub fn config(git_dir: Option<&std::path::Path>, options: &open::Options) -> Result { +pub fn config(git_dir: Option<&std::path::Path>, options: &open::Options) -> Result { let environment = options.permissions.env; let git_install_dir = path::install_dir().ok(); let home = gix_path::env::home_dir().and_then(|home| environment.home.check_opt(home)); @@ -437,17 +429,18 @@ pub fn config(git_dir: Option<&std::path::Path>, options: &open::Options) -> Res /// Use this to inspect, prepare or load the file before calling [`config_mut()`]. The file and its parent directories /// do not have to exist. No configuration transaction is opened, no lock is acquired, and no directories are created. /// Discovering the Git installation path, or the system path on Windows, may invoke Git. -pub fn config_path( - source: config::Source, - options: &open::Options, -) -> Result { - use config::file_mut::Error; +pub fn config_path(source: config::Source, options: &open::Options) -> Result { + use gix_error::{ErrorExt, ResultExt, message}; if !matches!( source, config::Source::GitInstallation | config::Source::System | config::Source::Git | config::Source::User ) { - return Err(Error::UnsupportedSource(source)); + return Err( + message!("Configuration source {source:?} requires a repository or has no physical file") + .raise() + .into(), + ); } let path = config::cache::source_path( source, @@ -456,11 +449,13 @@ pub fn config_path( options.permissions.config, &mut config::Cache::make_source_env(options.permissions.env), ) - .ok_or(Error::SourceUnavailable(source))?; + .ok_or_else(|| message!("Configuration source {source:?} has no available path with these options").raise())?; Ok(if path.is_absolute() { path } else { - std::env::current_dir().map_err(Error::CurrentDir)?.join(path) + std::env::current_dir() + .or_raise(|| message("Could not obtain the current directory for a relative configuration path"))? + .join(path) }) } @@ -485,12 +480,11 @@ pub fn config_path( /// file.commit()?; /// # Ok(()) } /// ``` -pub fn config_mut( - source: config::Source, - options: &open::Options, -) -> Result { +pub fn config_mut(source: config::Source, options: &open::Options) -> Result { + use gix_error::{ResultExt, message}; + let path = config_path(source, options)?; - let resolved = config(None, options)?; + let resolved = config(None, options).or_raise(|| message("Could not load global configuration"))?; let filter = options.filter_config_section.unwrap_or(config::section::is_trusted); let lock_mode = config::cache::access::config_lock_timeout(&resolved, options.lenient_config, filter)?; let shared_repository_permissions = config::file_mut::shared_repository_permissions(&resolved, filter)?; diff --git a/gix/src/mailmap.rs b/gix/src/mailmap.rs index 0d8bec4811b..51db8fc1df3 100644 --- a/gix/src/mailmap.rs +++ b/gix/src/mailmap.rs @@ -3,16 +3,5 @@ pub use gix_mailmap::*; /// pub mod load { /// The error returned by [`crate::Repository::open_mailmap_into()`]. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error("The mailmap file declared in `mailmap.file` could not be read")] - Io(#[from] std::io::Error), - #[error("The configured mailmap.blob could not be parsed")] - BlobSpec(#[from] crate::revision::spec::parse::single::Error), - #[error(transparent)] - PathInterpolate(#[from] gix_error::Error), - #[error("Could not find object configured in `mailmap.blob`")] - FindExisting(#[source] crate::object::find::existing::Error), - } + pub type Error = gix_error::Error; } diff --git a/gix/src/note.rs b/gix/src/note.rs index b94e6b008da..790774220f6 100644 --- a/gix/src/note.rs +++ b/gix/src/note.rs @@ -400,9 +400,7 @@ fn add_refs(repo: &Repository, pattern: &BStr, out: &mut Vec) -> Resul .all() .or_raise(|| message!("Could not iterate notes references matching {pattern}"))?; for reference in references { - let reference = reference - .map_err(gix_error::Error::from_boxed) - .or_raise(|| message!("Could not read notes reference matching {pattern}"))?; + let reference = reference.or_raise(|| message!("Could not read notes reference matching {pattern}"))?; if parsed.matches(reference.name().as_bstr(), gix_glob::wildmatch::Mode::empty()) { push_unique(reference.inner.name); } diff --git a/gix/src/object/blob.rs b/gix/src/object/blob.rs index ef5539c00bc..a277baea1e2 100644 --- a/gix/src/object/blob.rs +++ b/gix/src/object/blob.rs @@ -4,6 +4,7 @@ use crate::{Blob, ObjectDetached}; #[cfg(feature = "blob-diff")] pub mod diff { use gix_diff::blob::platform::prepare_diff::Operation; + use gix_error::ResultExt; use crate::bstr::ByteSlice; @@ -25,17 +26,7 @@ pub mod diff { use crate::bstr::BStr; /// The error returned by [Platform::lines()](super::Platform::lines()). - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error - where - E: std::error::Error + Send + Sync + 'static, - { - #[error(transparent)] - ProcessHunk(E), - #[error(transparent)] - PrepareDiff(#[from] gix_diff::blob::platform::prepare_diff::Error), - } + pub type Error = gix_error::Error; /// A change to a hunk of lines. pub enum Change<'a, 'data> { @@ -68,14 +59,14 @@ pub mod diff { pub fn lines( &mut self, mut process_hunk: FnH, - ) -> Result, lines::Error> + ) -> Result, lines::Error> where FnH: FnMut(lines::Change<'_, '_>) -> Result<(), E>, E: std::error::Error + Send + Sync + 'static, { self.resource_cache.options.skip_internal_diff_if_external_is_configured = false; - let prep = self.resource_cache.prepare_diff()?; + let prep = self.resource_cache.prepare_diff().or_erased()?; match prep.operation { Operation::InternalDiff { algorithm } => { let input = prep.interned_input(); @@ -117,7 +108,7 @@ pub mod diff { } if let Some(err) = err { - return Err(lines::Error::ProcessHunk(err)); + return Err(gix_error::Error::from_error(err)); } } Operation::ExternalCommand { .. } => { diff --git a/gix/src/object/commit.rs b/gix/src/object/commit.rs index 77023448924..5900ea0494c 100644 --- a/gix/src/object/commit.rs +++ b/gix/src/object/commit.rs @@ -1,23 +1,9 @@ use crate::{Commit, ObjectDetached, Tree, bstr, bstr::BStr}; +use gix_error::ResultExt; mod error { - use crate::object; - - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - FindExistingObject(#[from] object::find::existing::Error), - #[error("The commit could not be decoded fully or partially")] - Decode(#[source] gix_object::decode::Error), - #[error("The commit date could not be parsed")] - ParseDate(#[source] gix_date::Error), - #[error("Expected object of type {}, but got {}", .expected, .actual)] - ObjectKind { - expected: gix_object::Kind, - actual: gix_object::Kind, - }, - } + /// The error returned by commit accessors. + pub type Error = gix_error::Error; } pub use error::Error; @@ -103,8 +89,11 @@ impl<'repo> Commit<'repo> { /// /// For the time at which it was authored, refer to `.author()?.time()`. pub fn time(&self) -> Result { - let committer = self.committer().map_err(Error::Decode)?; - committer.time().map_err(Error::ParseDate) + self.committer() + .or_raise(|| gix_error::message("The commit could not be decoded fully or partially"))? + .time() + .or_raise(|| gix_error::message("The commit date could not be parsed")) + .map_err(Into::into) } /// Decode the entire commit object and return it for accessing all commit information. @@ -175,10 +164,7 @@ impl<'repo> Commit<'repo> { /// # Ok(()) } /// ``` pub fn tree(&self) -> Result, Error> { - match self.tree_id().map_err(Error::Decode)?.object()?.try_into_tree() { - Ok(tree) => Ok(tree), - Err(crate::object::try_into::Error { actual, expected, .. }) => Err(Error::ObjectKind { actual, expected }), - } + Ok(self.tree_id().or_erased()?.object()?.try_into_tree().or_erased()?) } /// Parse the commit and return the tree id it points to. diff --git a/gix/src/object/errors.rs b/gix/src/object/errors.rs index c1e6f284462..51ec3ad3f93 100644 --- a/gix/src/object/errors.rs +++ b/gix/src/object/errors.rs @@ -1,26 +1,18 @@ +pub(crate) fn existing_error(err: gix_object::find::existing::Error) -> gix_error::Error { + err.into_error() +} + /// pub mod conversion { /// The error returned by [`crate::object::try_to_()`][crate::Object::try_to_commit_ref()]. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - Decode(#[from] gix_object::decode::Error), - #[error("Expected object type {}, but got {}", .expected, .actual)] - UnexpectedType { - expected: gix_object::Kind, - actual: gix_object::Kind, - }, - } + pub type Error = gix_error::Error; } /// pub mod find { /// Indicate that an error occurred when trying to find an object. - #[derive(Debug, thiserror::Error)] - #[error(transparent)] - pub struct Error(#[from] pub gix_object::find::Error); + pub type Error = gix_error::Error; /// pub mod existing { @@ -29,14 +21,7 @@ pub mod find { /// pub mod with_conversion { /// The error returned by [Repository::find_commit()](crate::Repository::find_commit). - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - Find(#[from] crate::object::find::existing::Error), - #[error(transparent)] - Convert(#[from] crate::object::try_into::Error), - } + pub type Error = gix_error::Error; } } } @@ -44,7 +29,5 @@ pub mod find { /// pub mod write { /// An error to indicate writing to the loose object store failed. - #[derive(Debug, thiserror::Error)] - #[error(transparent)] - pub struct Error(#[from] pub gix_object::write::Error); + pub type Error = gix_error::Error; } diff --git a/gix/src/object/mod.rs b/gix/src/object/mod.rs index 34d31f4c868..438d9f8f065 100644 --- a/gix/src/object/mod.rs +++ b/gix/src/object/mod.rs @@ -1,5 +1,7 @@ //! #![allow(clippy::empty_docs)] +use gix_error::ResultExt; + use gix_hash::ObjectId; pub use gix_object::Kind; @@ -9,6 +11,7 @@ mod errors; pub(crate) mod cache { pub use gix_pack::cache::object::MemoryCappedHashmap; } +pub(crate) use errors::existing_error; pub use errors::{conversion, find, write}; /// pub mod blob; @@ -22,14 +25,8 @@ pub mod tree; /// pub mod try_into { - #[derive(thiserror::Error, Debug)] - #[expect(missing_docs)] - #[error("Object named {id} was supposed to be of kind {expected}, but was kind {actual}.")] - pub struct Error { - pub actual: gix_object::Kind, - pub expected: gix_object::Kind, - pub id: gix_hash::ObjectId, - } + /// The error returned when converting an object to an unexpected kind. + pub type Error = gix_error::ValidationError; } impl ObjectDetached { @@ -107,37 +104,49 @@ impl<'repo> Object<'repo> { /// Transform this object into a commit, or return it as part of the `Err` if it is no commit. pub fn try_into_commit(self) -> Result, try_into::Error> { - self.try_into().map_err(|this: Self| try_into::Error { - id: this.id, - actual: this.kind, - expected: gix_object::Kind::Commit, + self.try_into().map_err(|this: Self| { + gix_error::ValidationError::new(format!( + "Object named {} was supposed to be of kind {}, but was kind {}.", + this.id, + gix_object::Kind::Commit, + this.kind + )) }) } /// Transform this object into a tag, or return it as part of the `Err` if it is no commit. pub fn try_into_tag(self) -> Result, try_into::Error> { - self.try_into().map_err(|this: Self| try_into::Error { - id: this.id, - actual: this.kind, - expected: gix_object::Kind::Commit, + self.try_into().map_err(|this: Self| { + gix_error::ValidationError::new(format!( + "Object named {} was supposed to be of kind {}, but was kind {}.", + this.id, + gix_object::Kind::Tag, + this.kind + )) }) } /// Transform this object into a tree, or return it as part of the `Err` if it is no tree. pub fn try_into_tree(self) -> Result, try_into::Error> { - self.try_into().map_err(|this: Self| try_into::Error { - id: this.id, - actual: this.kind, - expected: gix_object::Kind::Tree, + self.try_into().map_err(|this: Self| { + gix_error::ValidationError::new(format!( + "Object named {} was supposed to be of kind {}, but was kind {}.", + this.id, + gix_object::Kind::Tree, + this.kind + )) }) } /// Transform this object into a blob, or return it as part of the `Err` if it is no blob. pub fn try_into_blob(self) -> Result, try_into::Error> { - self.try_into().map_err(|this: Self| try_into::Error { - id: this.id, - actual: this.kind, - expected: gix_object::Kind::Blob, + self.try_into().map_err(|this: Self| { + gix_error::ValidationError::new(format!( + "Object named {} was supposed to be of kind {}, but was kind {}.", + this.id, + gix_object::Kind::Blob, + this.kind + )) }) } } @@ -173,11 +182,15 @@ impl<'repo> Object<'repo> { /// Obtain a fully parsed commit whose fields reference our data buffer. pub fn try_to_commit_ref(&self) -> Result, conversion::Error> { gix_object::Data::new(&self.data, self.kind, self.id.kind()) - .decode()? + .decode() + .or_erased()? .into_commit() - .ok_or(conversion::Error::UnexpectedType { - expected: gix_object::Kind::Commit, - actual: self.kind, + .ok_or_else(|| { + gix_error::Error::from_error(gix_error::ValidationError::new(format!( + "Expected object type {}, but got {}", + gix_object::Kind::Commit, + self.kind + ))) }) } @@ -230,11 +243,15 @@ impl<'repo> Object<'repo> { /// Obtain a fully parsed tag object whose fields reference our data buffer. pub fn try_to_tag_ref(&self) -> Result, conversion::Error> { gix_object::Data::new(&self.data, self.kind, self.id.kind()) - .decode()? + .decode() + .or_erased()? .into_tag() - .ok_or(conversion::Error::UnexpectedType { - expected: gix_object::Kind::Tag, - actual: self.kind, + .ok_or_else(|| { + gix_error::Error::from_error(gix_error::ValidationError::new(format!( + "Expected object type {}, but got {}", + gix_object::Kind::Tag, + self.kind + ))) }) } diff --git a/gix/src/object/peel.rs b/gix/src/object/peel.rs index 3156688d3a3..33380527afc 100644 --- a/gix/src/object/peel.rs +++ b/gix/src/object/peel.rs @@ -8,22 +8,8 @@ use crate::{ /// pub mod to_kind { mod error { - - use crate::object; - /// The error returned by [`Object::peel_to_kind()`][crate::Object::peel_to_kind()]. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - FindExistingObject(#[from] object::find::existing::Error), - #[error("Last encountered object {oid} was {actual} while trying to peel to {expected}")] - NotFound { - oid: gix_hash::Prefix, - actual: object::Kind, - expected: object::Kind, - }, - } + pub type Error = gix_error::Error; } pub use error::Error; } @@ -57,11 +43,11 @@ impl<'repo> Object<'repo> { self = repo.find_object(target_id)?; } Kind::Tree | Kind::Blob => { - return Err(peel::to_kind::Error::NotFound { - oid: self.id().shorten().unwrap_or_else(|_| self.id.into()), - actual: self.kind, - expected: kind, - }); + return Err(gix_error::Error::from_error(gix_error::ValidationError::new(format!( + "Last encountered object {} was {} while trying to peel to {kind}", + self.id().shorten().unwrap_or_else(|_| self.id.into()), + self.kind, + )))); } } } diff --git a/gix/src/object/tree/diff/for_each.rs b/gix/src/object/tree/diff/for_each.rs index db7ff16dc94..3a942e42725 100644 --- a/gix/src/object/tree/diff/for_each.rs +++ b/gix/src/object/tree/diff/for_each.rs @@ -1,17 +1,11 @@ +use gix_error::ResultExt; use gix_object::TreeRefIter; use super::{Action, Change, Platform}; use crate::Tree; /// The error return by methods on the [diff platform][Platform]. -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] -pub enum Error { - #[error(transparent)] - Diff(#[from] gix_diff::tree::Error), - #[error(transparent)] - ResourceCache(#[from] crate::repository::diff_resource_cache::Error), -} +pub type Error = gix_error::Error; /// Add the item to compare to. impl<'old> Platform<'_, 'old> { @@ -75,6 +69,7 @@ impl<'old> Platform<'_, 'old> { }) }, opts, - )?) + ) + .or_erased()?) } } diff --git a/gix/src/object/tree/diff/mod.rs b/gix/src/object/tree/diff/mod.rs index 53473c71e4f..b3c4ca55f16 100644 --- a/gix/src/object/tree/diff/mod.rs +++ b/gix/src/object/tree/diff/mod.rs @@ -167,14 +167,7 @@ pub struct Stats { /// pub mod stats { /// The error returned by [`stats()`](super::Platform::stats()). - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - CreateResourceCache(#[from] crate::repository::diff_resource_cache::Error), - #[error(transparent)] - ForEachChange(#[from] crate::object::tree::diff::for_each::Error), - } + pub type Error = gix_error::Error; } /// Convenience diff --git a/gix/src/object/tree/editor.rs b/gix/src/object/tree/editor.rs index 5eae339a0c1..b8a701b8ee9 100644 --- a/gix/src/object/tree/editor.rs +++ b/gix/src/object/tree/editor.rs @@ -1,3 +1,4 @@ +use gix_error::{ErrorExt, ResultExt}; use gix_hash::ObjectId; use gix_object::tree::EntryKind; @@ -10,40 +11,13 @@ use crate::{ /// pub mod init { /// The error returned by [`Editor::new()](crate::object::tree::Editor::new()). - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - DecodeTree(#[from] gix_object::decode::Error), - #[error(transparent)] - ValidationOptions(#[from] crate::config::boolean::Error), - } + pub type Error = gix_error::Error; } /// pub mod write { - use crate::bstr::BString; - /// The error returned by [`Editor::write()](crate::object::tree::Editor::write()) and [`Cursor::write()](super::Cursor::write). - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - WriteTree(#[from] crate::object::write::Error), - #[error("The object {} ({}) at '{}' could not be found", id, kind.as_octal_str(), filename)] - MissingObject { - filename: BString, - kind: gix_object::tree::EntryKind, - id: gix_hash::ObjectId, - }, - #[error("The object {} ({}) has an invalid filename: '{}'", id, kind.as_octal_str(), filename)] - InvalidFilename { - filename: BString, - kind: gix_object::tree::EntryKind, - id: gix_hash::ObjectId, - source: gix_validate::path::component::Error, - }, - } + pub type Error = gix_error::Error; } /// A cursor at a specific portion of a tree to [edit](super::Editor). @@ -57,9 +31,9 @@ pub struct Cursor<'a, 'repo> { impl<'repo> super::Editor<'repo> { /// Initialize a new editor from the given `tree`. pub fn new(tree: &crate::Tree<'repo>) -> Result { - let tree_ref = tree.decode()?; + let tree_ref = tree.decode().or_erased()?; let repo = tree.repo; - let validate = repo.config.protect_options()?; + let validate = repo.config.protect_options().or_erased()?; Ok(super::Editor { inner: gix_object::tree::Editor::new(tree_ref.into(), &repo.objects, repo.object_hash()), validate, @@ -293,6 +267,7 @@ fn write_cursor<'repo>(cursor: &mut Cursor<'_, 'repo>) -> Result, writ .inner .write(|tree| -> Result { for entry in &tree.entries { + let kind: EntryKind = entry.mode.into(); gix_validate::path::component( entry.filename.as_ref(), entry @@ -301,21 +276,24 @@ fn write_cursor<'repo>(cursor: &mut Cursor<'_, 'repo>) -> Result, writ .then_some(gix_validate::path::component::Mode::Symlink), cursor.validate, ) - .map_err(|err| write::Error::InvalidFilename { - filename: entry.filename.clone(), - kind: entry.mode.into(), - id: entry.oid, - source: err, + .map_err(|err| { + gix_error::Error::from(err.and_raise(gix_error::message!( + "The object {} ({}) has an invalid filename: '{}'", + entry.oid, + kind.as_octal_str(), + entry.filename + ))) })?; if !entry.mode.is_commit() && !cursor.repo.has_object(entry.oid) { - return Err(write::Error::MissingObject { - filename: entry.filename.clone(), - kind: entry.mode.into(), - id: entry.oid, - }); + return Err(gix_error::Error::from_error(gix_error::message!( + "The object {} ({}) at '{}' could not be found", + entry.oid, + kind.as_octal_str(), + entry.filename + ))); } } - Ok(cursor.repo.write_object(tree)?.detach()) + Ok(cursor.repo.write_object(tree).or_erased()?.detach()) }) .map(|id| id.attach(cursor.repo)) } diff --git a/gix/src/object/tree/mod.rs b/gix/src/object/tree/mod.rs index e887fe6f9d2..8ad595d1dbc 100644 --- a/gix/src/object/tree/mod.rs +++ b/gix/src/object/tree/mod.rs @@ -85,7 +85,7 @@ impl<'repo> Tree<'repo> { loop { data = match next_entry(&mut iter, data) { - ControlFlow::Continue(oid) => self.repo.find(&oid, buf)?, + ControlFlow::Continue(oid) => self.repo.find(&oid, buf).map_err(crate::object::existing_error)?, ControlFlow::Break(entry) => { let mapped = entry.map(|e| Entry { inner: e.into(), @@ -120,7 +120,10 @@ impl<'repo> Tree<'repo> { loop { data = match next_entry(&mut iter, data) { ControlFlow::Continue(id) => { - let res = self.repo.find(&id, &mut self.data)?; + let res = self + .repo + .find(&id, &mut self.data) + .map_err(crate::object::existing_error)?; data_id = id; if res.kind.is_tree() { self.id = data_id; @@ -132,7 +135,9 @@ impl<'repo> Tree<'repo> { let inner = e.into(); if e.mode.is_tree() { data_id = e.oid.to_owned(); - self.repo.find(&data_id, &mut self.data)?; + self.repo + .find(&data_id, &mut self.data) + .map_err(crate::object::existing_error)?; self.id = data_id; } @@ -143,7 +148,9 @@ impl<'repo> Tree<'repo> { if data_id != self.id { // Ensure that our data always matches our id, even if this means an extra lookup. - self.repo.find(&self.id, &mut self.data)?; + self.repo + .find(&self.id, &mut self.data) + .map_err(crate::object::existing_error)?; } break Ok(entry); diff --git a/gix/src/open/mod.rs b/gix/src/open/mod.rs index 4de9a4e72fb..d490a27e821 100644 --- a/gix/src/open/mod.rs +++ b/gix/src/open/mod.rs @@ -1,6 +1,6 @@ use std::path::PathBuf; -use crate::{bstr::BString, config}; +use crate::bstr::BString; /// Permissions associated with various resources of a git repository #[derive(Copy, Clone, Ord, PartialOrd, PartialEq, Eq, Debug, Hash)] @@ -51,22 +51,7 @@ pub struct Options { } /// The error returned by [`crate::open()`]. -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] -pub enum Error { - #[error("Failed to load the git configuration")] - Config(#[from] config::Error), - #[error("\"{path}\" does not appear to be a git repository")] - NotARepository { source: gix_error::Error, path: PathBuf }, - #[error(transparent)] - Io(#[from] std::io::Error), - #[error("The git directory at '{}' is considered unsafe as it's not owned by the current user.", .path.display())] - UnsafeGitDir { path: PathBuf }, - #[error(transparent)] - EnvironmentAccessDenied(#[from] gix_sec::permission::Error), - #[error(transparent)] - PrefixNotRelative(#[from] gix_path::relative_path::Error), -} +pub type Error = gix_error::Error; mod options; pub mod permissions; diff --git a/gix/src/open/repository.rs b/gix/src/open/repository.rs index 86ff58dd5d1..d6874706d2d 100644 --- a/gix/src/open/repository.rs +++ b/gix/src/open/repository.rs @@ -1,6 +1,6 @@ #![allow(clippy::result_large_err)] use gix_config::file::Metadata; -use gix_error::ErrorExt; +use gix_error::{ErrorExt, ResultExt}; use gix_features::threading::OwnShared; use gix_object::bstr::ByteSlice; use gix_path::RelativePath; @@ -24,6 +24,15 @@ use crate::{ open::Permissions, }; +fn not_a_repository(source: gix_discover::is_git::Error, path: PathBuf) -> Error { + source + .raise(gix_error::NotFoundError::new(format!( + "\"{}\" does not appear to be a git repository", + path.display() + ))) + .into_error() +} + #[derive(Default, Clone)] pub(crate) struct EnvironmentOverrides { /// An override of the worktree typically from the environment, and overrides even worktree dirs set as parameter. @@ -83,33 +92,25 @@ impl ThreadSafeRepository { Ok(kind) => (candidate, kind), Err(_) => match gix_discover::is_git(&path) { Ok(kind) => (path, kind), - Err(err) => { - return Err(Error::NotARepository { - source: err.into_error(), - path, - }); - } + Err(err) => return Err(not_a_repository(err, path)), }, }, None => match gix_discover::is_git(&path) { Ok(kind) => (path, kind), Err(err) => { - return Err(Error::NotARepository { - source: err.into_error(), - path, - }); + return Err(not_a_repository(err, path)); } }, } }; // To be altered later based on `core.precomposeUnicode`. - let cwd = gix_fs::current_dir(false)?; + let cwd = gix_fs::current_dir(false).or_erased()?; let (git_dir, worktree_dir) = gix_discover::repository::Path::from_dot_git_dir(path, kind, &cwd) .expect("we have sanitized path with is_git()") .into_repository_and_work_tree_directories(); if options.git_dir_trust.is_none() { - options.git_dir_trust = gix_sec::Trust::from_path_ownership(&git_dir)?.into(); + options.git_dir_trust = gix_sec::Trust::from_path_ownership(&git_dir).or_erased()?.into(); } options.current_dir = Some(cwd); ThreadSafeRepository::open_from_paths(git_dir, worktree_dir, options, None) @@ -133,33 +134,27 @@ impl ThreadSafeRepository { trust_map: gix_sec::trust::Mapping, ) -> Result { let _span = gix_trace::coarse!("ThreadSafeRepository::open_with_environment_overrides()"); - let overrides = EnvironmentOverrides::from_env()?; + let overrides = EnvironmentOverrides::from_env().or_erased()?; let (path, path_kind): (PathBuf, _) = match overrides.git_dir { Some(git_dir) => gix_discover::is_git(&git_dir) - .map_err(|err| Error::NotARepository { - source: err.into_error(), - path: git_dir.clone(), - }) + .map_err(|err| not_a_repository(err, git_dir.clone())) .map(|kind| (git_dir, kind))?, None => { let fallback_directory = fallback_directory.into(); gix_discover::is_git(&fallback_directory) - .map_err(|err| Error::NotARepository { - source: err.into_error(), - path: fallback_directory.clone(), - }) + .map_err(|err| not_a_repository(err, fallback_directory.clone())) .map(|kind| (fallback_directory, kind))? } }; // To be altered later based on `core.precomposeUnicode`. - let cwd = gix_fs::current_dir(false)?; + let cwd = gix_fs::current_dir(false).or_erased()?; let (git_dir, worktree_dir) = gix_discover::repository::Path::from_dot_git_dir(path, path_kind, &cwd) .expect("we have sanitized path with is_git()") .into_repository_and_work_tree_directories(); let worktree_dir = worktree_dir.or(overrides.worktree_dir); - let git_dir_trust = gix_sec::Trust::from_path_ownership(&git_dir)?; + let git_dir_trust = gix_sec::Trust::from_path_ownership(&git_dir).or_erased()?; let mut options = trust_map.into_value_by_level(git_dir_trust); options.git_dir_trust = git_dir_trust.into(); options.current_dir = Some(cwd); @@ -200,7 +195,8 @@ impl ThreadSafeRepository { let mut common_dir = match known_common_dir { Some(common_dir) => Some(common_dir), None => gix_discover::path::from_plain_file(git_dir.join("commondir").as_ref()) - .transpose()? + .transpose() + .or_erased()? .map(|cd| git_dir.join(cd)), }; let repo_config = config::cache::StageOne::new( @@ -209,7 +205,13 @@ impl ThreadSafeRepository { *git_dir_trust, lossy_config, lenient_config, - )?; + ) + .map_err(|err| { + use gix_error::ErrorExt; + gix_error::Error::from(err.and_raise(gix_error::CorruptionError::new( + "Repository configuration could not be loaded", + ))) + })?; if repo_config.precompose_unicode { git_dir = gix_utils::str::precompose_path(git_dir.into()).into_owned(); @@ -275,7 +277,8 @@ impl ThreadSafeRepository { api_config_overrides, cli_config_overrides, use_repository_local_environment, - )?; + ) + .or_raise(|| gix_error::message("Repository configuration could not be loaded"))?; // Git's precedence is: GIT_WORK_TREE, core.bare, core.worktree, inferred worktree. let configured_worktree = config .resolved @@ -291,11 +294,12 @@ impl ThreadSafeRepository { if let Some((worktree, source)) = configured_worktree.filter(|_| may_use_configured_worktree) { if worktree.is_empty() { - return Err(config::Error::PathInterpolation { - path: worktree, - source: gix_error::NotFoundError::new("path is missing").raise().into_error(), - } - .into()); + return Err(gix_error::NotFoundError::new("path is missing") + .and_raise(gix_error::ValidationError::new_with_input( + "The path at the 'core.worktree' configuration could not be interpolated", + worktree, + )) + .into_error()); } // Git treats core.worktree as a literal path, without tilde or prefix interpolation. let worktree = gix_path::from_bstr(worktree.as_bstr()).into_owned(); @@ -329,15 +333,15 @@ impl ThreadSafeRepository { is_eligible_worktree_config_section(section, &git_dir, current_dir, &mut filter_config_section) }) .map_err(|err| { - Error::from(config::Error::ConfigTypedString( + gix_error::Error::from( config::key::GenericErrorWithValue::from(&Core::WORKTREE).with_source(err.into_error()), - )) + ) })? .is_some() { - return Err(Error::from(config::Error::ConfigTypedString( - config::key::GenericErrorWithValue::from(&Core::WORKTREE), - ))); + return Err(gix_error::Error::from(config::key::GenericErrorWithValue::< + gix_error::Error, + >::from(&Core::WORKTREE))); } // Without an explicit path, a non-bare `.git` directory implies its parent as worktree. @@ -436,7 +440,7 @@ impl ThreadSafeRepository { { Some(value) => { gitoxide::Core::INDEX_FILE.validate(value.as_bstr()).map_err(|_| { - config::Error::ConfigTypedString(config::key::GenericErrorWithValue::from_value( + gix_error::Error::from(config::key::GenericErrorWithValue::::from_value( &gitoxide::Core::INDEX_FILE, value.clone(), )) @@ -459,7 +463,7 @@ impl ThreadSafeRepository { Ok(Some(value)) => value, Ok(None) => gitoxide::Objects::ALLOC_LIMIT_IF_REDUCED_TRUST_DEFAULT, Err(_) if config.lenient_config => gitoxide::Objects::ALLOC_LIMIT_IF_REDUCED_TRUST_DEFAULT, - Err(err) => return Err(Error::from(config::Error::from(err))), + Err(err) => return Err(err.into()), }; if alloc_limit_if_reduced_trust != 0 { config.alloc_limit_bytes = Some(alloc_limit_if_reduced_trust); @@ -477,7 +481,7 @@ impl ThreadSafeRepository { let replacements = match prefix { Some(prefix) => { - let prefix: &RelativePath = prefix.as_bstr().try_into()?; + let prefix: &RelativePath = prefix.as_bstr().try_into().map_err(gix_error::Exn::into_error)?; Some(prefix).and_then(|prefix| { let _span = gix_trace::detail!("find replacement objects"); @@ -500,18 +504,21 @@ impl ThreadSafeRepository { let replacements = replacements.unwrap_or_default(); Ok(ThreadSafeRepository { - objects: OwnShared::new(gix_odb::Store::at_opts( - common_dir_ref.join("objects"), - config.object_hash, - &mut replacements.into_iter(), - gix_odb::store::init::Options { - slots: object_store_slots, - use_multi_pack_index: config.use_multi_pack_index, - alloc_limit_bytes: config.alloc_limit_bytes, - loose_compression: config.loose_compression, - current_dir: current_dir.to_owned().into(), - }, - )?), + objects: OwnShared::new( + gix_odb::Store::at_opts( + common_dir_ref.join("objects"), + config.object_hash, + &mut replacements.into_iter(), + gix_odb::store::init::Options { + slots: object_store_slots, + use_multi_pack_index: config.use_multi_pack_index, + alloc_limit_bytes: config.alloc_limit_bytes, + loose_compression: config.loose_compression, + current_dir: current_dir.to_owned().into(), + }, + ) + .or_erased()?, + ), common_dir, refs, work_tree: worktree_dir, @@ -603,7 +610,7 @@ fn replacement_objects_refs_prefix( mut filter_config_section: fn(&gix_config::file::Metadata) -> bool, ) -> Result, Error> { let is_disabled = config::shared::is_replace_refs_enabled(config, lenient, filter_config_section) - .map_err(config::Error::ConfigBoolean)? + .map_err(gix_error::Error::from)? .unwrap_or(true); if is_disabled { @@ -670,6 +677,9 @@ fn check_safe_directories( if is_safe { Ok(()) } else { - Err(Error::UnsafeGitDir { path: path_to_test }) + Err(gix_error::Error::from_error(gix_error::ValidationError::new(format!( + "The git directory at '{}' is considered unsafe as it's not owned by the current user.", + path_to_test.display() + )))) } } diff --git a/gix/src/pathspec.rs b/gix/src/pathspec.rs index 20eb1ade33d..5297eb2f5f8 100644 --- a/gix/src/pathspec.rs +++ b/gix/src/pathspec.rs @@ -2,24 +2,12 @@ pub use gix_pathspec::*; use crate::{AttributeStack, Pathspec, PathspecDetached, Repository, bstr::BStr}; +use gix_error::ResultExt; /// pub mod init { /// The error returned by [`Pathspec::new()`](super::Pathspec::new()). - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - MakeAttributes(#[from] Box), - #[error(transparent)] - Defaults(#[from] crate::repository::pathspec_defaults_ignore_case::Error), - #[error(transparent)] - ParseSpec(gix_pathspec::parse::Error), - #[error("Could not obtain the repository prefix as the relative path of the CWD as seen from the working tree")] - NormalizeSpec(#[source] gix_pathspec::normalize::Error), - #[error(transparent)] - RepoPrefix(#[from] gix_path::realpath::Error), - } + pub type Error = gix_error::Error; } /// Lifecycle @@ -41,19 +29,19 @@ impl<'repo> Pathspec<'repo> { empty_patterns_match_prefix: bool, patterns: impl IntoIterator>, inherit_ignore_case: bool, - make_attributes: impl FnOnce() -> Result>, + make_attributes: impl FnOnce() -> Result, ) -> Result { let defaults = repo.pathspec_defaults_inherit_ignore_case(inherit_ignore_case)?; let patterns = patterns .into_iter() .map(move |p| parse(p.as_ref(), defaults)) .collect::, _>>() - .map_err(init::Error::ParseSpec)?; + .or_erased()?; let needs_cache = patterns.iter().any(|p| !p.attributes.is_empty()); let prefix = if patterns.is_empty() && !empty_patterns_match_prefix { None } else { - repo.prefix()? + repo.prefix().map_err(gix_error::Exn::into_error)? }; let search = Search::from_specs( patterns, @@ -62,9 +50,14 @@ impl<'repo> Pathspec<'repo> { repo.workdir().unwrap_or_else(|| repo.git_dir()), repo.options.current_dir_or_empty(), gix_path::realpath::MAX_SYMLINKS, - )?, + ) + .map_err(gix_error::Exn::into_error)?, ) - .map_err(init::Error::NormalizeSpec)?; + .or_raise(|| { + gix_error::message( + "Could not obtain the repository prefix as the relative path of the CWD as seen from the working tree", + ) + })?; let cache = needs_cache.then(make_attributes).transpose()?; gix_trace::debug!( diff --git a/gix/src/reference/edits.rs b/gix/src/reference/edits.rs index c10fcd13c46..6242e82b0d2 100644 --- a/gix/src/reference/edits.rs +++ b/gix/src/reference/edits.rs @@ -5,17 +5,8 @@ pub mod set_target_id { use crate::{Reference, bstr::BString}; mod error { - use gix_ref::FullName; - /// The error returned by [`Reference::set_target_id()`][super::Reference::set_target_id()]. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error("Cannot change symbolic reference {name:?} into a direct one by setting it to an id")] - SymbolicReference { name: FullName }, - #[error(transparent)] - ReferenceEdit(#[from] crate::reference::edit::Error), - } + pub type Error = gix_error::Error; } pub use error::Error; @@ -33,7 +24,11 @@ pub mod set_target_id { reflog_message: impl Into, ) -> Result<(), Error> { match &self.inner.target { - Target::Symbolic(name) => return Err(Error::SymbolicReference { name: name.clone() }), + Target::Symbolic(name) => { + return Err(gix_error::Error::from_error(gix_error::message!( + "Cannot change symbolic reference {name:?} into a direct one by setting it to an id" + ))); + } Target::Object(current_id) => { let changed = self.repo.reference( self.name(), diff --git a/gix/src/reference/errors.rs b/gix/src/reference/errors.rs index eb8d7a4cede..3e1a47372ce 100644 --- a/gix/src/reference/errors.rs +++ b/gix/src/reference/errors.rs @@ -1,55 +1,20 @@ /// pub mod edit { - use crate::config; - /// The error returned by [`edit_references(…)`][crate::Repository::edit_references()], and others /// which ultimately create a reference. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - FileTransactionPrepare(#[from] gix_ref::file::transaction::prepare::Error), - #[error(transparent)] - FileTransactionCommit(#[from] gix_ref::file::transaction::commit::Error), - #[error(transparent)] - NameValidation(#[from] gix_validate::reference::name::Error), - #[error( - "Could not interpret core.filesRefLockTimeout or core.packedRefsTimeout, it must be the number in milliseconds to wait for locks or negative to wait forever" - )] - LockTimeoutConfiguration(#[from] config::lock_timeout::Error), - #[error(transparent)] - ParseCommitterTime(#[from] crate::config::time::Error), - } + pub type Error = gix_error::Error; } /// pub mod peel { /// The error returned by [`Reference::peel_to_id()`](crate::Reference::peel_to_id()) and /// [`Reference::into_fully_peeled_id()`](crate::Reference::into_fully_peeled_id()). - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - ToId(#[from] gix_ref::peel::to_id::Error), - #[error(transparent)] - PackedRefsOpen(#[from] gix_ref::packed::buffer::open::Error), - } + pub type Error = gix_error::Error; /// pub mod to_kind { /// The error returned by [`Reference::peel_to_kind(…)`](crate::Reference::peel_to_kind()). - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - FollowToObject(#[from] gix_ref::peel::to_object::Error), - #[error(transparent)] - PackedRefsOpen(#[from] gix_ref::packed::buffer::open::Error), - #[error(transparent)] - FindObject(#[from] crate::object::find::existing::Error), - #[error(transparent)] - PeelObject(#[from] crate::object::peel::to_kind::Error), - } + pub type Error = gix_error::Error; } } @@ -58,91 +23,42 @@ pub mod follow { /// pub mod to_object { /// The error returned by [`Reference::follow_to_object(…)`](crate::Reference::follow_to_object()). - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - FollowToObject(#[from] gix_ref::peel::to_object::Error), - #[error(transparent)] - PackedRefsOpen(#[from] gix_ref::packed::buffer::open::Error), - } + pub type Error = gix_error::Error; } } /// pub mod head_id { /// The error returned by [`Repository::head_id(…)`](crate::Repository::head_id()). - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - Head(#[from] crate::reference::find::existing::Error), - #[error(transparent)] - PeelToId(#[from] crate::head::peel::into_id::Error), - } + pub type Error = gix_error::Error; } /// pub mod head_commit { /// The error returned by [`Repository::head_commit`(…)](crate::Repository::head_commit()). - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - Head(#[from] crate::reference::find::existing::Error), - #[error(transparent)] - PeelToCommit(#[from] crate::head::peel::to_commit::Error), - } + pub type Error = gix_error::Error; } /// pub mod head_tree_id { /// The error returned by [`Repository::head_tree_id`(…)](crate::Repository::head_tree_id()). - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - HeadCommit(#[from] crate::reference::head_commit::Error), - #[error(transparent)] - DecodeCommit(#[from] gix_object::decode::Error), - } + pub type Error = gix_error::Error; } /// pub mod head_tree { /// The error returned by [`Repository::head_tree`(…)](crate::Repository::head_tree()). - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - HeadCommit(#[from] crate::reference::head_commit::Error), - #[error(transparent)] - CommitTree(#[from] crate::object::commit::Error), - } + pub type Error = gix_error::Error; } /// pub mod find { /// pub mod existing { - use gix_ref::PartialName; - /// The error returned by [`find_reference(…)`][crate::Repository::find_reference()], and others. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - Find(#[from] crate::reference::find::Error), - #[error("The reference '{}' did not exist", name.as_ref().as_bstr())] - NotFound { name: PartialName }, - } + pub type Error = gix_error::Error; } /// The error returned by [`try_find_reference(…)`][crate::Repository::try_find_reference()]. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - Find(#[from] gix_ref::file::find::Error), - } + pub type Error = gix_error::Error; } diff --git a/gix/src/reference/iter.rs b/gix/src/reference/iter.rs index 1db0cf65435..0a27dcec573 100644 --- a/gix/src/reference/iter.rs +++ b/gix/src/reference/iter.rs @@ -1,6 +1,8 @@ //! #![allow(clippy::empty_docs)] +use gix_error::{ErrorExt, ResultExt}; + use gix_path::RelativePath; use gix_ref::file::ReferenceExt; @@ -38,7 +40,7 @@ impl<'repo> Platform<'repo> { /// Even broken or otherwise unparsable or inaccessible references are returned and have to be handled by the caller on a /// case by case basis. pub fn all(&self) -> Result, init::Error> { - Ok(Iter::new(self.repo, self.platform.all()?)) + Ok(Iter::new(self.repo, self.platform.all().or_erased()?)) } /// Return an iterator over all references that match the given `prefix`. @@ -48,14 +50,16 @@ impl<'repo> Platform<'repo> { &self, prefix: impl TryInto<&'a RelativePath, Error = gix_path::relative_path::Error>, ) -> Result, init::Error> { - Ok(Iter::new(self.repo, self.platform.prefixed(prefix.try_into()?)?)) + let prefix = prefix.try_into().map_err(gix_error::Exn::into_error)?; + Ok(Iter::new(self.repo, self.platform.prefixed(prefix).or_erased()?)) } /// Return an iterator over all references that are tags. /// /// They are all prefixed with `refs/tags`. pub fn tags(&self) -> Result, init::Error> { - Ok(Iter::new(self.repo, self.platform.prefixed(b"refs/tags/".try_into()?)?)) + let prefix = b"refs/tags/".try_into().map_err(gix_error::Exn::into_error)?; + Ok(Iter::new(self.repo, self.platform.prefixed(prefix).or_erased()?)) } // TODO: tests @@ -63,16 +67,14 @@ impl<'repo> Platform<'repo> { /// /// They are all prefixed with `refs/heads`. pub fn local_branches(&self) -> Result, init::Error> { - Ok(Iter::new( - self.repo, - self.platform.prefixed(b"refs/heads/".try_into()?)?, - )) + let prefix = b"refs/heads/".try_into().map_err(gix_error::Exn::into_error)?; + Ok(Iter::new(self.repo, self.platform.prefixed(prefix).or_erased()?)) } // TODO: tests /// Return an iterator over all local pseudo references. pub fn pseudo(&self) -> Result, init::Error> { - Ok(Iter::new(self.repo, self.platform.pseudo()?)) + Ok(Iter::new(self.repo, self.platform.pseudo().or_erased()?)) } // TODO: tests @@ -80,10 +82,8 @@ impl<'repo> Platform<'repo> { /// /// They are all prefixed with `refs/remotes`. pub fn remote_branches(&self) -> Result, init::Error> { - Ok(Iter::new( - self.repo, - self.platform.prefixed(b"refs/remotes/".try_into()?)?, - )) + let prefix = b"refs/remotes/".try_into().map_err(gix_error::Exn::into_error)?; + Ok(Iter::new(self.repo, self.platform.prefixed(prefix).or_erased()?)) } } @@ -104,16 +104,16 @@ impl Iter<'_, '_> { } impl<'r> Iterator for Iter<'_, 'r> { - type Item = Result, Box>; + type Item = Result, crate::Error>; fn next(&mut self) -> Option { self.inner.next().map(|res| { - res.map_err(|err| Box::new(err) as Box) + res.map_err(|err| gix_error::Error::from(err.raise())) .and_then(|mut r| { if self.peel { let repo = &self.repo; r.peel_to_id_packed(&repo.refs, &repo.objects, self.peel_with_packed.as_ref().map(|p| &***p)) - .map_err(|err| Box::new(err) as Box) + .map_err(|err| gix_error::Error::from(err.raise())) .map(|_| r) } else { Ok(r) @@ -127,14 +127,7 @@ impl<'r> Iterator for Iter<'_, 'r> { /// pub mod init { /// The error returned by [`Platform::all()`](super::Platform::all()) or [`Platform::prefixed()`](super::Platform::prefixed()). - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - Io(#[from] std::io::Error), - #[error(transparent)] - RelativePath(#[from] gix_path::relative_path::Error), - } + pub type Error = gix_error::Error; } /// The error returned by [references()][crate::Repository::references()]. diff --git a/gix/src/reference/mod.rs b/gix/src/reference/mod.rs index 7e7da382931..c2bab2f75df 100644 --- a/gix/src/reference/mod.rs +++ b/gix/src/reference/mod.rs @@ -2,7 +2,7 @@ #![allow(clippy::empty_docs)] use crate::ext::ObjectIdExt; -use gix_ref::file; +use gix_error::ResultExt; use gix_ref::file::ReferenceExt; use crate::{Blob, Commit, Id, Object, Reference, Tag, Tree}; @@ -80,7 +80,7 @@ impl<'repo> Reference<'repo> { /// the chain of symbolic refs and annotated tags. #[deprecated = "Use `peel_to_id()` instead"] pub fn peel_to_id_in_place(&mut self) -> Result, peel::Error> { - let oid = self.inner.peel_to_id(&self.repo.refs, &self.repo.objects)?; + let oid = self.inner.peel_to_id(&self.repo.refs, &self.repo.objects).or_erased()?; Ok(Id::from_id(oid, self.repo)) } @@ -109,7 +109,7 @@ impl<'repo> Reference<'repo> { /// # Ok(()) } /// ``` pub fn peel_to_id(&mut self) -> Result, peel::Error> { - let oid = self.inner.peel_to_id(&self.repo.refs, &self.repo.objects)?; + let oid = self.inner.peel_to_id(&self.repo.refs, &self.repo.objects).or_erased()?; Ok(Id::from_id(oid, self.repo)) } @@ -125,7 +125,8 @@ impl<'repo> Reference<'repo> { ) -> Result, peel::Error> { let oid = self .inner - .peel_to_id_packed(&self.repo.refs, &self.repo.objects, packed)?; + .peel_to_id_packed(&self.repo.refs, &self.repo.objects, packed) + .or_erased()?; Ok(Id::from_id(oid, self.repo)) } @@ -140,7 +141,8 @@ impl<'repo> Reference<'repo> { pub fn peel_to_id_packed(&mut self, packed: Option<&gix_ref::packed::Buffer>) -> Result, peel::Error> { let oid = self .inner - .peel_to_id_packed(&self.repo.refs, &self.repo.objects, packed)?; + .peel_to_id_packed(&self.repo.refs, &self.repo.objects, packed) + .or_erased()?; Ok(Id::from_id(oid, self.repo)) } @@ -159,11 +161,7 @@ impl<'repo> Reference<'repo> { /// instead. #[doc(alias = "peel", alias = "git2")] pub fn peel_to_kind(&mut self, kind: gix_object::Kind) -> Result, peel::to_kind::Error> { - let packed = self.repo.refs.cached_packed_buffer().map_err(|err| { - peel::to_kind::Error::FollowToObject(gix_ref::peel::to_object::Error::Follow( - file::find::existing::Error::Find(file::find::Error::PackedOpen(err)), - )) - })?; + let packed = self.repo.refs.cached_packed_buffer().or_erased()?; self.peel_to_kind_packed(kind, packed.as_ref().map(|p| &***p)) } @@ -218,9 +216,10 @@ impl<'repo> Reference<'repo> { ) -> Result, peel::to_kind::Error> { let target = self .inner - .follow_to_object_packed(&self.repo.refs, packed)? + .follow_to_object_packed(&self.repo.refs, packed) + .or_erased()? .attach(self.repo); - Ok(target.object()?.peel_to_kind(kind)?) + target.object()?.peel_to_kind(kind) } /// Follow all symbolic references we point to up to the first object, which is typically (but not always) a tag, @@ -229,11 +228,7 @@ impl<'repo> Reference<'repo> { /// a symbolic target ref was looked up from packed-refs. #[doc(alias = "resolve", alias = "git2")] pub fn follow_to_object(&mut self) -> Result, follow::to_object::Error> { - let packed = self.repo.refs.cached_packed_buffer().map_err(|err| { - follow::to_object::Error::FollowToObject(gix_ref::peel::to_object::Error::Follow( - file::find::existing::Error::Find(file::find::Error::PackedOpen(err)), - )) - })?; + let packed = self.repo.refs.cached_packed_buffer().or_erased()?; self.follow_to_object_packed(packed.as_ref().map(|p| &***p)) } @@ -246,7 +241,8 @@ impl<'repo> Reference<'repo> { ) -> Result, follow::to_object::Error> { Ok(self .inner - .follow_to_object_packed(&self.repo.refs, packed)? + .follow_to_object_packed(&self.repo.refs, packed) + .or_erased()? .attach(self.repo)) } diff --git a/gix/src/remote/build.rs b/gix/src/remote/build.rs index b8acbbc3108..1814001a18d 100644 --- a/gix/src/remote/build.rs +++ b/gix/src/remote/build.rs @@ -1,3 +1,5 @@ +use gix_error::ResultExt; + use crate::{Remote, bstr::BStr, remote}; /// Builder methods @@ -10,7 +12,7 @@ impl Remote<'_> { Url: TryInto, E: std::error::Error + Send + Sync + 'static, { - self.url_inner(url.try_into().map_err(gix_error::Error::from_error)?, true) + self.url_inner(url.try_into().or_erased()?, true) } /// Set the `url` to be used when fetching data from a remote, without applying rewrite rules in case these could be faulty, @@ -22,7 +24,7 @@ impl Remote<'_> { Url: TryInto, E: std::error::Error + Send + Sync + 'static, { - self.url_inner(url.try_into().map_err(gix_error::Error::from_error)?, false) + self.url_inner(url.try_into().or_erased()?, false) } /// Set the `url` to be used when pushing data to a remote. @@ -44,7 +46,7 @@ impl Remote<'_> { Url: TryInto, E: std::error::Error + Send + Sync + 'static, { - self.push_url_inner(url.try_into().map_err(gix_error::Error::from_error)?, true) + self.push_url_inner(url.try_into().or_erased()?, true) } /// Set the `url` to be used when pushing data to a remote, without applying rewrite rules in case these could be faulty, @@ -65,7 +67,7 @@ impl Remote<'_> { Url: TryInto, E: std::error::Error + Send + Sync + 'static, { - self.push_url_inner(url.try_into().map_err(gix_error::Error::from_error)?, false) + self.push_url_inner(url.try_into().or_erased()?, false) } /// Configure how tags should be handled when fetching from the remote. diff --git a/gix/src/remote/connect.rs b/gix/src/remote/connect.rs index c73f4ec1ef5..1386c3cb169 100644 --- a/gix/src/remote/connect.rs +++ b/gix/src/remote/connect.rs @@ -2,6 +2,7 @@ use std::borrow::Cow; +use gix_error::{ErrorExt, ResultExt}; #[cfg(feature = "async-network-client")] use gix_transport::client::async_io::{Transport, connect}; #[cfg(feature = "blocking-network-client")] @@ -9,48 +10,8 @@ use gix_transport::client::blocking_io::{Transport, connect}; use crate::{Remote, config::tree::Protocol, remote::Connection}; -mod error { - use super::connect; - use crate::{bstr::BString, config, remote}; - - /// The error returned by [connect()][crate::Remote::connect()]. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error("Could not obtain options for connecting via ssh")] - SshOptions(#[from] config::ssh_connect_options::Error), - #[error("Could not obtain the current directory")] - CurrentDir(#[from] std::io::Error), - #[error("Could not access remote repository at \"{}\"", directory.display())] - InvalidRemoteRepositoryPath { directory: std::path::PathBuf }, - #[error(transparent)] - SchemePermission(#[from] remote::url::scheme_permission::Error), - #[error("Protocol {scheme:?} of url {url:?} is denied per configuration")] - ProtocolDenied { url: BString, scheme: gix_url::Scheme }, - #[error(transparent)] - Connect(#[from] connect::Error), - #[error("The {} url was missing - don't know where to establish a connection to", direction.as_str())] - MissingUrl { direction: remote::Direction }, - #[error("The given protocol version was invalid. Choose between 1 and 2")] - UnknownProtocol { source: config::key::GenericErrorWithValue }, - #[error("Could not verify that \"{}\" url is a valid git directory before attempting to use it", url.to_bstring())] - FileUrl { - source: Box, - url: gix_url::Url, - }, - } - - impl Error { - /// Return `true` if retrying might result in a different outcome due to IO working out differently. - pub fn can_retry(&self) -> bool { - match self { - Error::Connect(err) => err.can_retry_lenient(), - _ => false, - } - } - } -} -pub use error::Error; +/// The error returned by [connect()][crate::Remote::connect()]. +pub type Error = gix_error::Error; /// Establishing connections to remote hosts (without performing a git-handshake). impl<'repo> Remote<'repo> { @@ -97,12 +58,14 @@ impl<'repo> Remote<'repo> { #[cfg(feature = "blocking-network-client")] ssh: scheme_is_ssh .then(|| self.repo.ssh_connect_options()) - .transpose()? + .transpose() + .or_raise(|| gix_error::message("Could not obtain options for connecting via ssh"))? .unwrap_or_default(), trace: self.repo.config.trace_packet(), }, ) - .await?; + .await + .map_err(gix_error::Error::from)?; Ok(self.to_connection_with_transport(transport)) } @@ -121,19 +84,25 @@ impl<'repo> Remote<'repo> { dir.to_mut().push(gix_discover::DOT_GIT_DIR); gix_discover::is_git(dir.as_ref()) }) - .map_err(|err| Error::FileUrl { - source: err.into_error().into(), - url: url.clone(), + .map_err(|err| { + gix_error::Error::from(err.raise(gix_error::message!( + "Could not verify that {:?} is a valid git directory before attempting to use it", + url.to_bstring() + ))) })?; let (git_dir, _work_dir) = gix_discover::repository::Path::from_dot_git_dir( dir.clone().into_owned(), kind, // precomposed unicode doesn't matter here as long as the produced path is accessible, // which is a given either way. - &gix_fs::current_dir(false)?, + &gix_fs::current_dir(false) + .or_raise(|| gix_error::message("Could not obtain the current directory"))?, ) - .ok_or_else(|| Error::InvalidRemoteRepositoryPath { - directory: dir.into_owned(), + .ok_or_else(|| { + gix_error::Error::from_error(gix_error::ValidationError::new_with_input( + "Could not access remote repository", + gix_path::into_bstr(dir.clone().into_owned()).into_owned(), + )) })? .into_repository_and_work_tree_directories(); url.path = gix_path::into_bstr(git_dir).into_owned(); @@ -143,14 +112,28 @@ impl<'repo> Remote<'repo> { let version = crate::config::tree::Protocol::VERSION .try_into_protocol_version(self.repo.config.resolved.integer(Protocol::VERSION)) - .map_err(|err| Error::UnknownProtocol { source: err })?; + .map_err(|err| { + gix_error::Error::from(err.and_raise(gix_error::ValidationError::new( + "The given protocol version was invalid. Choose between 1 and 2", + ))) + })?; - let url = self.url(direction).ok_or(Error::MissingUrl { direction })?.to_owned(); - if !self.repo.config.url_scheme()?.allow(&url.scheme) { - return Err(Error::ProtocolDenied { - url: url.to_bstring(), - scheme: url.scheme, - }); + let url = self + .url(direction) + .ok_or_else(|| { + gix_error::Error::from_error(gix_error::ValidationError::new(format!( + "The {} url was missing - don't know where to establish a connection to", + direction.as_str() + ))) + })? + .to_owned(); + if !self.repo.config.url_scheme().or_erased()?.allow(&url.scheme) { + return Err(gix_error::Error::from_error( + gix_error::ValidationError::new_with_input( + format!("Protocol {:?} is denied per configuration", url.scheme), + url.to_bstring(), + ), + )); } Ok((sanitize(url)?, version)) } diff --git a/gix/src/remote/connection/access.rs b/gix/src/remote/connection/access.rs index 0c1e4e6dd4c..6d026c964a5 100644 --- a/gix/src/remote/connection/access.rs +++ b/gix/src/remote/connection/access.rs @@ -2,7 +2,7 @@ use crate::{ Remote, remote::{Connection, connection::AuthenticateFn, connection::ConnectionDetached}, }; -use gix_error::{OptionExt, ResultExt, ValidationError, message}; +use gix_error::{OptionExt, ResultExt, ValidationError}; #[cfg(feature = "async-network-client")] use gix_transport::client::async_io::Transport; #[cfg(feature = "blocking-network-client")] @@ -153,7 +153,7 @@ fn configured_credentials_for_current_url(repo: crate::Repository) -> Authentica let (mut cascade, _action_with_normalized_url, prompt_opts) = repo .config_snapshot() .credential_helpers(gix_url::parse(&url).or_erased()?) - .or_raise_erased(|| message("Could not configure credential helpers"))?; + .or_raise_erased(|| gix_error::CorruptionError::new("Credential helper configuration is invalid"))?; let outcome = cascade.invoke(action, prompt_opts.clone()); previous_cascade_and_prompt = Some((cascade, prompt_opts)); outcome diff --git a/gix/src/remote/connection/fetch/config.rs b/gix/src/remote/connection/fetch/config.rs index 1ff1c2ca0f6..a3294e89069 100644 --- a/gix/src/remote/connection/fetch/config.rs +++ b/gix/src/remote/connection/fetch/config.rs @@ -3,20 +3,31 @@ use crate::{ Repository, config::{cache::util::ApplyLeniency, tree::Pack}, }; +use gix_error::ErrorExt; pub fn index_threads(repo: &Repository) -> Result, Error> { - Ok(Pack::THREADS + Pack::THREADS .try_into_usize( repo.config .resolved .integer_filter(Pack::THREADS, &mut repo.filter_config_section()), ) - .with_leniency(repo.options.lenient_config)?) + .with_leniency(repo.options.lenient_config) + .map_err(|err| { + gix_error::Error::from(err.and_raise(gix_error::CorruptionError::new( + "The configured pack thread count is invalid", + ))) + }) } pub fn pack_index_version(repo: &Repository) -> Result { Ok(Pack::INDEX_VERSION .try_into_index_version(repo.config.resolved.integer(Pack::INDEX_VERSION)) - .with_leniency(repo.options.lenient_config)? + .with_leniency(repo.options.lenient_config) + .map_err(|err| { + gix_error::Error::from(err.and_raise(gix_error::CorruptionError::new( + "The configured pack index version is invalid", + ))) + })? .unwrap_or(gix_pack::index::Version::V2)) } diff --git a/gix/src/remote/connection/fetch/error.rs b/gix/src/remote/connection/fetch/error.rs index eb5cf4ec8c1..b9f45dd4ee6 100644 --- a/gix/src/remote/connection/fetch/error.rs +++ b/gix/src/remote/connection/fetch/error.rs @@ -1,49 +1,2 @@ -use crate::config; - /// The error returned by [`receive()`](super::Prepare::receive()). -// TODO: remove unused variants -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] -pub enum Error { - #[error(transparent)] - Fetch(#[from] gix_error::Error), - #[error("The value to configure pack threads should be 0 to auto-configure or the amount of threads to use")] - PackThreads(#[from] config::unsigned_integer::Error), - #[error("The value to configure the pack index version should be 1 or 2")] - PackIndexVersion(#[from] config::key::GenericError), - #[error("Cannot fetch from a remote that uses {remote} while local repository uses {local} for object hashes")] - IncompatibleObjectHash { - local: gix_hash::Kind, - remote: gix_hash::Kind, - }, - #[error(transparent)] - LoadAlternates(#[from] gix_odb::store::load_index::Error), - #[error(transparent)] - Client(#[from] gix_protocol::transport::client::Error), - #[error(transparent)] - UpdateRefs(#[from] super::refs::update::Error), - #[error("Failed to remove .keep file at \"{}\"", path.display())] - RemovePackKeepFile { - path: std::path::PathBuf, - 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::>().join(", "))] - NoMapping { - refspecs: Vec, - num_remote_refs: usize, - }, - #[error("Could not obtain configuration to learn if shallow remotes should be rejected")] - RejectShallowRemoteConfig(#[from] config::boolean::Error), - #[error(transparent)] - NegotiationAlgorithmConfig(#[from] config::key::GenericErrorWithValue), -} - -impl Error { - pub(crate) fn can_retry(&self) -> bool { - match self { - Error::Fetch(err) => err.can_retry_lenient(), - Error::Client(err) => gix_error::can_retry_lenient(err), - _ => false, - } - } -} +pub type Error = gix_error::Error; diff --git a/gix/src/remote/connection/fetch/mod.rs b/gix/src/remote/connection/fetch/mod.rs index 4991b1894d6..db5a002c004 100644 --- a/gix/src/remote/connection/fetch/mod.rs +++ b/gix/src/remote/connection/fetch/mod.rs @@ -102,23 +102,7 @@ pub use gix_protocol::fetch::ProgressId; /// pub mod prepare { /// The error returned by [`prepare_fetch()`][super::Connection::prepare_fetch()]. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error("Cannot perform a meaningful fetch operation without any configured ref-specs")] - MissingRefSpecs, - #[error(transparent)] - RefMap(#[from] crate::remote::ref_map::Error), - } - - impl Error { - pub(crate) fn can_retry(&self) -> bool { - match self { - Error::RefMap(err) => err.can_retry(), - _ => false, - } - } - } + pub type Error = gix_error::Error; } impl<'auth, 'repo, T> Connection<'_, 'auth, 'repo, T> @@ -161,7 +145,9 @@ where options: ref_map::Options, ) -> Result, prepare::Error> { if self.remote.fetch_refspecs().is_empty() && options.extra_refspecs.is_empty() { - return Err(prepare::Error::MissingRefSpecs); + return Err(gix_error::Error::from_error(gix_error::ValidationError::new( + "Cannot perform a meaningful fetch operation without any configured ref-specs", + ))); } let ref_map = self.ref_map_by_ref(repo, progress, options).await?; Ok(PrepareDetached { diff --git a/gix/src/remote/connection/fetch/receive_pack.rs b/gix/src/remote/connection/fetch/receive_pack.rs index 53d7f182a00..da796b3325c 100644 --- a/gix/src/remote/connection/fetch/receive_pack.rs +++ b/gix/src/remote/connection/fetch/receive_pack.rs @@ -100,10 +100,15 @@ where if ref_map.is_missing_required_mapping() { let mut specs = ref_map.refspecs.clone(); specs.extend(ref_map.extra_refspecs.clone()); - return Err(Error::NoMapping { - refspecs: specs, - num_remote_refs: ref_map.remote_refs.len(), - }); + return Err(gix_error::Error::from_error(gix_error::ValidationError::new(format!( + "None of the refspec(s) {} matched any of the {} refs on the remote", + specs + .iter() + .map(|spec| spec.to_ref().instruction().to_bstring().to_string()) + .collect::>() + .join(", "), + ref_map.remote_refs.len() + )))); } let mut con = self.con.take().expect("receive() can only be called once"); @@ -111,10 +116,10 @@ where let expected_object_hash = repo.object_hash(); if ref_map.object_hash != expected_object_hash { - return Err(Error::IncompatibleObjectHash { - local: expected_object_hash, - remote: ref_map.object_hash, - }); + return Err(gix_error::Error::from_error(gix_error::ValidationError::new(format!( + "Cannot fetch from a remote that uses {} while local repository uses {expected_object_hash} for object hashes", + ref_map.object_hash + )))); } let fetch_options = gix_protocol::fetch::Options { @@ -126,7 +131,10 @@ where repo.config .resolved .boolean_filter("clone.rejectShallow", &mut repo.filter_config_section()), - )? + ) + .or_raise(|| { + gix_error::message("Could not obtain configuration to learn if shallow remotes should be rejected") + })? .unwrap_or(false), }; let context = gix_protocol::fetch::Context { @@ -155,7 +163,7 @@ where }; let cache = graph_repo.commit_graph_if_enabled().ok().flatten(); let mut graph = graph_repo.revision_graph(cache.as_ref()); - let alternates = repo.objects.store_ref().alternate_db_paths()?; + let alternates = repo.objects.store_ref().alternate_db_paths().or_erased()?; let mut negotiate = Negotiate { objects: &graph_repo.objects, refs: &graph_repo.refs, @@ -237,7 +245,7 @@ where && (!update_refs.edits.is_empty() || bundle.index.num_objects == 0) && let Some(path) = bundle.keep_path.take() { - std::fs::remove_file(&path).map_err(|err| Error::RemovePackKeepFile { path, source: err })?; + std::fs::remove_file(&path).or_raise(|| message!("Failed to remove .keep file at {:?}", path.display()))?; } let out = Outcome { diff --git a/gix/src/remote/connection/fetch/update_refs/mod.rs b/gix/src/remote/connection/fetch/update_refs/mod.rs index 4c72f937adb..2278180888f 100644 --- a/gix/src/remote/connection/fetch/update_refs/mod.rs +++ b/gix/src/remote/connection/fetch/update_refs/mod.rs @@ -1,4 +1,5 @@ #![allow(clippy::result_large_err)] +use gix_error::{ErrorExt, ResultExt}; use gix_object::Exists; use gix_ref::{ Target, TargetRef, @@ -74,15 +75,7 @@ pub(crate) fn update( let mut updates = Vec::new(); let mut edit_indices_to_validate = Vec::new(); - let mut checked_out_branches = repo.checked_out_branches().map_err(|err| match err { - crate::repository::worktree::CheckedOutBranchesError::WorktreeListing(err) => { - update::Error::WorktreeListing(err) - } - crate::repository::worktree::CheckedOutBranchesError::OpenWorktreeRepo(err) => { - update::Error::OpenWorktreeRepo(err) - } - crate::repository::worktree::CheckedOutBranchesError::FollowSymref(err) => update::Error::FollowSymref(err), - })?; + let mut checked_out_branches = repo.checked_out_branches()?; let implicit_tag_refspec = fetch_tags .to_refspec() .filter(|_| matches!(fetch_tags, crate::remote::fetch::Tags::Included)); @@ -120,48 +113,59 @@ pub(crate) fn update( } let (mode, edit_index, type_change) = match local { Some(name) => { - let (mode, reflog_message, name, previous_value) = match repo.try_find_reference(name)? { - Some(existing) => { - if let Some(wt_dirs) = checked_out_branches.get_mut(existing.name()) { - wt_dirs.sort(); - wt_dirs.dedup(); - let mode = Mode::RejectedCurrentlyCheckedOut { - worktree_dirs: wt_dirs.to_owned(), - }; - updates.push(mode.into()); - continue; - } - - match existing - .try_id() - .map_or_else(|| existing.clone().peel_to_id(), Ok) - .map(crate::Id::detach) - { - Ok(local_id) => { - let remote_id = match remote_id { - Some(id) => id, - None => { - // we don't allow to go back to unborn state if there is a local reference already present. - // Note that we will be changing it to a symbolic reference just fine. - updates.push(Mode::RejectedToReplaceWithUnborn.into()); - continue; - } + let (mode, reflog_message, name, previous_value) = + match repo.try_find_reference(name)? { + Some(existing) => { + if let Some(wt_dirs) = checked_out_branches.get_mut(existing.name()) { + wt_dirs.sort(); + wt_dirs.dedup(); + let mode = Mode::RejectedCurrentlyCheckedOut { + worktree_dirs: wt_dirs.to_owned(), }; - let (mode, reflog_message) = if local_id == remote_id { - (Mode::NoChangeNeeded, "no update will be performed") - } else if let Some(gix_ref::Category::Tag) = existing.name().category() { - if spec.allow_non_fast_forward() { - (Mode::Forced, "updating tag") + updates.push(mode.into()); + continue; + } + + match existing + .try_id() + .map_or_else(|| existing.clone().peel_to_id(), Ok) + .map_err(|err| { + gix_error::Error::from(err.and_raise(gix_error::message( + "Could not peel symbolic local reference to its ID", + ))) + }) + .map(crate::Id::detach) + { + Ok(local_id) => { + let remote_id = match remote_id { + Some(id) => id, + None => { + // we don't allow to go back to unborn state if there is a local reference already present. + // Note that we will be changing it to a symbolic reference just fine. + updates.push(Mode::RejectedToReplaceWithUnborn.into()); + continue; + } + }; + let (mode, reflog_message) = if local_id == remote_id { + (Mode::NoChangeNeeded, "no update will be performed") + } else if let Some(gix_ref::Category::Tag) = existing.name().category() { + if spec.allow_non_fast_forward() { + (Mode::Forced, "updating tag") + } else { + updates.push(Mode::RejectedTagUpdate.into()); + continue; + } } else { - updates.push(Mode::RejectedTagUpdate.into()); - continue; - } - } else { - let mut force = spec.allow_non_fast_forward(); - let is_fast_forward = match dry_run { - fetch::DryRun::No => { - let ancestors = repo - .find_object(local_id)? + let mut force = spec.allow_non_fast_forward(); + let is_fast_forward = match dry_run { + fetch::DryRun::No => { + let ancestors = repo + .find_object(local_id) + .or_raise(|| { + gix_error::message( + "Could not find local commit for fast-forward ancestor check", + ) + })? .try_into_commit() .map_err(|_| ()) .and_then(|c| c.committer().map(|a| a.seconds()).map_err(|_| ())) @@ -177,74 +181,84 @@ pub(crate) fn update( ) .map_err(|_| ()) }); - match ancestors { - Ok(mut ancestors) => { - ancestors.any(|cid| cid.is_ok_and(|c| c.id == local_id)) - } - Err(_) => { - force = true; - false + match ancestors { + Ok(mut ancestors) => { + ancestors.any(|cid| cid.is_ok_and(|c| c.id == local_id)) + } + Err(_) => { + force = true; + false + } } } + fetch::DryRun::Yes => true, + }; + if is_fast_forward { + ( + Mode::FastForward, + matches!(dry_run, fetch::DryRun::Yes) + .then(|| "fast-forward (guessed in dry-run)") + .unwrap_or("fast-forward"), + ) + } else if force { + (Mode::Forced, "forced-update") + } else { + updates.push(Mode::RejectedNonFastForward.into()); + continue; } - fetch::DryRun::Yes => true, }; - if is_fast_forward { - ( - Mode::FastForward, - matches!(dry_run, fetch::DryRun::Yes) - .then(|| "fast-forward (guessed in dry-run)") - .unwrap_or("fast-forward"), - ) - } else if force { - (Mode::Forced, "forced-update") - } else { - updates.push(Mode::RejectedNonFastForward.into()); - continue; - } - }; - ( - mode, - reflog_message, - existing.name().to_owned(), - PreviousValue::MustExistAndMatch(existing.target().into_owned()), - ) + ( + mode, + reflog_message, + existing.name().to_owned(), + PreviousValue::MustExistAndMatch(existing.target().into_owned()), + ) + } + Err(err) + if matches!( + err.downcast_any_ref::(), + Some(gix_ref::peel::to_id::Error::FollowToObject( + gix_ref::peel::to_object::Error::Follow(_) + )) + ) => + { + // An unborn reference, always allow it to be changed to whatever the remote wants. + ( + if existing.target().try_name().map(gix_ref::FullNameRef::as_bstr) + == remote.as_target() + { + Mode::NoChangeNeeded + } else { + Mode::Forced + }, + "change unborn ref", + existing.name().to_owned(), + PreviousValue::MustExistAndMatch(existing.target().into_owned()), + ) + } + Err(err) => return Err(err), } - Err(crate::reference::peel::Error::ToId(gix_ref::peel::to_id::Error::FollowToObject( - gix_ref::peel::to_object::Error::Follow(_), - ))) => { - // An unborn reference, always allow it to be changed to whatever the remote wants. - ( - if existing.target().try_name().map(gix_ref::FullNameRef::as_bstr) - == remote.as_target() - { - Mode::NoChangeNeeded - } else { - Mode::Forced - }, - "change unborn ref", - existing.name().to_owned(), - PreviousValue::MustExistAndMatch(existing.target().into_owned()), + } + None => { + let name = gix_ref::FullName::try_from(name).or_raise(|| { + gix_error::message( + "A remote reference had a name that wasn't considered valid. \ + Corrupt remote repo or insufficient checks on remote?", ) - } - Err(err) => return Err(err.into()), + })?; + let reflog_msg = match name.category() { + Some(gix_ref::Category::Tag) => "storing tag", + Some(gix_ref::Category::LocalBranch) => "storing head", + _ => "storing ref", + }; + ( + Mode::New, + reflog_msg, + name, + PreviousValue::ExistingMustMatch(new_value_by_remote(remote)?), + ) } - } - None => { - let name: gix_ref::FullName = name.try_into()?; - let reflog_msg = match name.category() { - Some(gix_ref::Category::Tag) => "storing tag", - Some(gix_ref::Category::LocalBranch) => "storing head", - _ => "storing ref", - }; - ( - Mode::New, - reflog_msg, - name, - PreviousValue::ExistingMustMatch(new_value_by_remote(remote)?), - ) - } - }; + }; let new = new_value_by_remote(remote)?; let type_change = match (&previous_value, &new) { @@ -316,10 +330,9 @@ pub(crate) fn update( let edits = match dry_run { fetch::DryRun::No => { let _span = gix_trace::detail!("apply", edits = edits.len()); - let (file_lock_fail, packed_refs_lock_fail) = repo - .config - .lock_timeout() - .map_err(crate::reference::edit::Error::from)?; + let (file_lock_fail, packed_refs_lock_fail) = repo.config.lock_timeout().or_raise(|| { + gix_error::message("Failed to update references to their new position to match their remote locations") + })?; repo.refs .transaction() .packed_refs( @@ -330,9 +343,25 @@ pub(crate) fn update( } ) .prepare(edits, file_lock_fail, packed_refs_lock_fail) - .map_err(crate::reference::edit::Error::from)? - .commit(repo.committer().transpose().map_err(|err| update::Error::EditReferences(crate::reference::edit::Error::ParseCommitterTime(err)))?) - .map_err(crate::reference::edit::Error::from)? + .or_raise(|| { + gix_error::message( + "Failed to update references to their new position to match their remote locations", + ) + })? + .commit( + repo.committer() + .transpose() + .or_raise(|| { + gix_error::message( + "Failed to update references to their new position to match their remote locations", + ) + })?, + ) + .or_raise(|| { + gix_error::message( + "Failed to update references to their new position to match their remote locations", + ) + })? } fetch::DryRun::Yes => edits, }; @@ -397,7 +426,12 @@ fn new_value_by_remote(remote: &Source) -> Result { match remote_id { Some(desired_id) => Target::Object(desired_id.to_owned()), // Unborn branches we create as such, with the location they point to on the remote which helps mirroring. - None => Target::Symbolic(target.try_into()?), + None => Target::Symbolic(gix_ref::FullName::try_from(target).or_raise(|| { + gix_error::message( + "A remote reference had a name that wasn't considered valid. \ + Corrupt remote repo or insufficient checks on remote?", + ) + })?), } } else { Target::Object(remote_id.expect("unborn case handled earlier").to_owned()) diff --git a/gix/src/remote/connection/fetch/update_refs/tests.rs b/gix/src/remote/connection/fetch/update_refs/tests.rs index b7a029a11c0..8c239dc0312 100644 --- a/gix/src/remote/connection/fetch/update_refs/tests.rs +++ b/gix/src/remote/connection/fetch/update_refs/tests.rs @@ -341,6 +341,14 @@ mod update { #[test] fn unborn_remote_branches_can_update_local_unborn_branches() -> Result { let repo = named_repo("unborn"); + let peel_err = repo + .find_reference("refs/heads/existing-unborn-symbolic")? + .peel_to_id() + .expect_err("the local symbolic reference points to a missing branch"); + assert!( + peel_err.downcast_any_ref::().is_some(), + "chain mode retains the typed peel error used by update recovery" + ); let (mappings, specs) = mapping_from_spec("HEAD:refs/heads/existing-unborn-symbolic", &repo); assert_eq!(mappings.len(), 1); let out = fetch::refs::update( @@ -510,6 +518,50 @@ mod update { Ok(()) } + #[test] + fn symbolic_tags_with_missing_objects_are_not_unborn() -> Result { + let (repo, _tmp) = repo_rw("two-origins"); + let worktree = repo.workdir().expect("fixture has a worktree"); + let missing_id = hex_to_id(&"1".repeat(repo.object_hash().len_in_hex())); + gix_testtools::git(worktree, "symbolic-ref refs/tags/broken refs/tags/missing")?; + std::fs::write(repo.git_dir().join("refs/tags/missing"), format!("{missing_id}\n"))?; + + let git_error = gix_testtools::git(worktree, "fetch --no-tags origin refs/heads/main:refs/tags/broken") + .expect_err("Git rejects replacing the broken tag without force"); + assert!( + git_error.to_string().contains("bad object refs/tags/broken"), + "Git rejects the fetch because of the broken tag: {git_error}" + ); + + let (mappings, specs) = mapping_from_spec("refs/heads/main:refs/tags/broken", &repo); + for dry_run in [fetch::DryRun::Yes, fetch::DryRun::No] { + let err = fetch::refs::update( + &repo, + prefixed("action"), + &mappings, + &specs, + &[], + fetch::Tags::None, + dry_run, + fetch::WritePackedRefs::Never, + ) + .expect_err("a missing object is a peeling failure, not an unborn reference"); + assert!( + matches!( + err.downcast_any_ref::(), + Some(gix_ref::peel::to_id::Error::NotFound { oid, .. }) if *oid == missing_id + ), + "the missing-object peeling error is propagated: {err:?}" + ); + } + assert_eq!( + repo.find_reference("refs/tags/broken")?.target().into_owned(), + Target::Symbolic("refs/tags/missing".try_into()?), + "the symbolic tag is preserved" + ); + Ok(()) + } + #[test] fn local_symbolic_refs_can_be_overwritten() { let repo = repo("two-origins"); diff --git a/gix/src/remote/connection/fetch/update_refs/update.rs b/gix/src/remote/connection/fetch/update_refs/update.rs index bda67480a15..df39ea51aa0 100644 --- a/gix/src/remote/connection/fetch/update_refs/update.rs +++ b/gix/src/remote/connection/fetch/update_refs/update.rs @@ -4,30 +4,7 @@ use crate::remote::fetch; mod error { /// The error returned when updating references. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - FindReference(#[from] crate::reference::find::Error), - #[error( - "A remote reference had a name that wasn't considered valid. Corrupt remote repo or insufficient checks on remote?" - )] - InvalidRefName(#[from] gix_validate::reference::name::Error), - #[error("Failed to update references to their new position to match their remote locations")] - EditReferences(#[from] crate::reference::edit::Error), - #[error("Failed to read or iterate worktree dir")] - WorktreeListing(#[from] std::io::Error), - #[error("Could not open worktree repository")] - OpenWorktreeRepo(#[from] crate::open::Error), - #[error("Could not find local commit for fast-forward ancestor check")] - FindCommit(#[from] crate::object::find::existing::Error), - #[error("Could not peel symbolic local reference to its ID")] - PeelToId(#[from] crate::reference::peel::Error), - #[error("Failed to follow a symbolic reference to assure worktree isn't affected")] - FollowSymref(#[from] gix_ref::file::find::existing::Error), - #[error(transparent)] - FindObject(#[from] crate::object::find::Error), - } + pub type Error = gix_error::Error; } pub use error::Error; diff --git a/gix/src/remote/connection/ref_map.rs b/gix/src/remote/connection/ref_map.rs index ba7a2713337..9ff60b126c5 100644 --- a/gix/src/remote/connection/ref_map.rs +++ b/gix/src/remote/connection/ref_map.rs @@ -1,44 +1,14 @@ +use gix_error::{ErrorExt, ResultExt}; use gix_features::progress::Progress; #[cfg(feature = "async-network-client")] use gix_transport::client::async_io::Transport; #[cfg(feature = "blocking-network-client")] use gix_transport::client::blocking_io::Transport; -use crate::{ - bstr::BString, - remote::{Connection, connection::ConnectionDetached, fetch}, -}; +use crate::remote::{Connection, connection::ConnectionDetached, fetch}; /// The error returned by [`Connection::ref_map()`]. -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] -pub enum Error { - #[error(transparent)] - InitRefMap(gix_error::Error), - #[error("Failed to configure the transport before connecting to {url:?}")] - GatherTransportConfig { - url: BString, - source: crate::config::transport::Error, - }, - #[error("Failed to configure the transport layer")] - ConfigureTransport(#[from] gix_error::Error), - #[error(transparent)] - Handshake(gix_error::Error), - #[error(transparent)] - Transport(#[from] gix_protocol::transport::client::Error), - #[error(transparent)] - ConfigureCredentials(#[from] crate::config::credential_helpers::Error), -} - -impl Error { - pub(crate) fn can_retry(&self) -> bool { - match self { - Error::Transport(err) => gix_error::can_retry_lenient(err), - Error::Handshake(err) => err.can_retry_lenient(), - _ => false, - } - } -} +pub type Error = gix_error::Error; /// For use in [`Connection::ref_map()`]. #[derive(Debug, Clone)] @@ -145,16 +115,17 @@ where if self.transport_options.is_none() { self.transport_options = repo .transport_options(url.as_ref(), self.remote.name().map(crate::remote::Name::as_bstr)) - .map_err(|err| Error::GatherTransportConfig { - source: err, - url: url.into_owned(), + .map_err(|err| { + gix_error::Error::from(err.and_raise(gix_error::CorruptionError::new(format!( + "Failed to configure the transport before connecting to {url:?}" + )))) })?; } if let Some(config) = self.transport_options.as_ref() { self.transport .inner .configure(&**config) - .map_err(gix_error::Exn::into_error)?; + .or_raise(|| gix_error::message("Failed to configure the transport layer"))?; } let mut handshake = gix_protocol::handshake( &mut self.transport.inner, @@ -164,8 +135,7 @@ where &mut progress, ) .await - .map_err(gix_error::Exn::into_error) - .map_err(Error::Handshake)?; + .map_err(gix_error::Error::from)?; let context = fetch::refmap::init::Context { fetch_refspecs: self.remote.fetch_specs.clone(), @@ -178,21 +148,18 @@ where prefix_from_spec_as_filter_on_remote, context, ) - .map_err(gix_error::Exn::into_error) - .map_err(Error::InitRefMap)?; + .map_err(gix_error::Exn::into_error)?; #[cfg(feature = "async-network-client")] let ref_map = fetch_refmap .fetch_async(progress, &mut self.transport.inner, self.trace) .await - .map_err(gix_error::Exn::into_error) - .map_err(Error::InitRefMap)?; + .map_err(gix_error::Exn::into_error)?; #[cfg(feature = "blocking-network-client")] let ref_map = fetch_refmap .fetch_blocking(progress, &mut self.transport.inner, self.trace) - .map_err(gix_error::Exn::into_error) - .map_err(Error::InitRefMap)?; + .map_err(gix_error::Exn::into_error)?; self.handshake = Some(handshake); Ok(ref_map) diff --git a/gix/src/remote/errors.rs b/gix/src/remote/errors.rs index 6dbb3e38cb7..acc2bf9e362 100644 --- a/gix/src/remote/errors.rs +++ b/gix/src/remote/errors.rs @@ -1,62 +1,17 @@ /// pub mod find { - use crate::{bstr::BString, config, remote}; - /// The error returned by [`Repository::find_remote(…)`](crate::Repository::find_remote()). - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error("The value for 'remote..tagOpt` is invalid and must either be '--tags' or '--no-tags'")] - TagOpt(#[from] config::key::GenericErrorWithValue), - #[error("{kind} ref-spec under `remote.{remote_name}` was invalid")] - RefSpec { - kind: &'static str, - remote_name: BString, - source: config::refspec::Error, - }, - #[error("The {kind} url under `remote.{remote_name}` was invalid")] - Url { - kind: &'static str, - remote_name: BString, - source: config::url::Error, - }, - #[error(transparent)] - Init(#[from] remote::init::Error), - } + pub type Error = gix_error::Error; /// pub mod existing { - use crate::bstr::BString; - /// The error returned by [`Repository::find_remote(…)`](crate::Repository::find_remote()). - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - Find(#[from] super::Error), - #[error("remote name could not be parsed as URL")] - UrlParse(#[from] gix_error::Error), - #[error("The remote named {name:?} did not exist")] - NotFound { name: BString }, - } + pub type Error = gix_error::Error; } /// pub mod for_fetch { /// The error returned by [`Repository::find_fetch_remote(…)`](crate::Repository::find_fetch_remote()). - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - FindExisting(#[from] super::existing::Error), - #[error(transparent)] - FindExistingReferences(#[from] crate::reference::find::existing::Error), - #[error("Could not initialize a URL remote")] - Init(#[from] crate::remote::init::Error), - #[error("remote name could not be parsed as URL")] - UrlParse(#[from] gix_error::Error), - #[error("No configured remote could be found, or too many were available")] - ExactlyOneRemoteNotAvailable, - } + pub type Error = gix_error::Error; } } diff --git a/gix/src/remote/init.rs b/gix/src/remote/init.rs index 28521df0ac7..5968cf759f7 100644 --- a/gix/src/remote/init.rs +++ b/gix/src/remote/init.rs @@ -1,25 +1,11 @@ +use gix_error::ResultExt; + use gix_refspec::RefSpec; use crate::{Remote, Repository, config, remote}; -mod error { - use crate::bstr::BString; - - /// The error returned by [`Repository::remote_at(…)`][crate::Repository::remote_at()]. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - Url(#[from] gix_error::Error), - #[error("The rewritten {kind} url {rewritten_url:?} failed to parse")] - RewrittenUrlInvalid { - kind: &'static str, - rewritten_url: BString, - source: gix_error::Error, - }, - } -} -pub use error::Error; +/// The error returned by [`Repository::remote_at(…)`][crate::Repository::remote_at()]. +pub type Error = gix_error::Error; use crate::bstr::BString; @@ -71,11 +57,7 @@ impl<'repo> Remote<'repo> { Url: TryInto, E: std::error::Error + Send + Sync + 'static, { - Self::from_fetch_url_inner( - url.try_into().map_err(gix_error::Error::from_error)?, - should_rewrite_urls, - repo, - ) + Self::from_fetch_url_inner(url.try_into().or_erased()?, should_rewrite_urls, repo) } fn from_fetch_url_inner( @@ -114,13 +96,14 @@ pub(crate) fn rewrite_url( .url_rewrite() .longest(url, direction) .map(|url| { - gix_url::parse(&url).map_err(|err| Error::RewrittenUrlInvalid { - kind: match error_kind { + gix_url::parse(&url).map_err(|err| { + let kind = match error_kind { remote::Direction::Fetch => "fetch", remote::Direction::Push => "push", - }, - source: err.into_error(), - rewritten_url: url, + }; + gix_error::Error::from( + err.raise(gix_error::message!("The rewritten {kind} url {url:?} failed to parse")), + ) }) }) .transpose() diff --git a/gix/src/remote/name.rs b/gix/src/remote/name.rs index bccdd3665ef..74d273a1974 100644 --- a/gix/src/remote/name.rs +++ b/gix/src/remote/name.rs @@ -4,13 +4,7 @@ use super::Name; use crate::bstr::{BStr, BString, ByteSlice, ByteVec}; /// The error returned by [validated()]. -#[derive(Debug, thiserror::Error)] -#[error("remote names must be valid within refspecs for fetching: {name:?}")] -#[expect(missing_docs)] -pub struct Error { - pub source: gix_error::Error, - pub name: BString, -} +pub type Error = gix_error::Error; /// Return `name` if it is valid as symbolic remote name. /// @@ -22,10 +16,12 @@ pub fn validated(name: impl Into) -> Result { gix_refspec::parse::Operation::Fetch, ) { Ok(_) => Ok(name), - Err(err) => Err(Error { - source: err.into_error(), - name, - }), + Err(err) => Err(gix_error::Error::from(err.raise( + gix_error::ValidationError::new_with_input( + "remote names must be valid within refspecs for fetching", + name.clone(), + ), + ))), } } diff --git a/gix/src/remote/save.rs b/gix/src/remote/save.rs index 23d28038e61..815d3b6131a 100644 --- a/gix/src/remote/save.rs +++ b/gix/src/remote/save.rs @@ -1,27 +1,13 @@ +use gix_error::ResultExt; + use crate::{Remote, bstr::BStr, config, remote}; use gix_utils::AsBStr; /// The error returned by [`Remote::save_to()`]. -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] -pub enum Error { - #[error("The remote pointing to {} is anonymous and can't be saved.", url.to_bstring())] - NameMissing { url: gix_url::Url }, - #[error(transparent)] - Config(#[from] gix_error::ValidationError), -} +pub type Error = gix_error::Error; /// The error returned by [`Remote::save_as_to()`]. -/// -/// Note that this type should rather be in the `as` module, but cannot be as it's part of the Rust syntax. -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] -pub enum AsError { - #[error(transparent)] - Save(#[from] Error), - #[error(transparent)] - Name(#[from] crate::remote::name::Error), -} +pub type AsError = gix_error::Error; /// Serialize into git-config. impl Remote<'_> { @@ -30,18 +16,17 @@ impl Remote<'_> { /// Note that all sections named `remote ""` will be cleared of all values we are about to write, /// and the last `remote ""` section will be containing all relevant values so that reloading the remote /// from `config` would yield the same in-memory state. - #[expect( - clippy::result_large_err, - reason = "will be removed once `gix-error` is used consistently" - )] pub fn save_to(&self, config: &mut gix_config::File) -> Result<(), Error> { - let name = self.name().ok_or_else(|| Error::NameMissing { - url: self + let name = self.name().ok_or_else(|| { + let url = self .urls .first() .or_else(|| self.push_urls.first()) .expect("one url is always set") - .to_owned(), + .to_bstring(); + gix_error::Error::from_error(gix_error::message!( + "The remote pointing to {url} is anonymous and can't be saved." + )) })?; let target_meta = config.meta().clone(); let mut needs_url_reset = false; @@ -102,26 +87,30 @@ impl Remote<'_> { .expect("section name is validated and 'remote' is acceptable") }; if needs_url_reset { - section.push(config::tree::Remote::URL.name, "")?; + section.push(config::tree::Remote::URL.name, "").or_erased()?; } for url in &self.urls { - section.push("url", url.to_bstring())?; + section.push("url", url.to_bstring()).or_erased()?; } if needs_push_url_reset { - section.push(config::tree::Remote::PUSH_URL.name, "")?; + section.push(config::tree::Remote::PUSH_URL.name, "").or_erased()?; } for url in &self.push_urls { - section.push("pushurl", url.to_bstring())?; + section.push("pushurl", url.to_bstring()).or_erased()?; } if self.fetch_tags != Default::default() { - section.push( - config::tree::Remote::TAG_OPT.name, - BStr::new(match self.fetch_tags { - remote::fetch::Tags::All => "--tags", - remote::fetch::Tags::None => "--no-tags", - remote::fetch::Tags::Included => unreachable!("BUG: the default shouldn't be written and we try"), - }), - )?; + section + .push( + config::tree::Remote::TAG_OPT.name, + BStr::new(match self.fetch_tags { + remote::fetch::Tags::All => "--tags", + remote::fetch::Tags::None => "--no-tags", + remote::fetch::Tags::Included => { + unreachable!("BUG: the default shouldn't be written and we try") + } + }), + ) + .or_erased()?; } for (key, spec) in self .fetch_specs @@ -129,7 +118,7 @@ impl Remote<'_> { .map(|spec| ("fetch", spec)) .chain(self.push_specs.iter().map(|spec| ("push", spec))) { - section.push(key, spec.to_ref().to_bstring())?; + section.push(key, spec.to_ref().to_bstring()).or_erased()?; } Ok(()) } @@ -139,17 +128,12 @@ impl Remote<'_> { /// Note that this sets a name for anonymous remotes, but overwrites the name for those who were named before. /// If this name is different from the current one, the git configuration will still contain the previous name, /// and the caller should account for that. - #[expect( - clippy::result_large_err, - reason = "will be removed once `gix-error` is used consistently" - )] pub fn save_as_to(&mut self, name: impl AsBStr, config: &mut gix_config::File) -> Result<(), AsError> { let name = crate::remote::name::validated(name.as_bstr().to_owned())?; let prev_name = self.name.take(); self.name = Some(name.into()); - self.save_to(config).map_err(|err| { + self.save_to(config).inspect_err(|_| { self.name = prev_name; - err.into() }) } } diff --git a/gix/src/remote/url/scheme_permission.rs b/gix/src/remote/url/scheme_permission.rs index 103c51ea8ff..f508db7fec2 100644 --- a/gix/src/remote/url/scheme_permission.rs +++ b/gix/src/remote/url/scheme_permission.rs @@ -1,8 +1,9 @@ +use gix_error::ResultExt; + use std::collections::BTreeMap; use crate::{ bstr::{BStr, BString, ByteSlice}, - config, config::tree::{Protocol, gitoxide}, }; @@ -18,15 +19,7 @@ pub enum Allow { } /// The error returned when obtaining transport permissions from configuration. -#[derive(Debug, thiserror::Error)] -pub enum Error { - /// The value of `protocol[.].allow` was invalid. - #[error(transparent)] - Allow(#[from] config::protocol::allow::Error), - /// `GIT_PROTOCOL_FROM_USER` was not a valid Git boolean. - #[error(transparent)] - ProtocolFromUser(#[from] config::boolean::Error), -} +pub type Error = gix_error::Error; impl Allow { /// Return true if we represent something like 'allow == true'. @@ -107,7 +100,8 @@ impl SchemePermission { }; let user_allowed = gitoxide::Allow::PROTOCOL_FROM_USER - .enrich_error(config.boolean_filter(gitoxide::Allow::PROTOCOL_FROM_USER, &mut filter))?; + .enrich_error(config.boolean_filter(gitoxide::Allow::PROTOCOL_FROM_USER, &mut filter)) + .or_erased()?; Ok(SchemePermission { allow, allow_per_scheme, diff --git a/gix/src/repository/attributes.rs b/gix/src/repository/attributes.rs index ea62cea328e..5986b9030e0 100644 --- a/gix/src/repository/attributes.rs +++ b/gix/src/repository/attributes.rs @@ -1,15 +1,10 @@ //! exclude information +use gix_error::ResultExt; + use crate::{AttributeStack, Repository, config}; /// The error returned by [`Repository::attributes()`]. -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] -pub enum Error { - #[error(transparent)] - ConfigureAttributes(#[from] config::attribute_stack::Error), - #[error(transparent)] - ConfigureExcludes(#[from] config::exclude_stack::Error), -} +pub type Error = gix_error::Error; impl Repository { /// Configure a file-system cache for accessing git attributes *and* excludes on a per-path basis. @@ -38,14 +33,18 @@ impl Repository { } else { gix_glob::pattern::Case::Sensitive }; - let (attributes, mut buf) = self.config.assemble_attribute_globals( - self.common_dir(), - attributes_source, - self.options.permissions.attributes, - )?; - let ignore = - self.config - .assemble_exclude_globals(self.common_dir(), exclude_overrides, ignore_source, &mut buf)?; + let (attributes, mut buf) = self + .config + .assemble_attribute_globals( + self.common_dir(), + attributes_source, + self.options.permissions.attributes, + ) + .or_erased()?; + let ignore = self + .config + .assemble_exclude_globals(self.common_dir(), exclude_overrides, ignore_source, &mut buf) + .or_erased()?; let state = gix_worktree::stack::State::AttributesAndIgnoreStack { attributes, ignore }; let attribute_list = state.id_mappings_from_index(index, index.path_backing(), case); Ok(AttributeStack::new( diff --git a/gix/src/repository/blame.rs b/gix/src/repository/blame.rs index f0125cfdeac..35f5b33ec7c 100644 --- a/gix/src/repository/blame.rs +++ b/gix/src/repository/blame.rs @@ -1,3 +1,5 @@ +use gix_error::ResultExt; + use gix_blame::Start; use gix_hash::ObjectId; use gix_ref::bstr::BStr; @@ -27,7 +29,7 @@ impl Repository { } = options; let diff_algorithm = match diff_algorithm { Some(diff_algorithm) => diff_algorithm, - None => self.diff_algorithm()?, + None => self.diff_algorithm().or_erased()?, }; let options = gix_blame::Options { @@ -46,7 +48,7 @@ impl Repository { file_path, options, ) - .map_err(|err| blame_file::Error::Blame(err.into_error()))?; + .map_err(gix_error::Exn::into_error)?; Ok(outcome) } diff --git a/gix/src/repository/branch.rs b/gix/src/repository/branch.rs index a60b1cab41d..ac5ba3a1087 100644 --- a/gix/src/repository/branch.rs +++ b/gix/src/repository/branch.rs @@ -1,3 +1,4 @@ +use gix_error::{ErrorExt, ResultExt}; use gix_ref::{ Category, FullName, transaction::{PreviousValue, RefEdit}, @@ -9,58 +10,55 @@ pub mod delete { use gix_ref::FullName; + /// A branch-deletion rejection because the branch is checked out in a worktree. + #[derive(Debug)] + pub struct CheckedOutError { + /// The local branch whose checkout prevents deletion. + pub name: FullName, + /// The worktree directories in which the branch is checked out. + pub worktree_dirs: Vec, + } + + impl std::fmt::Display for CheckedOutError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "The local branch {:?} is checked out in {:?}", + self.name, self.worktree_dirs + ) + } + } + + impl std::error::Error for CheckedOutError {} + /// A configuration-cleanup failure after all requested references were made absent. - #[derive(Debug, thiserror::Error)] - pub enum CleanupError { - /// The updated configuration could not be written to its lock file; the existing config file is unchanged. - #[error("Could not write the updated local configuration")] - Write(#[source] std::io::Error), - /// The lock file containing the updated configuration could not replace the existing config file. - #[error("Could not commit the updated local configuration")] - Commit(#[source] std::io::Error), + #[derive(Debug)] + pub struct CleanupError { + /// Every requested reference name, including names which were already missing before the call. + /// + /// All of these references and their reflogs are guaranteed to be absent. Their `branch.` configuration + /// sections may remain. + pub references: Vec, + /// The branches actually deleted, as would have been returned on success. + /// + /// Names are sorted and deduplicated; branches missing when locked for deletion are excluded. + pub deleted: Vec, } - /// The error returned by [`Repository::delete_local_branches()`][crate::Repository::delete_local_branches()]. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error("{name:?} is not a local branch")] - NotLocal { name: FullName }, - - #[error("The local branch {name:?} is checked out in {worktree_dirs:?}")] - CheckedOut { - name: FullName, - worktree_dirs: Vec, - }, - #[error("Failed to read or iterate worktree directories")] - WorktreeListing(#[source] std::io::Error), - #[error("Could not open a worktree repository")] - OpenWorktreeRepo(#[source] crate::open::Error), - #[error("Failed to follow a symbolic reference while inspecting worktrees")] - FollowSymref(#[source] gix_ref::file::find::existing::Error), - #[error("Could not acquire the local configuration lock")] - ConfigLock(#[source] gix_error::Error), - #[error("Could not read the local configuration")] - ConfigRead(#[source] gix_config::file::init::from_paths::Error), - #[error("Could not delete local branches")] - EditReferences(#[from] crate::reference::edit::Error), - /// Reference deletion succeeded, but configuration cleanup failed. - #[error("References {references:?} are absent, but local branch configuration cleanup failed")] - Cleanup { - /// Every requested reference name, including names which were already missing before the call. - /// - /// All of these references and their reflogs are guaranteed to be absent. Their `branch.` configuration - /// sections may remain; inspect `source` to determine which cleanup phase failed. - references: Vec, - /// The branches actually deleted, as would have been returned on success. - /// - /// Names are sorted and deduplicated; branches missing when locked for deletion are excluded. - deleted: Vec, - /// The configuration cleanup phase that failed. - #[source] - source: CleanupError, - }, + impl std::fmt::Display for CleanupError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "References {:?} are absent, but local branch configuration cleanup failed", + self.references + ) + } } + + impl std::error::Error for CleanupError {} + + /// The error returned by [`Repository::delete_local_branches()`][crate::Repository::delete_local_branches()]. + pub type Error = gix_error::Error; } impl crate::Repository { @@ -70,15 +68,18 @@ impl crate::Repository { /// any name belongs to another reference category or is checked out in any worktree. Missing branches are accepted so any /// associated local configuration is still removed. **It deliberately performs no merged-state check**. /// + /// A checked-out branch rejection contains [`delete::CheckedOutError`] in its error chain, identifying the branch and + /// the worktree directories that prevented deletion. + /// /// On success, every requested reference and its reflog is absent, and every matching `branch.` section has been /// removed from the local configuration. Return the sorted, deduplicated names of branches that existed when locked for /// deletion. Missing branches are omitted from the returned vector, but their configuration is still removed. /// /// Reference deletion and configuration cleanup cannot be one atomic transaction. Once reference deletion succeeds, a - /// configuration write or commit failure is returned as [`delete::Error::Cleanup`]. Its `references` field contains - /// every requested name—including names which were missing initially—and guarantees only that their references and reflogs - /// are absent. Its `deleted` field contains the branches actually deleted, just as in the success case. - /// See [`delete::CleanupError`] to determine whether the on-disk configuration was updated. + /// configuration write or commit failure contains [`delete::CleanupError`] in its error chain. Its `references` field + /// contains every requested name—including names which were missing initially—and guarantees only that their references and + /// reflogs are absent. Its `deleted` field contains the branches actually deleted, just as in the success case. + /// The remaining error chain identifies the failed cleanup phase. pub fn delete_local_branches( &mut self, names: impl IntoIterator, @@ -92,21 +93,19 @@ impl crate::Repository { for name in &names { if name.category_and_short_name().map(|(category, _)| category) != Some(Category::LocalBranch) { - return Err(delete::Error::NotLocal { name: name.clone() }); + return Err(gix_error::message!("{name:?} is not a local branch").raise().into()); } } - let checked_out = self.checked_out_branches().map_err(|err| match err { - super::worktree::CheckedOutBranchesError::WorktreeListing(err) => delete::Error::WorktreeListing(err), - super::worktree::CheckedOutBranchesError::OpenWorktreeRepo(err) => delete::Error::OpenWorktreeRepo(err), - super::worktree::CheckedOutBranchesError::FollowSymref(err) => delete::Error::FollowSymref(err), - })?; + let checked_out = self.checked_out_branches()?; for name in &names { if let Some(worktree_dirs) = checked_out.get(name) { - return Err(delete::Error::CheckedOut { + return Err(delete::CheckedOutError { name: name.clone(), worktree_dirs: worktree_dirs.clone(), - }); + } + .raise() + .into()); } } @@ -118,7 +117,7 @@ impl crate::Repository { let config_path = self.common_dir().join("config"); let mut config_lock = gix_lock::File::acquire_to_update_resource(&config_path, gix_lock::acquire::Fail::Immediately, None) - .map_err(|err| delete::Error::ConfigLock(err.into_error()))?; + .or_raise(|| gix_error::message("Could not acquire the local configuration lock"))?; let mut config = match gix_config::File::from_path_no_includes(config_path.clone(), gix_config::Source::Local) { Ok(config) => Some(config), Err(err) @@ -128,14 +127,19 @@ impl crate::Repository { { None } - Err(err) => return Err(delete::Error::ConfigRead(err)), + Err(err) => { + return Err(err + .raise(gix_error::message("Could not read the local configuration")) + .into()); + } }; let removed_config = config .as_mut() .is_some_and(|config| remove_branch_config(config, &names, |_| true)); let deleted: Vec<_> = self - .edit_references(edits)? + .edit_references(edits) + .or_raise(|| gix_error::message("Could not delete local branches"))? .into_iter() .filter_map(|edit| edit.change.previous_value().is_some().then_some(edit.name)) .collect(); @@ -144,16 +148,19 @@ impl crate::Repository { let config = config.expect("configuration was present when sections were removed"); config .write_to(&mut config_lock) - .map_err(|source| delete::Error::Cleanup { + .or_raise(|| gix_error::message("Could not write the updated local configuration")) + .or_raise(|| delete::CleanupError { + references: names.clone(), + deleted: deleted.clone(), + })?; + config_lock + .commit() + .map_err(|err| err.error) + .or_raise(|| gix_error::message("Could not commit the updated local configuration")) + .or_raise(|| delete::CleanupError { references: names.clone(), deleted: deleted.clone(), - source: delete::CleanupError::Write(source), })?; - config_lock.commit().map_err(|err| delete::Error::Cleanup { - references: names.clone(), - deleted: deleted.clone(), - source: delete::CleanupError::Commit(err.error), - })?; remove_branch_config( gix_features::threading::OwnShared::make_mut(&mut self.config.resolved), &names, diff --git a/gix/src/repository/config/branch.rs b/gix/src/repository/config/branch.rs index 4c564cb0704..de1ddc2b3f1 100644 --- a/gix/src/repository/config/branch.rs +++ b/gix/src/repository/config/branch.rs @@ -1,5 +1,6 @@ use std::collections::BTreeSet; +use gix_error::{ErrorExt, ResultExt}; use gix_ref::{FullName, FullNameRef}; use crate::{ @@ -58,13 +59,17 @@ impl crate::Repository { } else { gix_ref::Category::LocalBranch.to_full_name(name.as_bstr()) } - .map_err(Into::into) + .map_err(|err| { + gix_error::Error::from(err.and_raise(gix_error::ValidationError::new( + "The configured name of the remote ref to merge wasn't valid", + ))) + }) }) } remote::Direction::Push => { let remote = match self.branch_remote(name.shorten(), direction)? { Ok(r) => r, - Err(err) => return Some(Err(err.into())), + Err(err) => return Some(Err(err)), }; if remote.push_specs.is_empty() { let push_default = @@ -92,7 +97,7 @@ impl crate::Repository { } } else { matching_remote(name, remote.push_specs.iter(), self.object_hash()) - .map(|res| res.map_err(Into::into)) + .map(|res| -> Result<_, gix_error::Error> { Ok(res.or_erased()?) }) } } } @@ -123,18 +128,31 @@ impl crate::Repository { ) -> Option> { let remote_ref = match self.branch_remote_ref_name(name, direction)? { Ok(r) => r, - Err(err) => return Some(Err(err.into())), + Err(err) => { + return Some(Err(gix_error::Error::from(err.and_raise(gix_error::message( + "Could not get the remote reference to translate into the local tracking branch", + ))))); + } }; let remote = match self.branch_remote(name.shorten(), direction)? { Ok(r) => r, - Err(err) => return Some(Err(err.into())), + Err(err) => { + return Some(Err(gix_error::Error::from(err.and_raise(gix_error::message( + "Couldn't find remote to obtain fetch-specs for mapping to the tracking reference", + ))))); + } }; if remote.fetch_specs.is_empty() { return None; } - matching_remote(remote_ref.as_ref(), remote.fetch_specs.iter(), self.object_hash()) - .map(|res| res.map_err(Into::into)) + matching_remote(remote_ref.as_ref(), remote.fetch_specs.iter(), self.object_hash()).map(|res| { + res.map_err(|err| { + gix_error::Error::from( + err.and_raise(gix_error::message("The name of the tracking reference was invalid")), + ) + }) + }) } /// Given a local `tracking_branch` name, find the remote that maps to it along with the name of the branch on @@ -147,11 +165,11 @@ impl crate::Repository { &self, tracking_branch: &FullNameRef, ) -> Result)>, upstream_branch_and_remote_name_for_tracking_branch::Error> { - use upstream_branch_and_remote_name_for_tracking_branch::Error; if tracking_branch.category() != Some(gix_ref::Category::RemoteBranch) { - return Err(Error::BranchCategory { - full_name: tracking_branch.to_owned(), - }); + return Err(gix_error::Error::from_error(gix_error::ValidationError::new(format!( + "The input branch '{}' needs to be a remote tracking branch", + tracking_branch.as_bstr() + )))); } let null = self.object_hash().null(); @@ -184,14 +202,17 @@ impl crate::Repository { return Ok(Some((upstream_branch, remote))); } if !ambiguous_remotes.is_empty() || candidates.len() > 1 { - return Err(Error::AmbiguousRemotes { - remotes: ambiguous_remotes - .into_iter() - .map(|r| r.name) - .chain(candidates.into_iter().map(|(r, _)| r.name)) - .flatten() - .collect(), - }); + let remotes = ambiguous_remotes + .into_iter() + .map(|r| r.name) + .chain(candidates.into_iter().map(|(r, _)| r.name)) + .flatten() + .map(|name| name.as_bstr().to_string()) + .collect::>() + .join(", "); + return Err(gix_error::Error::from_error(gix_error::ValidationError::new(format!( + "Found ambiguous remotes without 1:1 mapping or more than one match: {remotes}" + )))); } Ok(None) } @@ -239,25 +260,25 @@ impl crate::Repository { direction: remote::Direction, ) -> Option, remote::find::existing::Error>> { let name = self.branch_remote_name(short_branch_name, direction)?; - self.try_find_remote(name.as_bstr()) - .map(|res| res.map_err(Into::into)) - .or_else(|| match name { - remote::Name::Url(url) => gix_url::parse(&url) - .map_err(gix_error::Exn::into_error) - .map_err(Into::into) - .and_then(|url| { - self.remote_at(url) - .map_err(|err| remote::find::existing::Error::Find(remote::find::Error::Init(err))) - }) - .into(), - remote::Name::Symbol(_) => None, - }) + self.try_find_remote(name.as_bstr()).or_else(|| match name { + remote::Name::Url(url) => gix_url::parse(&url) + .map_err(gix_error::Exn::into_error) + .and_then(|url| self.remote_at(url)) + .into(), + remote::Name::Symbol(_) => None, + }) } } -fn source_ref_to_full_name(source: gix_refspec::match_group::SourceRef<'_>) -> Result { +fn source_ref_to_full_name(source: gix_refspec::match_group::SourceRef<'_>) -> Result { match source { - gix_refspec::match_group::SourceRef::FullName(name) => gix_ref::FullName::try_from(name.into_owned()), + gix_refspec::match_group::SourceRef::FullName(name) => { + gix_ref::FullName::try_from(name.into_owned()).map_err(|err| { + gix_error::Error::from( + err.and_raise(gix_error::ValidationError::new("The upstream branch name is invalid")), + ) + }) + } gix_refspec::match_group::SourceRef::ObjectId(_) => { unreachable!("Such a reverse mapping isn't ever produced") } diff --git a/gix/src/repository/config/mod.rs b/gix/src/repository/config/mod.rs index 2f5a3d35765..f393ecfca03 100644 --- a/gix/src/repository/config/mod.rs +++ b/gix/src/repository/config/mod.rs @@ -36,8 +36,9 @@ impl crate::Repository { /// The file and its parent directories do not have to exist. No [configuration transaction](config::FileTransaction) /// is opened, no lock is acquired, and no directories are created. Pass the path to /// [`config_file_mut()`](Self::config_file_mut) to edit it. - pub fn config_path(&self, source: config::Source) -> Result { - use config::{Source, file_mut::Error}; + pub fn config_path(&self, source: config::Source) -> Result { + use config::Source; + use gix_error::{ErrorExt, message}; let path = match source { Source::Local => self.common_dir().join("config"), @@ -51,9 +52,17 @@ impl crate::Repository { options.permissions.config, &mut config::Cache::make_source_env(options.permissions.env), ) - .ok_or(Error::SourceUnavailable(source))? + .ok_or_else(|| { + message!("Configuration source {source:?} has no available path with these options").raise() + })? + } + _ => { + return Err( + message!("Configuration source {source:?} requires a repository or has no physical file") + .raise() + .into(), + ); } - _ => return Err(Error::UnsupportedSource(source)), }; Ok(self.current_dir().join(path)) } @@ -252,7 +261,8 @@ impl crate::Repository { .string_filter("ssh.variant", &mut trusted) .and_then(|variant| Ssh::VARIANT.try_into_variant(variant).transpose()) .transpose() - .with_leniency(self.options.lenient_config)?, + .with_leniency(self.options.lenient_config) + .map_err(gix_error::Error::from)?, }; Ok(opts) } @@ -272,7 +282,8 @@ impl crate::Repository { stderr: { gitoxide::Core::EXTERNAL_COMMAND_STDERR .enrich_error(self.config.resolved.boolean(gitoxide::Core::EXTERNAL_COMMAND_STDERR)) - .with_leniency(self.config.lenient_config)? + .with_leniency(self.config.lenient_config) + .map_err(gix_error::Error::from)? .unwrap_or(true) .into() }, @@ -282,13 +293,15 @@ impl crate::Repository { &self.config.resolved, self.config.lenient_config, self.filter_config_section(), - )? + ) + .map_err(gix_error::Error::from)? .map(|enabled| !enabled), ref_namespace: self.refs.namespace.as_ref().map(|ns| ns.as_bstr().to_owned()), - literal_pathspecs: pathspec_boolean(&gitoxide::Pathspec::LITERAL)?, - glob_pathspecs: pathspec_boolean(&gitoxide::Pathspec::GLOB)? - .or(pathspec_boolean(&gitoxide::Pathspec::NOGLOB)?), - icase_pathspecs: pathspec_boolean(&gitoxide::Pathspec::ICASE)?, + literal_pathspecs: pathspec_boolean(&gitoxide::Pathspec::LITERAL).map_err(gix_error::Error::from)?, + glob_pathspecs: pathspec_boolean(&gitoxide::Pathspec::GLOB) + .map_err(gix_error::Error::from)? + .or(pathspec_boolean(&gitoxide::Pathspec::NOGLOB).map_err(gix_error::Error::from)?), + icase_pathspecs: pathspec_boolean(&gitoxide::Pathspec::ICASE).map_err(gix_error::Error::from)?, }) } diff --git a/gix/src/repository/config/transport.rs b/gix/src/repository/config/transport.rs index 8a70492fcde..14d34034050 100644 --- a/gix/src/repository/config/transport.rs +++ b/gix/src/repository/config/transport.rs @@ -2,6 +2,7 @@ use std::any::Any; use crate::bstr::BStr; +use gix_error::ResultExt; impl crate::Repository { /// Produce configuration suitable for `url`, as differentiated by its protocol/scheme, to be passed to a transport instance via @@ -26,382 +27,408 @@ impl crate::Repository { url: impl Into<&'a BStr>, remote_name: Option<&BStr>, ) -> Result>, crate::config::transport::Error> { - let url = gix_url::parse(url.into()).map_err(gix_error::Exn::into_error)?; + let url = gix_url::parse(url.into()).or_raise(|| gix_error::message("Invalid URL passed for configuration"))?; use gix_url::Scheme::*; match &url.scheme { Http | Https => { - #[cfg(not(any( - feature = "blocking-http-transport-reqwest", - feature = "blocking-http-transport-curl" - )))] - { - Ok(None) - } - #[cfg(any( - feature = "blocking-http-transport-reqwest", - feature = "blocking-http-transport-curl" - ))] - { - use std::sync::{Arc, Mutex}; + let options: Result>, crate::config::transport::http::Error> = { + #[cfg(not(any( + feature = "blocking-http-transport-reqwest", + feature = "blocking-http-transport-curl" + )))] + { + Ok(None) + } + #[cfg(any( + feature = "blocking-http-transport-reqwest", + feature = "blocking-http-transport-curl" + ))] + { + use std::sync::{Arc, Mutex}; - use gix_transport::client::blocking_io::http::{ - self, - options::{ProxyAuthMethod, SslVersion, SslVersionRangeInclusive}, - }; + use gix_transport::client::blocking_io::http::{ + self, + options::{ProxyAuthMethod, SslVersion, SslVersionRangeInclusive}, + }; - use crate::{ - bstr::BString, - config, - config::{ - cache::util::ApplyLeniency, - tree::{Key, Remote, gitoxide}, - }, - }; - fn try_to_string( - v: BString, - lenient: bool, - key_str: impl Into, - key: &'static config::tree::keys::String, - ) -> Result, config::transport::Error> { - key.try_into_string(v) - .map_err(|err| config::transport::Error::IllformedUtf8 { - source: err, - key: key_str.into(), - }) - .map(Some) - .with_leniency(lenient) - } + use crate::{ + bstr::BString, + config, + config::{ + cache::util::ApplyLeniency, + tree::{Key, Remote, gitoxide}, + }, + }; + use gix_error::ErrorExt; + fn try_to_string( + v: BString, + lenient: bool, + key_str: impl Into, + key: &'static config::tree::keys::String, + ) -> Result, config::transport::Error> { + let key_str = key_str.into(); + key.try_into_string(v) + .map_err(|err| { + gix_error::Error::from(err.and_raise(gix_error::message!( + "Could not decode value at key {:?} as UTF-8 string", + key_str + ))) + }) + .map(Some) + .with_leniency(lenient) + } - fn proxy_auth_method( - value_and_key: Option<(BString, BString, &'static config::tree::http::ProxyAuthMethod)>, - ) -> Result { - let value = value_and_key - .map(|(method, key, key_type)| { - key_type.try_into_proxy_auth_method(method).map_err(|err| { - config::transport::http::Error::InvalidProxyAuthMethod { source: err, key } + fn proxy_auth_method( + value_and_key: Option<(BString, BString, &'static config::tree::http::ProxyAuthMethod)>, + ) -> Result { + let value = value_and_key + .map(|(method, key, key_type)| { + let _ = &key; // CodeQL doesn't inspect formatting macro arguments. + key_type.try_into_proxy_auth_method(method).map_err(|err| { + gix_error::Error::from(err.and_raise(gix_error::message!( + "The proxy authentication at key `{key}` is invalid" + ))) + }) }) - }) - .transpose()? - .unwrap_or_default(); - Ok(value) - } + .transpose()? + .unwrap_or_default(); + Ok(value) + } - fn ssl_version( - config: &gix_config::File, - key_str: &'static str, - key: &'static config::tree::http::SslVersion, - mut filter: fn(&gix_config::file::Metadata) -> bool, - lenient: bool, - ) -> Result, config::transport::Error> { - debug_assert_eq!( - key_str, - key.logical_name(), - "BUG: hardcoded and generated key names must match" - ); - config - .string_filter(key_str, &mut filter) - .filter(|v| !v.is_empty()) - .map(|v| { - key.try_into_ssl_version(v) - .map_err(crate::config::transport::http::Error::from) - }) - .transpose() - .with_leniency(lenient) - .map_err(Into::into) - } + fn ssl_version( + config: &gix_config::File, + key_str: &'static str, + key: &'static config::tree::http::SslVersion, + mut filter: fn(&gix_config::file::Metadata) -> bool, + lenient: bool, + ) -> Result, config::transport::Error> { + debug_assert_eq!( + key_str, + key.logical_name(), + "BUG: hardcoded and generated key names must match" + ); + config + .string_filter(key_str, &mut filter) + .filter(|v| !v.is_empty()) + .map(|v| key.try_into_ssl_version(v).map_err(gix_error::Error::from)) + .transpose() + .with_leniency(lenient) + } - fn proxy( - value: Option<(BString, BString, &'static config::tree::keys::String)>, - lenient: bool, - ) -> Result, config::transport::Error> { - Ok(value - .and_then(|(v, k, key)| try_to_string(v, lenient, k.clone(), key).transpose()) - .transpose()? - .map(|mut proxy| { - if !proxy.trim().is_empty() && !proxy.contains("://") { - proxy.insert_str(0, "http://"); - proxy - } else { - proxy - } - })) - } + fn proxy( + value: Option<(BString, BString, &'static config::tree::keys::String)>, + lenient: bool, + ) -> Result, config::transport::Error> { + Ok(value + .and_then(|(v, k, key)| try_to_string(v, lenient, k.clone(), key).transpose()) + .transpose()? + .map(|mut proxy| { + if !proxy.trim().is_empty() && !proxy.contains("://") { + proxy.insert_str(0, "http://"); + proxy + } else { + proxy + } + })) + } - let mut opts = http::Options::default(); - let config = &self.config.resolved; - let mut trusted_only = self.filter_config_section(); - let lenient = self.config.lenient_config; - opts.extra_headers = { - let key = "http.extraHeader"; - debug_assert_eq!(key, &config::tree::Http::EXTRA_HEADER.logical_name()); - config - .strings_filter(key, &mut trusted_only) - .map(|values| config::tree::Http::EXTRA_HEADER.try_into_extra_header(values)) - .transpose() - .map_err(|err| config::transport::Error::IllformedUtf8 { - source: err, - key: key.into(), - })? - .unwrap_or_default() - }; + let mut opts = http::Options::default(); + let config = &self.config.resolved; + let mut trusted_only = self.filter_config_section(); + let lenient = self.config.lenient_config; + opts.extra_headers = { + let key = "http.extraHeader"; + debug_assert_eq!(key, &config::tree::Http::EXTRA_HEADER.logical_name()); + config + .strings_filter(key, &mut trusted_only) + .map(|values| config::tree::Http::EXTRA_HEADER.try_into_extra_header(values)) + .transpose() + .map_err(|err| { + gix_error::Error::from(err.and_raise(gix_error::message!( + "Could not decode value at key {key:?} as UTF-8 string" + ))) + })? + .unwrap_or_default() + }; - opts.follow_redirects = { - let key = "http.followRedirects"; + opts.follow_redirects = { + let key = "http.followRedirects"; - config::tree::Http::FOLLOW_REDIRECTS - .try_into_follow_redirects( - config.string_filter(key, &mut trusted_only).unwrap_or_default(), - || config.boolean_filter(key, &mut trusted_only).with_leniency(lenient), - ) - .map_err(config::transport::http::Error::InvalidFollowRedirects)? - }; + config::tree::Http::FOLLOW_REDIRECTS + .try_into_follow_redirects( + config.string_filter(key, &mut trusted_only).unwrap_or_default(), + || config.boolean_filter(key, &mut trusted_only).with_leniency(lenient), + ) + .map_err(|err| { + gix_error::Error::from(err.and_raise(gix_error::message!( + "The follow redirects value must be 'initial', or boolean true or false" + ))) + })? + }; - opts.low_speed_time_seconds = config::tree::Http::LOW_SPEED_TIME - .try_into_u64(config.integer_filter("http.lowSpeedTime", &mut trusted_only)) - .with_leniency(lenient) - .map_err(config::transport::http::Error::from)? - .unwrap_or_default(); - opts.low_speed_limit_bytes_per_second = config::tree::Http::LOW_SPEED_LIMIT - .try_into_u32(config.integer_filter("http.lowSpeedLimit", &mut trusted_only)) - .with_leniency(lenient) - .map_err(config::transport::http::Error::from)? - .unwrap_or_default(); - opts.proxy = proxy( - remote_name - .and_then(|name| { - config - .string_filter( - &format!("remote.{}.{}", name, Remote::PROXY.name), - &mut trusted_only, + opts.low_speed_time_seconds = config::tree::Http::LOW_SPEED_TIME + .try_into_u64(config.integer_filter("http.lowSpeedTime", &mut trusted_only)) + .with_leniency(lenient) + .map_err(gix_error::Error::from)? + .unwrap_or_default(); + opts.low_speed_limit_bytes_per_second = config::tree::Http::LOW_SPEED_LIMIT + .try_into_u32(config.integer_filter("http.lowSpeedLimit", &mut trusted_only)) + .with_leniency(lenient) + .map_err(gix_error::Error::from)? + .unwrap_or_default(); + opts.proxy = proxy( + remote_name + .and_then(|name| { + config + .string_filter( + &format!("remote.{}.{}", name, Remote::PROXY.name), + &mut trusted_only, + ) + .map(|v| (v, format!("remote.{name}.proxy").into(), &Remote::PROXY)) + }) + .or_else(|| { + let key = "http.proxy"; + debug_assert_eq!(key, config::tree::Http::PROXY.logical_name()); + let http_proxy = config + .string_filter(key, &mut trusted_only) + .map(|v| (v, key.into(), &config::tree::Http::PROXY)) + .or_else(|| { + let key = "gitoxide.http.proxy"; + debug_assert_eq!(key, gitoxide::Http::PROXY.logical_name()); + config + .string_filter(key, &mut trusted_only) + .map(|v| (v, key.into(), &gitoxide::Http::PROXY)) + }); + if url.scheme == Https { + http_proxy.or_else(|| { + let key = "gitoxide.https.proxy"; + debug_assert_eq!(key, gitoxide::Https::PROXY.logical_name()); + config + .string_filter(key, &mut trusted_only) + .map(|v| (v, key.into(), &gitoxide::Https::PROXY)) + }) + } else { + http_proxy + } + }) + .or_else(|| { + let key = "gitoxide.http.allProxy"; + debug_assert_eq!(key, gitoxide::Http::ALL_PROXY.logical_name()); + config + .string_filter(key, &mut trusted_only) + .map(|v| (v, key.into(), &gitoxide::Http::ALL_PROXY)) + }), + lenient, + )?; + { + let key = "gitoxide.http.noProxy"; + debug_assert_eq!(key, gitoxide::Http::NO_PROXY.logical_name()); + opts.no_proxy = config + .string_filter(key, &mut trusted_only) + .and_then(|v| try_to_string(v, lenient, key, &gitoxide::Http::NO_PROXY).transpose()) + .transpose()?; + } + opts.proxy_auth_method = proxy_auth_method({ + let key = "gitoxide.http.proxyAuthMethod"; + debug_assert_eq!(key, gitoxide::Http::PROXY_AUTH_METHOD.logical_name()); + config + .string_filter(key, &mut trusted_only) + .map(|v| (v, key.into(), &gitoxide::Http::PROXY_AUTH_METHOD)) + .or_else(|| { + remote_name + .and_then(|name| { + config + .string_filter( + &format!("remote.{name}.proxyAuthMethod"), + &mut trusted_only, + ) + .map(|v| { + ( + v, + format!("remote.{name}.proxyAuthMethod").into(), + &Remote::PROXY_AUTH_METHOD, + ) + }) + }) + .or_else(|| { + let key = "http.proxyAuthMethod"; + debug_assert_eq!(key, config::tree::Http::PROXY_AUTH_METHOD.logical_name()); + config + .string_filter(key, &mut trusted_only) + .map(|v| (v, key.into(), &config::tree::Http::PROXY_AUTH_METHOD)) + }) + }) + })?; + opts.proxy_authenticate = opts + .proxy + .as_deref() + .filter(|url| !url.is_empty()) + .map(gix_url::parse) + .transpose() + .or_raise(|| gix_error::message("Invalid URL passed for configuration"))? + .filter(|url| url.user().is_some()) + .map(|url| -> Result<_, config::transport::http::Error> { + let (mut cascade, action_with_normalized_url, prompt_opts) = + self.config_snapshot().credential_helpers(url).or_raise(|| { + gix_error::message( + "Could not configure the credential helpers for the authenticated proxy url", ) - .map(|v| (v, format!("remote.{name}.proxy").into(), &Remote::PROXY)) - }) - .or_else(|| { - let key = "http.proxy"; - debug_assert_eq!(key, config::tree::Http::PROXY.logical_name()); - let http_proxy = config - .string_filter(key, &mut trusted_only) - .map(|v| (v, key.into(), &config::tree::Http::PROXY)) - .or_else(|| { - let key = "gitoxide.http.proxy"; - debug_assert_eq!(key, gitoxide::Http::PROXY.logical_name()); - config - .string_filter(key, &mut trusted_only) - .map(|v| (v, key.into(), &gitoxide::Http::PROXY)) - }); - if url.scheme == Https { - http_proxy.or_else(|| { - let key = "gitoxide.https.proxy"; - debug_assert_eq!(key, gitoxide::Https::PROXY.logical_name()); - config - .string_filter(key, &mut trusted_only) - .map(|v| (v, key.into(), &gitoxide::Https::PROXY)) - }) - } else { - http_proxy - } + })?; + Ok(( + action_with_normalized_url, + Arc::new(Mutex::new(move |action| cascade.invoke(action, prompt_opts.clone()))) + as Arc>, + )) }) - .or_else(|| { - let key = "gitoxide.http.allProxy"; - debug_assert_eq!(key, gitoxide::Http::ALL_PROXY.logical_name()); - config - .string_filter(key, &mut trusted_only) - .map(|v| (v, key.into(), &gitoxide::Http::ALL_PROXY)) - }), - lenient, - )?; - { - let key = "gitoxide.http.noProxy"; - debug_assert_eq!(key, gitoxide::Http::NO_PROXY.logical_name()); - opts.no_proxy = config - .string_filter(key, &mut trusted_only) - .and_then(|v| try_to_string(v, lenient, key, &gitoxide::Http::NO_PROXY).transpose()) .transpose()?; - } - opts.proxy_auth_method = proxy_auth_method({ - let key = "gitoxide.http.proxyAuthMethod"; - debug_assert_eq!(key, gitoxide::Http::PROXY_AUTH_METHOD.logical_name()); - config - .string_filter(key, &mut trusted_only) - .map(|v| (v, key.into(), &gitoxide::Http::PROXY_AUTH_METHOD)) - .or_else(|| { - remote_name - .and_then(|name| { - config - .string_filter(&format!("remote.{name}.proxyAuthMethod"), &mut trusted_only) - .map(|v| { - ( - v, - format!("remote.{name}.proxyAuthMethod").into(), - &Remote::PROXY_AUTH_METHOD, - ) - }) - }) - .or_else(|| { - let key = "http.proxyAuthMethod"; - debug_assert_eq!(key, config::tree::Http::PROXY_AUTH_METHOD.logical_name()); - config - .string_filter(key, &mut trusted_only) - .map(|v| (v, key.into(), &config::tree::Http::PROXY_AUTH_METHOD)) - }) - }) - })?; - opts.proxy_authenticate = opts - .proxy - .as_deref() - .filter(|url| !url.is_empty()) - .map(gix_url::parse) - .transpose() - .map_err(gix_error::Exn::into_error)? - .filter(|url| url.user().is_some()) - .map(|url| -> Result<_, config::transport::http::Error> { - let (mut cascade, action_with_normalized_url, prompt_opts) = - self.config_snapshot().credential_helpers(url)?; - Ok(( - action_with_normalized_url, - Arc::new(Mutex::new(move |action| cascade.invoke(action, prompt_opts.clone()))) - as Arc>, - )) - }) - .transpose()?; - opts.connect_timeout = { - let key = "gitoxide.http.connectTimeout"; - debug_assert_eq!(key, gitoxide::Http::CONNECT_TIMEOUT.logical_name()); - gitoxide::Http::CONNECT_TIMEOUT - .try_into_duration(config.integer_filter(key, &mut trusted_only)) - .map_err(crate::config::transport::http::Error::from) - .with_leniency(lenient)? - }; - { - let key = "http.userAgent"; - opts.user_agent = config - .string_filter(key, &mut trusted_only) - .and_then(|v| try_to_string(v, lenient, key, &config::tree::Http::USER_AGENT).transpose()) - .transpose()? - .or_else(|| Some(crate::env::agent().into())); - } + opts.connect_timeout = { + let key = "gitoxide.http.connectTimeout"; + debug_assert_eq!(key, gitoxide::Http::CONNECT_TIMEOUT.logical_name()); + gitoxide::Http::CONNECT_TIMEOUT + .try_into_duration(config.integer_filter(key, &mut trusted_only)) + .map_err(gix_error::Error::from) + .with_leniency(lenient)? + }; + { + let key = "http.userAgent"; + opts.user_agent = config + .string_filter(key, &mut trusted_only) + .and_then(|v| { + try_to_string(v, lenient, key, &config::tree::Http::USER_AGENT).transpose() + }) + .transpose()? + .or_else(|| Some(crate::env::agent().into())); + } - { - let key = "http.version"; - opts.http_version = config - .string_filter(key, &mut trusted_only) - .map(|v| { - config::tree::Http::VERSION - .try_into_http_version(v) - .map_err(config::transport::http::Error::InvalidHttpVersion) - }) - .transpose()?; - } + { + let key = "http.version"; + opts.http_version = config + .string_filter(key, &mut trusted_only) + .map(|v| { + config::tree::Http::VERSION.try_into_http_version(v).map_err(|err| { + gix_error::Error::from(err.and_raise(gix_error::message!( + "The HTTP version must be 'HTTP/2' or 'HTTP/1.1'" + ))) + }) + }) + .transpose()?; + } - { - opts.verbose = config - .boolean_filter(gitoxide::Http::VERBOSE, &mut trusted_only) - .ok() - .flatten() - .unwrap_or_default(); - } + { + opts.verbose = config + .boolean_filter(gitoxide::Http::VERBOSE, &mut trusted_only) + .ok() + .flatten() + .unwrap_or_default(); + } - let may_use_cainfo = { - let key = "http.schannelUseSSLCAInfo"; - config::tree::Http::SCHANNEL_USE_SSL_CA_INFO - .enrich_error(config.boolean_filter(key, &mut trusted_only)) - .with_leniency(lenient) - .map_err(config::transport::http::Error::from)? - .unwrap_or(true) - }; + let may_use_cainfo = { + let key = "http.schannelUseSSLCAInfo"; + config::tree::Http::SCHANNEL_USE_SSL_CA_INFO + .enrich_error(config.boolean_filter(key, &mut trusted_only)) + .with_leniency(lenient) + .map_err(gix_error::Error::from)? + .unwrap_or(true) + }; - if may_use_cainfo { - let key = "http.sslCAInfo"; - debug_assert_eq!(key, config::tree::Http::SSL_CA_INFO.logical_name()); - opts.ssl_ca_info = config - .path_filter(key, &mut trusted_only) - .map(|p| { - use crate::config::cache::interpolate_context; - p.interpolate(interpolate_context( - self.install_dir().ok().as_deref(), - self.config.home_dir().as_deref(), - )) - }) - .transpose() - .with_leniency(lenient) - .map_err(|err| config::transport::Error::InterpolatePath { - source: err.into_error(), - key, - })?; - } + if may_use_cainfo { + let key = "http.sslCAInfo"; + debug_assert_eq!(key, config::tree::Http::SSL_CA_INFO.logical_name()); + opts.ssl_ca_info = config + .path_filter(key, &mut trusted_only) + .map(|p| { + use crate::config::cache::interpolate_context; + p.interpolate(interpolate_context( + self.install_dir().ok().as_deref(), + self.config.home_dir().as_deref(), + )) + }) + .transpose() + .with_leniency(lenient) + .map_err(|err| { + gix_error::Error::from( + err.raise(gix_error::message!("Could not interpolate path at key {key:?}")), + ) + })?; + } - { - opts.ssl_version = ssl_version( - config, - "http.sslVersion", - &config::tree::Http::SSL_VERSION, - trusted_only, - lenient, - )? - .map(|v| SslVersionRangeInclusive { min: v, max: v }); - let min_max = ssl_version( - config, - "gitoxide.http.sslVersionMin", - &gitoxide::Http::SSL_VERSION_MIN, - trusted_only, - lenient, - ) - .and_then(|min| { - ssl_version( + { + opts.ssl_version = ssl_version( + config, + "http.sslVersion", + &config::tree::Http::SSL_VERSION, + trusted_only, + lenient, + )? + .map(|v| SslVersionRangeInclusive { min: v, max: v }); + let min_max = ssl_version( config, - "gitoxide.http.sslVersionMax", - &gitoxide::Http::SSL_VERSION_MAX, + "gitoxide.http.sslVersionMin", + &gitoxide::Http::SSL_VERSION_MIN, trusted_only, lenient, ) - .map(|max| min.zip(max)) - })?; - if let Some((min, max)) = min_max { - let v = opts.ssl_version.get_or_insert(SslVersionRangeInclusive { - min: SslVersion::TlsV1_3, - max: SslVersion::TlsV1_3, - }); - v.min = min; - v.max = max; + .and_then(|min| { + ssl_version( + config, + "gitoxide.http.sslVersionMax", + &gitoxide::Http::SSL_VERSION_MAX, + trusted_only, + lenient, + ) + .map(|max| min.zip(max)) + })?; + if let Some((min, max)) = min_max { + let v = opts.ssl_version.get_or_insert(SslVersionRangeInclusive { + min: SslVersion::TlsV1_3, + max: SslVersion::TlsV1_3, + }); + v.min = min; + v.max = max; + } } - } - { - let key = "gitoxide.http.sslNoVerify"; - let ssl_no_verify = config::tree::gitoxide::Http::SSL_NO_VERIFY - .enrich_error(config.boolean_filter(key, &mut trusted_only)) - .with_leniency(lenient) - .map_err(config::transport::http::Error::from)? - .unwrap_or_default(); + { + let key = "gitoxide.http.sslNoVerify"; + let ssl_no_verify = config::tree::gitoxide::Http::SSL_NO_VERIFY + .enrich_error(config.boolean_filter(key, &mut trusted_only)) + .with_leniency(lenient) + .map_err(gix_error::Error::from)? + .unwrap_or_default(); - if ssl_no_verify { - opts.ssl_verify = false; - } else { - let key = "http.sslVerify"; - opts.ssl_verify = config::tree::Http::SSL_VERIFY + if ssl_no_verify { + opts.ssl_verify = false; + } else { + let key = "http.sslVerify"; + opts.ssl_verify = config::tree::Http::SSL_VERIFY + .enrich_error(config.boolean_filter(key, &mut trusted_only)) + .with_leniency(lenient) + .map_err(gix_error::Error::from)? + .unwrap_or(true); + } + } + + #[cfg(feature = "blocking-http-transport-curl")] + { + let key = "http.schannelCheckRevoke"; + let schannel_check_revoke = config::tree::Http::SCHANNEL_CHECK_REVOKE .enrich_error(config.boolean_filter(key, &mut trusted_only)) .with_leniency(lenient) - .map_err(config::transport::http::Error::from)? - .unwrap_or(true); + .map_err(gix_error::Error::from)?; + let backend = gix_protocol::transport::client::blocking_io::http::curl::Options { + schannel_check_revoke, + }; + opts.backend = + Some(Arc::new(Mutex::new(backend)) as Arc>); } - } - #[cfg(feature = "blocking-http-transport-curl")] - { - let key = "http.schannelCheckRevoke"; - let schannel_check_revoke = config::tree::Http::SCHANNEL_CHECK_REVOKE - .enrich_error(config.boolean_filter(key, &mut trusted_only)) - .with_leniency(lenient) - .map_err(config::transport::http::Error::from)?; - let backend = - gix_protocol::transport::client::blocking_io::http::curl::Options { schannel_check_revoke }; - opts.backend = - Some(Arc::new(Mutex::new(backend)) as Arc>); + Ok(Some(Box::new(opts) as Box)) } - - Ok(Some(Box::new(opts))) - } + }; + options + .or_raise(|| gix_error::message("Could obtain configuration for an HTTP url")) + .map_err(Into::into) } File | Git | Ssh | Ext | Helper(_) | HelperUrl(_) => Ok(None), } diff --git a/gix/src/repository/diff.rs b/gix/src/repository/diff.rs index 77c48126ed8..9e31dbbf7d9 100644 --- a/gix/src/repository/diff.rs +++ b/gix/src/repository/diff.rs @@ -1,3 +1,4 @@ +use gix_error::ResultExt; use gix_object::TreeRefIter; use crate::{ @@ -23,7 +24,7 @@ impl Repository { worktree_roots: gix_diff::blob::pipeline::WorktreeRoots, ) -> Result { let index = self.index_or_load_from_head_or_empty()?; - Ok(crate::diff::resource_cache( + crate::diff::resource_cache( self, mode, self.attributes_only( @@ -33,10 +34,11 @@ impl Repository { } else { gix_worktree::stack::state::attributes::Source::WorktreeThenIdMapping }, - )? + ) + .or_raise(|| gix_error::message("Could not obtain resource cache for diffing"))? .inner, worktree_roots, - )?) + ) } /// Produce the changes that would need to be applied to `old_tree` to create `new_tree`. @@ -74,7 +76,8 @@ impl Repository { Ok(std::ops::ControlFlow::Continue(())) }, opts, - )?; + ) + .or_erased()?; Ok(out) } diff --git a/gix/src/repository/dirwalk.rs b/gix/src/repository/dirwalk.rs index 2a992182807..688e5d175e5 100644 --- a/gix/src/repository/dirwalk.rs +++ b/gix/src/repository/dirwalk.rs @@ -7,6 +7,7 @@ use crate::{ util::OwnedOrStaticAtomicBool, worktree::IndexPersistedOrInMemory, }; +use gix_error::ResultExt; impl Repository { /// Return default options suitable for performing a directory walk on this repository. @@ -40,7 +41,11 @@ impl Repository { delegate: &mut dyn gix_dir::walk::Delegate, ) -> Result, dirwalk::Error> { let _span = gix_trace::coarse!("gix::dirwalk"); - let workdir = self.workdir().ok_or(dirwalk::Error::MissingWorkDir)?; + let workdir = self.workdir().ok_or_else(|| { + gix_error::Error::from_error(gix_error::message( + "A working tree is required to perform a directory walk", + )) + })?; let mut excludes = self.excludes( index, None, @@ -55,19 +60,23 @@ impl Repository { )?; let git_dir_realpath = - crate::path::realpath_opts(self.git_dir(), self.current_dir(), crate::path::realpath::MAX_SYMLINKS)?; - let fs_caps = self.filesystem_options()?; + crate::path::realpath_opts(self.git_dir(), self.current_dir(), crate::path::realpath::MAX_SYMLINKS) + .map_err(gix_error::Exn::into_error)?; + let fs_caps = self.filesystem_options().map_err(gix_error::Error::from)?; let accelerate_lookup = fs_caps.ignore_case.then(|| index.prepare_icase_backing()); let mut opts = gix_dir::walk::Options::from(options); let worktree_relative_worktree_dirs_storage; if let Some(workdir) = self.workdir().filter(|_| opts.for_deletion.is_some()) { - let linked_worktrees = self.worktrees()?; + let linked_worktrees = self.worktrees().or_raise(|| { + gix_error::message("Could not list worktrees to assure they are no candidates for deletion") + })?; if !linked_worktrees.is_empty() { let real_workdir = gix_path::realpath_opts( workdir, self.options.current_dir_or_empty(), gix_path::realpath::MAX_SYMLINKS, - )?; + ) + .map_err(gix_error::Exn::into_error)?; worktree_relative_worktree_dirs_storage = linked_worktrees .into_iter() .filter_map(|proxy| proxy.base().ok()) @@ -105,7 +114,7 @@ impl Repository { opts, delegate, ) - .map_err(|err| dirwalk::Error::Walk(err.into_error()))?; + .map_err(gix_error::Exn::into_error)?; Ok(dirwalk::Outcome { dirwalk: outcome, diff --git a/gix/src/repository/filter.rs b/gix/src/repository/filter.rs index ffd7330ec8d..a349e14ef0f 100644 --- a/gix/src/repository/filter.rs +++ b/gix/src/repository/filter.rs @@ -1,24 +1,11 @@ -use crate::{Id, Repository, filter, worktree::IndexPersistedOrInMemory}; +use gix_error::ResultExt; + +use crate::{Repository, filter, worktree::IndexPersistedOrInMemory}; /// pub mod pipeline { /// The error returned by [Repository::filter_pipeline()](super::Repository::filter_pipeline()). - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error("Could not obtain head commit of bare repository")] - HeadCommit(#[from] crate::reference::head_commit::Error), - #[error(transparent)] - DecodeCommit(#[from] gix_object::decode::Error), - #[error("Could not create index from tree at HEAD^{{tree}}")] - TreeTraverse(#[from] crate::repository::index_from_tree::Error), - #[error(transparent)] - BareAttributes(#[from] crate::config::attribute_stack::Error), - #[error(transparent)] - WorktreeIndex(#[from] crate::worktree::open_index::Error), - #[error(transparent)] - Init(#[from] crate::filter::pipeline::options::Error), - } + pub type Error = gix_error::Error; } impl Repository { @@ -41,22 +28,30 @@ impl Repository { tree_if_bare: Option, ) -> Result<(filter::Pipeline<'_>, IndexPersistedOrInMemory), pipeline::Error> { let (cache, index) = if self.is_bare() { - let index = self.index_from_tree(&tree_if_bare.map_or_else( - || { - self.head_commit() - .map_err(pipeline::Error::from) - .and_then(|c| c.tree_id().map(Id::detach).map_err(Into::into)) + let tree = tree_if_bare.map_or_else( + || -> Result<_, gix_error::Error> { + let commit = self + .head_commit() + .or_raise(|| gix_error::message("Could not obtain head commit of bare repository"))?; + Ok(commit.tree_id().or_erased()?.detach()) }, Ok, - )?)?; - let cache = self.attributes_only(&index, gix_worktree::stack::state::attributes::Source::IdMapping)?; + )?; + let index = self + .index_from_tree(&tree) + .or_raise(|| gix_error::message("Could not create index from tree at HEAD^{tree}"))?; + let cache = self + .attributes_only(&index, gix_worktree::stack::state::attributes::Source::IdMapping) + .or_erased()?; (cache, IndexPersistedOrInMemory::InMemory(index)) } else { let index = self.index_or_empty()?; - let cache = self.attributes_only( - &index, - gix_worktree::stack::state::attributes::Source::WorktreeThenIdMapping, - )?; + let cache = self + .attributes_only( + &index, + gix_worktree::stack::state::attributes::Source::WorktreeThenIdMapping, + ) + .or_erased()?; (cache, IndexPersistedOrInMemory::Persisted(index)) }; Ok((filter::Pipeline::new(self, cache.detach())?, index)) diff --git a/gix/src/repository/graph.rs b/gix/src/repository/graph.rs index 1d2e74e4142..b2c7e4f636b 100644 --- a/gix/src/repository/graph.rs +++ b/gix/src/repository/graph.rs @@ -34,14 +34,13 @@ impl crate::Repository { pub fn commit_graph_if_enabled( &self, ) -> Result, super::commit_graph_if_enabled::Error> { - Ok(self - .config + self.config .may_use_commit_graph()? .then(|| gix_commitgraph::at(self.objects.store_ref().path().join("info"))) .transpose() .or_else(|err| match err.downcast_any_ref::() { Some(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None), _ => Err(err.into_error()), - })?) + }) } } diff --git a/gix/src/repository/identity.rs b/gix/src/repository/identity.rs index f5223f54743..8c55bc04f60 100644 --- a/gix/src/repository/identity.rs +++ b/gix/src/repository/identity.rs @@ -1,3 +1,5 @@ +use gix_error::ResultExt; + use crate::{ bstr::{BString, ByteSlice}, config, @@ -67,9 +69,9 @@ impl crate::Repository { config.set_raw_value(gitoxide::Committer::NAME_FALLBACK, name)?; config.set_raw_value(gitoxide::Committer::EMAIL_FALLBACK, email)?; let mut repo_config = self.config_snapshot_mut(); - repo_config.append(config)?; + repo_config.append(config).or_erased()?; } - Ok(self.committer().expect("committer was just set")?) + Ok(self.committer().expect("committer was just set").or_erased()?) } /// Return the configured committer or install a generic fallback in memory on this instance. diff --git a/gix/src/repository/index.rs b/gix/src/repository/index.rs index 7a3e37c9228..426bd6cf5e0 100644 --- a/gix/src/repository/index.rs +++ b/gix/src/repository/index.rs @@ -1,3 +1,5 @@ +use gix_error::ResultExt; + use crate::{ config::{cache::util::ApplyLeniencyDefault, tree::Index}, worktree, @@ -46,7 +48,7 @@ impl crate::Repository { alloc_limit_bytes: self.config.alloc_limit_bytes, }, ) - .map_err(|err| worktree::open_index::Error::IndexFile(err.into_error()))?; + .map_err(gix_error::Exn::into_error)?; Ok(index) } @@ -75,12 +77,10 @@ impl crate::Repository { pub fn index(&self) -> Result { self.try_index().and_then(|opt| match opt { Some(index) => Ok(index), - None => Err(worktree::open_index::Error::IndexFile(gix_error::Error::from_error( - gix_error::NotFoundError::new(format!( - "Could not find index file at '{index_path}' for opening.", - index_path = self.index_path().display() - )), - ))), + None => Err(gix_error::Error::from_error(gix_error::NotFoundError::new(format!( + "Could not find index file at '{index_path}' for opening.", + index_path = self.index_path().display() + )))), }) } @@ -115,17 +115,17 @@ impl crate::Repository { self.index.recent_snapshot( || self.index_path().metadata().and_then(|m| m.modified()).ok(), || { - self.open_index().map(Some).or_else(|err| match err { - worktree::open_index::Error::IndexFile(err) - if err - .downcast_any_ref::() - .is_some_and(|err| { - err.path == self.index_path() && err.source.kind() == std::io::ErrorKind::NotFound - }) => + self.open_index().map(Some).or_else(|err| { + if err + .downcast_any_ref::() + .is_some_and(|err| { + err.path == self.index_path() && err.source.kind() == std::io::ErrorKind::NotFound + }) { Ok(None) + } else { + Err(err) } - err => Err(err), }) }, ) @@ -160,7 +160,7 @@ impl crate::Repository { Ok(match self.try_index()? { Some(index) => IndexPersistedOrInMemory::Persisted(index), None => { - let tree = self.head_commit()?.tree_id()?; + let tree = self.head_commit()?.tree_id().or_erased()?; IndexPersistedOrInMemory::InMemory(self.index_from_tree(&tree)?) } }) @@ -191,7 +191,7 @@ impl crate::Repository { Some(index) => IndexPersistedOrInMemory::Persisted(index), None => match self.head()?.id() { Some(id) => { - let head_tree_id = id.object()?.peel_to_commit()?.tree_id()?; + let head_tree_id = id.object()?.peel_to_commit()?.tree_id().or_erased()?; IndexPersistedOrInMemory::InMemory(self.index_from_tree(&head_tree_id)?) } None => IndexPersistedOrInMemory::InMemory(gix_index::File::from_state( @@ -207,12 +207,13 @@ impl crate::Repository { /// Note that this is an expensive operation as it requires recursively traversing the entire tree to unpack it into the index. pub fn index_from_tree(&self, tree: &gix_hash::oid) -> Result { Ok(gix_index::File::from_state( - gix_index::State::from_tree(tree, self, self.config.protect_options()?).map_err(|err| { - super::index_from_tree::Error::IndexFromTree { - id: tree.into(), - source: err.into_error(), - } - })?, + gix_index::State::from_tree( + tree, + self, + self.config.protect_options().map_err(gix_error::Error::from)?, + ) + .or_raise(|| gix_error::message!("Could not create index from tree at {tree}")) + .map_err(gix_error::Error::from)?, self.index_path(), )) } diff --git a/gix/src/repository/location.rs b/gix/src/repository/location.rs index dec8e6b72c0..36f67650646 100644 --- a/gix/src/repository/location.rs +++ b/gix/src/repository/location.rs @@ -130,37 +130,56 @@ impl crate::Repository { &self, path: &'a (impl gix_utils::AsBStr + ?Sized), ) -> Result, crate::repository::normalize_path::Error> { - use crate::repository::normalize_path::Error; - let path = gix_path::from_bstr(Cow::Borrowed(path.as_bstr())); let path = if gix_path::is_absolute(path.as_ref()) { let root = gix_path::realpath_opts( self.workdir().unwrap_or_else(|| self.git_dir()), self.current_dir(), MAX_SYMLINKS, - )?; + ) + .map_err(gix_error::Exn::into_error)?; let absolute = path.into_owned(); let relative = if let Ok(relative) = absolute.strip_prefix(&root) { relative.to_owned() } else { - gix_path::realpath_opts(&absolute, self.current_dir(), MAX_SYMLINKS)? + gix_path::realpath_opts(&absolute, self.current_dir(), MAX_SYMLINKS) + .map_err(gix_error::Exn::into_error)? .strip_prefix(&root) - .map_err(|_| Error::AbsolutePathOutsideOfRepository { path: absolute, root })? + .map_err(|_| { + gix_error::Error::from_error(gix_error::ValidationError::new(format!( + "The absolute path '{}' is not inside the repository at '{}'", + absolute.display(), + root.display() + ))) + })? .to_owned() }; Cow::Owned(relative) - } else if let Some(prefix) = self.prefix()?.filter(|prefix| !prefix.as_os_str().is_empty()) { + } else if let Some(prefix) = self + .prefix() + .map_err(gix_error::Exn::into_error)? + .filter(|prefix| !prefix.as_os_str().is_empty()) + { Cow::Owned(prefix.join(path.as_ref())) } else { path }; let path = match path { - Cow::Borrowed(path) => gix_path::normalize_and_clean(Cow::Borrowed(path), Path::new("")) - .ok_or_else(|| Error::OutsideOfRepository { path: path.to_owned() })?, + Cow::Borrowed(path) => { + gix_path::normalize_and_clean(Cow::Borrowed(path), Path::new("")).ok_or_else(|| { + gix_error::Error::from_error(gix_error::ValidationError::new(format!( + "The path '{}' leaves the repository", + path.display() + ))) + })? + } Cow::Owned(path) => { if gix_path::normalize_and_clean(Cow::Borrowed(path.as_path()), Path::new("")).is_none() { - return Err(Error::OutsideOfRepository { path }); + return Err(gix_error::Error::from_error(gix_error::ValidationError::new(format!( + "The path '{}' leaves the repository", + path.display() + )))); } gix_path::normalize_and_clean(Cow::Owned(path), Path::new("")) .expect("path was just validated as normalizable") diff --git a/gix/src/repository/mailmap.rs b/gix/src/repository/mailmap.rs index c5ff8231759..ac9b8b5c284 100644 --- a/gix/src/repository/mailmap.rs +++ b/gix/src/repository/mailmap.rs @@ -1,4 +1,5 @@ use crate::{Id, bstr::ByteSlice, config::tree::Mailmap}; +use gix_error::ErrorExt; impl crate::Repository { /// Similar to [`open_mailmap_into()`][crate::Repository::open_mailmap_into()], but ignores all errors and returns at worst @@ -26,7 +27,12 @@ impl crate::Repository { let mut buf = Vec::new(); let mut blob_id = self.config.resolved.string(Mailmap::BLOB).and_then(|spec| { self.rev_parse_single(spec.as_bstr()) - .map_err(|e| err.get_or_insert(e.into())) + .map_err(|e| { + err.get_or_insert( + e.and_raise(gix_error::message("The configured mailmap.blob could not be parsed")) + .into(), + ) + }) .map(Id::detach) .ok() }); @@ -46,13 +52,25 @@ impl crate::Repository { .open(root.join(".mailmap")) .map_err(|e| { if e.kind() != std::io::ErrorKind::NotFound { - err.get_or_insert(e.into()); + err.get_or_insert( + e.and_raise(gix_error::message( + "The mailmap file declared in `mailmap.file` could not be read", + )) + .into(), + ); } }) { buf.clear(); std::io::copy(&mut file, &mut buf) - .map_err(|e| err.get_or_insert(e.into())) + .map_err(|e| { + err.get_or_insert( + e.and_raise(gix_error::message( + "The mailmap file declared in `mailmap.file` could not be read", + )) + .into(), + ) + }) .ok(); target.merge(gix_mailmap::parse_ignore_errors(&buf)); } @@ -61,7 +79,12 @@ impl crate::Repository { if let Some(blob) = blob_id.and_then(|id| { self.find_object(id) - .map_err(|e| err.get_or_insert(crate::mailmap::load::Error::FindExisting(e))) + .map_err(|e| { + err.get_or_insert( + e.and_raise(gix_error::message("Could not find object configured in `mailmap.blob`")) + .into(), + ) + }) .ok() }) { target.merge(gix_mailmap::parse_ignore_errors(&blob.data)); @@ -70,16 +93,32 @@ impl crate::Repository { let configured_path = self .config_snapshot() .trusted_path(Mailmap::FILE) - .map_err(|e| err.get_or_insert(e.into())) + .map_err(|e| err.get_or_insert(gix_error::Error::from_error(e))) .ok() .flatten(); - if let Some(mut file) = - configured_path.and_then(|path| std::fs::File::open(path).map_err(|e| err.get_or_insert(e.into())).ok()) - { + if let Some(mut file) = configured_path.and_then(|path| { + std::fs::File::open(path) + .map_err(|e| { + err.get_or_insert( + e.and_raise(gix_error::message( + "The mailmap file declared in `mailmap.file` could not be read", + )) + .into(), + ) + }) + .ok() + }) { buf.clear(); std::io::copy(&mut file, &mut buf) - .map_err(|e| err.get_or_insert(e.into())) + .map_err(|e| { + err.get_or_insert( + e.and_raise(gix_error::message( + "The mailmap file declared in `mailmap.file` could not be read", + )) + .into(), + ) + }) .ok(); target.merge(gix_mailmap::parse_ignore_errors(&buf)); } diff --git a/gix/src/repository/merge.rs b/gix/src/repository/merge.rs index 663af4b92e4..38f13e9fe77 100644 --- a/gix/src/repository/merge.rs +++ b/gix/src/repository/merge.rs @@ -1,3 +1,5 @@ +use gix_error::ResultExt; + use gix_merge::blob::builtin_driver::text; use gix_object::Write; @@ -27,7 +29,8 @@ impl Repository { let mode = { let renormalize = tree::Merge::RENORMALIZE .enrich_error(self.config.resolved.boolean(tree::Merge::RENORMALIZE)) - .with_lenient_default(self.config.lenient_config)? + .with_lenient_default(self.config.lenient_config) + .or_erased()? .unwrap_or_default(); if renormalize { gix_merge::blob::pipeline::Mode::Renormalize @@ -43,7 +46,8 @@ impl Repository { } else { gix_worktree::stack::state::attributes::Source::WorktreeThenIdMapping }, - )? + ) + .or_erased()? .inner; let filter = gix_filter::Pipeline::new(self.command_context()?, crate::filter::Pipeline::options(self)?); let filter = gix_merge::blob::Pipeline::new(worktree_roots, filter, self.config.merge_pipeline_options()?); @@ -61,7 +65,7 @@ impl Repository { is_virtual_ancestor: false, resolve_binary_with: None, text: gix_merge::blob::builtin_driver::text::Options { - diff_algorithm: self.diff_algorithm()?, + diff_algorithm: self.diff_algorithm().or_erased()?, conflict: text::Conflict::Keep { style: self .config @@ -72,7 +76,8 @@ impl Repository { .try_into_conflict_style(value) .with_lenient_default(self.config.lenient_config) }) - .transpose()? + .transpose() + .or_erased()? .unwrap_or_default(), marker_size: text::Conflict::DEFAULT_MARKER_SIZE.try_into().unwrap(), }, @@ -148,9 +153,10 @@ impl Repository { &mut diff_cache, &mut blob_merge, options.into(), - )?; + ) + .map_err(gix_error::Exn::into_error)?; - let validate = self.config.protect_options()?; + let validate = self.config.protect_options().or_erased()?; Ok(crate::merge::tree::Outcome { tree: crate::object::tree::Editor { inner: tree, @@ -209,9 +215,10 @@ impl Repository { self, &mut |id| id.to_owned().attach(self).shorten_or_id().to_string(), options.into(), - )?; + ) + .map_err(gix_error::Error::from)?; - let validate = self.config.protect_options()?; + let validate = self.config.protect_options().or_erased()?; let tree_merge = crate::merge::tree::Outcome { tree: crate::object::tree::Editor { inner: tree, @@ -245,7 +252,7 @@ impl Repository { ) -> Result, virtual_merge_base::Error> { let commit_graph = self.commit_graph_if_enabled()?; let mut graph = self.revision_graph(commit_graph.as_ref()); - Ok(self.virtual_merge_base_with_graph(merge_bases, &mut graph, options)?) + self.virtual_merge_base_with_graph(merge_bases, &mut graph, options) } /// Like [`Self::virtual_merge_base()`], but also allows to reuse a `graph` for faster merge-base calculation, @@ -259,9 +266,9 @@ impl Repository { let mut merge_bases: Vec<_> = merge_bases.into_iter().map(Into::into).collect(); let first = merge_bases .pop() - .ok_or(virtual_merge_base_with_graph::Error::MissingCommit)?; + .ok_or_else(|| gix_error::Error::from_error(gix_error::message("No commit was provided as merge-base")))?; let Some(second) = merge_bases.pop() else { - let tree_id = self.find_commit(first)?.tree_id()?; + let tree_id = self.find_commit(first)?.tree_id().or_erased()?; let commit_id = first.attach(self); return Ok(crate::merge::virtual_merge_base::Outcome { virtual_merge_bases: Vec::new(), @@ -287,7 +294,8 @@ impl Repository { self, &mut |id| id.to_owned().attach(self).shorten_or_id().to_string(), options.into(), - )?; + ) + .map_err(gix_error::Error::from)?; Ok(crate::merge::virtual_merge_base::Outcome { virtual_merge_bases: virtual_merge_bases.into_iter().map(|id| id.attach(self)).collect(), diff --git a/gix/src/repository/mod.rs b/gix/src/repository/mod.rs index 9e1c1c2411f..8d3ba4a2bfa 100644 --- a/gix/src/repository/mod.rs +++ b/gix/src/repository/mod.rs @@ -67,29 +67,13 @@ pub(crate) mod worktree; /// mod new_commit { /// The error returned by [`new_commit(…)`](crate::Repository::new_commit()). - #[derive(Debug, thiserror::Error)] - pub enum Error { - #[error(transparent)] - ParseTime(#[from] crate::config::time::Error), - #[error("Committer identity is not configured")] - CommitterMissing, - #[error("Author identity is not configured")] - AuthorMissing, - #[error(transparent)] - NewCommitAs(#[from] crate::repository::new_commit_as::Error), - } + pub type Error = gix_error::Error; } /// mod new_commit_as { /// The error returned by [`new_commit_as(…)`](crate::Repository::new_commit_as()). - #[derive(Debug, thiserror::Error)] - pub enum Error { - #[error(transparent)] - WriteObject(#[from] crate::object::write::Error), - #[error(transparent)] - FindCommit(#[from] crate::object::find::existing::Error), - } + pub type Error = gix_error::Error; } /// @@ -109,451 +93,176 @@ pub mod blame_file { } /// The error returned by [Repository::blame_file()](crate::Repository::blame_file()). - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - CommitGraphIfEnabled(#[from] super::commit_graph_if_enabled::Error), - #[error(transparent)] - DiffAlgorithm(#[from] crate::config::diff::algorithm::Error), - #[error(transparent)] - DiffResourceCache(#[from] super::diff_resource_cache::Error), - #[error(transparent)] - Blame(gix_error::Error), - } + pub type Error = gix_error::Error; } /// #[cfg(feature = "blob-diff")] pub mod diff_tree_to_tree { /// The error returned by [Repository::diff_tree_to_tree()](crate::Repository::diff_tree_to_tree()). - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - DiffOptions(#[from] crate::diff::options::init::Error), - #[error(transparent)] - CreateResourceCache(#[from] super::diff_resource_cache::Error), - #[error(transparent)] - TreeDiff(#[from] gix_diff::tree::Error), - } + pub type Error = gix_error::Error; } /// #[cfg(feature = "merge")] pub mod blob_merge_options { /// The error returned by [Repository::blob_merge_options()](crate::Repository::blob_merge_options()). - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - DiffAlgorithm(#[from] crate::config::diff::algorithm::Error), - #[error(transparent)] - ConflictStyle(#[from] crate::config::key::GenericErrorWithValue), - } + pub type Error = gix_error::Error; } /// #[cfg(feature = "merge")] pub mod merge_resource_cache { /// The error returned by [Repository::merge_resource_cache()](crate::Repository::merge_resource_cache()). - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - RenormalizeConfig(#[from] crate::config::boolean::Error), - #[error(transparent)] - PipelineOptions(#[from] crate::config::merge::pipeline_options::Error), - #[error(transparent)] - Index(#[from] crate::repository::index_or_load_from_head_or_empty::Error), - #[error(transparent)] - AttributeStack(#[from] crate::config::attribute_stack::Error), - #[error(transparent)] - CommandContext(#[from] crate::config::command_context::Error), - #[error(transparent)] - FilterPipeline(#[from] crate::filter::pipeline::options::Error), - #[error(transparent)] - DriversConfig(#[from] crate::config::merge::drivers::Error), - } + pub type Error = gix_error::Error; } /// #[cfg(feature = "merge")] pub mod merge_trees { /// The error returned by [Repository::merge_trees()](crate::Repository::merge_trees()). - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - MergeResourceCache(#[from] super::merge_resource_cache::Error), - #[error(transparent)] - DiffResourceCache(#[from] super::diff_resource_cache::Error), - #[error(transparent)] - TreeMerge(#[from] gix_merge::tree::Error), - #[error(transparent)] - ValidationOptions(#[from] crate::config::boolean::Error), - } + pub type Error = gix_error::Error; } /// #[cfg(feature = "merge")] pub mod merge_commits { /// The error returned by [Repository::merge_commits()](crate::Repository::merge_commits()). - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - OpenCommitGraph(#[from] super::commit_graph_if_enabled::Error), - #[error(transparent)] - MergeResourceCache(#[from] super::merge_resource_cache::Error), - #[error(transparent)] - DiffResourceCache(#[from] super::diff_resource_cache::Error), - #[error(transparent)] - CommitMerge(#[from] gix_merge::commit::Error), - #[error(transparent)] - ValidationOptions(#[from] crate::config::boolean::Error), - } + pub type Error = gix_error::Error; } /// #[cfg(feature = "merge")] pub mod virtual_merge_base { /// The error returned by [Repository::virtual_merge_base()](crate::Repository::virtual_merge_base()). - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - OpenCommitGraph(#[from] super::commit_graph_if_enabled::Error), - #[error(transparent)] - VirtualMergeBase(#[from] super::virtual_merge_base_with_graph::Error), - } + pub type Error = gix_error::Error; } /// #[cfg(feature = "merge")] pub mod virtual_merge_base_with_graph { /// The error returned by [Repository::virtual_merge_base_with_graph()](crate::Repository::virtual_merge_base_with_graph()). - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error("No commit was provided as merge-base")] - MissingCommit, - #[error(transparent)] - MergeResourceCache(#[from] super::merge_resource_cache::Error), - #[error(transparent)] - DiffResourceCache(#[from] super::diff_resource_cache::Error), - #[error(transparent)] - CommitMerge(#[from] gix_merge::commit::Error), - #[error(transparent)] - FindCommit(#[from] crate::object::find::existing::with_conversion::Error), - #[error(transparent)] - DecodeCommit(#[from] gix_object::decode::Error), - } + pub type Error = gix_error::Error; } /// #[cfg(feature = "revision")] pub mod merge_base_octopus_with_graph { /// The error returned by [Repository::merge_base_octopus_with_graph()](crate::Repository::merge_base_octopus_with_graph()). - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error("No commit was provided")] - MissingCommit, - #[error("No merge base was found between the given commits")] - NoMergeBase, - #[error(transparent)] - MergeBase(#[from] gix_revision::merge_base::Error), - } + pub type Error = gix_error::Error; } /// #[cfg(feature = "revision")] pub mod merge_base_octopus { /// The error returned by [Repository::merge_base_octopus()](crate::Repository::merge_base_octopus()). - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - OpenCache(#[from] crate::repository::commit_graph_if_enabled::Error), - #[error(transparent)] - MergeBaseOctopus(#[from] super::merge_base_octopus_with_graph::Error), - } + pub type Error = gix_error::Error; } /// #[cfg(feature = "revision")] pub mod merge_bases_many { /// The error returned by [Repository::merge_bases_many()](crate::Repository::merge_bases_many()). - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - OpenCache(#[from] crate::repository::commit_graph_if_enabled::Error), - #[error(transparent)] - MergeBase(#[from] gix_revision::merge_base::Error), - } + pub type Error = gix_error::Error; } /// #[cfg(feature = "merge")] pub mod tree_merge_options { /// The error returned by [Repository::tree_merge_options()](crate::Repository::tree_merge_options()). - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - BlobMergeOptions(#[from] super::blob_merge_options::Error), - #[error(transparent)] - RewritesConfig(#[from] crate::diff::new_rewrites::Error), - #[error(transparent)] - CommandContext(#[from] crate::config::command_context::Error), - } + pub type Error = gix_error::Error; } /// #[cfg(feature = "blob-diff")] pub mod diff_resource_cache { /// The error returned by [Repository::diff_resource_cache()](crate::Repository::diff_resource_cache()). - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error("Could not obtain resource cache for diffing")] - ResourceCache(#[from] crate::diff::resource_cache::Error), - #[error(transparent)] - Index(#[from] crate::repository::index_or_load_from_head_or_empty::Error), - #[error(transparent)] - AttributeStack(#[from] crate::config::attribute_stack::Error), - } + pub type Error = gix_error::Error; } /// pub mod edit_tree { /// The error returned by [Repository::edit_tree()](crate::Repository::edit_tree). - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - FindTree(#[from] crate::object::find::existing::with_conversion::Error), - #[error(transparent)] - InitEditor(#[from] crate::object::tree::editor::init::Error), - } + pub type Error = gix_error::Error; } /// #[cfg(feature = "revision")] pub mod merge_base { /// The error returned by [Repository::merge_base()](crate::Repository::merge_base()). - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - OpenCache(#[from] crate::repository::commit_graph_if_enabled::Error), - #[error(transparent)] - FindMergeBase(#[from] gix_revision::merge_base::Error), - #[error("Could not find a merge-base between commits {first} and {second}")] - NotFound { - first: gix_hash::ObjectId, - second: gix_hash::ObjectId, - }, - } + pub type Error = gix_error::Error; } /// #[cfg(feature = "revision")] pub mod merge_base_with_graph { /// The error returned by [Repository::merge_base_with_cache()](crate::Repository::merge_base_with_graph()). - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - FindMergeBase(#[from] gix_revision::merge_base::Error), - #[error("Could not find a merge-base between commits {first} and {second}")] - NotFound { - first: gix_hash::ObjectId, - second: gix_hash::ObjectId, - }, - } + pub type Error = gix_error::Error; } /// pub mod commit_graph_if_enabled { /// The error returned by [Repository::commit_graph_if_enabled()](crate::Repository::commit_graph_if_enabled()). - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - ConfigBoolean(#[from] crate::config::boolean::Error), - #[error(transparent)] - OpenCommitGraph(#[from] crate::Error), - } + pub type Error = gix_error::Error; } /// #[cfg(feature = "index")] pub mod index_from_tree { /// The error returned by [Repository::index_from_tree()](crate::Repository::index_from_tree). - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error("Could not create index from tree at {id}")] - IndexFromTree { - id: gix_hash::ObjectId, - source: gix_error::Error, - }, - #[error("Couldn't obtain configuration for core.protect*")] - BooleanConfig(#[from] crate::config::boolean::Error), - } + pub type Error = gix_error::Error; } /// pub mod branch_remote_ref_name { /// The error returned by [Repository::branch_remote_ref_name()](crate::Repository::branch_remote_ref_name()). - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error("The configured name of the remote ref to merge wasn't valid")] - ValidateFetchRemoteRefName(#[from] gix_validate::reference::name::Error), - #[error(transparent)] - PushDefault(#[from] crate::config::key::GenericErrorWithValue), - #[error(transparent)] - FindPushRemote(#[from] crate::remote::find::existing::Error), - } + pub type Error = gix_error::Error; } /// pub mod branch_remote_tracking_ref_name { /// The error returned by [Repository::branch_remote_tracking_ref_name()](crate::Repository::branch_remote_tracking_ref_name()). - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error("The name of the tracking reference was invalid")] - ValidateTrackingRef(#[from] gix_validate::reference::name::Error), - #[error("Could not get the remote reference to translate into the local tracking branch")] - RemoteRef(#[from] super::branch_remote_ref_name::Error), - #[error("Couldn't find remote to obtain fetch-specs for mapping to the tracking reference")] - FindRemote(#[from] crate::remote::find::existing::Error), - } + pub type Error = gix_error::Error; } /// pub mod upstream_branch_and_remote_name_for_tracking_branch { /// The error returned by [Repository::upstream_branch_and_remote_name_for_tracking_branch()](crate::Repository::upstream_branch_and_remote_for_tracking_branch()). - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error("The input branch '{}' needs to be a remote tracking branch", full_name.as_bstr())] - BranchCategory { full_name: gix_ref::FullName }, - #[error(transparent)] - FindRemote(#[from] crate::remote::find::existing::Error), - #[error("Found ambiguous remotes without 1:1 mapping or more than one match: {}", remotes.iter() - .map(|r| r.as_bstr().to_string()) - .collect::>().join(", "))] - AmbiguousRemotes { remotes: Vec> }, - #[error(transparent)] - ValidateUpstreamBranch(#[from] gix_ref::name::Error), - } + pub type Error = gix_error::Error; } /// pub mod normalize_path { /// The error returned by [Repository::normalize_path()](crate::Repository::normalize_path()). - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - Realpath(#[from] gix_path::realpath::Error), - #[error("The path '{}' leaves the repository", path.display())] - OutsideOfRepository { path: std::path::PathBuf }, - #[error( - "The absolute path '{}' is not inside the repository at '{}'", - path.display(), - root.display() - )] - AbsolutePathOutsideOfRepository { - path: std::path::PathBuf, - root: std::path::PathBuf, - }, - } + pub type Error = gix_error::Error; } /// #[cfg(feature = "attributes")] pub mod pathspec_defaults_ignore_case { /// The error returned by [Repository::pathspec_defaults_ignore_case()](crate::Repository::pathspec_defaults_inherit_ignore_case()). - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error("Filesystem configuration could not be obtained to learn about case sensitivity")] - FilesystemConfig(#[from] crate::config::boolean::Error), - #[error(transparent)] - Defaults(#[from] gix_error::Error), - } + pub type Error = gix_error::Error; } /// #[cfg(feature = "index")] pub mod index_or_load_from_head { /// The error returned by [`Repository::index_or_load_from_head()`](crate::Repository::index_or_load_from_head()). - #[derive(thiserror::Error, Debug)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - HeadCommit(#[from] crate::reference::head_commit::Error), - #[error(transparent)] - TreeId(#[from] gix_object::decode::Error), - #[error(transparent)] - TraverseTree(#[from] crate::repository::index_from_tree::Error), - #[error(transparent)] - OpenIndex(#[from] crate::worktree::open_index::Error), - } + pub type Error = gix_error::Error; } /// #[cfg(feature = "index")] pub mod index_or_load_from_head_or_empty { /// The error returned by [`Repository::index_or_load_from_head_or_empty()`](crate::Repository::index_or_load_from_head_or_empty()). - #[derive(thiserror::Error, Debug)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - ReadHead(#[from] crate::reference::find::existing::Error), - #[error(transparent)] - FindCommit(#[from] crate::object::find::existing::Error), - #[error(transparent)] - PeelToTree(#[from] crate::object::peel::to_kind::Error), - #[error(transparent)] - TreeId(#[from] gix_object::decode::Error), - #[error(transparent)] - TraverseTree(#[from] crate::repository::index_from_tree::Error), - #[error(transparent)] - OpenIndex(#[from] crate::worktree::open_index::Error), - } + pub type Error = gix_error::Error; } /// #[cfg(feature = "worktree-stream")] pub mod worktree_stream { /// The error returned by [`Repository::worktree_stream()`](crate::Repository::worktree_stream()). - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - FindTree(#[from] crate::object::find::existing::Error), - #[error(transparent)] - OpenTree(#[from] crate::repository::index_from_tree::Error), - #[error(transparent)] - AttributesCache(#[from] crate::config::attribute_stack::Error), - #[error(transparent)] - FilterPipeline(#[from] crate::filter::pipeline::options::Error), - #[error(transparent)] - CommandContext(#[from] crate::config::command_context::Error), - #[error("Needed {id} to be a tree to turn into a workspace stream, got {actual}")] - NotATree { - id: gix_hash::ObjectId, - actual: gix_object::Kind, - }, - } + pub type Error = gix_error::Error; } /// diff --git a/gix/src/repository/object.rs b/gix/src/repository/object.rs index 14ee7fd3856..0721550d20f 100644 --- a/gix/src/repository/object.rs +++ b/gix/src/repository/object.rs @@ -1,7 +1,8 @@ #![allow(clippy::result_large_err)] +use gix_error::ResultExt; + use std::ops::DerefMut; -use gix_error::ResultExt; use gix_hash::ObjectId; use gix_object::{Exists, Find, FindExt, Write}; use gix_odb::{Header, HeaderExt}; @@ -22,7 +23,7 @@ impl crate::Repository { id: impl Into, ) -> Result, crate::repository::edit_tree::Error> { let tree = self.find_tree(id)?; - Ok(tree.edit()?) + tree.edit() } } @@ -61,7 +62,11 @@ impl crate::Repository { }); } let mut buf = self.free_buf(); - let kind = self.objects.find(&id, &mut buf)?.kind; + let kind = self + .objects + .find(&id, &mut buf) + .map_err(crate::object::existing_error)? + .kind; Ok(Object::from_data(id, kind, buf, self)) } @@ -82,7 +87,7 @@ impl crate::Repository { &self, id: impl Into, ) -> Result, object::find::existing::with_conversion::Error> { - Ok(self.find_object(id)?.try_into_commit()?) + Ok(self.find_object(id)?.try_into_commit().or_erased()?) } /// Find a tree with `id` or fail if there was no object or the object wasn't a tree. @@ -102,12 +107,12 @@ impl crate::Repository { &self, id: impl Into, ) -> Result, object::find::existing::with_conversion::Error> { - Ok(self.find_object(id)?.try_into_tree()?) + Ok(self.find_object(id)?.try_into_tree().or_erased()?) } /// Find an annotated tag with `id` or fail if there was no object or the object wasn't a tag. pub fn find_tag(&self, id: impl Into) -> Result, object::find::existing::with_conversion::Error> { - Ok(self.find_object(id)?.try_into_tag()?) + Ok(self.find_object(id)?.try_into_tag().or_erased()?) } /// Find a blob with `id` or fail if there was no object or the object wasn't a blob. @@ -115,7 +120,7 @@ impl crate::Repository { &self, id: impl Into, ) -> Result, object::find::existing::with_conversion::Error> { - Ok(self.find_object(id)?.try_into_blob()?) + Ok(self.find_object(id)?.try_into_blob().or_erased()?) } /// Obtain information about an object without fully decoding it, or fail if the object doesn't exist. @@ -144,7 +149,7 @@ impl crate::Repository { size: 0, }); } - self.objects.header(id).map_err(Into::into) + self.objects.header(id).map_err(crate::object::existing_error) } /// Return `true` if `id` exists in the object database. @@ -201,7 +206,7 @@ impl crate::Repository { size: 0, })); } - self.objects.try_header(&id).map_err(Into::into) + Ok(self.objects.try_header(&id).or_erased()?) } /// Try to find the object with `id` or return `None` if it wasn't found. @@ -229,7 +234,7 @@ impl crate::Repository { } let mut buf = self.free_buf(); - match self.objects.try_find(&id, &mut buf)? { + match self.objects.try_find(&id, &mut buf).or_erased()? { Some(obj) => { let kind = obj.kind; Ok(Some(Object::from_data(id, kind, buf, self))) @@ -258,10 +263,11 @@ impl crate::Repository { return Ok(oid.attach(self)); } - self.objects + Ok(self + .objects .write_buf_with_known_id(kind, buf, oid) .map(|oid| oid.attach(self)) - .map_err(Into::into) + .or_erased()?) } /// Write a blob from the given `bytes`. @@ -287,10 +293,11 @@ impl crate::Repository { if self.objects.exists(&oid) { return Ok(oid.attach(self)); } - self.objects + Ok(self + .objects .write_buf_with_known_id(gix_object::Kind::Blob, bytes, oid) - .map_err(Into::into) .map(|oid| oid.attach(self)) + .or_erased()?) } /// Write a blob from the given `Read` implementation. @@ -312,10 +319,11 @@ impl crate::Repository { return Ok(oid.attach(self)); } - self.objects + Ok(self + .objects .write_buf_with_known_id(gix_object::Kind::Blob, buf, oid) - .map_err(Into::into) .map(|oid| oid.attach(self)) + .or_erased()?) } } @@ -339,12 +347,12 @@ impl crate::Repository { target: target.as_ref().into(), target_kind, name: name.as_ref().into(), - tagger: tagger.map(|t| t.to_owned()).transpose()?, + tagger: tagger.map(|t| t.to_owned()).transpose().or_erased()?, message: message.as_ref().into(), signature: None, }; let tag_id = self.write_object(&tag)?; - self.tag_reference(name, tag_id, constraint).map_err(Into::into) + self.tag_reference(name, tag_id, constraint) } /// Similar to [`commit(…)`](crate::Repository::commit()), but allows to create the commit with `committer` and `author` specified. @@ -361,12 +369,12 @@ impl crate::Repository { ) -> Result, commit::Error> where Name: TryInto, - commit::Error: From, + E: std::error::Error + Send + Sync + 'static, { self.commit_as_inner( committer.into(), author.into(), - reference.try_into()?, + reference.try_into().or_erased()?, message.as_ref(), tree.into(), parents.into_iter().map(Into::into).collect(), @@ -449,10 +457,16 @@ impl crate::Repository { ) -> Result, commit::Error> where Name: TryInto, - commit::Error: From, + E: std::error::Error + Send + Sync + 'static, { - let author = self.author().ok_or(commit::Error::AuthorMissing)??; - let committer = self.committer().ok_or(commit::Error::CommitterMissing)??; + let author = self + .author() + .ok_or_else(|| gix_error::Error::from_error(gix_error::message("Author identity is not configured")))? + .or_erased()?; + let committer = self + .committer() + .ok_or_else(|| gix_error::Error::from_error(gix_error::message("Committer identity is not configured")))? + .or_erased()?; self.commit_as(committer, author, reference, message, tree, parents) } @@ -468,9 +482,19 @@ impl crate::Repository { tree: impl Into, parents: impl IntoIterator>, ) -> Result, new_commit::Error> { - let author = self.author().ok_or(new_commit::Error::AuthorMissing)??; - let committer = self.committer().ok_or(new_commit::Error::CommitterMissing)??; - Ok(self.new_commit_as(committer, author, message, tree, parents)?) + let author = self + .author() + .ok_or_else(|| { + gix_error::Error::from_error(gix_error::ValidationError::new("Author identity is not configured")) + })? + .map_err(gix_error::Error::from)?; + let committer = self + .committer() + .ok_or_else(|| { + gix_error::Error::from_error(gix_error::ValidationError::new("Committer identity is not configured")) + })? + .map_err(gix_error::Error::from)?; + self.new_commit_as(committer, author, message, tree, parents) } /// Create a nwe commit object with `message` referring to `tree` with `parents`, using the specified diff --git a/gix/src/repository/pathspec.rs b/gix/src/repository/pathspec.rs index a527a2e62f6..7d5478e1906 100644 --- a/gix/src/repository/pathspec.rs +++ b/gix/src/repository/pathspec.rs @@ -1,3 +1,4 @@ +use gix_error::{ErrorExt, ResultExt}; use gix_pathspec::MagicSignature; use crate::{AttributeStack, Pathspec, Repository, bstr::BStr, config::cache::util::ApplyLeniencyDefault}; @@ -25,7 +26,7 @@ impl Repository { Pathspec::new(self, empty_patterns_match_prefix, patterns, inherit_ignore_case, || { self.attributes_only(index, attributes_source) .map(AttributeStack::detach) - .map_err(Into::into) + .or_erased() }) } @@ -48,7 +49,12 @@ impl Repository { && self .config .fs_capabilities() - .with_lenient_default(self.config.lenient_config)? + .with_lenient_default(self.config.lenient_config) + .map_err(|err| { + gix_error::Error::from(err.and_raise(gix_error::message( + "Filesystem configuration could not be obtained to learn about case sensitivity", + ))) + })? .ignore_case { defaults.signature |= MagicSignature::ICASE; diff --git a/gix/src/repository/reference.rs b/gix/src/repository/reference.rs index bc0cc7a254a..3ecf6153ccb 100644 --- a/gix/src/repository/reference.rs +++ b/gix/src/repository/reference.rs @@ -5,6 +5,7 @@ use gix_ref::{ }; use crate::{Reference, bstr::BString, ext::ReferenceExt, reference}; +use gix_error::{ErrorExt, ResultExt}; /// Obtain and alter references comfortably impl crate::Repository { @@ -19,12 +20,14 @@ impl crate::Repository { constraint: PreviousValue, ) -> Result, reference::edit::Error> { let id = target.into(); - let mut edits = self.edit_reference(RefEdit::update( - format!("refs/tags/{}", name.as_ref()).try_into()?, - id, - constraint, - "", - ))?; + let name = format!("refs/tags/{}", name.as_ref()).try_into().map_err( + |err: gix_validate::reference::name::Error| { + gix_error::Error::from( + err.and_raise(gix_error::ValidationError::new("The tag reference name is invalid")), + ) + }, + )?; + let mut edits = self.edit_reference(RefEdit::update(name, id, constraint, ""))?; assert_eq!(edits.len(), 1, "reference splits should ever happen"); let edit = edits.pop().expect("exactly one item"); Ok(Reference { @@ -85,7 +88,13 @@ impl crate::Repository { gix_validate::reference::name::Error: From, { self.reference_inner( - name.try_into().map_err(gix_validate::reference::name::Error::from)?, + name.try_into() + .map_err(gix_validate::reference::name::Error::from) + .map_err(|err| { + gix_error::Error::from( + err.and_raise(gix_error::ValidationError::new("The reference name is invalid")), + ) + })?, target.into(), constraint, log_message.into(), @@ -144,12 +153,19 @@ impl crate::Repository { edits: impl IntoIterator, committer: Option>, ) -> Result, reference::edit::Error> { - let (file_lock_fail, packed_refs_lock_fail) = self.config.lock_timeout()?; - self.refs + let (file_lock_fail, packed_refs_lock_fail) = self.config.lock_timeout().map_err(|err| { + gix_error::Error::from(err.and_raise(gix_error::message( + "Could not interpret core.filesRefLockTimeout or core.packedRefsTimeout, it must be the number in \ + milliseconds to wait for locks or negative to wait forever", + ))) + })?; + Ok(self + .refs .transaction() - .prepare(edits, file_lock_fail, packed_refs_lock_fail)? + .prepare(edits, file_lock_fail, packed_refs_lock_fail) + .or_erased()? .commit(committer) - .map_err(Into::into) + .or_erased()?) } /// Return the repository head, an abstraction to help dealing with the `HEAD` reference. @@ -174,7 +190,7 @@ impl crate::Repository { Ok(match head.inner.target { Target::Symbolic(branch) => match self.find_reference(&branch) { Ok(r) => crate::head::Kind::Symbolic(r.detach()), - Err(reference::find::existing::Error::NotFound { .. }) => crate::head::Kind::Unborn(branch), + Err(err) if err.is_not_found() => crate::head::Kind::Unborn(branch), Err(err) => return Err(err), }, Target::Object(target) => crate::head::Kind::Detached { @@ -194,7 +210,7 @@ impl crate::Repository { /// Also note that the returned id is likely to point to a commit, but could also /// point to a tree or blob. It won't, however, point to a tag as these are always peeled. pub fn head_id(&self) -> Result, reference::head_id::Error> { - Ok(self.head()?.into_peeled_id()?) + self.head()?.into_peeled_id() } /// Return the name to the symbolic reference `HEAD` points to, or `None` if the head is detached. @@ -236,7 +252,7 @@ impl crate::Repository { /// # Ok(()) } /// ``` pub fn head_commit(&self) -> Result, reference::head_commit::Error> { - Ok(self.head()?.peel_to_commit()?) + self.head()?.peel_to_commit() } /// Return the tree id the `HEAD` reference currently points to after peeling it fully, @@ -246,23 +262,17 @@ impl crate::Repository { /// is freshly initialized and doesn't have any commits yet. It could also fail if the /// head does not point to a commit. pub fn head_tree_id(&self) -> Result, reference::head_tree_id::Error> { - Ok(self.head_commit()?.tree_id()?) + Ok(self.head_commit()?.tree_id().or_erased()?) } /// Like [`Self::head_tree_id()`], but will return an empty tree hash if the repository HEAD is unborn. pub fn head_tree_id_or_empty(&self) -> Result, reference::head_tree_id::Error> { - self.head_tree_id().or_else(|err| { - if let reference::head_tree_id::Error::HeadCommit(reference::head_commit::Error::PeelToCommit( - crate::head::peel::to_commit::Error::PeelToObject(crate::head::peel::to_object::Error::Unborn { - .. - }), - )) = err - { - Ok(self.empty_tree().id()) - } else { - Err(err) - } - }) + let mut head = self.head()?; + if head.is_unborn() { + Ok(self.empty_tree().id()) + } else { + Ok(head.peel_to_commit()?.tree_id().or_erased()?) + } } /// Return the tree object the `HEAD^{tree}` reference currently points to after peeling it fully, @@ -284,7 +294,7 @@ impl crate::Repository { /// # Ok(()) } /// ``` pub fn head_tree(&self) -> Result, reference::head_tree::Error> { - Ok(self.head_commit()?.tree()?) + self.head_commit()?.tree() } /// Find the reference with the given partial or full `name`, like `main`, `HEAD`, `heads/branch` or `origin/other`, @@ -315,11 +325,14 @@ impl crate::Repository { let partial_name = name .clone() .try_into() - .map_err(|err| reference::find::Error::Find(gix_ref::file::find::Error::from(err)))?; - self.try_find_reference(name)? - .ok_or_else(|| reference::find::existing::Error::NotFound { - name: partial_name.to_owned(), - }) + .map_err(gix_ref::file::find::Error::from) + .or_erased()?; + self.try_find_reference(name)?.ok_or_else(|| { + gix_error::Error::from_error(gix_error::NotFoundError::new(format!( + "The reference '{}' did not exist", + partial_name.as_bstr() + ))) + }) } /// Return a platform for iterating references. @@ -362,7 +375,7 @@ impl crate::Repository { Some(r) => Ok(Some(Reference::from_ref(r, self))), None => Ok(None), }, - Err(err) => Err(err.into()), + Err(err) => Err(gix_error::Error::from_error(err)), } } } diff --git a/gix/src/repository/remote.rs b/gix/src/repository/remote.rs index ff76e264b27..fcfb09264af 100644 --- a/gix/src/repository/remote.rs +++ b/gix/src/repository/remote.rs @@ -1,5 +1,6 @@ #![allow(clippy::result_large_err)] use crate::{Remote, bstr::BStr, config, remote, remote::find}; +use gix_error::{ErrorExt, ResultExt}; impl crate::Repository { /// Create a new remote available at the given `url`. @@ -67,11 +68,11 @@ impl crate::Repository { /// ``` pub fn find_remote(&self, name_or_url: impl gix_utils::AsBStr) -> Result, find::existing::Error> { let name_or_url = name_or_url.as_bstr(); - Ok(self - .try_find_remote(name_or_url) - .ok_or_else(|| find::existing::Error::NotFound { - name: name_or_url.into(), - })??) + self.try_find_remote(name_or_url).ok_or_else(|| { + gix_error::Error::from_error(gix_error::NotFoundError::new(format!( + "The remote named {name_or_url:?} did not exist" + ))) + })? } /// Find the default remote as configured, or `None` if no such configuration could be found. @@ -151,7 +152,9 @@ impl crate::Repository { Ok(match name_or_url { Some(name) => match self.try_find_remote(name).and_then(Result::ok) { Some(remote) => remote, - None => self.remote_at(gix_url::parse(name).map_err(gix_error::Exn::into_error)?)?, + None => self.remote_at( + gix_url::parse(name).or_raise(|| gix_error::message("remote name could not be parsed as URL"))?, + )?, }, None => self .head()? @@ -159,7 +162,11 @@ impl crate::Repository { .transpose()? .map(Ok) .or_else(|| self.find_default_remote(remote::Direction::Fetch)) - .ok_or(find::for_fetch::Error::ExactlyOneRemoteNotAvailable)??, + .ok_or_else(|| { + gix_error::Error::from_error(gix_error::NotFoundError::new( + "No configured remote could be found, or too many were available", + )) + })??, }) } @@ -188,10 +195,11 @@ impl crate::Repository { specs .into_iter() .map(|spec| { - key.try_into_refspec(spec, op).map_err(|err| find::Error::RefSpec { - remote_name: name_or_url.into(), - kind, - source: err, + key.try_into_refspec(spec, op).map_err(|err| { + gix_error::Error::from(err.and_raise(gix_error::ValidationError::new_with_input( + format!("{kind} ref-spec under `remote.{name_or_url}` was invalid"), + name_or_url.to_owned(), + ))) }) }) .collect::, _>>() @@ -232,10 +240,11 @@ impl crate::Repository { effective_urls .into_iter() .map(|url| { - key.try_into_url(url).map_err(|err| find::Error::Url { - kind, - remote_name: name_or_url.into(), - source: err, + key.try_into_url(url).map_err(|err| { + gix_error::Error::from(err.and_raise(gix_error::ValidationError::new_with_input( + format!("The {kind} url under `remote.{name_or_url}` was invalid"), + name_or_url.to_owned(), + ))) }) }) .collect() @@ -268,9 +277,11 @@ impl crate::Repository { let fetch_tags = config .string_filter(&format!("remote.{}.{}", name_or_url, "tagOpt"), &mut filter) .map(|value| { - config::tree::Remote::TAG_OPT - .try_into_tag_opt(value) - .map_err(Into::into) + config::tree::Remote::TAG_OPT.try_into_tag_opt(value).map_err(|err| { + gix_error::Error::from(err.and_raise(gix_error::message( + "The value for 'remote..tagOpt` is invalid and must either be '--tags' or '--no-tags'", + ))) + }) }); let fetch_tags = match fetch_tags { Some(Ok(v)) => v, @@ -310,29 +321,27 @@ impl crate::Repository { Ok(url) if name_is_url || url.scheme != gix_url::Scheme::File => urls.push(url), Ok(_) => {} Err(source) if name_is_url => { - return Some(Err(find::Error::Url { - kind: "fetch", - remote_name: name_or_url.into(), - source, - })); + return Some(Err(gix_error::Error::from(source.and_raise( + gix_error::ValidationError::new_with_input( + format!("The fetch url under `remote.{name_or_url}` was invalid"), + name_or_url.to_owned(), + ), + )))); } Err(_) => {} } } - Some( - Remote::from_preparsed_config( - Some(name_or_url.to_owned()), - urls, - push_urls, - fetch_specs, - push_specs, - rewrite_urls, - fetch_tags, - self, - ) - .map_err(Into::into), - ) + Some(Remote::from_preparsed_config( + Some(name_or_url.to_owned()), + urls, + push_urls, + fetch_specs, + push_specs, + rewrite_urls, + fetch_tags, + self, + )) } } } diff --git a/gix/src/repository/revision.rs b/gix/src/repository/revision.rs index 905a40d3189..df92c16ccd7 100644 --- a/gix/src/repository/revision.rs +++ b/gix/src/repository/revision.rs @@ -1,3 +1,6 @@ +#[cfg(feature = "revision")] +use gix_error::ResultExt; + use crate::revision; #[cfg(feature = "revision")] use crate::{Id, bstr::BStr}; @@ -44,9 +47,12 @@ impl crate::Repository { spec: impl Into<&'a BStr>, ) -> Result, revision::spec::parse::single::Error> { let spec = spec.into(); - self.rev_parse(spec)? - .single() - .ok_or(revision::spec::parse::single::Error::RangedRev { spec: spec.into() }) + self.rev_parse(spec)?.single().ok_or_else(|| { + let spec: crate::bstr::BString = spec.into(); + gix_error::Error::from_error(gix_error::message!( + "revspec {spec:?} did not resolve to a single object" + )) + }) } /// Obtain the best merge-base between commit `one` and `two`, or fail if there is none. @@ -65,10 +71,13 @@ impl crate::Repository { let two = two.into(); let cache = self.commit_graph_if_enabled()?; let mut graph = self.revision_graph(cache.as_ref()); - let bases = gix_revision::merge_base(one, &[two], &mut graph)?.ok_or(super::merge_base::Error::NotFound { - first: one, - second: two, - })?; + let bases = gix_revision::merge_base(one, &[two], &mut graph) + .or_erased()? + .ok_or_else(|| { + gix_error::Error::from_error(gix_error::message!( + "Could not find a merge-base between commits {one} and {two}" + )) + })?; Ok(bases.first().attach(self)) } @@ -87,10 +96,12 @@ impl crate::Repository { use crate::prelude::ObjectIdExt; let one = one.into(); let two = two.into(); - let bases = - gix_revision::merge_base(one, &[two], graph)?.ok_or(super::merge_base_with_graph::Error::NotFound { - first: one, - second: two, + let bases = gix_revision::merge_base(one, &[two], graph) + .or_erased()? + .ok_or_else(|| { + gix_error::Error::from_error(gix_error::message!( + "Could not find a merge-base between commits {one} and {two}" + )) })?; Ok(bases.first().attach(self)) } @@ -131,7 +142,7 @@ impl crate::Repository { ) -> Result>, crate::repository::merge_bases_many::Error> { let cache = self.commit_graph_if_enabled()?; let mut graph = self.revision_graph(cache.as_ref()); - Ok(self.merge_bases_many_with_graph(one, others, &mut graph)?) + Ok(self.merge_bases_many_with_graph(one, others, &mut graph).or_erased()?) } /// Return the best merge-base among all `commits`, or fail if `commits` yields no commit or no merge-base was found. @@ -143,14 +154,17 @@ impl crate::Repository { commits: impl IntoIterator>, graph: &mut gix_revwalk::Graph<'_, '_, gix_revwalk::graph::Commit>, ) -> Result, crate::repository::merge_base_octopus_with_graph::Error> { - use crate::{prelude::ObjectIdExt, repository::merge_base_octopus_with_graph}; + use crate::prelude::ObjectIdExt; let commits: Vec<_> = commits.into_iter().map(Into::into).collect(); let first = commits .first() .copied() - .ok_or(merge_base_octopus_with_graph::Error::MissingCommit)?; - gix_revision::merge_base::octopus(first, &commits[1..], graph)? - .ok_or(merge_base_octopus_with_graph::Error::NoMergeBase) + .ok_or_else(|| gix_error::Error::from_error(gix_error::message("No commit was provided")))?; + gix_revision::merge_base::octopus(first, &commits[1..], graph) + .or_erased()? + .ok_or_else(|| { + gix_error::Error::from_error(gix_error::message("No merge base was found between the given commits")) + }) .map(|id| id.attach(self)) } @@ -164,7 +178,7 @@ impl crate::Repository { ) -> Result, crate::repository::merge_base_octopus::Error> { let cache = self.commit_graph_if_enabled()?; let mut graph = self.revision_graph(cache.as_ref()); - Ok(self.merge_base_octopus_with_graph(commits, &mut graph)?) + self.merge_base_octopus_with_graph(commits, &mut graph) } /// Create the baseline for a revision walk by initializing it with the `tips` to start iterating on. diff --git a/gix/src/repository/submodule.rs b/gix/src/repository/submodule.rs index b9183d4fc8a..10b1eca7291 100644 --- a/gix/src/repository/submodule.rs +++ b/gix/src/repository/submodule.rs @@ -1,6 +1,7 @@ use std::rc::Rc; use crate::{Repository, submodule}; +use gix_error::ResultExt; impl Repository { /// Open the `.gitmodules` file as present in the worktree, or return `None` if no such file is available. @@ -21,12 +22,12 @@ impl Repository { let metadata = match std::fs::symlink_metadata(&path) { Ok(metadata) => metadata, Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None), - Err(err) => return Err(err.into()), + Err(err) => return Err(gix_error::Error::from_error(err)), }; if metadata.file_type().is_symlink() { return Ok(None); } - let buf = std::fs::read(&path)?; + let buf = std::fs::read(&path).or_raise(|| gix_error::message("Could not read '.gitmodules' file"))?; Ok(Some( gix_submodule::File::from_bytes(&buf, path, &self.config.resolved).map_err(gix_error::Exn::into_error)?, )) @@ -44,13 +45,17 @@ impl Repository { /// // TODO(submodule): make it use an updated snapshot instead once we have `config()`. pub fn modules(&self) -> Result, submodule::modules::Error> { - match self.modules.recent_snapshot( - || { - self.modules_path() - .and_then(|path| path.metadata().and_then(|m| m.modified()).ok()) - }, - || self.open_modules_file(), - )? { + match self + .modules + .recent_snapshot( + || { + self.modules_path() + .and_then(|path| path.metadata().and_then(|m| m.modified()).ok()) + }, + || self.open_modules_file(), + ) + .or_erased()? + { Some(m) => Ok(Some(m)), None => { let id = match self.try_index()?.and_then(|index| { @@ -60,7 +65,8 @@ impl Repository { }) { Some(id) => id, None => match self - .head()? + .head() + .or_erased()? .try_peel_to_id()? .map(|id| -> Result, submodule::modules::Error> { Ok(id @@ -78,9 +84,18 @@ impl Repository { }, }; Ok(Some(gix_features::threading::OwnShared::new( - gix_submodule::File::from_bytes(&self.find_object(id)?.data, None, &self.config.resolved) - .map_err(|err| submodule::open_modules_file::Error::from(err.into_error()))? - .into(), + gix_submodule::File::from_bytes( + &self + .find_object(id) + .or_raise(|| { + gix_error::message("Could not find the .gitmodules file by id in the object database") + })? + .data, + None, + &self.config.resolved, + ) + .map_err(gix_error::Exn::into_error)? + .into(), ))) } } diff --git a/gix/src/repository/worktree.rs b/gix/src/repository/worktree.rs index 4540e4c58a4..1d4e360221e 100644 --- a/gix/src/repository/worktree.rs +++ b/gix/src/repository/worktree.rs @@ -2,19 +2,8 @@ use std::{collections::BTreeMap, path::PathBuf}; use crate::bstr::BStr; use crate::{Worktree, worktree}; -#[cfg(feature = "worktree-archive")] use gix_error::ResultExt; -#[derive(Debug, thiserror::Error)] -pub(crate) enum CheckedOutBranchesError { - #[error("Failed to read or iterate worktree directories")] - WorktreeListing(#[from] std::io::Error), - #[error("Could not open a worktree repository")] - OpenWorktreeRepo(#[from] crate::open::Error), - #[error("Failed to follow a symbolic reference")] - FollowSymref(#[from] gix_ref::file::find::existing::Error), -} - /// Interact with individual worktrees and their information. impl crate::Repository { /// Return all references checked out in this repository's main and linked worktrees. @@ -25,11 +14,16 @@ impl crate::Repository { /// a `HEAD` entry. Bare repositories and worktrees whose head cannot be read are ignored. pub(crate) fn checked_out_branches( &self, - ) -> Result>, CheckedOutBranchesError> { + ) -> Result>, gix_error::Exn> { let mut map = BTreeMap::new(); insert_head(self.head().ok(), &mut map)?; - for proxy in self.worktrees()? { - let repo = proxy.into_repo_with_possibly_inaccessible_worktree()?; + for proxy in self + .worktrees() + .or_raise(|| gix_error::message("Failed to read or iterate worktree directories"))? + { + let repo = proxy + .into_repo_with_possibly_inaccessible_worktree() + .or_raise(|| gix_error::message("Could not open a worktree repository"))?; insert_head(repo.head().ok(), &mut map)?; } Ok(map) @@ -112,10 +106,10 @@ impl crate::Repository { let id = id.into(); let header = self.objects.header(id).map_err(gix_error::Exn::into_error)?; if !header.kind().is_tree() { - return Err(crate::repository::worktree_stream::Error::NotATree { - id, - actual: header.kind(), - }); + return Err(gix_error::Error::from_error(gix_error::ValidationError::new(format!( + "Needed {id} to be a tree to turn into a workspace stream, got {}", + header.kind() + )))); } // TODO(perf): potential performance improvements could be to use the index at `HEAD` if possible (`index_from_head_tree…()`) @@ -123,7 +117,8 @@ impl crate::Repository { // an object cache between the copies of the ODB handles isn't trivial and needs a lock. let index = self.index_from_tree(&id)?; let mut cache = self - .attributes_only(&index, gix_worktree::stack::state::attributes::Source::IdMapping)? + .attributes_only(&index, gix_worktree::stack::state::attributes::Source::IdMapping) + .or_erased()? .detach(); let pipeline = gix_filter::Pipeline::new(self.command_context()?, crate::filter::Pipeline::options(self)?); let objects = self.objects.clone().into_arc().expect("TBD error handling"); @@ -196,7 +191,7 @@ impl crate::Repository { fn insert_head( head: Option>, out: &mut BTreeMap>, -) -> Result<(), CheckedOutBranchesError> { +) -> Result<(), gix_error::Exn> { let Some((head, workdir)) = head.and_then(|head| head.repo.workdir().map(|workdir| (head, workdir))) else { return Ok(()); }; @@ -208,7 +203,10 @@ fn insert_head( out.entry(reference.name().to_owned()) .or_default() .push(workdir.to_owned()); - cursor = reference.follow().transpose()?; + cursor = reference + .follow() + .transpose() + .or_raise(|| gix_error::message("Failed to follow a symbolic reference"))?; } Ok(()) } diff --git a/gix/src/revision/spec/parse/delegate/revision.rs b/gix/src/revision/spec/parse/delegate/revision.rs index d603d9a9652..690d8711df5 100644 --- a/gix/src/revision/spec/parse/delegate/revision.rs +++ b/gix/src/revision/spec/parse/delegate/revision.rs @@ -207,18 +207,16 @@ impl delegate::Revision for Delegate<'_> { self.refs[self.idx] = Some(r.detach()); id } - Err(crate::reference::find::existing::Error::NotFound { .. }) => { - match ObjectId::from_hex(ref_name.as_ref()) { - Ok(id) if id.kind() == self.repo.object_hash() => id, - _ => { - return Err(message!( - "Previous checkout '{name}' does not resolve to an existing revision", - name = ref_name.as_bstr() - ) - .raise_erased()); - } + Err(err) if err.is_not_found() => match ObjectId::from_hex(ref_name.as_ref()) { + Ok(id) if id.kind() == self.repo.object_hash() => id, + _ => { + return Err(message!( + "Previous checkout '{name}' does not resolve to an existing revision", + name = ref_name.as_bstr() + ) + .raise_erased()); } - } + }, Err(err) => return Err(err.raise_erased()), }; let objs = self.objs[self.idx].get_or_insert_with(Vec::new); diff --git a/gix/src/revision/spec/parse/mod.rs b/gix/src/revision/spec/parse/mod.rs index 386ebf6e7ab..d29bc106814 100644 --- a/gix/src/revision/spec/parse/mod.rs +++ b/gix/src/revision/spec/parse/mod.rs @@ -9,17 +9,8 @@ use crate::bstr::BString; /// pub mod single { - use crate::bstr::BString; - /// The error returned by [`crate::Repository::rev_parse_single()`]. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - Parse(#[from] gix_error::Error), - #[error("revspec {spec:?} did not resolve to a single object")] - RangedRev { spec: BString }, - } + pub type Error = gix_error::Error; } /// diff --git a/gix/src/revision/walk.rs b/gix/src/revision/walk.rs index 4d48c8041dd..216463ac8b0 100644 --- a/gix/src/revision/walk.rs +++ b/gix/src/revision/walk.rs @@ -1,3 +1,5 @@ +use gix_error::ResultExt; + use gix_hash::ObjectId; use gix_object::FindExt; use gix_traverse::commit::simple::CommitTimeOrder; @@ -5,22 +7,7 @@ use gix_traverse::commit::simple::CommitTimeOrder; use crate::{Repository, ext::ObjectIdExt, revision}; /// The error returned by [`Platform::all()`] and [`Platform::selected()`]. -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] -pub enum Error { - #[error(transparent)] - SimpleTraversal(crate::Error), - #[error(transparent)] - ShallowCommits(#[from] crate::Error), - #[error(transparent)] - ConfigBoolean(#[from] crate::config::boolean::Error), -} - -impl From for Error { - fn from(err: gix_traverse::commit::simple::Error) -> Self { - Error::SimpleTraversal(err.into_error()) - } -} +pub type Error = gix_error::Error; /// Specify how to sort commits during a [revision::Walk] traversal. /// @@ -326,16 +313,19 @@ impl<'repo> Platform<'repo> { } } }) - .sorting(sorting.into_simple().expect("for now there is nothing else"))? + .sorting(sorting.into_simple().expect("for now there is nothing else")) + .map_err(gix_error::Exn::into_error)? .parents(parents) .commit_graph( commit_graph.or(use_commit_graph - .map_or_else(|| self.repo.config.may_use_commit_graph(), Ok)? + .map_or_else(|| self.repo.config.may_use_commit_graph(), Ok) + .or_erased()? .then(|| self.repo.commit_graph().ok()) .flatten()), ) - .hide(hidden)? - .map(|res| res.map_err(iter::Error::from)), + .hide(hidden) + .map_err(gix_error::Exn::into_error)? + .map(|res| res.map_err(gix_error::Exn::into_error)), ), }) } @@ -353,18 +343,7 @@ impl<'repo> Platform<'repo> { /// pub mod iter { /// The error returned by the [Walk](crate::revision::Walk) iterator. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - SimpleTraversal(crate::Error), - } - - impl From for Error { - fn from(err: gix_traverse::commit::simple::Error) -> Self { - Error::SimpleTraversal(err.into_error()) - } - } + pub type Error = gix_error::Error; } pub(crate) mod iter_impl { diff --git a/gix/src/status/index_worktree.rs b/gix/src/status/index_worktree.rs index f2901b41cf1..74eb68e5681 100644 --- a/gix/src/status/index_worktree.rs +++ b/gix/src/status/index_worktree.rs @@ -1,5 +1,7 @@ use std::sync::atomic::AtomicBool; +use gix_error::ResultExt; + use crate::{ Repository, bstr::{BStr, BString}, @@ -9,26 +11,7 @@ use crate::{ use gix_status::index_as_worktree::traits::{CompareBlobs, SubmoduleStatus}; /// The error returned by [Repository::index_worktree_status()]. -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] -pub enum Error { - #[error("A working tree is required to perform a directory walk")] - MissingWorkDir, - #[error(transparent)] - AttributesAndExcludes(#[from] crate::repository::attributes::Error), - #[error(transparent)] - Pathspec(#[from] crate::pathspec::init::Error), - #[error(transparent)] - Prefix(#[from] gix_path::realpath::Error), - #[error(transparent)] - FilesystemOptions(#[from] config::boolean::Error), - #[error(transparent)] - IndexAsWorktreeWithRenames(gix_error::Error), - #[error(transparent)] - StatOptions(#[from] config::stat_options::Error), - #[error(transparent)] - ResourceCache(#[from] crate::diff::resource_cache::Error), -} +pub type Error = gix_error::Error; /// Options for use with [Repository::index_worktree_status()]. #[derive(Default, Debug, Clone, Copy, PartialEq)] @@ -107,22 +90,29 @@ impl Repository { U: Send + Clone, { let _span = gix_trace::coarse!("gix::index_worktree_status"); - let workdir = self.workdir().ok_or(Error::MissingWorkDir)?; - let attrs_and_excludes = self.attributes( - index, - crate::worktree::stack::state::attributes::Source::WorktreeThenIdMapping, - crate::worktree::stack::state::ignore::Source::WorktreeThenIdMappingIfNotSkipped, - None, - )?; - let pathspec = - self.index_worktree_status_pathspec::(patterns, index, options.dirwalk_options.as_ref())?; + let workdir = self.workdir().ok_or_else(|| { + gix_error::Error::from_error(gix_error::message( + "A working tree is required to perform a directory walk", + )) + })?; + let attrs_and_excludes = self + .attributes( + index, + crate::worktree::stack::state::attributes::Source::WorktreeThenIdMapping, + crate::worktree::stack::state::ignore::Source::WorktreeThenIdMappingIfNotSkipped, + None, + ) + .or_erased()?; + let pathspec = self.index_worktree_status_pathspec(patterns, index, options.dirwalk_options.as_ref())?; let cwd = self.current_dir(); - let git_dir_realpath = crate::path::realpath_opts(self.git_dir(), cwd, crate::path::realpath::MAX_SYMLINKS)?; - let fs_caps = self.filesystem_options()?; + let git_dir_realpath = crate::path::realpath_opts(self.git_dir(), cwd, crate::path::realpath::MAX_SYMLINKS) + .map_err(gix_error::Exn::into_error)?; + let fs_caps = self.filesystem_options().or_erased()?; let fscache = config::tree::Core::FS_CACHE .enrich_error(self.config.resolved.boolean(config::tree::Core::FS_CACHE)) - .with_lenient_default(self.config.lenient_config)? + .with_lenient_default(self.config.lenient_config) + .or_erased()? // if unset, default to enabled on Windows. Good for missing Git installations that would turn it on by installation config .unwrap_or(cfg!(windows)); let accelerate_lookup = fs_caps.ignore_case.then(|| index.prepare_icase_backing()); @@ -168,33 +158,32 @@ impl Repository { rewrites: options.rewrites, }, ) - .map_err(|err| Error::IndexAsWorktreeWithRenames(err.into_error()))?; + .map_err(gix_error::Exn::into_error)?; Ok(out) } - pub(super) fn index_worktree_status_pathspec( + pub(super) fn index_worktree_status_pathspec( &self, patterns: impl IntoIterator>, index: &gix_index::State, options: Option<&crate::dirwalk::Options>, - ) -> Result, E> - where - E: From + From, - { + ) -> Result, Error> { let empty_patterns_match_prefix = options.is_some_and(|opts| opts.empty_patterns_match_prefix); - let attrs_and_excludes = self.attributes( - index, - crate::worktree::stack::state::attributes::Source::WorktreeThenIdMapping, - crate::worktree::stack::state::ignore::Source::WorktreeThenIdMappingIfNotSkipped, - None, - )?; - Ok(crate::Pathspec::new( + let attrs_and_excludes = self + .attributes( + index, + crate::worktree::stack::state::attributes::Source::WorktreeThenIdMapping, + crate::worktree::stack::state::ignore::Source::WorktreeThenIdMappingIfNotSkipped, + None, + ) + .or_erased()?; + crate::Pathspec::new( self, empty_patterns_match_prefix, patterns, true, /* inherit ignore case */ move || Ok(attrs_and_excludes.inner), - )?) + ) } } @@ -599,12 +588,6 @@ pub mod iter { crate::status::Item::IndexWorktree(item) => item, crate::status::Item::TreeIndex(_) => unreachable!("BUG: we deactivated this kind of traversal"), }) - .map_err(|err| match err { - crate::status::iter::Error::IndexWorktree(err) => err, - crate::status::iter::Error::TreeIndex(_) => { - unreachable!("BUG: we deactivated this kind of traversal") - } - }) }) } } diff --git a/gix/src/status/iter/mod.rs b/gix/src/status/iter/mod.rs index eeee04dc17d..46a2790763e 100644 --- a/gix/src/status/iter/mod.rs +++ b/gix/src/status/iter/mod.rs @@ -1,11 +1,14 @@ use std::sync::atomic::Ordering; +#[cfg(feature = "parallel")] +use gix_error::ErrorExt; +use gix_error::ResultExt; use gix_status::index_as_worktree::{Change, EntryStatus}; use crate::{ bstr::BString, config::cache::util::ApplyLeniencyDefault, - status::{Platform, index_worktree, index_worktree::BuiltinSubmoduleStatus, tree_index}, + status::{Platform, index_worktree, index_worktree::BuiltinSubmoduleStatus}, worktree::IndexPersistedOrInMemory, }; @@ -48,7 +51,12 @@ where let obtain_tree_id = || -> Result, crate::status::into_iter::Error> { Ok(match self.head_tree { - Some(None) => Some(self.repo.head_tree_id_or_empty()?.into()), + Some(None) => Some( + self.repo + .head_tree_id_or_empty() + .or_raise(|| gix_error::message("Could not obtain the tree id pointed to by `HEAD`"))? + .into(), + ), Some(Some(tree_id)) => Some(tree_id), None => None, }) @@ -56,10 +64,11 @@ where let skip_hash = crate::config::tree::Index::SKIP_HASH .enrich_error(self.repo.config.resolved.boolean(crate::config::tree::Index::SKIP_HASH)) - .with_lenient_default(self.repo.config.lenient_config)? + .with_lenient_default(self.repo.config.lenient_config) + .or_erased()? .unwrap_or_default(); let should_interrupt = self.should_interrupt.clone().unwrap_or_default(); - let submodule = BuiltinSubmoduleStatus::new(self.repo.clone().into_sync(), self.submodules)?; + let submodule = BuiltinSubmoduleStatus::new(self.repo.clone().into_sync(), self.submodules).or_erased()?; #[cfg(feature = "parallel")] { let (tx, rx) = std::sync::mpsc::channel(); @@ -73,13 +82,11 @@ where let tx = tx.clone(); let tree_index_renames = self.tree_index_renames; let index = index.clone(); - let crate::Pathspec { repo: _, stack, search } = self - .repo - .index_worktree_status_pathspec::( - &patterns, - &index, - self.index_worktree_options.dirwalk_options.as_ref(), - )?; + let crate::Pathspec { repo: _, stack, search } = self.repo.index_worktree_status_pathspec( + &patterns, + &index, + self.index_worktree_options.dirwalk_options.as_ref(), + )?; move || -> Result<_, _> { let repo = repo.to_thread_local(); let mut pathspec = crate::Pathspec { @@ -105,7 +112,9 @@ where ) } }) - .map_err(crate::status::into_iter::Error::SpawnThread)? + .map_err(|err| { + gix_error::Error::from(err.and_raise(gix_error::message("Failed to spawn producer thread"))) + })? .into() } else { None @@ -139,7 +148,9 @@ where }) } }) - .map_err(crate::status::into_iter::Error::SpawnThread)?; + .map_err(|err| { + gix_error::Error::from(err.and_raise(gix_error::message("Failed to spawn producer thread"))) + })?; Ok(Iter { rx_and_join: Some((rx, join_index_worktree, join_tree_index)), @@ -158,7 +169,7 @@ where let patterns: Vec = patterns.into_iter().collect(); let (mut items, tree_index) = match obtain_tree_id()? { Some(tree_id) => { - let mut pathspec = repo.index_worktree_status_pathspec::( + let mut pathspec = repo.index_worktree_status_pathspec( &patterns, &index, self.index_worktree_options.dirwalk_options.as_ref(), @@ -220,14 +231,7 @@ where } /// The error returned for each item returned by [`Iter`]. -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] -pub enum Error { - #[error(transparent)] - IndexWorktree(#[from] index_worktree::Error), - #[error(transparent)] - TreeIndex(#[from] tree_index::Error), -} +pub type Error = gix_error::Error; impl Iterator for Iter { type Item = Result; @@ -256,7 +260,7 @@ impl Iterator for Iter { let tree_index = if let Some(handle) = tree_handle { match handle.join().expect("no panic") { Ok(out) => Some(out), - Err(err) => break Some(Err(err.into())), + Err(err) => break Some(Err(err)), } } else { None @@ -268,7 +272,7 @@ impl Iterator for Iter { self.out = Some(out); None } - Err(err) => Some(Err(err.into())), + Err(err) => Some(Err(err)), }; } } diff --git a/gix/src/status/mod.rs b/gix/src/status/mod.rs index 97c287bd812..d0dfe4bc180 100644 --- a/gix/src/status/mod.rs +++ b/gix/src/status/mod.rs @@ -64,14 +64,7 @@ impl Default for Submodule { } /// The error returned by [status()](Repository::status). -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] -pub enum Error { - #[error(transparent)] - DirwalkOptions(#[from] config::boolean::Error), - #[error(transparent)] - ConfigureUntrackedFiles(#[from] config::key::GenericErrorWithValue), -} +pub type Error = gix_error::Error; /// Status impl Repository { @@ -136,20 +129,7 @@ pub mod is_dirty { use crate::Repository; /// The error returned by [Repository::is_dirty()]. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - StatusPlatform(#[from] crate::status::Error), - #[error(transparent)] - CreateStatusIterator(#[from] crate::status::into_iter::Error), - #[error(transparent)] - TreeIndexStatus(#[from] crate::status::tree_index::Error), - #[error(transparent)] - HeadTreeId(#[from] crate::reference::head_tree_id::Error), - #[error(transparent)] - OpenWorktreeIndex(#[from] crate::worktree::open_index::Error), - } + pub type Error = gix_error::Error; impl Repository { /// Returns `true` if the repository is dirty. @@ -212,32 +192,7 @@ pub mod is_dirty { /// pub mod into_iter { /// The error returned by [status::Platform::into_iter()](crate::status::Platform::into_iter()). - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - Index(#[from] crate::worktree::open_index::Error), - #[error("Failed to spawn producer thread")] - #[cfg(feature = "parallel")] - SpawnThread(#[source] std::io::Error), - #[error(transparent)] - #[cfg(not(feature = "parallel"))] - IndexWorktreeStatus(#[from] crate::status::index_worktree::Error), - #[error(transparent)] - ConfigSkipHash(#[from] crate::config::boolean::Error), - #[error(transparent)] - PrepareSubmodules(#[from] crate::submodule::modules::Error), - #[error("Could not create an index for the head tree to compare with the worktree index")] - HeadTreeIndex(#[from] crate::repository::index_from_tree::Error), - #[error("Could not obtain the tree id pointed to by `HEAD`")] - HeadTreeId(#[from] crate::reference::head_tree_id::Error), - #[error(transparent)] - AttributesAndExcludes(#[from] crate::repository::attributes::Error), - #[error(transparent)] - Pathspec(#[from] crate::pathspec::init::Error), - #[error(transparent)] - HeadTreeDiff(#[from] crate::status::tree_index::Error), - } + pub type Error = gix_error::Error; } mod platform; diff --git a/gix/src/status/tree_index.rs b/gix/src/status/tree_index.rs index 529b1285da3..df5e5f823e7 100644 --- a/gix/src/status/tree_index.rs +++ b/gix/src/status/tree_index.rs @@ -1,24 +1,8 @@ use crate::{Repository, config::tree}; +use gix_error::ResultExt; /// The error returned by [Repository::tree_index_status()]. -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] -pub enum Error { - #[error(transparent)] - IndexFromMTree(#[from] crate::repository::index_from_tree::Error), - #[error(transparent)] - RewritesConfiguration(#[from] crate::diff::new_rewrites::Error), - #[error("Could not create diff-cache for similarity checks")] - DiffResourceCache(#[from] crate::repository::diff_resource_cache::Error), - #[error(transparent)] - TreeIndexDiff(gix_error::Error), -} - -impl From for Error { - fn from(err: gix_diff::index::Error) -> Self { - Error::TreeIndexDiff(err.into_error()) - } -} +pub type Error = gix_error::Error; /// Specify how to perform rewrite tracking [Repository::tree_index_status()]. #[derive(Default, Debug, Copy, Clone)] @@ -79,10 +63,12 @@ impl Repository { self.config.lenient_config, &tree::Status::RENAMES, &tree::Status::RENAME_LIMIT, - )?; + ) + .or_erased()?; if !is_configured { (rewrites, is_configured) = - crate::diff::utils::new_rewrites(&self.config.resolved, self.config.lenient_config)?; + crate::diff::utils::new_rewrites(&self.config.resolved, self.config.lenient_config) + .or_erased()?; } if !is_configured { rewrites = Some(Default::default()); @@ -94,7 +80,10 @@ impl Repository { }; let mut resource_cache = None; if rewrites.is_some() { - resource_cache = Some(self.diff_resource_cache_for_tree_diff()?); + resource_cache = Some( + self.diff_resource_cache_for_tree_diff() + .or_raise(|| gix_error::message("Could not create diff-cache for similarity checks"))?, + ); } let mut pathspec_storage = None; if pathspec.is_none() { @@ -135,7 +124,8 @@ impl Repository { ) .is_ok_and(|platform| platform.matching_attributes(out)) }, - )?; + ) + .map_err(gix_error::Error::from)?; Ok(Outcome { rewrite, tree_index }) } diff --git a/gix/src/submodule/errors.rs b/gix/src/submodule/errors.rs index 3e9656cfe7d..e05e29cb3c1 100644 --- a/gix/src/submodule/errors.rs +++ b/gix/src/submodule/errors.rs @@ -1,149 +1,53 @@ /// pub mod open_modules_file { /// The error returned by [Repository::open_modules_file()](crate::Repository::open_modules_file()). - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - Configuration(#[from] gix_error::Error), - #[error("Could not read '.gitmodules' file")] - Io(#[from] std::io::Error), - } + pub type Error = gix_error::Error; } /// pub mod modules { /// The error returned by [Repository::modules()](crate::Repository::modules()). - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - OpenModulesFile(#[from] crate::submodule::open_modules_file::Error), - #[error(transparent)] - OpenIndex(#[from] crate::worktree::open_index::Error), - #[error("Could not find the .gitmodules file by id in the object database")] - FindExistingBlob(#[from] crate::object::find::existing::Error), - #[error(transparent)] - FindHeadRef(#[from] crate::reference::find::existing::Error), - #[error(transparent)] - PeelHeadRef(#[from] crate::head::peel::Error), - #[error(transparent)] - PeelObjectToCommit(#[from] crate::object::peel::to_kind::Error), - #[error(transparent)] - TreeFromCommit(#[from] crate::object::commit::Error), - } + pub type Error = gix_error::Error; } /// pub mod is_active { /// The error returned by [Submodule::is_active()](crate::Submodule::is_active()). - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - InitIsActivePlatform(gix_error::Error), - #[error(transparent)] - QueryIsActive(#[from] gix_error::Error), - #[error(transparent)] - InitAttributes(#[from] crate::config::attribute_stack::Error), - #[error(transparent)] - InitPathspecDefaults(gix_error::Error), - #[error(transparent)] - ObtainIndex(#[from] crate::repository::index_or_load_from_head::Error), - } + pub type Error = gix_error::Error; } /// pub mod fetch_recurse { /// The error returned by [Submodule::fetch_recurse()](crate::Submodule::fetch_recurse()). - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - ModuleBoolean(#[from] gix_submodule::config::Error), - #[error(transparent)] - ConfigurationFallback(#[from] crate::config::key::GenericErrorWithValue), - } + pub type Error = gix_error::Error; } /// pub mod open { /// The error returned by [Submodule::open()](crate::Submodule::open()). - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - OpenRepository(#[from] crate::open::Error), - #[error(transparent)] - GitDir(#[from] crate::submodule::git_dir_try_old_form::Error), - #[error(transparent)] - PathConfiguration(#[from] gix_submodule::config::path::Error), - #[error(transparent)] - WorktreeDirInaccessible(#[from] std::io::Error), - } + pub type Error = gix_error::Error; } /// pub mod git_dir_try_old_form { /// The error returned by [Submodule::git_dir_try_old_form()](crate::Submodule::git_dir_try_old_form()). - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - PathConfiguration(#[from] gix_submodule::config::path::Error), - #[error("The gitdir file at '{}' contains an invalid gitdir target: {:?}", .gitdir_file.display(), .target)] - InvalidGitDirFileTarget { - gitdir_file: std::path::PathBuf, - target: Option, - #[source] - source: Option, - }, - #[error(transparent)] - GitDir(#[from] gix_validate::submodule::name::Error), - } + pub type Error = gix_error::Error; } /// pub mod state { /// The error returned by [Submodule::state()](crate::Submodule::state()). - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - GitDirTryOldForm(#[from] crate::submodule::git_dir_try_old_form::Error), - #[error(transparent)] - GitDir(#[from] gix_validate::submodule::name::Error), - #[error(transparent)] - PathConfiguration(#[from] gix_submodule::config::path::Error), - } + pub type Error = gix_error::Error; } /// pub mod index_id { /// The error returned by [Submodule::index_id()](crate::Submodule::index_id()). - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - PathConfiguration(#[from] gix_submodule::config::path::Error), - #[error(transparent)] - Index(#[from] crate::repository::index_or_load_from_head::Error), - } + pub type Error = gix_error::Error; } /// pub mod head_id { /// The error returned by [Submodule::head_id()](crate::Submodule::head_id()). - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - HeadCommit(#[from] crate::reference::head_commit::Error), - #[error("Could not get tree of head commit")] - CommitTree(#[from] crate::object::commit::Error), - #[error("Could not peel tree to submodule path")] - PeelTree(#[from] crate::object::find::existing::Error), - #[error(transparent)] - PathConfiguration(#[from] gix_submodule::config::path::Error), - } + pub type Error = gix_error::Error; } diff --git a/gix/src/submodule/mod.rs b/gix/src/submodule/mod.rs index 1679659c62f..47de241cc21 100644 --- a/gix/src/submodule/mod.rs +++ b/gix/src/submodule/mod.rs @@ -6,6 +6,7 @@ use std::{ path::{Path, PathBuf}, }; +use gix_error::{ErrorExt, ResultExt}; pub use gix_submodule::*; use crate::{ @@ -67,9 +68,9 @@ impl<'repo> SharedState<'repo> { self.repo .config .pathspec_defaults() - .map_err(|err| is_active::Error::InitPathspecDefaults(err.into_error()))?, + .map_err(gix_error::Exn::into_error)?, ) - .map_err(|err| is_active::Error::InitIsActivePlatform(gix_error::Error::from_error(err)))?; + .or_erased()?; let index = self.index()?; let attributes = self .repo @@ -139,10 +140,11 @@ impl Submodule<'_> { /// Return the `fetchRecurseSubmodules` field from this submodule's configuration, or retrieve the value from `fetch.recurseSubmodules` if unset. pub fn fetch_recurse(&self) -> Result, fetch_recurse::Error> { - Ok(match self.state.modules.fetch_recurse(self.name())? { + Ok(match self.state.modules.fetch_recurse(self.name()).or_erased()? { Some(val) => Some(val), None => crate::config::tree::Fetch::RECURSE_SUBMODULES - .try_into_recurse_submodules(self.state.repo.config.resolved.boolean("fetch.recurseSubmodules"))?, + .try_into_recurse_submodules(self.state.repo.config.resolved.boolean("fetch.recurseSubmodules")) + .map_err(gix_error::Error::from)?, }) } @@ -176,7 +178,7 @@ impl Submodule<'_> { .is_ok_and(|platform| platform.matching_attributes(out)) }, ) - .map_err(|err| is_active::Error::QueryIsActive(err.into_error()))?; + .map_err(gix_error::Exn::into_error)?; Ok(is_active) } @@ -187,7 +189,7 @@ impl Submodule<'_> { /// wasn't yet committed. Note that `None` is also returned if the entry at the submodule path isn't a submodule. /// If `Some()`, but `None` when calling [`Self::head_id()`], then the submodule was just added without having committed the change. pub fn index_id(&self) -> Result, index_id::Error> { - let path = self.path()?; + let path = self.path().or_erased()?; Ok(self .state .index()? @@ -201,13 +203,15 @@ impl Submodule<'_> { /// wasn't yet committed. Note that `None` is also returned if the entry at the submodule path isn't a submodule. /// If `None`, but `Some()` when calling [`Self::index_id()`], then the submodule was just added without having committed the change. pub fn head_id(&self) -> Result, head_id::Error> { - let path = self.path()?; + let path = self.path().or_erased()?; Ok(self .state .repo .head_commit()? - .tree()? - .peel_to_entry_by_path(gix_path::from_bstring(path))? + .tree() + .or_raise(|| gix_error::message("Could not get tree of head commit"))? + .peel_to_entry_by_path(gix_path::from_bstring(path)) + .or_raise(|| gix_error::message("Could not peel tree to submodule path"))? .and_then(|entry| (entry.mode().is_commit()).then_some(entry.inner.oid))) } @@ -252,25 +256,26 @@ impl Submodule<'_> { &self, validate_gitdir_file_target: bool, ) -> Result { - let git_dir = self.git_dir()?; - let worktree_gitdir = self.worktree_gitdir()?; + let git_dir = self.git_dir().map_err(|err| { + gix_error::Error::from(err.and_raise(gix_error::ValidationError::new("The submodule name is invalid"))) + })?; + let worktree_gitdir = self.worktree_gitdir().or_erased()?; let git_dir = if worktree_gitdir.is_dir() { worktree_gitdir } else if worktree_gitdir.is_file() { if validate_gitdir_file_target { - let git_dir = gix_discover::path::from_gitdir_file(&worktree_gitdir).map_err(|source| { - git_dir_try_old_form::Error::InvalidGitDirFileTarget { - gitdir_file: worktree_gitdir.clone(), - target: None, - source: Some(source.into_error()), - } + let git_dir = gix_discover::path::from_gitdir_file(&worktree_gitdir).map_err(|err| { + gix_error::Error::from(err.raise(gix_error::ValidationError::new(format!( + "The gitdir file at '{}' contains an invalid gitdir target", + worktree_gitdir.display() + )))) })?; if !git_dir.is_dir() { - return Err(git_dir_try_old_form::Error::InvalidGitDirFileTarget { - gitdir_file: worktree_gitdir, - target: Some(git_dir), - source: None, - }); + return Err(gix_error::Error::from_error(gix_error::ValidationError::new(format!( + "The gitdir file at '{}' contains an invalid gitdir target: '{}'", + worktree_gitdir.display(), + git_dir.display() + )))); } git_dir } else { @@ -284,7 +289,7 @@ impl Submodule<'_> { fn state_inner(&self, validate_gitdir_file_target: bool) -> Result { let maybe_old_path = self.git_dir_try_old_form_inner(validate_gitdir_file_target)?; - let worktree_git = self.worktree_gitdir()?; + let worktree_git = self.worktree_gitdir().or_erased()?; let superproject_configuration = self .state .repo @@ -330,19 +335,20 @@ impl Submodule<'_> { match crate::open_opts(self.git_dir_try_old_form()?, options) { Ok(mut repo) => { if repo.workdir().is_none() { - let wd = self.work_dir()?; + let wd = self.work_dir().or_erased()?; // We should always have a workdir, as bare submodules don't exist. // However, it's possible for no workdir to be accessible if there is a symlink in the way. // Just setting it by hand fixes this issue effectively, even though the question remains // if this should work automatically. // For now, let's *not* use the `self.worktree_git()` directory which has its own edge-cases, // while the current solution yields the cleanest paths (i.e. it keeps relative ones). - repo.set_workdir(Some(wd))?; + repo.set_workdir(Some(wd)).or_erased()?; } Ok(Some(repo)) } - Err(crate::open::Error::NotARepository { .. }) => Ok(None), - Err(err) => Err(err.into()), + // Nested not-found causes may instead describe invalid repository configuration. + Err(err) if err.error().is::() => Ok(None), + Err(err) => Err(err), } } @@ -398,30 +404,11 @@ mod tests { pub mod status { use gix_submodule::config; - use super::{Status, head_id, index_id, open, state}; + use super::Status; use crate::Submodule; /// The error returned by [Submodule::status()]. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - State(#[from] state::Error), - #[error(transparent)] - HeadId(#[from] head_id::Error), - #[error(transparent)] - IndexId(#[from] index_id::Error), - #[error(transparent)] - OpenRepository(#[from] open::Error), - #[error(transparent)] - IgnoreConfiguration(#[from] config::Error), - #[error(transparent)] - StatusPlatform(#[from] crate::status::Error), - #[error(transparent)] - StatusIter(#[from] crate::status::into_iter::Error), - #[error(transparent)] - NextStatusItem(#[from] crate::status::iter::Error), - } + pub type Error = gix_error::Error; impl Submodule<'_> { /// Return the status of the submodule. diff --git a/gix/src/tag.rs b/gix/src/tag.rs index 65902867d74..97d6dd6082b 100644 --- a/gix/src/tag.rs +++ b/gix/src/tag.rs @@ -3,17 +3,6 @@ mod error { /// The error returned by [`tag(…)`][crate::Repository::tag()]. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - ReferenceNameValidation(#[from] gix_ref::name::Error), - #[error(transparent)] - WriteObject(#[from] crate::object::write::Error), - #[error(transparent)] - ReferenceEdit(#[from] crate::reference::edit::Error), - #[error(transparent)] - DateParseError(#[from] gix_date::Error), - } + pub type Error = gix_error::Error; } pub use error::Error; diff --git a/gix/src/worktree/mod.rs b/gix/src/worktree/mod.rs index 2625a4b8dea..104fcc24929 100644 --- a/gix/src/worktree/mod.rs +++ b/gix/src/worktree/mod.rs @@ -119,16 +119,7 @@ pub mod proxy; #[cfg(feature = "index")] pub mod open_index { /// The error returned by [`Worktree::open_index()`][crate::Worktree::open_index()]. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - ConfigIndexThreads(#[from] crate::config::key::GenericErrorWithValue), - #[error(transparent)] - ConfigSkipHash(#[from] crate::config::boolean::Error), - #[error(transparent)] - IndexFile(#[from] gix_error::Error), - } + pub type Error = gix_error::Error; impl crate::Worktree<'_> { /// A shortcut to [`crate::Repository::open_index()`]. @@ -149,14 +140,7 @@ pub mod excludes { use crate::AttributeStack; /// The error returned by [`Worktree::excludes()`][crate::Worktree::excludes()]. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - OpenIndex(#[from] crate::worktree::open_index::Error), - #[error(transparent)] - CreateCache(#[from] crate::config::exclude_stack::Error), - } + pub type Error = gix_error::Error; impl crate::Worktree<'_> { /// Configure a file-system cache checking if files below the repository are excluded. @@ -170,11 +154,11 @@ pub mod excludes { /// [`Worktree::attributes()`][crate::Worktree::attributes()] for accessing both attributes and excludes. pub fn excludes(&self, overrides: Option) -> Result, Error> { let index = self.index()?; - Ok(self.parent.excludes( + self.parent.excludes( &index, overrides, gix_worktree::stack::state::ignore::Source::WorktreeThenIdMappingIfNotSkipped, - )?) + ) } } } @@ -185,14 +169,7 @@ pub mod attributes { use crate::{AttributeStack, Worktree}; /// The error returned by [`Worktree::attributes()`]. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - OpenIndex(#[from] crate::worktree::open_index::Error), - #[error(transparent)] - CreateCache(#[from] crate::repository::attributes::Error), - } + pub type Error = gix_error::Error; impl<'repo> Worktree<'repo> { /// Configure a file-system cache checking if files below the repository are excluded or for querying their attributes. @@ -203,23 +180,21 @@ pub mod attributes { /// * `$GIT_DIR/info/exclude|attributes` if present. pub fn attributes(&self, overrides: Option) -> Result, Error> { let index = self.index()?; - Ok(self.parent.attributes( + self.parent.attributes( &index, gix_worktree::stack::state::attributes::Source::WorktreeThenIdMapping, gix_worktree::stack::state::ignore::Source::WorktreeThenIdMappingIfNotSkipped, overrides, - )?) + ) } /// Like [attributes()][Self::attributes()], but without access to exclude/ignore information. pub fn attributes_only(&self) -> Result, Error> { let index = self.index()?; - self.parent - .attributes_only( - &index, - gix_worktree::stack::state::attributes::Source::WorktreeThenIdMapping, - ) - .map_err(|err| Error::CreateCache(err.into())) + self.parent.attributes_only( + &index, + gix_worktree::stack::state::attributes::Source::WorktreeThenIdMapping, + ) } } } @@ -234,14 +209,7 @@ pub mod pathspec { }; /// The error returned by [`Worktree::pathspec()`]. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - Init(#[from] crate::pathspec::init::Error), - #[error(transparent)] - OpenIndex(#[from] crate::worktree::open_index::Error), - } + pub type Error = gix_error::Error; impl<'repo> Worktree<'repo> { /// Configure pathspecs `patterns` to be matched against, with pathspec attributes read from the worktree and then from the index @@ -268,15 +236,15 @@ pub mod pathspec { self.parent.config.lenient_config, Some(gitoxide::Pathspec::INHERIT_IGNORE_CASE_DEFAULT), ) - .map_err(|err| Error::Init(crate::pathspec::init::Error::Defaults(err.into())))? + .map_err(gix_error::Error::from)? .unwrap_or(gitoxide::Pathspec::INHERIT_IGNORE_CASE_DEFAULT); - Ok(self.parent.pathspec( + self.parent.pathspec( true, /* empty patterns match prefix */ patterns, inherit_ignore_case, &index, gix_worktree::stack::state::attributes::Source::WorktreeThenIdMapping, - )?) + ) } } } diff --git a/gix/src/worktree/proxy.rs b/gix/src/worktree/proxy.rs index bb9198afb1e..e1b8fbb29d0 100644 --- a/gix/src/worktree/proxy.rs +++ b/gix/src/worktree/proxy.rs @@ -1,4 +1,6 @@ #![allow(clippy::result_large_err)] +use gix_error::ResultExt; + use std::path::{Path, PathBuf}; use crate::{ @@ -9,19 +11,8 @@ use crate::{ #[expect(missing_docs)] pub mod into_repo { - use std::path::PathBuf; - /// The error returned by [`Proxy::into_repo()`][super::Proxy::into_repo()]. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] - pub enum Error { - #[error(transparent)] - Open(#[from] crate::open::Error), - #[error("Worktree at '{}' is inaccessible", .base.display())] - MissingWorktree { base: PathBuf }, - #[error(transparent)] - MissingGitDirFile(#[from] std::io::Error), - } + pub type Error = gix_error::Error; } impl<'repo> Proxy<'repo> { @@ -115,9 +106,12 @@ impl Proxy<'_> { /// /// Note that it won't fail if the worktree doesn't exist. pub fn into_repo(self) -> Result { - let base = self.base()?; + let base = self.base().or_erased()?; if !base.is_dir() { - return Err(into_repo::Error::MissingWorktree { base }); + return Err(gix_error::Error::from_error(gix_error::message!( + "Worktree at '{}' is inaccessible", + base.display() + ))); } let options = self.parent.options.clone().without_repository_environment_overrides(); let common_dir = self.parent.common_dir().to_owned(); diff --git a/gix/tests/gix-init.rs b/gix/tests/gix-init.rs index c5d7bf41b07..8d75a32e682 100644 --- a/gix/tests/gix-init.rs +++ b/gix/tests/gix-init.rs @@ -127,7 +127,7 @@ mod config { } mod config_mut { - use gix::config::{Source, file_mut::Error}; + use gix::config::Source; use gix_sec::Permission; use gix_testtools::{Env, Result}; use serial_test::serial; @@ -327,10 +327,8 @@ mod config_mut { drop(file); options.permissions.env.home = Permission::Deny; assert!( - matches!( - gix::config_mut(Source::Git, &options), - Err(Error::SourceUnavailable(Source::Git)) - ), + gix::config_mut(Source::Git, &options) + .is_err_and(|err| err == "Configuration source Git has no available path with these options"), "a source with no permitted path is unavailable" ); Ok(()) @@ -384,7 +382,9 @@ mod config_mut { drop(file); let _no_system = Env::new().set("GIT_CONFIG_NOSYSTEM", "true"); assert!( - matches!(gix::config_mut(source, &options), Err(Error::SourceUnavailable(actual)) if actual == source), + gix::config_mut(source, &options) + .is_err_and(|err| err + == format!("Configuration source {source:?} has no available path with these options")), "GIT_CONFIG_NOSYSTEM suppresses explicit installation and system paths" ); options.permissions.env.git_prefix = Permission::Deny; @@ -399,7 +399,7 @@ mod config_mut { options.permissions.env.git_prefix = Permission::Allow; let _file = gix::config_mut(Source::User, &options)?; assert!( - matches!(gix::config_mut(Source::Git, &options), Err(Error::AcquireLock(_))), + gix::config_mut(Source::Git, &options).is_err_and(|err| err.can_retry()), "sources overridden to the same file share its lock" ); Ok(()) @@ -414,7 +414,9 @@ mod config_mut { .system_config_path(temp.path().join("system.config")); for source in [Source::GitInstallation, Source::System, Source::Git, Source::User] { assert!( - matches!(gix::config_mut(source, &options), Err(Error::SourceUnavailable(actual)) if actual == source), + gix::config_mut(source, &options) + .is_err_and(|err| err + == format!("Configuration source {source:?} has no available path with these options")), "disabled sources cannot be opened for writing" ); } @@ -427,7 +429,8 @@ mod config_mut { Source::EnvOverride, ] { assert!( - matches!(gix::config_mut(source, &options), Err(Error::UnsupportedSource(actual)) if actual == source), + gix::config_mut(source, &options).is_err_and(|err| err + == format!("Configuration source {source:?} requires a repository or has no physical file")), "repository and in-memory sources have no standalone transaction" ); } @@ -446,12 +449,9 @@ mod config_mut { let path = temp.path().join("missing/global.config"); let options = options_for(Source::System).system_config_path(&path); assert!( - matches!( - gix::config_mut(Source::System, &options), - Err(Error::AcquireLock(err)) if err - .downcast_any_ref::() - .is_some_and(|err| err.kind() == std::io::ErrorKind::NotFound) - ), + gix::config_mut(Source::System, &options).is_err_and(|err| err + .downcast_any_ref::() + .is_some_and(|err| err.kind() == std::io::ErrorKind::NotFound)), "the parent directory must already exist" ); assert!( @@ -479,7 +479,9 @@ mod config_mut { let malformed = "[unterminated"; std::fs::write(&path, malformed)?; assert!( - matches!(gix::config_mut(Source::System, &options), Err(Error::Config(_))), + gix::config_mut(Source::System, &options).is_err_and(|err| err + .iter_errors() + .any(|source| source.to_string() == "Could not load global configuration")), "malformed configuration cannot be overwritten through a transaction" ); assert_eq!( @@ -512,27 +514,25 @@ mod config_mut { .cli_overrides(["core.configLockTimeout=invalid"]) .config_overrides(["core.configLockTimeout=0"]); assert!( - matches!( - gix::config_mut(Source::System, &options), - Err(Error::AcquireLock(err)) - if err.downcast_any_ref::().is_some() - && err.to_string().contains("immediately") - ), + gix::config_mut(Source::System, &options).is_err_and(|err| err.can_retry() + && err + .iter_errors() + .any(|source| source.to_string().contains("immediately"))), "API overrides take precedence over CLI and disk values" ); let options = options.filter_config_section(|meta| meta.source != Source::Api); assert!( - matches!(gix::config_mut(Source::System, &options), Err(Error::LockTimeout(_))), + gix::config_mut(Source::System, &options).is_err_and(|err| err + .iter_errors() + .any(|source| source.to_string().contains("core.configLockTimeout"))), "section filtering exposes the invalid CLI timeout in strict mode" ); let options = options.strict_config(false); assert!( - matches!( - gix::config_mut(Source::System, &options), - Err(Error::AcquireLock(err)) - if err.downcast_any_ref::().is_some() - && err.to_string().contains("after 1.00s") - ), + gix::config_mut(Source::System, &options).is_err_and(|err| err.can_retry() + && err + .iter_errors() + .any(|source| source.to_string().contains("after 1.00s"))), "lenient invalid timeouts use the one-second default" ); @@ -555,17 +555,17 @@ mod config_mut { .strict_config(true); options.permissions.config.includes = true; assert!( - matches!( - gix::config_mut(Source::System, &options), - Err(Error::AcquireLock(err)) - if err.downcast_any_ref::().is_some() - && err.to_string().contains("immediately") - ), + gix::config_mut(Source::System, &options).is_err_and(|err| err.can_retry() + && err + .iter_errors() + .any(|source| source.to_string().contains("immediately"))), "expanded global includes can supply the lock timeout" ); options.permissions.config.includes = false; assert!( - matches!(gix::config_mut(Source::System, &options), Err(Error::LockTimeout(_))), + gix::config_mut(Source::System, &options).is_err_and(|err| err + .iter_errors() + .any(|source| source.to_string().contains("core.configLockTimeout"))), "disabling includes exposes the invalid physical timeout" ); Ok(()) @@ -635,10 +635,9 @@ mod config_mut { ); let options = options.config_overrides(["core.sharedRepository=invalid"]); assert!( - matches!( - gix::config_mut(Source::System, &options), - Err(Error::InvalidSharedRepository(_)) - ), + gix::config_mut(Source::System, &options).is_err_and(|err| err + .iter_errors() + .any(|source| source.to_string().contains("core.sharedRepository"))), "invalid sharing policies in overrides are rejected even for existing files" ); Ok(()) @@ -1259,6 +1258,47 @@ fn git_index_file_override_is_not_inherited_by_opened_submodules() -> gix_testto Ok(()) } +#[test] +#[serial] +#[cfg(feature = "attributes")] +fn submodule_open_propagates_missing_environment_configuration() -> gix_testtools::Result { + let fixture = gix_testtools::scripted_fixture_read_only("make_submodules.sh")?; + let _empty_config = gix_testtools::Env::new().set("GIT_CONFIG_COUNT", "0"); + let mut options = gix::open::Options::isolated(); + options.permissions.config.env = true; + let repo = gix::open_opts(fixture.join("with-submodules"), options)?; + let submodule = repo + .submodules()? + .expect("modules present") + .next() + .expect("one submodule"); + assert!(submodule.open()?.is_some(), "the submodule is initialized"); + + for missing in ["GIT_CONFIG_KEY_0", "GIT_CONFIG_VALUE_0"] { + let _env = gix_testtools::Env::new() + .set("GIT_CONFIG_COUNT", "1") + .set("GIT_CONFIG_KEY_0", "core.key") + .set("GIT_CONFIG_VALUE_0", "value") + .unset(missing); + let err = submodule + .open() + .expect_err("invalid environment configuration does not make the repository absent"); + assert_eq!( + err.downcast_any_ref::() + .expect("the missing configuration input remains in the error chain") + .message, + format!("{missing} was not set"), + "the original configuration diagnostic is preserved" + ); + #[cfg(feature = "status")] + assert!( + submodule.status(gix::submodule::config::Ignore::None, false).is_err(), + "status must propagate the configuration failure as well" + ); + } + Ok(()) +} + #[test] #[serial] fn git_index_file_relative_paths_use_the_cwd_when_opening() -> gix_testtools::Result { diff --git a/gix/tests/gix/clone.rs b/gix/tests/gix/clone.rs index 9a6dcb629a9..d2af5ae84f0 100644 --- a/gix/tests/gix/clone.rs +++ b/gix/tests/gix/clone.rs @@ -11,6 +11,7 @@ mod blocking_io { use gix::{ bstr::BString, config::tree::{Clone, Core, Init, Key}, + error::ResultExt, refs::transaction::PreviousValue, remote::{ Direction, @@ -146,7 +147,8 @@ mod blocking_io { "+refs/tags/b-tag:refs/tags/b-tag".to_owned().into(), ], Direction::Fetch, - )?; + ) + .or_erased()?; Ok(r) } }) @@ -295,12 +297,14 @@ mod blocking_io { )? .fetch_only(gix::progress::Discard, &AtomicBool::default()) .unwrap_err(); - let gix::clone::fetch::Error::Fetch(gix::remote::fetch::Error::Fetch(err)) = err else { - panic!("expected the protocol fetch to reject the shallow remote") - }; assert!(err.is_validation(), "the configured rejection is a validation failure"); assert!( - err.to_string().contains("clone.rejectShallow"), + matches!( + err.downcast_any_ref::(), + Some(err) + if err.message + == "Receiving objects from shallow remotes is prohibited due to the value of `clone.rejectShallow`" + ), "we can avoid fetching from remotes with this setting" ); Ok(()) @@ -347,7 +351,8 @@ mod blocking_io { let (repo, _change) = gix::prepare_clone_bare(remote::repo("base").path(), tmp.path())? .with_shallow(Shallow::DepthAtRemote(2.try_into()?)) .configure_remote(|mut r| { - r.replace_refspecs(Some("refs/heads/main:refs/remotes/origin/main"), Direction::Fetch)?; + r.replace_refspecs(Some("refs/heads/main:refs/remotes/origin/main"), Direction::Fetch) + .or_erased()?; Ok(r) }) .fetch_only(gix::progress::Discard, &AtomicBool::default())?; @@ -474,7 +479,8 @@ mod blocking_io { move |r| { called_configure_remote.store(true, std::sync::atomic::Ordering::Relaxed); let r = r - .with_refspecs(Some("+refs/tags/b-tag:refs/tags/b-tag"), gix::remote::Direction::Fetch)? + .with_refspecs(Some("+refs/tags/b-tag:refs/tags/b-tag"), gix::remote::Direction::Fetch) + .or_erased()? .with_fetch_tags(desired_fetch_tags); Ok(r) } @@ -859,14 +865,11 @@ mod blocking_io { .map(drop) .expect_err("an existing .git directory must not be reused for clone"); - assert!( - matches!( - err, - gix::clone::Error::Init(gix::init::Error::Init(gix::create::Error::DirectoryExists { ref path })) - if *path == dot_git - ), - "unexpected error: {err}" - ); + assert!(matches!( + err.downcast_any_ref::(), + Some(gix_error::ValidationError { input: Some(path), .. }) + if path.as_bstr() == dot_git.to_string_lossy().as_bytes() + )); assert_eq!(std::fs::read(&existing_path)?, EXISTING_CONTENT); assert_eq!(std::fs::read(&head_path)?, EXISTING_HEAD_CONTENT); Ok(()) @@ -1087,10 +1090,7 @@ mod blocking_io { let err = missing .fetch_only(gix::progress::Discard, &AtomicBool::default()) .expect_err("missing full references fail"); - assert!( - matches!(err, gix::clone::fetch::Error::RevisionMissing { .. }), - "the missing revision is reported directly: {err}" - ); + assert!(err.is_not_found(), "the missing revision is reported directly: {err}"); let tree_id = remote_repo .find_reference("refs/heads/a")? @@ -1107,10 +1107,7 @@ mod blocking_io { let err = tree .fetch_only(gix::progress::Discard, &AtomicBool::default()) .expect_err("tree revisions cannot become HEAD"); - assert!( - matches!(err, gix::clone::fetch::Error::PeelRevision(_)), - "non-commit revisions are rejected: {err}" - ); + assert!(err.is_validation(), "non-commit revisions are rejected: {err}"); Ok(()) } @@ -1143,7 +1140,7 @@ mod blocking_io { } #[test] - fn fetch_succeeds_despite_remote_head_ref() -> crate::Result { + fn fetch_retries_without_the_implicit_head_refspec_on_conflict() -> crate::Result { if gix_testtools::run_in_isolated_process()? { return Ok(()); } @@ -1159,7 +1156,14 @@ mod blocking_io { let (mut checkout, _out) = prepare.fetch_then_checkout(gix::progress::Discard, &AtomicBool::default())?; let (repo, _) = checkout.main_worktree(gix::progress::Discard, &AtomicBool::default())?; - assert!(repo.head().is_ok(), "we could handle the HEAD normaller"); + assert!( + repo.head().is_ok(), + "the clone completed after recovering from the conflict" + ); + assert!( + repo.try_find_reference("refs/remotes/origin/HEAD")?.is_some(), + "retrying without the implicit refspec still fetches the remote branch named HEAD" + ); Ok(()) } @@ -1387,10 +1391,14 @@ fn clone_and_destination_must_be_empty() -> crate::Result { restricted(), ) { Ok(_) => unreachable!("this should fail as the directory isn't empty"), - Err(err) => assert!( - err.to_string() - .starts_with("Refusing to initialize the non-empty directory as ") - ), + Err(err) => { + assert!(err.is_validation()); + let validation = err + .downcast_any_ref::() + .expect("the non-empty destination remains a typed validation failure"); + assert_eq!(validation.message, "Refusing to initialize the non-empty directory as"); + assert!(validation.input.is_some(), "the rejected destination is retained"); + } } Ok(()) } @@ -1408,10 +1416,12 @@ fn clone_with_worktree_and_destination_must_be_empty() -> crate::Result { ) .map(drop) .expect_err("this should fail as the directory isn't empty"); - assert!( - err.to_string() - .starts_with("Refusing to initialize the non-empty directory as ") - ); + assert!(err.is_validation()); + let validation = err + .downcast_any_ref::() + .expect("the non-empty destination remains a typed validation failure"); + assert_eq!(validation.message, "Refusing to initialize the non-empty directory as"); + assert!(validation.input.is_some(), "the rejected destination is retained"); Ok(()) } diff --git a/gix/tests/gix/commit.rs b/gix/tests/gix/commit.rs index b9a4a2f73ff..33456a5a84a 100644 --- a/gix/tests/gix/commit.rs +++ b/gix/tests/gix/commit.rs @@ -232,16 +232,13 @@ mod signature { .find_commit(id)? .verify_signature() .expect_err("the configured verifier does not exist"); - let gix::commit::verify::Error::Verify(err) = err else { - panic!("expected a verifier spawn failure, got {err:?}"); - }; - assert_eq!( - err.to_string(), - format!( - "Could not execute signature verifier {:?}", - home.join("bin/missing-gpg") - ), - "the configured verifier path is expanded relative to home" + let expected = format!( + "Could not execute signature verifier {:?}", + home.join("bin/missing-gpg") + ); + assert!( + err.iter_errors().any(|source| source.to_string() == expected), + "the configured verifier path is expanded relative to home: {err:?}" ); Ok(()) } diff --git a/gix/tests/gix/config/file_mut.rs b/gix/tests/gix/config/file_mut.rs index 29f4c376088..7bf07599092 100644 --- a/gix/tests/gix/config/file_mut.rs +++ b/gix/tests/gix/config/file_mut.rs @@ -34,11 +34,14 @@ leading = value repo.config_snapshot_mut().set_raw_value("memory.value", "transient")?; let mut file = repo.config_file_mut(&target_path)?; - match repo.config_file_mut(&target_path) { - Err(gix::config::file_mut::Error::AcquireLock(_)) => {} - Err(err) => panic!("lock contention must be reported precisely: {err:?}"), + let err = match repo.config_file_mut(&target_path) { + Err(err) => err, Ok(_) => panic!("a second transaction must not acquire the same lock"), - } + }; + assert!( + err.can_retry(), + "lock contention must be reported as retryable: {err:?}" + ); file.set_raw_value("a.local-override", "written")?; file.commit()?; @@ -113,16 +116,14 @@ fn honors_core_config_lock_timeout() -> crate::Result { Err(err) => err, }; std::fs::remove_file(lock_path)?; - let gix::config::file_mut::Error::AcquireLock(err) = err else { - panic!("a held lock must cause a lock-acquisition error") - }; assert!( - err.downcast_any_ref::().is_some(), - "lock contention is retryable" + err.can_retry(), + "lock contention must be reported as retryable: {err:?}" ); assert!( - err.to_string().contains("immediately"), - "the configured zero timeout must be retained in the error" + err.iter_errors() + .any(|source| source.to_string().contains("immediately")), + "the configured zero timeout must be retained in the error: {err:?}" ); Ok(()) } @@ -185,11 +186,11 @@ fn follows_symlinked_configuration_files() -> crate::Result { symlink("target.config", &link)?; let mut file = repo.config_file_mut(&link)?; - match repo.config_file_mut(&target) { - Err(gix::config::file_mut::Error::AcquireLock(_)) => {} - Err(err) => panic!("the target must report lock contention: {err:?}"), + let err = match repo.config_file_mut(&target) { + Err(err) => err, Ok(_) => panic!("the symlink and its target must use the same lock"), - } + }; + assert!(err.can_retry(), "the target must report lock contention: {err:?}"); file.set_raw_value("core.abbrev", "8")?; file.commit()?; @@ -268,12 +269,10 @@ fn semantic_validation_happens_on_reload() -> crate::Result { Ok(_) => panic!("the persisted repository format must be rejected while reopening"), Err(err) => err, }; - assert!( - matches!( - err, - gix::open::Error::Config(gix::config::Error::UnsupportedRepositoryFormatVersion { version: 2 }) - ), - "reload reports the semantic error: {err:?}" + assert!(err.is_validation(), "reload reports the semantic error: {err:?}"); + assert_eq!( + err.probable_cause().to_string(), + "Unsupported repository format version 2; only versions 0 and 1 are supported" ); assert_eq!( repo.config_snapshot().integer("core.repositoryFormatVersion"), diff --git a/gix/tests/gix/config/tree.rs b/gix/tests/gix/config/tree.rs index c1ae640de1a..994963db7e5 100644 --- a/gix/tests/gix/config/tree.rs +++ b/gix/tests/gix/config/tree.rs @@ -107,23 +107,22 @@ mod keys { } for invalid in [-1, -100] { - assert_eq!( - gix::config::tree::Core::DELTA_BASE_CACHE_LIMIT - .validate(invalid.to_string().as_str().into()) - .unwrap_err() - .to_string(), - "cannot use sign for unsigned integer" - ); + let err = gix::config::tree::Core::DELTA_BASE_CACHE_LIMIT + .validate(invalid.to_string().as_str().into()) + .expect_err("negative values are not unsigned"); + assert_eq!(err.probable_cause().to_string(), "cannot use sign for unsigned integer"); + assert!(err.is_validation()); } let out_of_bounds = ((i64::MAX as u64) + 1).to_string(); + let err = gix::config::tree::Core::DELTA_BASE_CACHE_LIMIT + .validate(out_of_bounds.as_bytes().into()) + .expect_err("the value is larger than the config integer range"); assert_eq!( - gix::config::tree::Core::DELTA_BASE_CACHE_LIMIT - .validate(out_of_bounds.as_bytes().into()) - .unwrap_err() - .to_string(), + err.probable_cause().to_string(), "Integers needs to be positive or negative numbers which may have a suffix like 1k, 42, or 50G: \"9223372036854775808\"" ); + assert!(err.is_validation()); } } @@ -608,7 +607,12 @@ mod core { .to_string(), "The key \"core.logAllRefUpdates=invalid\" was invalid" ); - assert!(Core::LOG_ALL_REF_UPDATES.validate("invalid".into()).is_err()); + assert!( + Core::LOG_ALL_REF_UPDATES + .validate("invalid".into()) + .expect_err("the value is neither a boolean nor 'always'") + .is_validation() + ); Ok(()) } @@ -923,10 +927,11 @@ mod protocol { assert!(Protocol::VERSION.validate(valid.to_string().as_str().into()).is_ok()); } - assert_eq!( - Protocol::VERSION.validate("5".into()).unwrap_err().to_string(), - "protocol version 5 is unknown" - ); + let err = Protocol::VERSION + .validate("5".into()) + .expect_err("protocol version 5 is unsupported"); + assert_eq!(err.probable_cause().to_string(), "protocol version 5 is unknown"); + assert!(err.is_validation()); #[cfg(any(feature = "blocking-network-client", feature = "async-network-client"))] { diff --git a/gix/tests/gix/head.rs b/gix/tests/gix/head.rs index edde1d717af..afd2a3f903e 100644 --- a/gix/tests/gix/head.rs +++ b/gix/tests/gix/head.rs @@ -19,6 +19,20 @@ mod peel { } Ok(()) } + + #[test] + fn a_missing_detached_head_object_is_not_an_empty_tree() -> crate::Result { + let (repo, _keep) = crate::basic_rw_repo()?; + let mut missing_id = repo.object_hash().null(); + missing_id.as_mut_slice()[0] = 1; + repo.reference("HEAD", missing_id, gix::refs::transaction::PreviousValue::Any, "")?; + + let err = repo + .head_tree_id_or_empty() + .expect_err("a born HEAD with a missing object must remain an error"); + assert!(err.is_not_found()); + Ok(()) + } } mod into_remote { diff --git a/gix/tests/gix/id.rs b/gix/tests/gix/id.rs index 85b31d471b6..50577fb2a2e 100644 --- a/gix/tests/gix/id.rs +++ b/gix/tests/gix/id.rs @@ -35,19 +35,24 @@ fn prefix() -> crate::Result { "By default gitoxide acts like `libgit2` here and we prefer to be lenient when possible" ); + let err = gix::open_opts( + work_dir, + gix::open::Options::isolated() + .strict_config(true) + .config_overrides(Some(BString::from("core.abbrev=invalid"))), + ) + .expect_err("invalid core.abbrev must fail"); assert!( - matches!( - gix::open_opts( - work_dir, - gix::open::Options::isolated() - .strict_config(true) - .config_overrides(Some(BString::from("core.abbrev=invalid"))) - ) - .unwrap_err(), - gix::open::Error::Config(gix::config::Error::CoreAbbrev(_)) - ), + err.is_validation(), "an empty core.abbrev fails the open operation in strict config mode, emulating git behaviour" ); + assert_eq!( + err.probable_cause().to_string(), + format!( + "Invalid value for 'core.abbrev' = 'invalid'. It must be between 4 and {}", + repo.object_hash().len_in_hex() + ) + ); Ok(()) } diff --git a/gix/tests/gix/init.rs b/gix/tests/gix/init.rs index 8c2db1d12b5..1184b2834b9 100644 --- a/gix/tests/gix/init.rs +++ b/gix/tests/gix/init.rs @@ -112,11 +112,12 @@ mod non_bare { ) .unwrap_err(); assert!(matches!( - err, - gix::init::Error::InvalidBranchName { - name, - source: gix_validate::reference::name::Error::Reserved { name: reserved } - } if name == "HEAD" && reserved == "refs/heads/HEAD" + err.downcast_any_ref::(), + Some(gix_error::ValidationError { input: Some(name), .. }) if name == "HEAD" + )); + assert!(matches!( + err.downcast_any_ref(), + Some(gix_validate::reference::name::Error::Reserved { name }) if name == "refs/heads/HEAD" )); Ok(()) } @@ -136,11 +137,12 @@ mod non_bare { ) .unwrap_err(); assert!(matches!( - err, - gix::init::Error::InvalidBranchName { - name, - source: gix_validate::reference::name::Error::Reserved { name: reserved } - } if name == "refs/heads/HEAD" && reserved == "refs/heads/HEAD" + err.downcast_any_ref::(), + Some(gix_error::ValidationError { input: Some(name), .. }) if name == "refs/heads/HEAD" + )); + assert!(matches!( + err.downcast_any_ref(), + Some(gix_validate::reference::name::Error::Reserved { name }) if name == "refs/heads/HEAD" )); Ok(()) } diff --git a/gix/tests/gix/reference/mod.rs b/gix/tests/gix/reference/mod.rs index d73657f1e3b..1964eb58015 100644 --- a/gix/tests/gix/reference/mod.rs +++ b/gix/tests/gix/reference/mod.rs @@ -92,6 +92,15 @@ mod find { crate::repo("make_references_repo.sh").map(Into::into) } + #[test] + fn missing_reference_is_classified() -> crate::Result { + let err = repo()? + .find_reference("does-not-exist") + .expect_err("the reference is missing"); + assert!(err.is_not_found()); + Ok(()) + } + #[test] fn and_peel() -> crate::Result { let repo = repo()?; diff --git a/gix/tests/gix/reference/remote.rs b/gix/tests/gix/reference/remote.rs index 0e787c99a27..6d1d5c8bbde 100644 --- a/gix/tests/gix/reference/remote.rs +++ b/gix/tests/gix/reference/remote.rs @@ -65,12 +65,11 @@ fn not_configured() -> crate::Result { assert_eq!(branch.remote_name(gix::remote::Direction::Fetch), None); assert_eq!(branch.remote(gix::remote::Direction::Fetch).transpose()?, None); assert_eq!(head.into_remote(gix::remote::Direction::Fetch).transpose()?, None); - assert!( - matches!( - repo.find_fetch_remote(None), - Err(gix::remote::find::for_fetch::Error::ExactlyOneRemoteNotAvailable) - ), - "there is no remote to be found" + let err = repo.find_fetch_remote(None).unwrap_err(); + assert!(err.is_not_found(), "there is no remote to be found"); + assert_eq!( + err.probable_cause().to_string(), + "No configured remote could be found, or too many were available" ); Ok(()) diff --git a/gix/tests/gix/remote/connect.rs b/gix/tests/gix/remote/connect.rs index 1d0fd64f236..c3d61a136d0 100644 --- a/gix/tests/gix/remote/connect.rs +++ b/gix/tests/gix/remote/connect.rs @@ -105,13 +105,13 @@ mod blocking_io { for name in ["protocol_denied", "protocol_file_denied"] { let repo = remote::repo(name); let remote = repo.find_remote("origin").unwrap(); - assert!(matches!( - remote.connect(Fetch).err(), - Some(gix::remote::connect::Error::ProtocolDenied { - url: _, - scheme: gix::url::Scheme::File - }) - )); + let err = remote.connect(Fetch).err().expect("protocol is denied"); + assert!(err.is_validation()); + let validation = err + .downcast_any_ref::() + .expect("protocol denial retains its validation details"); + assert_eq!(validation.message, "Protocol File is denied per configuration"); + assert!(validation.input.is_some(), "the denied URL is retained"); } } @@ -144,10 +144,7 @@ mod blocking_io { if let Some(should_allow) = should_allow { assert_eq!(result.is_ok(), should_allow, "Value = {env_value:?}"); } else { - assert!( - matches!(result, Err(gix::remote::connect::Error::SchemePermission(_))), - "invalid booleans must be reported" - ); + assert!(result.is_err(), "invalid booleans must be reported"); } } Ok(()) diff --git a/gix/tests/gix/remote/fetch.rs b/gix/tests/gix/remote/fetch.rs index 96f9ee79f7f..a79ac446fb7 100644 --- a/gix/tests/gix/remote/fetch.rs +++ b/gix/tests/gix/remote/fetch.rs @@ -264,8 +264,7 @@ mod blocking_and_async_io { #[test] #[cfg(feature = "blocking-network-client")] - #[expect(clippy::result_large_err)] - fn collate_fetch_error() -> Result<(), gix::env::collate::fetch::Error> { + fn collate_fetch_error() -> Result<(), gix::env::collate::fetch::Error> { let (repo, _tmp) = try_repo_rw("two-origins")?; let remote = repo .head()? @@ -282,7 +281,7 @@ mod blocking_and_async_io { repo.path() .join("HEAD") .metadata() - .map_err(gix::env::collate::fetch::Error::Other)? + .map_err(gix::Error::from_error)? .is_file(), "just to show off the 'Other' error type" ); @@ -292,6 +291,8 @@ mod blocking_and_async_io { #[test] #[cfg(feature = "blocking-network-client")] fn fetch_with_alternates_adds_tips_from_alternates() -> crate::Result<()> { + use gix::error::ResultExt; + // Isolated repository options don't sanitize the ambient Git config inherited by local `upload-pack`. // Use a child to keep this clone parallel with other tests without changing the parent environment. if gix_testtools::run_in_isolated_process()? { @@ -312,10 +313,13 @@ mod blocking_and_async_io { r.repo().objects.store_ref().path().join("info").join("alternates"), format!( "{}\n", - gix::path::realpath(remote_repo.objects.store_ref().path())?.display() + gix::path::realpath(remote_repo.objects.store_ref().path()) + .or_erased()? + .display() ) .as_bytes(), - )?; + ) + .or_erased()?; Ok(r) } }) diff --git a/gix/tests/gix/remote/save.rs b/gix/tests/gix/remote/save.rs index 04c00257b9c..19732c999ab 100644 --- a/gix/tests/gix/remote/save.rs +++ b/gix/tests/gix/remote/save.rs @@ -51,10 +51,13 @@ mod save_as_to { fn anonymous_remotes_cannot_be_saved_lacking_a_name() -> crate::Result { let repo = basic_repo()?; let remote = repo.remote_at("https://example.com/path")?; - assert!(matches!( - remote.save_to(&mut gix::config::File::default()).unwrap_err(), - gix::remote::save::Error::NameMissing { .. } - )); + assert_eq!( + remote + .save_to(&mut gix::config::File::default()) + .unwrap_err() + .to_string(), + "The remote pointing to https://example.com/path is anonymous and can't be saved." + ); Ok(()) } diff --git a/gix/tests/gix/repository/branch.rs b/gix/tests/gix/repository/branch.rs index e884e5dd7a4..ef558c4b56c 100644 --- a/gix/tests/gix/repository/branch.rs +++ b/gix/tests/gix/repository/branch.rs @@ -107,20 +107,32 @@ fn deletes_a_batch_and_all_of_its_local_config_without_inspecting_commits() -> c #[test] fn validation_failure_leaves_the_entire_batch_unchanged() -> crate::Result { let (mut repo, _tmp) = crate::repo_rw("make_references_repo.sh")?; + let work_dir = repo + .workdir() + .expect("the reference fixture is a non-bare repository") + .to_owned(); let deletable = refname("refs/heads/d1"); let checked_out = refname("refs/heads/main"); let err = repo .delete_local_branches([deletable.clone(), checked_out.clone()]) .expect_err("the checked-out branch prevents the whole batch"); - assert!(matches!(err, gix::repository::branch::delete::Error::CheckedOut { name, .. } if name == checked_out)); + assert_eq!( + err.to_string(), + format!("The local branch {checked_out:?} is checked out in [{work_dir:?}]") + ); + let cause = err + .downcast_any_ref::() + .expect("checked-out branches remain identifiable without parsing the diagnostic"); + assert_eq!(cause.name, checked_out, "the protected branch is retained"); + assert_eq!(cause.worktree_dirs, [work_dir], "the main worktree path is retained"); assert!(repo.try_find_reference(deletable.as_ref())?.is_some()); let tag = refname("refs/tags/t1"); let err = repo .delete_local_branches([tag.clone()]) .expect_err("non-local references are rejected"); - assert!(matches!(err, gix::repository::branch::delete::Error::NotLocal { name } if name == tag)); + assert_eq!(err.to_string(), format!("{tag:?} is not a local branch")); assert!(repo.try_find_reference(tag.as_ref())?.is_some()); Ok(()) } @@ -233,10 +245,26 @@ fn linked_worktree_branches_are_protected_and_common_config_is_updated() -> crat std::fs::write(linked_repo.git_dir().join("HEAD"), b"ref: refs/heads/linked\n")?; let mut main = gix::open_opts(&main_path, crate::restricted())?; + let linked_work_dir = main + .worktree_proxy_by_id("linked") + .expect("the linked worktree is registered") + .base()?; let err = main .delete_local_branches([checked_out.clone()]) .expect_err("a linked worktree checkout is protected"); - assert!(matches!(err, gix::repository::branch::delete::Error::CheckedOut { name, .. } if name == checked_out)); + assert_eq!( + err.to_string(), + format!("The local branch {checked_out:?} is checked out in [{linked_work_dir:?}]") + ); + let cause = err + .downcast_any_ref::() + .expect("linked worktree checkouts retain the same typed rejection"); + assert_eq!(cause.name, checked_out, "the protected branch is retained"); + assert_eq!( + cause.worktree_dirs, + [linked_work_dir], + "the linked worktree path is retained" + ); let mut config = std::fs::OpenOptions::new() .append(true) diff --git a/gix/tests/gix/repository/config/mod.rs b/gix/tests/gix/repository/config/mod.rs index f9009592abf..9dcc7b2bf02 100644 --- a/gix/tests/gix/repository/config/mod.rs +++ b/gix/tests/gix/repository/config/mod.rs @@ -47,6 +47,27 @@ fn big_file_threshold() -> crate::Result { Ok(()) } +#[cfg(feature = "index")] +#[test] +fn invalid_stat_boolean_is_validation_error() -> crate::Result { + for key in [ + "core.trustCTime", + "gitoxide.core.useNsec", + "gitoxide.core.useStdev", + "core.checkStat", + ] { + let mut repo = repo("with-hasconfig"); + repo.config_snapshot_mut().set_raw_value(key, "invalid")?; + assert!( + repo.stat_options() + .expect_err("invalid value must fail") + .is_validation(), + "invalid {key} is classified as a validation error" + ); + } + Ok(()) +} + #[cfg(feature = "blocking-network-client")] mod ssh_options { use std::ffi::OsStr; diff --git a/gix/tests/gix/repository/mod.rs b/gix/tests/gix/repository/mod.rs index bda42521f74..13439e1b686 100644 --- a/gix/tests/gix/repository/mod.rs +++ b/gix/tests/gix/repository/mod.rs @@ -116,6 +116,10 @@ mod index { #[test] fn basics() -> crate::Result { let repo = crate::named_subrepo_opts("make_basic_repo.sh", "unborn", gix::open::Options::isolated())?; + assert!( + repo.index().expect_err("the fixture has no index").is_not_found(), + "a missing index has standard not-found classification" + ); assert!( repo.index_or_load_from_head().is_err(), "can't read index if `HEAD^{{tree}}` can't be resolved" diff --git a/gix/tests/gix/repository/object.rs b/gix/tests/gix/repository/object.rs index 0f4e600984e..956a39ceb13 100644 --- a/gix/tests/gix/repository/object.rs +++ b/gix/tests/gix/repository/object.rs @@ -620,13 +620,18 @@ fn empty_objects_are_always_present_but_not_in_plumbing() -> crate::Result { assert!(!repo.objects.contains(&empty_blob_id)); assert!( - repo.find_header(empty_blob_id).is_err(), - "Empty blob doesn't exist automatically just like in Git" + repo.find_header(empty_blob_id) + .expect_err("empty blob doesn't exist automatically just like in Git") + .is_not_found() ); assert_eq!(repo.objects.try_header(&empty_blob_id)?, None); assert_eq!(repo.try_find_header(empty_blob_id)?, None); - assert!(repo.find_object(empty_blob_id).is_err()); + assert!( + repo.find_object(empty_blob_id) + .expect_err("empty blob doesn't exist") + .is_not_found() + ); assert!(repo.try_find_object(empty_blob_id)?.is_none()); let mut buf = Vec::new(); diff --git a/gix/tests/gix/repository/open.rs b/gix/tests/gix/repository/open.rs index 6cef9cc1f0c..c88af6d914c 100644 --- a/gix/tests/gix/repository/open.rs +++ b/gix/tests/gix/repository/open.rs @@ -1,4 +1,4 @@ -use std::{borrow::Cow, error::Error}; +use std::borrow::Cow; use gix::bstr::BString; use gix::config::tree::Key; @@ -143,7 +143,7 @@ fn non_bare_reftable() -> crate::Result { let repo = gix::open_opts(root.join("reftable-clone"), gix::open::Options::isolated())?; let err = repo.head_id().expect_err("reftable references are not supported"); assert_eq!( - err.source().expect("reference decoding error").to_string(), + err.probable_cause().to_string(), "This reference uses an unsupported storage backend, such as reftable", "accessing HEAD explains that the reference storage backend is unsupported" ); @@ -260,9 +260,9 @@ fn git_index_file_empty_is_invalid_even_with_lenient_config() -> crate::Result { ) .expect_err("an empty index path must be rejected"); - assert_eq!( - err.source().expect("configuration error").to_string(), - "The key \"gitoxide.core.indexFile=\" (possibly from GIT_INDEX_FILE) was invalid", + assert!( + err.iter_errors().any(|source| source.to_string() + == "The key \"gitoxide.core.indexFile=\" (possibly from GIT_INDEX_FILE) was invalid"), "an empty index path is never ignored, even though configuration is lenient by default" ); Ok(()) @@ -506,10 +506,11 @@ fn non_bare_split_worktree_invalid_worktree_path_boolean() -> crate::Result { gix::open::Options::isolated().strict_config(true), ) .unwrap_err(); - assert_eq!( - err.source().expect("present").to_string(), - "The key \"core.worktree\" (possibly from GIT_WORK_TREE) was invalid", - "in strict mode, we fail just like git does" + assert!(err.is_validation(), "in strict mode, we fail just like git does"); + assert!( + err.iter_errors().any(|source| { + source.to_string() == "The key \"core.worktree\" (possibly from GIT_WORK_TREE) was invalid" + }) ); Ok(()) } @@ -524,12 +525,16 @@ fn non_bare_split_worktree_invalid_worktree_path_empty() -> crate::Result { ) .unwrap_err(); assert!( - matches!( - err, - gix::open::Error::Config(gix::config::Error::PathInterpolation { .. }) - ), + err.is_validation(), "DEVIATION: could not read path at core.worktree as empty is always invalid, git tries to use an empty path, even though it's better to reject it" ); + let validation = err + .downcast_any_ref::() + .expect("the invalid core.worktree path remains classified"); + assert_eq!( + validation.message, + "The path at the 'core.worktree' configuration could not be interpolated" + ); Ok(()) } @@ -603,7 +608,9 @@ mod not_a_repository { let name = format!("not-a-repo-{name}"); let repo_path = gix_testtools::scripted_fixture_read_only("make_config_repos.sh")?.join(name); let err = gix::open_opts(&repo_path, gix::open::Options::isolated()).unwrap_err(); - assert!(matches!(err, gix::open::Error::NotARepository { path, .. } if path == repo_path)); + assert!(err.is_not_found()); + assert!(err.iter_errors().any(|source| source.to_string() + == format!("\"{}\" does not appear to be a git repository", repo_path.display()))); } Ok(()) } @@ -624,12 +631,13 @@ mod object_format_extension { let err = named_subrepo_opts("make_config_repos.sh", name, gix::open::Options::isolated()) .expect_err("a v0 repository setting extensions.objectFormat must be rejected"); assert!( - matches!( - err, - gix::open::Error::Config(gix::config::Error::ObjectFormatRequiresV1) - ), + err.is_validation(), "objectFormat on a v0 repository must be rejected, got {err:?} for {name}" ); + assert_eq!( + err.probable_cause().to_string(), + "extensions.objectFormat is a v1-only extension, but the repository format version is 0; set core.repositoryFormatVersion=1 to use it, or remove extensions.objectFormat to fall back to the default Sha1 format (if supported by this build)" + ); } Ok(()) } @@ -643,12 +651,13 @@ mod object_format_extension { ) .expect_err("future repository format versions must be rejected"); assert!( - matches!( - err, - gix::open::Error::Config(gix::config::Error::UnsupportedRepositoryFormatVersion { version: 2 }) - ), + err.is_validation(), "future repository format versions must be rejected before interpreting extensions, got {err:?}" ); + assert_eq!( + err.probable_cause().to_string(), + "Unsupported repository format version 2; only versions 0 and 1 are supported" + ); Ok(()) } } @@ -670,7 +679,11 @@ mod open_path_as_is { #[test] fn worktrees_cannot_be_opened() -> crate::Result { let err = repo_opts("make_basic_repo.sh", open_path_as_is()).unwrap_err(); - assert!(matches!(err, gix::open::Error::NotARepository { .. })); + assert!(err.is_not_found()); + assert!( + err.downcast_any_ref::().is_some(), + "the precise not-a-repository marker survives erasure" + ); Ok(()) } diff --git a/gix/tests/gix/repository/remote.rs b/gix/tests/gix/repository/remote.rs index aeab08451cd..ed54827565e 100644 --- a/gix/tests/gix/repository/remote.rs +++ b/gix/tests/gix/repository/remote.rs @@ -224,10 +224,12 @@ mod find_remote { ); } assert!(count > 0, "should have seen more than one commit"); - assert!(matches!( - repo.find_remote("unknown").unwrap_err(), - gix::remote::find::existing::Error::NotFound { .. } - )); + let err = repo.find_remote("unknown").unwrap_err(); + assert!(err.is_not_found()); + assert!( + err.iter_errors() + .any(|source| source.to_string() == "The remote named \"unknown\" did not exist") + ); Ok(()) } @@ -547,9 +549,11 @@ mod find_remote { let repo = remote::repo("bad-explicit-push-url-rewriting"); let expected_err_msg = "The rewritten push url \":://repo\" failed to parse"; - assert_eq!( - repo.find_remote("origin").unwrap_err().to_string(), - expected_err_msg, + assert!( + repo.find_remote("origin") + .unwrap_err() + .iter_errors() + .any(|source| source.to_string() == expected_err_msg), "explicit pushUrl values use normal insteadOf rewriting, so a malformed result must fail and be labeled as a push URL error" ); diff --git a/gix/tests/gix/repository/worktree.rs b/gix/tests/gix/repository/worktree.rs index b99c5519418..c300519eb53 100644 --- a/gix/tests/gix/repository/worktree.rs +++ b/gix/tests/gix/repository/worktree.rs @@ -467,12 +467,10 @@ fn run_assertions(main_repo: gix::Repository, should_be_bare: bool) { ); repo } else { + let err = actual.clone().into_repo().unwrap_err().to_string(); assert!( - matches!( - actual.clone().into_repo(), - Err(gix::worktree::proxy::into_repo::Error::MissingWorktree { .. }) - ), - "missing bases are detected" + err.starts_with("Worktree at '") && err.ends_with("' is inaccessible"), + "missing bases are detected, but got: {err}" ); actual.clone().into_repo_with_possibly_inaccessible_worktree().unwrap() }; diff --git a/gix/tests/gix/revision/spec/from_bytes/ambiguous.rs b/gix/tests/gix/revision/spec/from_bytes/ambiguous.rs index c52306a1591..2dec286d8cf 100644 --- a/gix/tests/gix/revision/spec/from_bytes/ambiguous.rs +++ b/gix/tests/gix/revision/spec/from_bytes/ambiguous.rs @@ -59,7 +59,7 @@ fn fully_failed_disambiguation_still_yields_an_ambiguity_error() { | └─ Message("Short id 0000000000 is ambiguous. Candidates are:\n\t0000000000e commit 2005-04-07 \"a2onsxbvj\"\n\t0000000000c tree\n\t0000000000b blob") | - └─ NotFound { oid: Prefix { bytes: Sha1(0000000000c00000000000000000000000000000), hex_len: 11 }, actual: Tree, expected: Tag } + └─ ValidationError { message: "Last encountered object 0000000000c was tree while trying to peel to tag", input: None } "#); use std::error::Error; assert_eq!( diff --git a/gix/tests/gix/submodule.rs b/gix/tests/gix/submodule.rs index 91c2a279b82..96adb7b3081 100644 --- a/gix/tests/gix/submodule.rs +++ b/gix/tests/gix/submodule.rs @@ -14,6 +14,40 @@ mod open { use crate::{submodule::repo, util::named_subrepo_opts}; + #[test] + fn empty_worktree_config_is_not_an_uninitialized_submodule() -> crate::Result { + let fixture = gix_testtools::scripted_fixture_writable("make_submodule_with_worktree.sh")?; + let repo = gix::open_opts( + fixture.path().join("submodule-with-extra-worktree-host"), + gix::open::Options::isolated(), + )?; + let worktree = repo.workdir().expect("fixture has a worktree"); + gix_testtools::git(worktree, "submodule deinit -f m1")?; + let sm = repo + .submodules()? + .expect("fixture has submodules") + .next() + .expect("fixture has m1"); + assert!( + sm.open()?.is_some(), + "deinitializing preserves the submodule repository" + ); + gix_testtools::git(worktree, "config --file .git/modules/m1/config core.worktree ''")?; + + let err = sm + .open() + .expect_err("invalid core.worktree configuration must propagate"); + assert!( + err.error().is::(), + "the failure is classified as invalid configuration: {err:?}" + ); + assert!( + err.downcast_any_ref::().is_some(), + "the nested missing-path error must not hide the configuration failure" + ); + Ok(()) + } + #[test] fn various() -> crate::Result { for (name, expected) in [ @@ -320,51 +354,29 @@ mod open { .next() .expect("one submodule"); - assert!(matches!( - sm.git_dir_try_old_form(), - Err(submodule::git_dir_try_old_form::Error::InvalidGitDirFileTarget { - target: Some(target), - source: None, - .. - }) if target.ends_with("missing") - )); - assert!(matches!( - sm.state(), - Err(submodule::state::Error::GitDirTryOldForm( - submodule::git_dir_try_old_form::Error::InvalidGitDirFileTarget { - target: Some(target), - source: None, - .. - } - )) if target.ends_with("missing") - )); - assert!(matches!( - sm.open(), - Err(submodule::open::Error::GitDir( - submodule::git_dir_try_old_form::Error::InvalidGitDirFileTarget { - target: Some(target), - source: None, - .. - } - )) if target.ends_with("missing") - )); + for err in [ + sm.git_dir_try_old_form().expect_err("the gitlink target is missing"), + sm.state().expect_err("state validates the gitlink target"), + sm.open().expect_err("opening validates the gitlink target"), + ] { + assert!(err.is_validation()); + let cause = err.probable_cause().to_string(); + assert!( + cause.starts_with("The gitdir file at '") + && cause.contains("invalid gitdir target") + && cause.contains("missing"), + "the missing target and its validation context are retained: {cause}" + ); + } #[cfg(feature = "status")] - assert!( - matches!( - sm.status(gix::submodule::config::Ignore::None, false), - Err(submodule::status::Error::State( - submodule::state::Error::GitDirTryOldForm( - submodule::git_dir_try_old_form::Error::InvalidGitDirFileTarget { - target: Some(target), - source: None, - .. - } - ) - )) if target.ends_with("missing") - ), - "ignore=none fails as some submodules can't be opened" - ); + { + let err = sm + .status(gix::submodule::config::Ignore::None, false) + .expect_err("ignore=none fails as some submodules can't be opened"); + assert!(err.is_validation()); + assert!(err.probable_cause().to_string().contains("invalid gitdir target")); + } #[cfg(feature = "status")] { @@ -393,32 +405,15 @@ mod open { .next() .expect("one submodule"); - assert!(matches!( - sm.git_dir_try_old_form(), - Err(submodule::git_dir_try_old_form::Error::InvalidGitDirFileTarget { - target: None, - source: Some(_), - .. - }) - )); + let err = sm.git_dir_try_old_form().expect_err("the gitlink target is malformed"); + assert!(err.is_validation()); #[cfg(feature = "status")] { - assert!( - matches!( - sm.status(gix::submodule::config::Ignore::None, false), - Err(submodule::status::Error::State( - submodule::state::Error::GitDirTryOldForm( - submodule::git_dir_try_old_form::Error::InvalidGitDirFileTarget { - target: None, - source: Some(_), - .. - } - ) - )) - ), - "ignore=none fails as some submodules can't be opened" - ); + let err = sm + .status(gix::submodule::config::Ignore::None, false) + .expect_err("ignore=none fails as some submodules can't be opened"); + assert!(err.is_validation()); let status = sm.status(gix::submodule::config::Ignore::All, false)?; assert_eq!( @@ -787,29 +782,20 @@ mod advisory { sm.git_dir(), Err(gix_validate::submodule::name::Error::ParentComponent) )); - assert!(matches!( - sm.git_dir_try_old_form(), - Err(gix::submodule::git_dir_try_old_form::Error::GitDir( - gix_validate::submodule::name::Error::ParentComponent - )) - )); - - assert!(matches!( - sm.open(), - Err(gix::submodule::open::Error::GitDir( - gix::submodule::git_dir_try_old_form::Error::GitDir( - gix_validate::submodule::name::Error::ParentComponent - ) - )) - )); - assert!(matches!( - sm.state(), - Err(gix::submodule::state::Error::GitDirTryOldForm( - gix::submodule::git_dir_try_old_form::Error::GitDir( - gix_validate::submodule::name::Error::ParentComponent - ) - )) - )); + for err in [ + sm.git_dir_try_old_form().expect_err("the traversal name is rejected"), + sm.open().expect_err("opening rejects the traversal name"), + sm.state().expect_err("state rejects the traversal name"), + ] { + assert!(err.is_validation()); + assert!( + matches!( + err.downcast_any_ref::(), + Some(gix_validate::submodule::name::Error::ParentComponent) + ), + "the precise traversal-name failure remains in the erased API's chain" + ); + } let redirected_repo = fixture.path().join("escaped-target.git"); assert!( diff --git a/gix/tests/gix/util.rs b/gix/tests/gix/util.rs index 6d9de0f9db6..7ae1c63c7e4 100644 --- a/gix/tests/gix/util.rs +++ b/gix/tests/gix/util.rs @@ -239,7 +239,7 @@ pub fn named_subrepo_opts( opts: open::Options, ) -> std::result::Result { let repo_path = gix_testtools::scripted_fixture_read_only(fixture) - .map_err(|err| gix::open::Error::Io(std::io::Error::other(err)))? + .map_err(gix::open::Error::from_boxed)? .join(name); Ok(ThreadSafeRepository::open_opts(repo_path, opts)?.to_thread_local()) } diff --git a/gix/tests/repository.rs b/gix/tests/repository.rs index de295431d6c..719456225de 100644 --- a/gix/tests/repository.rs +++ b/gix/tests/repository.rs @@ -7,7 +7,7 @@ use serial_test::serial; #[test] #[serial] fn config_path_uses_repository_options_for_global_sources() -> gix_testtools::Result { - use gix::config::{Source, file_mut::Error}; + use gix::config::Source; let fixture = gix::path::realpath(gix_testtools::scripted_fixture_read_only("make_config_repos.sh")?)?; let git_dir = fixture.join("bare-repo"); @@ -45,13 +45,17 @@ fn config_path_uses_repository_options_for_global_sources() -> gix_testtools::Re let repo = gix::open_opts(&git_dir, gix::open::Options::isolated())?; for source in [Source::GitInstallation, Source::System, Source::Git, Source::User] { assert!( - matches!(repo.config_path(source), Err(Error::SourceUnavailable(actual)) if actual == source), + repo.config_path(source).is_err_and( + |err| err == format!("Configuration source {source:?} has no available path with these options") + ), "{source:?} remains unavailable when disabled by the repository's permissions" ); } for source in [Source::Env, Source::Cli, Source::Api, Source::EnvOverride] { assert!( - matches!(repo.config_path(source), Err(Error::UnsupportedSource(actual)) if actual == source), + repo.config_path(source) + .is_err_and(|err| err + == format!("Configuration source {source:?} requires a repository or has no physical file")), "{source:?} has no physical configuration file even with a repository" ); } @@ -230,12 +234,16 @@ fn paths_cannot_leave_the_repository() -> gix_testtools::Result { "some-with-file/very/deeply/nested/subdir/empty-file", "absolute paths inside the worktree become repository-relative" ); - assert!( - matches!( - repo.normalize_path("../../outside"), - Err(gix::repository::normalize_path::Error::OutsideOfRepository { .. }) - ), - "relative paths cannot traverse above the worktree" + let err = repo + .normalize_path("../../outside") + .expect_err("relative paths cannot traverse above the worktree"); + assert!(err.is_validation(), "leaving the worktree is a validation error"); + assert_eq!( + err.probable_cause().to_string(), + format!( + "The path '{}' leaves the repository", + std::path::Path::new("some").join("../../outside").display() + ) ); assert_eq!( @@ -254,19 +262,18 @@ fn absolute_paths_outside_the_repository_are_rejected() -> gix_testtools::Result let outside = root.parent().expect("fixture has a parent").to_owned(); let outside_as_bstr = gix::path::into_bstr(outside.clone()); - match repo + let err = repo .normalize_path(&outside_as_bstr) - .expect_err("an absolute path outside the repository must fail") - { - gix::repository::normalize_path::Error::AbsolutePathOutsideOfRepository { - path, - root: actual_root, - } => { - assert_eq!(path, outside, "the rejected path is retained"); - assert_eq!(actual_root, root, "the repository root is retained"); - } - err => panic!("expected an absolute-path-outside error, got {err:?}"), - } + .expect_err("an absolute path outside the repository must fail"); + assert!(err.is_validation(), "an outside path is a validation error"); + assert_eq!( + err.probable_cause().to_string(), + format!( + "The absolute path '{}' is not inside the repository at '{}'", + outside.display(), + root.display() + ) + ); Ok(()) } @@ -444,8 +451,7 @@ fn open_options_preset_system_config_paths_avoid_running_git() -> gix_testtools: #[cfg(feature = "revision")] fn probable_cause(res: Result, gix::revision::spec::parse::single::Error>) -> String { - match res.expect_err("the revspec must not resolve") { - gix::revision::spec::parse::single::Error::Parse(err) => err.probable_cause().to_string(), - err => panic!("expected a failure while parsing, got {err:?}"), - } + res.expect_err("the revspec must not resolve") + .probable_cause() + .to_string() } diff --git a/tests/snapshots/porcelain/init/fail b/tests/snapshots/porcelain/init/fail index 3897b81e206..a00c55bc966 100644 --- a/tests/snapshots/porcelain/init/fail +++ b/tests/snapshots/porcelain/init/fail @@ -1,4 +1,4 @@ Error: Repository initialization failed Caused by: - Refusing to initialize the existing '.git' directory \ No newline at end of file + Refusing to initialize an existing directory: ".git" \ No newline at end of file diff --git a/tests/snapshots/porcelain/init/fail-with-multi-element-directory b/tests/snapshots/porcelain/init/fail-with-multi-element-directory index fb1ec202ce8..2850ec45349 100644 --- a/tests/snapshots/porcelain/init/fail-with-multi-element-directory +++ b/tests/snapshots/porcelain/init/fail-with-multi-element-directory @@ -1,4 +1,4 @@ Error: Repository initialization failed Caused by: - Refusing to initialize the existing 'foo/bar/.git' directory \ No newline at end of file + Refusing to initialize an existing directory: "foo/bar/.git" \ No newline at end of file From aa835279c132653c7d23c177fd110cd00c52b3c6 Mon Sep 17 00:00:00 2001 From: Codex Date: Sat, 29 Aug 2026 10:15:37 +0200 Subject: [PATCH 29/51] fix(gix-worktree-stream): preserve allocation failure sources rubberstamp Keep the original `TryReserveError` inside the `OutOfMemory` I/O error so structured classification can distinguish allocator failure without treating malformed stream metadata as corruption. --- gix-worktree-stream/src/protocol.rs | 3 ++- gix-worktree-stream/tests/stream.rs | 35 +++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/gix-worktree-stream/src/protocol.rs b/gix-worktree-stream/src/protocol.rs index 328ac61e819..409271c88b2 100644 --- a/gix-worktree-stream/src/protocol.rs +++ b/gix-worktree-stream/src/protocol.rs @@ -138,7 +138,8 @@ fn mode_to_byte(m: gix_object::tree::EntryMode) -> u8 { fn clear_and_set_len(buf: &mut Vec, len: usize) -> io::Result<()> { buf.clear(); - buf.try_reserve(len).map_err(|_| ErrorKind::OutOfMemory)?; + buf.try_reserve(len) + .map_err(|err| io::Error::new(ErrorKind::OutOfMemory, err))?; buf.resize(len, 0); Ok(()) } diff --git a/gix-worktree-stream/tests/stream.rs b/gix-worktree-stream/tests/stream.rs index 71d52f71fe2..273747b5825 100644 --- a/gix-worktree-stream/tests/stream.rs +++ b/gix-worktree-stream/tests/stream.rs @@ -7,6 +7,41 @@ fn hex_to_id(hex_sha1: &str, hex_sha256: &str) -> gix_hash::ObjectId { } } +#[test] +fn impossible_path_allocation_preserves_its_source() { + let mut input = Vec::new(); + input.extend_from_slice(&usize::MAX.to_le_bytes()); + input.extend_from_slice(&0usize.to_le_bytes()); + input.extend_from_slice(&[0, 0]); + input.extend_from_slice(&[0; 20]); + + let err = gix_worktree_stream::Stream::from_read(std::io::Cursor::new(input)) + .next_entry() + .expect_err("the declared path length cannot be allocated") + .into_error(); + let io_err = err + .downcast_any_ref::() + .expect("the I/O error is retained"); + assert_eq!(io_err.kind(), std::io::ErrorKind::OutOfMemory); + assert!( + io_err + .get_ref() + .expect("the allocation error is retained") + .is::(), + "the allocation error is retained" + ); + assert_eq!( + err.classify() + .map(|classification| classification.class()) + .collect::>(), + [gix_error::Class::ResourceExhaustion( + gix_error::ResourceExhaustionKind::AllocationFailure + )] + ); + assert!(!err.is_corrupted()); + assert!(!err.can_retry()); +} + mod from_tree { use std::{ convert::Infallible, From 2176245d4618faf4acd0da81e3608f4eba453192 Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 6 Sep 2026 21:11:59 +0200 Subject: [PATCH 30/51] change(gix)!: remove redundant error aliases rubberstamp The crate-by-crate migration retained operation-specific error aliases to limit downstream churn. With the migration complete, those names only hide the shared error types and keep otherwise empty API namespaces alive. Use the underlying `gix_error` types directly throughout the workspace, including indirect aliases, renamed exports, test helpers, and the URL fuzz target. Remove namespaces and files that only held forwarding aliases, and update documentation and migration guidance to use the canonical types. Adjust the source locations recorded in error snapshots after deleting the alias declarations. Keep `gix::{Error, Exn}` and `gix::error` as the central facade, along with unrenamed canonical re-exports, required associated types, concrete errors, and aliases that add structure. Preserve each `Exn` parameter, conditional error alternative, error message, and source chain. Include all downstream adaptations in this breaking change so the stack remains buildable. --- AGENTS.md | 6 +- Cargo.lock | 3 + etc/plan/gix-error.md | 12 +- gitoxide-core/src/hours/mod.rs | 2 +- gitoxide-core/src/index/checkout.rs | 6 +- gitoxide-core/src/net.rs | 2 +- gitoxide-core/src/organize.rs | 3 +- gitoxide-core/src/pack/explode.rs | 18 +- gitoxide-core/src/pack/receive.rs | 4 +- gitoxide-core/src/query/engine/update.rs | 7 +- gix-actor/src/signature/mod.rs | 4 +- gix-archive/src/lib.rs | 3 - gix-archive/src/write.rs | 16 +- gix-attributes/src/name.rs | 12 +- gix-attributes/src/parse.rs | 24 +- gix-attributes/src/search/attributes.rs | 2 +- gix-attributes/tests/attributes/main.rs | 1 - gix-attributes/tests/attributes/parse.rs | 12 +- gix-attributes/tests/attributes/search.rs | 10 +- gix-bitmap/src/ewah.rs | 8 +- gix-blame/src/error.rs | 2 - gix-blame/src/file/function.rs | 18 +- gix-blame/src/lib.rs | 2 - gix-blame/tests/blame.rs | 4 +- gix-config-value/src/boolean.rs | 12 +- gix-config-value/src/color.rs | 18 +- gix-config-value/src/integer.rs | 10 +- gix-config-value/src/lib.rs | 3 - gix-config-value/src/path.rs | 7 +- gix-config-value/tests/value/boolean.rs | 8 +- gix-config-value/tests/value/color.rs | 4 +- gix-config-value/tests/value/integer.rs | 2 +- gix-config-value/tests/value/main.rs | 2 - gix-config-value/tests/value/path.rs | 12 +- gix-config/src/file/access/comfort.rs | 26 +-- gix-config/src/file/access/mutate.rs | 38 +-- gix-config/src/file/access/raw.rs | 81 ++++--- gix-config/src/file/access/read_only.rs | 15 +- gix-config/src/file/includes/mod.rs | 21 +- gix-config/src/file/includes/types.rs | 3 - gix-config/src/file/init/comfort.rs | 12 +- gix-config/src/file/init/from_env.rs | 5 +- gix-config/src/file/init/from_paths.rs | 12 +- gix-config/src/file/init/mod.rs | 6 +- gix-config/src/file/init/types.rs | 3 - gix-config/src/file/mod.rs | 12 - gix-config/src/file/mutable/mod.rs | 2 +- gix-config/src/file/mutable/multi_value.rs | 12 +- gix-config/src/file/mutable/section.rs | 18 +- gix-config/src/file/mutable/value.rs | 7 +- gix-config/src/file/section/body.rs | 2 +- gix-config/src/file/section/mod.rs | 18 +- gix-config/src/file/util.rs | 4 +- gix-config/src/lookup.rs | 14 +- gix-config/src/parse/comment.rs | 2 +- gix-config/src/parse/event.rs | 4 +- gix-config/src/parse/format.rs | 4 +- gix-config/src/parse/mod.rs | 26 ++- gix-config/src/parse/section/header.rs | 21 +- gix-config/src/parse/section/mod.rs | 20 +- gix-config/src/value/mod.rs | 2 - .../init/from_paths/includes/unconditional.rs | 4 +- gix-config/tests/config/parse/section.rs | 5 +- gix-credentials/examples/custom-helper.rs | 2 +- .../examples/git-credential-lite.rs | 2 +- gix-credentials/src/helper/invoke.rs | 7 +- gix-credentials/src/helper/mod.rs | 7 +- gix-credentials/src/program/main.rs | 7 +- gix-credentials/src/protocol/context/mod.rs | 7 +- gix-credentials/src/protocol/context/serde.rs | 9 +- gix-credentials/src/protocol/mod.rs | 7 +- gix-date/src/lib.rs | 2 - gix-date/src/parse/function.rs | 10 +- gix-date/src/parse/mod.rs | 6 +- gix-date/src/parse/relative.rs | 22 +- gix-date/tests/time/parse/mod.rs | 2 +- gix-diff/src/blob/pipeline.rs | 10 +- gix-diff/src/blob/platform.rs | 23 +- gix-diff/src/index/function.rs | 10 +- gix-diff/src/index/mod.rs | 3 - gix-diff/src/rewrites/tracker.rs | 18 +- gix-diff/src/tree/mod.rs | 4 +- gix-diff/src/tree_with_rewrites/change.rs | 2 +- gix-diff/tests/diff/blob/platform.rs | 5 +- gix-diff/tests/diff/main.rs | 4 +- gix-dir/src/walk/classify.rs | 6 +- gix-dir/src/walk/function.rs | 6 +- gix-dir/src/walk/mod.rs | 3 - gix-dir/src/walk/readdir.rs | 4 +- gix-dir/tests/walk_utils/mod.rs | 16 +- gix-discover/src/is.rs | 4 +- gix-discover/src/lib.rs | 8 +- gix-discover/src/parse.rs | 14 +- gix-discover/src/path.rs | 8 +- gix-discover/src/upwards/mod.rs | 12 +- gix-discover/src/upwards/types.rs | 3 - gix-error/src/lib.rs | 14 +- gix-filter/src/driver/apply.rs | 7 +- gix-filter/src/driver/delayed.rs | 19 +- gix-filter/src/driver/init.rs | 7 +- gix-filter/src/driver/process/client.rs | 26 +-- gix-filter/src/driver/process/server.rs | 16 +- gix-filter/src/driver/shutdown.rs | 5 +- gix-filter/src/eol/convert_to_git.rs | 7 +- gix-filter/src/eol/convert_to_worktree.rs | 5 +- gix-filter/src/ident.rs | 12 +- gix-filter/src/pipeline/convert.rs | 18 +- gix-filter/src/pipeline/util.rs | 6 +- gix-filter/src/worktree/encode_to_git.rs | 7 +- gix-filter/src/worktree/encode_to_worktree.rs | 7 +- gix-filter/src/worktree/encoding.rs | 8 +- .../tests/filter/pipeline/convert_to_git.rs | 4 +- gix-fs/src/stack.rs | 24 +- gix-fsck/Cargo.toml | 1 + gix-fsck/src/lib.rs | 2 +- gix-hash/src/change_id.rs | 8 +- gix-hash/src/hasher.rs | 11 +- gix-hash/src/io.rs | 16 +- gix-hash/src/lib.rs | 2 +- gix-hash/src/object_id.rs | 16 +- gix-hash/src/oid.rs | 9 +- gix-hash/src/prefix.rs | 41 ++-- gix-hash/src/verify.rs | 11 +- gix-hash/tests/hash/hasher.rs | 4 +- gix-hash/tests/hash/object_id.rs | 4 +- gix-index/src/decode/entries.rs | 4 +- gix-index/src/decode/header.rs | 8 +- gix-index/src/decode/mod.rs | 14 +- gix-index/src/extension/decode.rs | 8 +- .../extension/end_of_index_entry/decode.rs | 2 +- .../src/extension/end_of_index_entry/write.rs | 2 +- gix-index/src/extension/link.rs | 13 +- gix-index/src/extension/tree/verify.rs | 20 +- gix-index/src/file/init.rs | 7 +- gix-index/src/file/verify.rs | 8 +- gix-index/src/file/write.rs | 7 +- gix-index/src/init.rs | 5 +- gix-index/src/verify.rs | 20 +- gix-index/src/write.rs | 2 +- gix-index/tests/index/file/read.rs | 2 +- gix-index/tests/index/fuzzed.rs | 4 +- gix-lock/src/acquire.rs | 19 +- gix-lock/tests/all.rs | 2 - gix-lock/tests/lock/file.rs | 18 +- gix-lock/tests/lock/marker.rs | 6 +- gix-mailmap/src/parse.rs | 10 +- gix-mailmap/tests/parse/mod.rs | 4 +- gix-merge/src/blob/pipeline.rs | 8 +- gix-merge/src/blob/platform/merge.rs | 10 +- gix-merge/src/blob/platform/prepare_merge.rs | 5 +- gix-merge/src/blob/platform/set_resource.rs | 5 +- gix-merge/src/commit/function.rs | 7 +- gix-merge/src/commit/mod.rs | 3 - gix-merge/src/commit/virtual_merge_base.rs | 8 +- gix-merge/src/tree/function/change.rs | 7 +- gix-merge/src/tree/function/resolve.rs | 8 +- gix-merge/src/tree/mod.rs | 3 - gix-merge/src/tree/utils.rs | 10 +- gix-merge/tests/merge/blob/mod.rs | 4 +- gix-negotiate/Cargo.toml | 1 + gix-negotiate/src/consecutive.rs | 23 +- gix-negotiate/src/lib.rs | 15 +- gix-negotiate/src/noop.rs | 14 +- gix-negotiate/src/skipping.rs | 25 +- gix-note/src/lib.rs | 41 ++-- gix-note/tests/note.rs | 32 +-- gix-object/benches/edit_tree.rs | 5 +- gix-object/src/commit/mod.rs | 17 +- gix-object/src/commit/ref_iter.rs | 20 +- gix-object/src/data.rs | 12 +- gix-object/src/encode.rs | 9 +- gix-object/src/find.rs | 52 ++--- gix-object/src/kind.rs | 7 +- gix-object/src/lib.rs | 24 +- gix-object/src/object/convert.rs | 6 +- gix-object/src/object/mod.rs | 13 +- gix-object/src/parse.rs | 4 +- gix-object/src/signature/sign.rs | 28 +-- gix-object/src/signature/verify.rs | 25 +- gix-object/src/tag/mod.rs | 6 +- gix-object/src/tag/ref_iter.rs | 12 +- gix-object/src/tag/write.rs | 5 +- gix-object/src/traits/_impls.rs | 45 ++-- gix-object/src/traits/find.rs | 61 ++--- gix-object/src/traits/mod.rs | 10 +- gix-object/src/tree/editor.rs | 19 +- gix-object/src/tree/ref_iter.rs | 12 +- gix-object/src/tree/write.rs | 7 +- gix-object/tests/object/tree/editor.rs | 2 +- gix-odb/src/alternate/mod.rs | 4 +- gix-odb/src/cache.rs | 26 +-- gix-odb/src/memory.rs | 18 +- gix-odb/src/sink.rs | 6 +- gix-odb/src/store_impls/dynamic/find.rs | 21 +- gix-odb/src/store_impls/dynamic/header.rs | 2 +- gix-odb/src/store_impls/dynamic/prefix.rs | 2 +- gix-odb/src/store_impls/dynamic/write.rs | 11 +- gix-odb/src/store_impls/loose/find.rs | 22 +- gix-odb/src/store_impls/loose/verify.rs | 6 +- gix-odb/src/store_impls/loose/write.rs | 10 +- gix-odb/src/traits.rs | 10 +- gix-pack/src/bundle/find.rs | 4 +- gix-pack/src/bundle/init.rs | 7 +- gix-pack/src/bundle/mod.rs | 2 +- gix-pack/src/bundle/write/error.rs | 2 - gix-pack/src/bundle/write/mod.rs | 14 +- gix-pack/src/bundle/write/types.rs | 2 +- gix-pack/src/cache/delta/from_offsets.rs | 7 +- gix-pack/src/cache/delta/mod.rs | 2 - gix-pack/src/cache/delta/traverse/mod.rs | 9 +- gix-pack/src/cache/delta/traverse/resolve.rs | 28 +-- gix-pack/src/cache/delta/tree.rs | 14 +- gix-pack/src/cache/mod.rs | 2 +- gix-pack/src/data/delta.rs | 20 +- gix-pack/src/data/entry/decode.rs | 31 +-- gix-pack/src/data/file/decode/entry.rs | 30 +-- gix-pack/src/data/file/decode/header.rs | 19 +- gix-pack/src/data/file/decode/mod.rs | 7 +- gix-pack/src/data/file/init.rs | 6 +- gix-pack/src/data/file/verify.rs | 8 +- gix-pack/src/data/header.rs | 8 +- gix-pack/src/data/input/bytes_to_entries.rs | 14 +- gix-pack/src/data/input/entries_to_bytes.rs | 12 +- gix-pack/src/data/input/entry.rs | 4 +- .../data/input/lookup_ref_delta_objects.rs | 6 +- gix-pack/src/data/input/mod.rs | 2 +- gix-pack/src/data/input/types.rs | 4 - gix-pack/src/data/mod.rs | 5 - gix-pack/src/data/output/bytes.rs | 7 +- gix-pack/src/data/output/count/mod.rs | 2 +- gix-pack/src/data/output/count/objects/mod.rs | 14 +- .../src/data/output/count/objects/types.rs | 3 - .../src/data/output/entry/iter_from_counts.rs | 11 +- gix-pack/src/data/output/entry/mod.rs | 7 +- gix-pack/src/find_traits.rs | 20 +- gix-pack/src/index/encode.rs | 2 +- gix-pack/src/index/init.rs | 15 +- gix-pack/src/index/traverse/error.rs | 2 - gix-pack/src/index/traverse/mod.rs | 10 +- gix-pack/src/index/traverse/reduce.rs | 4 +- gix-pack/src/index/traverse/with_index.rs | 5 +- gix-pack/src/index/traverse/with_lookup.rs | 6 +- gix-pack/src/index/verify.rs | 15 +- gix-pack/src/index/write/error.rs | 2 - gix-pack/src/index/write/mod.rs | 10 +- gix-pack/src/multi_index/chunk.rs | 8 +- gix-pack/src/multi_index/init.rs | 13 +- gix-pack/src/multi_index/verify.rs | 16 +- gix-pack/src/multi_index/write.rs | 7 +- gix-pack/src/testing.rs | 2 +- gix-pack/src/verify.rs | 8 +- gix-pack/tests/pack/data/input.rs | 8 +- .../pack/data/output/count_and_entries.rs | 9 +- gix-packetline/src/async_io/encode.rs | 14 +- gix-packetline/src/async_io/read.rs | 6 +- gix-packetline/src/async_io/sidebands.rs | 10 +- gix-packetline/src/blocking_io/encode.rs | 9 +- gix-packetline/src/blocking_io/read.rs | 9 +- gix-packetline/src/blocking_io/sidebands.rs | 4 +- gix-packetline/src/decode.rs | 31 +-- gix-packetline/src/encode.rs | 3 - gix-packetline/src/lib.rs | 16 +- gix-packetline/src/read.rs | 6 +- gix-packetline/tests/packetline/decode.rs | 33 +-- gix-packetline/tests/packetline/encode.rs | 8 +- gix-packetline/tests/packetline/main-async.rs | 2 - .../tests/packetline/main-blocking.rs | 2 - gix-packetline/tests/packetline/read/mod.rs | 16 +- .../tests/packetline/read/sideband.rs | 12 +- gix-packetline/tests/packetline/write.rs | 6 +- gix-path/src/convert.rs | 23 +- gix-path/src/realpath.rs | 8 +- gix-path/src/relative_path.rs | 20 +- gix-path/tests/path/convert/normalize.rs | 2 +- gix-path/tests/path/main.rs | 2 - gix-path/tests/path/realpath.rs | 16 +- gix-path/tests/path/relative_path.rs | 8 +- gix-pathspec/src/defaults.rs | 12 +- gix-pathspec/src/lib.rs | 8 +- gix-pathspec/src/parse.rs | 21 +- gix-pathspec/src/pattern.rs | 4 +- gix-pathspec/src/search/init.rs | 6 +- gix-pathspec/tests/normalize/mod.rs | 6 +- gix-prompt/examples/credentials.rs | 2 +- gix-prompt/src/lib.rs | 12 +- gix-prompt/src/types.rs | 3 - gix-prompt/src/unix.rs | 11 +- gix-protocol/src/command.rs | 10 +- gix-protocol/src/fetch/error.rs | 2 - gix-protocol/src/fetch/function.rs | 12 +- gix-protocol/src/fetch/mod.rs | 6 - gix-protocol/src/fetch/negotiate.rs | 31 ++- gix-protocol/src/fetch/refmap/init.rs | 11 +- gix-protocol/src/fetch/response/io.rs | 12 +- gix-protocol/src/fetch/response/mod.rs | 13 +- gix-protocol/src/fetch/types.rs | 4 +- gix-protocol/src/handshake/function.rs | 3 +- gix-protocol/src/handshake/mod.rs | 13 +- gix-protocol/src/handshake/refs/async_io.rs | 6 +- .../src/handshake/refs/blocking_io.rs | 6 +- gix-protocol/src/handshake/refs/mod.rs | 6 - gix-protocol/src/handshake/refs/shared.rs | 11 +- gix-protocol/src/ls_refs.rs | 11 +- gix-protocol/tests/protocol/fetch/_impl.rs | 3 +- gix-protocol/tests/protocol/fetch/mod.rs | 6 - gix-protocol/tests/protocol/handshake.rs | 4 +- gix-quote/src/ansi_c.rs | 10 +- gix-ref/src/peel.rs | 4 +- gix-ref/src/store/file/overlay_iter.rs | 11 +- gix-ref/src/store/file/transaction/prepare.rs | 2 +- gix-ref/src/store/packed/transaction.rs | 2 +- gix-ref/tests/refs/file/mod.rs | 2 +- gix-refspec/src/parse.rs | 21 +- gix-refspec/tests/refspec/parse/mod.rs | 6 +- gix-revision/src/describe.rs | 11 +- gix-revision/src/spec/parse/function.rs | 221 ++++++++++++------ gix-revision/src/spec/parse/mod.rs | 2 - gix-revision/tests/revision/describe/mod.rs | 7 +- gix-revision/tests/revision/spec/parse/mod.rs | 4 +- gix-revwalk/src/graph/commit.rs | 23 +- gix-revwalk/src/graph/mod.rs | 42 +--- gix-shallow/src/lib.rs | 14 +- gix-status/src/index_as_worktree/function.rs | 17 +- gix-status/src/index_as_worktree/mod.rs | 2 +- gix-status/src/index_as_worktree/traits.rs | 12 +- gix-status/src/index_as_worktree/types.rs | 3 - .../src/index_as_worktree_with_renames/mod.rs | 17 +- .../index_as_worktree_with_renames/types.rs | 3 - gix-status/tests/status/index_as_worktree.rs | 6 +- gix-submodule/src/access.rs | 24 +- gix-submodule/src/config.rs | 33 +-- gix-submodule/src/is_active_platform.rs | 5 +- gix-submodule/src/lib.rs | 8 +- gix-submodule/tests/file/mod.rs | 10 +- gix-tix/src/history.rs | 2 +- gix-tix/src/test_repository.rs | 4 +- .../src/client/async_io/bufread_ext.rs | 8 +- gix-transport/src/client/async_io/connect.rs | 9 +- .../src/client/blocking_io/bufread_ext.rs | 11 +- .../src/client/blocking_io/connect.rs | 9 +- .../src/client/blocking_io/http/curl/mod.rs | 19 +- .../client/blocking_io/http/curl/remote.rs | 5 +- .../src/client/blocking_io/http/mod.rs | 4 +- .../src/client/blocking_io/http/redirect.rs | 9 +- .../client/blocking_io/http/reqwest/mod.rs | 2 +- .../client/blocking_io/http/reqwest/remote.rs | 14 +- .../src/client/blocking_io/http/traits.rs | 7 +- gix-transport/src/client/capabilities.rs | 7 +- gix-transport/src/client/git/blocking_io.rs | 7 +- gix-transport/src/client/non_io_types.rs | 40 ++-- gix-traverse/src/commit/mod.rs | 6 +- gix-traverse/src/commit/simple.rs | 21 +- gix-traverse/src/commit/topo/init.rs | 4 +- gix-traverse/src/commit/topo/iter.rs | 30 +-- gix-traverse/src/commit/topo/mod.rs | 3 - gix-traverse/src/tree/breadthfirst.rs | 8 +- gix-traverse/src/tree/depthfirst.rs | 6 +- .../tests/traverse/commit/simple/hide.rs | 2 +- gix-url/fuzz/Cargo.toml | 1 + gix-url/fuzz/fuzz_targets/parse.rs | 2 +- gix-url/src/expand_path.rs | 7 +- gix-url/src/lib.rs | 18 +- gix-url/src/parse.rs | 21 +- gix-url/tests/url/access.rs | 36 +-- gix-url/tests/url/expand_path.rs | 4 +- gix-url/tests/url/main.rs | 2 - gix-url/tests/url/parse/file.rs | 48 ++-- gix-url/tests/url/parse/http.rs | 52 ++--- gix-url/tests/url/parse/mod.rs | 18 +- gix-url/tests/url/parse/remote_helper.rs | 30 +-- gix-url/tests/url/parse/ssh.rs | 78 +++---- gix-worktree-state/src/checkout/chunk.rs | 15 +- gix-worktree-state/src/checkout/entry.rs | 4 +- gix-worktree-state/src/checkout/function.rs | 4 +- gix-worktree-state/src/checkout/mod.rs | 3 - .../tests/worktree-state/checkout.rs | 6 +- gix-worktree-stream/src/entry.rs | 5 +- gix-worktree-stream/src/from_tree/mod.rs | 4 +- gix-worktree-stream/src/from_tree/traverse.rs | 20 +- gix-worktree-stream/src/lib.rs | 2 +- gix-worktree-stream/tests/stream.rs | 6 +- gix-worktree/Cargo.toml | 1 + gix-worktree/tests/worktree/stack/ignore.rs | 2 +- gix-zlib/src/decompress.rs | 5 +- gix-zlib/src/inflate.rs | 5 +- gix-zlib/src/lib.rs | 1 - gix-zlib/src/stream/deflate.rs | 10 +- gix-zlib/tests/zlib/stream/deflate.rs | 16 +- gix/src/clone/access.rs | 2 +- gix/src/clone/checkout.rs | 7 +- gix/src/clone/fetch/mod.rs | 7 +- gix/src/clone/fetch/util.rs | 21 +- gix/src/clone/mod.rs | 11 +- gix/src/commit/mod.rs | 20 +- gix/src/commit/sign.rs | 34 +-- gix/src/commit/verify.rs | 12 +- gix/src/config/cache/access.rs | 41 ++-- gix/src/config/cache/incubate.rs | 8 +- gix/src/config/cache/init.rs | 17 +- gix/src/config/cache/mod.rs | 2 +- gix/src/config/cache/util.rs | 16 +- gix/src/config/file_mut.rs | 9 +- gix/src/config/mod.rs | 120 +--------- gix/src/config/overrides.rs | 5 +- gix/src/config/snapshot/access.rs | 16 +- gix/src/config/snapshot/credential_helpers.rs | 8 +- gix/src/config/tree/keys.rs | 20 +- gix/src/config/tree/mod.rs | 14 -- gix/src/config/tree/sections/checkout.rs | 2 +- gix/src/config/tree/sections/core.rs | 10 +- gix/src/config/tree/sections/diff.rs | 2 +- gix/src/config/tree/sections/fetch.rs | 2 +- gix/src/config/tree/sections/http.rs | 2 +- gix/src/config/tree/sections/pack.rs | 2 +- gix/src/config/tree/sections/protocol.rs | 6 +- gix/src/config/tree/traits.rs | 20 +- gix/src/create.rs | 15 +- gix/src/diff.rs | 35 +-- gix/src/dirwalk/iter.rs | 9 +- gix/src/dirwalk/mod.rs | 5 +- gix/src/discover.rs | 11 +- gix/src/env.rs | 19 -- gix/src/filter.rs | 45 +--- gix/src/head/mod.rs | 5 +- gix/src/head/peel.rs | 44 +--- gix/src/id.rs | 18 +- gix/src/init.rs | 7 +- gix/src/lib.rs | 22 +- gix/src/mailmap.rs | 6 - gix/src/object/blob.rs | 15 +- gix/src/object/commit.rs | 31 +-- gix/src/object/errors.rs | 32 +-- gix/src/object/mod.rs | 20 +- gix/src/object/peel.rs | 22 +- gix/src/object/tag.rs | 6 +- gix/src/object/tree/diff/change.rs | 2 +- gix/src/object/tree/diff/for_each.rs | 9 +- gix/src/object/tree/diff/mod.rs | 10 +- gix/src/object/tree/editor.rs | 26 +-- gix/src/object/tree/entry.rs | 4 +- gix/src/object/tree/mod.rs | 16 +- gix/src/open/mod.rs | 3 - gix/src/open/options.rs | 4 +- gix/src/open/repository.rs | 16 +- gix/src/pathspec.rs | 8 +- gix/src/reference/edits.rs | 10 +- gix/src/reference/errors.rs | 64 ----- gix/src/reference/iter.rs | 20 +- gix/src/reference/mod.rs | 29 ++- gix/src/reference/remote.rs | 16 +- gix/src/remote/access.rs | 2 +- gix/src/remote/build.rs | 20 +- gix/src/remote/connect.rs | 9 +- gix/src/remote/connection/access.rs | 5 +- gix/src/remote/connection/fetch/config.rs | 5 +- gix/src/remote/connection/fetch/error.rs | 2 - gix/src/remote/connection/fetch/mod.rs | 13 +- .../remote/connection/fetch/receive_pack.rs | 10 +- .../connection/fetch/update_refs/mod.rs | 4 +- .../connection/fetch/update_refs/update.rs | 7 - gix/src/remote/connection/ref_map.rs | 9 +- gix/src/remote/errors.rs | 17 -- gix/src/remote/fetch.rs | 6 +- gix/src/remote/init.rs | 23 +- gix/src/remote/mod.rs | 3 - gix/src/remote/name.rs | 5 +- gix/src/remote/save.rs | 10 +- gix/src/remote/url/scheme_permission.rs | 5 +- gix/src/repository/attributes.rs | 11 +- gix/src/repository/blame.rs | 2 +- gix/src/repository/branch.rs | 5 +- gix/src/repository/checkout.rs | 4 +- gix/src/repository/config/branch.rs | 11 +- gix/src/repository/config/mod.rs | 19 +- gix/src/repository/config/transport.rs | 14 +- gix/src/repository/diff.rs | 11 +- gix/src/repository/dirwalk.rs | 4 +- gix/src/repository/filter.rs | 8 +- gix/src/repository/graph.rs | 4 +- gix/src/repository/identity.rs | 6 +- gix/src/repository/impls.rs | 14 +- gix/src/repository/index.rs | 18 +- gix/src/repository/init.rs | 2 +- gix/src/repository/location.rs | 4 +- gix/src/repository/mailmap.rs | 4 +- gix/src/repository/merge.rs | 18 +- gix/src/repository/mod.rs | 191 --------------- gix/src/repository/object.rs | 58 ++--- gix/src/repository/pathspec.rs | 4 +- gix/src/repository/reference.rs | 35 ++- gix/src/repository/remote.rs | 23 +- gix/src/repository/revision.rs | 15 +- gix/src/repository/submodule.rs | 8 +- gix/src/repository/worktree.rs | 6 +- gix/src/revision/spec/parse/error.rs | 2 +- gix/src/revision/spec/parse/mod.rs | 6 - gix/src/revision/walk.rs | 19 +- gix/src/shallow.rs | 10 - gix/src/status/index_worktree.rs | 16 +- gix/src/status/iter/mod.rs | 14 +- gix/src/status/iter/types.rs | 4 +- gix/src/status/mod.rs | 16 +- gix/src/status/tree_index.rs | 5 +- gix/src/submodule/errors.rs | 53 ----- gix/src/submodule/mod.rs | 45 ++-- gix/src/tag.rs | 6 - gix/src/worktree/mod.rs | 23 +- gix/src/worktree/proxy.rs | 10 +- gix/tests/doctest.rs | 4 +- gix/tests/gix-init.rs | 6 +- gix/tests/gix/config/tree.rs | 6 +- gix/tests/gix/object/tree/mod.rs | 2 +- gix/tests/gix/remote/connect.rs | 5 +- gix/tests/gix/remote/fetch.rs | 6 +- gix/tests/gix/repository/config/remote.rs | 2 +- gix/tests/gix/util.rs | 8 +- gix/tests/repository.rs | 2 +- src/porcelain/options.rs | 2 +- src/shared.rs | 4 +- tests/tools/src/lib.rs | 3 - 520 files changed, 2363 insertions(+), 3972 deletions(-) delete mode 100644 gix-blame/src/error.rs delete mode 100644 gix-pack/src/bundle/write/error.rs delete mode 100644 gix-pack/src/index/traverse/error.rs delete mode 100644 gix-pack/src/index/write/error.rs delete mode 100644 gix-protocol/src/fetch/error.rs delete mode 100644 gix/src/reference/errors.rs delete mode 100644 gix/src/remote/connection/fetch/error.rs delete mode 100644 gix/src/remote/errors.rs delete mode 100644 gix/src/submodule/errors.rs diff --git a/AGENTS.md b/AGENTS.md index a7c8485d67c..248896f114e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -37,7 +37,11 @@ Plumbing crates are migrating from `thiserror` enums to `gix-error`. Check wheth uses `gix-error` (look at its `Cargo.toml`); if it does, follow the patterns below. If it still uses `thiserror`, keep using `thiserror` for consistency within that crate. -- **Error type alias**: `pub type Error = gix_error::Exn;` +- **Error types**: use `gix_error::Exn`, `gix_error::Exn`, or the appropriate + concrete error directly, importing types under their canonical names as needed. Do not introduce + crate-specific or operation-specific forwarding aliases or renamed error exports. +- **Porcelain errors**: use the central `gix::Error` re-export at public API boundaries. Keep the + underlying error type and any `Exn` parameter when adapting an existing signature. - **Static messages**: `gix_error::message("something failed")` - **Formatted messages**: `gix_error::message!("failed to read {path}")` - **Wrapping callee errors with context**: `.or_raise(|| message("context about what failed"))?` diff --git a/Cargo.lock b/Cargo.lock index 3c467279596..4f77cac6c20 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1948,6 +1948,7 @@ dependencies = [ name = "gix-fsck" version = "0.25.1" dependencies = [ + "gix-error", "gix-hash", "gix-hashtable", "gix-object", @@ -2135,6 +2136,7 @@ dependencies = [ "bitflags 2.13.0", "gix-commitgraph", "gix-date", + "gix-error", "gix-hash", "gix-object", "gix-odb", @@ -2680,6 +2682,7 @@ dependencies = [ "document-features", "gix-attributes", "gix-discover", + "gix-error", "gix-features", "gix-fs", "gix-glob", diff --git a/etc/plan/gix-error.md b/etc/plan/gix-error.md index 59344f6b0bb..0f8b67aa1e8 100644 --- a/etc/plan/gix-error.md +++ b/etc/plan/gix-error.md @@ -60,7 +60,7 @@ Result on 2026-04-22: ## Migration Rules - Replace `thiserror` in `Cargo.toml` with `gix-error`. -- Prefer `pub type Error = gix_error::Exn;` unless the crate needs a more specific concrete error. +- Use canonical `gix-error` types directly in signatures, with imports where helpful. Forwarding aliases used during individual crate migrations are removed in the final cleanup pass. - Convert validation/parsing-only paths to `gix_error::ValidationError`. - Replace `#[from]` / `#[source]` propagation with `.or_raise(...)` or `.ok_or_raise(...)`. - Keep `gix_error::Error` as the erased boundary type, mainly at `gix` and in tests that benefit from downcasting or frame inspection. @@ -177,3 +177,13 @@ Result on 2026-04-22: - [ ] `cargo nextest --workflow` no longer excludes `gix-error`. - [ ] The `gix` boundary still returns `gix_error::Error` where type erasure is desired. - [ ] Validation-heavy crates still expose typed validation failures where callers need them. + +## Final Cleanup at the Stack Tip + +Remove the forwarding aliases retained during crate-by-crate migration in one self-contained breaking commit, including all downstream adaptations. + +- [x] Remove direct and indirect forwarding aliases, renamed error exports, and test-only synonyms. +- [x] Remove namespaces and files that only exposed those aliases, and update documentation links. +- [x] Keep the central `gix::{Error, Exn}` and `gix::error` exports, canonical unrenamed re-exports, required associated types, and aliases with additional structure. +- [x] Preserve concrete error types, `Exn` parameters, feature-dependent alternatives, messages, and error chains. +- [x] Pass `etc/scripts/ci-check-local.sh --thorough` on the completed tip commit. diff --git a/gitoxide-core/src/hours/mod.rs b/gitoxide-core/src/hours/mod.rs index 1cbeca2fbed..f974b8e2326 100644 --- a/gitoxide-core/src/hours/mod.rs +++ b/gitoxide-core/src/hours/mod.rs @@ -85,7 +85,7 @@ fn parse_trailer_identity(trailer: gix::objs::commit::message::body::TrailerRef< fn commit_author_identities( commit_data: &[u8], object_hash: gix::hash::Kind, -) -> Result<(gix::actor::SignatureRef<'_>, SmallVec<[ParsedIdentity<'_>; 2]>), gix::objs::decode::Error> { +) -> Result<(gix::actor::SignatureRef<'_>, SmallVec<[ParsedIdentity<'_>; 2]>), gix::error::ValidationError> { let commit = gix::objs::CommitRef::from_bytes(commit_data, object_hash)?; let author = commit.author()?.trim(); let mut authors = smallvec![ParsedIdentity::Borrowed(gix::actor::IdentityRef::from(author))]; diff --git a/gitoxide-core/src/index/checkout.rs b/gitoxide-core/src/index/checkout.rs index c2428d7fe7a..6bd4654b8bf 100644 --- a/gitoxide-core/src/index/checkout.rs +++ b/gitoxide-core/src/index/checkout.rs @@ -4,7 +4,7 @@ use std::{ }; use anyhow::bail; -use gix::{NestedProgress, Progress, objs::find::Error, worktree::state::checkout}; +use gix::{NestedProgress, Progress, worktree::state::checkout}; use crate::{ index, @@ -183,7 +183,7 @@ impl gix::objs::Find for EmptyOrDb where Find: gix::objs::Find, { - fn try_find<'a>(&self, id: &gix::oid, buf: &'a mut Vec) -> Result>, Error> { + fn try_find<'a>(&self, id: &gix::oid, buf: &'a mut Vec) -> Result>, gix::Exn> { if self.empty_files { // We always want to query the ODB here… let Some(kind) = self.db.try_find(id, buf)?.map(|d| d.kind) else { @@ -206,7 +206,7 @@ where struct Empty; impl gix::objs::Find for Empty { - fn try_find<'a>(&self, id: &gix::oid, buffer: &'a mut Vec) -> Result>, Error> { + fn try_find<'a>(&self, id: &gix::oid, buffer: &'a mut Vec) -> Result>, gix::Exn> { buffer.clear(); Ok(Some(gix::objs::Data { kind: gix::object::Kind::Blob, diff --git a/gitoxide-core/src/net.rs b/gitoxide-core/src/net.rs index 72231b07181..b01af00fbc1 100644 --- a/gitoxide-core/src/net.rs +++ b/gitoxide-core/src/net.rs @@ -45,7 +45,7 @@ mod impls { pub async fn connect( url: Url, options: io_mode::connect::Options, -) -> Result>, io_mode::connect::Error> +) -> Result>, gix::Exn> where Url: TryInto, E: std::error::Error + Send + Sync + 'static, diff --git a/gitoxide-core/src/organize.rs b/gitoxide-core/src/organize.rs index 50d35734414..eae3ff688ef 100644 --- a/gitoxide-core/src/organize.rs +++ b/gitoxide-core/src/organize.rs @@ -209,8 +209,7 @@ fn handle( None => return Ok(()), }) .join(to_relative({ - let mut path = - gix_url::expand_path(None, url.path.as_bstr()).map_err(gix::url::expand_path::Error::into_error)?; + let mut path = gix_url::expand_path(None, url.path.as_bstr()).map_err(gix::Exn::into_error)?; match kind { gix::repository::Kind::Submodule => { unreachable!("BUG: We should not try to relocate submodules and not find them the first place") diff --git a/gitoxide-core/src/pack/explode.rs b/gitoxide-core/src/pack/explode.rs index c860db41b50..5daf7766e18 100644 --- a/gitoxide-core/src/pack/explode.rs +++ b/gitoxide-core/src/pack/explode.rs @@ -76,31 +76,21 @@ enum OutputWriter { } impl gix::objs::Write for OutputWriter { - fn write_buf(&self, kind: object::Kind, from: &[u8]) -> Result { + fn write_buf(&self, kind: object::Kind, from: &[u8]) -> Result { match self { OutputWriter::Loose(db) => db.write_buf(kind, from), OutputWriter::Sink(db) => db.write_buf(kind, from), } } - fn write_buf_with_known_id( - &self, - kind: object::Kind, - from: &[u8], - id: ObjectId, - ) -> Result { + fn write_buf_with_known_id(&self, kind: object::Kind, from: &[u8], id: ObjectId) -> Result { match self { OutputWriter::Loose(db) => db.write_buf_with_known_id(kind, from, id), OutputWriter::Sink(db) => db.write_buf_with_known_id(kind, from, id), } } - fn write_stream( - &self, - kind: object::Kind, - size: u64, - from: &mut dyn Read, - ) -> Result { + fn write_stream(&self, kind: object::Kind, size: u64, from: &mut dyn Read) -> Result { match self { OutputWriter::Loose(db) => db.write_stream(kind, size, from), OutputWriter::Sink(db) => db.write_stream(kind, size, from), @@ -113,7 +103,7 @@ impl gix::objs::Write for OutputWriter { size: u64, from: &mut dyn Read, id: ObjectId, - ) -> Result { + ) -> Result { match self { OutputWriter::Loose(db) => db.write_stream_with_known_id(kind, size, from, id), OutputWriter::Sink(db) => db.write_stream_with_known_id(kind, size, from, id), diff --git a/gitoxide-core/src/pack/receive.rs b/gitoxide-core/src/pack/receive.rs index 29e68643efc..6650d687541 100644 --- a/gitoxide-core/src/pack/receive.rs +++ b/gitoxide-core/src/pack/receive.rs @@ -164,7 +164,7 @@ struct Negotiate<'a> { } impl gix::protocol::fetch::Negotiate for Negotiate<'_> { - fn mark_complete_and_common_ref(&mut self) -> Result { + fn mark_complete_and_common_ref(&mut self) -> Result> { Ok(negotiate::Action::MustNegotiate { remote_ref_target_known: vec![], /* we don't really negotiate */ }) @@ -184,7 +184,7 @@ impl gix::protocol::fetch::Negotiate for Negotiate<'_> { _state: &mut negotiate::one_round::State, _arguments: &mut Arguments, _previous_response: Option<&Response>, - ) -> Result<(negotiate::Round, bool), negotiate::Error> { + ) -> Result<(negotiate::Round, bool), gix::Exn> { Ok(( negotiate::Round { haves_sent: 0, diff --git a/gitoxide-core/src/query/engine/update.rs b/gitoxide-core/src/query/engine/update.rs index 8cdff3906ac..e835e718ee4 100644 --- a/gitoxide-core/src/query/engine/update.rs +++ b/gitoxide-core/src/query/engine/update.rs @@ -11,7 +11,6 @@ use gix::{ bstr::{BStr, BString, ByteSlice}, diff::{blob::platform::prepare_diff::Operation, rewrites::CopySource}, features::progress, - objs::find::Error, parallel::{InOrderIter, SequenceId}, prelude::ObjectIdExt, }; @@ -385,7 +384,11 @@ pub fn update( where Find: gix::prelude::Find + Clone, { - fn try_find<'b>(&self, id: &gix::oid, buf: &'b mut Vec) -> Result>, Error> { + fn try_find<'b>( + &self, + id: &gix::oid, + buf: &'b mut Vec, + ) -> Result>, gix::Exn> { let obj = self.inner.try_find(id, buf)?; let Some(obj) = obj else { return Ok(None) }; if !obj.kind.is_commit() { diff --git a/gix-actor/src/signature/mod.rs b/gix-actor/src/signature/mod.rs index df909737064..a5fcb560646 100644 --- a/gix-actor/src/signature/mod.rs +++ b/gix-actor/src/signature/mod.rs @@ -22,7 +22,7 @@ mod _ref { } /// Try to parse the timestamp and create an owned instance from this shared one. - pub fn to_owned(&self) -> Result { + pub fn to_owned(&self) -> Result { Ok(Signature { name: self.name.to_owned(), email: self.email.to_owned(), @@ -65,7 +65,7 @@ mod _ref { /// Parse the `time` field for access to the passed time since unix epoch, and the time offset. /// The format is expected to be [raw](gix_date::parse_header()). - pub fn time(&self) -> Result { + pub fn time(&self) -> Result { self.time.parse() } } diff --git a/gix-archive/src/lib.rs b/gix-archive/src/lib.rs index c93ed9de21b..e13884bd82a 100644 --- a/gix-archive/src/lib.rs +++ b/gix-archive/src/lib.rs @@ -18,9 +18,6 @@ use bstr::BString; -/// The error returned by [`write_stream()`]. -pub type Error = gix_error::Exn; - /// The supported container formats for use in [`write_stream()`]. #[derive(Default, PartialEq, Eq, Copy, Clone, Debug)] pub enum Format { diff --git a/gix-archive/src/write.rs b/gix-archive/src/write.rs index d8fcab29010..5cead321253 100644 --- a/gix-archive/src/write.rs +++ b/gix-archive/src/write.rs @@ -3,7 +3,7 @@ use gix_error::ResultExt; use gix_error::{ErrorExt, message}; use gix_worktree_stream::{Entry, Stream}; -use crate::{Error, Format, Options}; +use crate::{Format, Options}; #[cfg(feature = "zip")] use std::io::Write; @@ -22,7 +22,7 @@ pub fn write_stream( mut next_entry: NextFn, out: impl std::io::Write, opts: Options, -) -> Result<(), Error> +) -> Result<(), gix_error::Exn> where NextFn: FnMut(&mut Stream) -> Result>, gix_error::Exn>, { @@ -39,7 +39,11 @@ where } impl State { - pub fn new(format: Format, mtime: gix_date::SecondsSinceUnixEpoch, out: W) -> Result { + pub fn new( + format: Format, + mtime: gix_date::SecondsSinceUnixEpoch, + out: W, + ) -> Result> { match format { Format::InternalTransientNonPersistable => unreachable!("handled earlier"), Format::Zip { .. } => { @@ -148,7 +152,7 @@ pub fn write_stream_seek( mut next_entry: NextFn, out: impl std::io::Write + std::io::Seek, opts: Options, -) -> Result<(), Error> +) -> Result<(), gix_error::Exn> where NextFn: FnMut(&mut Stream) -> Result>, gix_error::Exn>, { @@ -204,7 +208,7 @@ fn append_zip_entry( mtime: rawzip::time::UtcDateTime, compression_level: Option, tree_prefix: Option<&bstr::BString>, -) -> Result<(), Error> { +) -> Result<(), gix_error::Exn> { use bstr::ByteSlice; let path = add_prefix(entry.relative_path(), tree_prefix).into_owned(); let unix_permissions = if entry.mode.is_executable() { 0o755 } else { 0o644 }; @@ -299,7 +303,7 @@ fn append_tar_entry( mut entry: gix_worktree_stream::Entry<'_>, mtime_seconds_since_epoch: i64, opts: &Options, -) -> Result<(), Error> { +) -> Result<(), gix_error::Exn> { let mut header = tar::Header::new_gnu(); header.set_mtime(mtime_seconds_since_epoch as u64); header.set_entry_type(tar_entry_type(entry.mode)); diff --git a/gix-attributes/src/name.rs b/gix-attributes/src/name.rs index 4cc4acb959a..bdb47346b6d 100644 --- a/gix-attributes/src/name.rs +++ b/gix-attributes/src/name.rs @@ -24,7 +24,7 @@ impl AsRef for NameRef<'_> { } impl<'a> TryFrom<&'a BStr> for NameRef<'a> { - type Error = Error; + type Error = gix_error::ValidationError; fn try_from(attr: &'a BStr) -> Result { fn attr_valid(attr: &BStr) -> bool { @@ -38,7 +38,12 @@ impl<'a> TryFrom<&'a BStr> for NameRef<'a> { attr_valid(attr) .then(|| NameRef(attr.to_str().expect("no illformed utf8"))) - .ok_or_else(|| Error::new_with_input("Attribute has non-ascii characters or starts with '-'", attr)) + .ok_or_else(|| { + gix_error::ValidationError::new_with_input( + "Attribute has non-ascii characters or starts with '-'", + attr, + ) + }) } } @@ -92,6 +97,3 @@ impl<'de> serde::Deserialize<'de> for Name { .map_err(serde::de::Error::custom) } } - -/// The error returned by [`parse::Iter`][crate::parse::Iter]. -pub type Error = gix_error::ValidationError; diff --git a/gix-attributes/src/parse.rs b/gix-attributes/src/parse.rs index 6a2cad26678..02cf334b81d 100644 --- a/gix-attributes/src/parse.rs +++ b/gix-attributes/src/parse.rs @@ -3,7 +3,7 @@ use std::borrow::Cow; use bstr::{BStr, ByteSlice}; use gix_error::{ErrorExt, ResultExt, ValidationError}; -use crate::{AssignmentRef, Name, NameRef, StateRef, name}; +use crate::{AssignmentRef, Name, NameRef, StateRef}; /// The kind of attribute that was parsed. #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] @@ -15,9 +15,6 @@ pub enum Kind { Macro(Name), } -/// The error returned by [`parse::Lines`][crate::parse::Lines]. -pub type Error = gix_error::Exn; - /// An iterator over attribute assignments, parsed line by line. pub struct Lines<'a> { lines: bstr::Lines<'a>, @@ -37,7 +34,7 @@ impl<'a> Iter<'a> { } } - fn parse_attr(&self, attr: &'a [u8]) -> Result, name::Error> { + fn parse_attr(&self, attr: &'a [u8]) -> Result, gix_error::ValidationError> { let mut tokens = attr.splitn(2, |b| *b == b'='); let attr = tokens.next().expect("attr itself").as_bstr(); let possibly_value = tokens.next(); @@ -52,16 +49,16 @@ impl<'a> Iter<'a> { } } -fn check_attr(attr: &BStr) -> Result, name::Error> { +fn check_attr(attr: &BStr) -> Result, gix_error::ValidationError> { NameRef::try_from(attr).and_then(|name| { - (!name.as_str().starts_with("builtin_")) - .then_some(name) - .ok_or_else(|| name::Error::new_with_input("Attribute name uses the reserved 'builtin_' prefix", attr)) + (!name.as_str().starts_with("builtin_")).then_some(name).ok_or_else(|| { + gix_error::ValidationError::new_with_input("Attribute name uses the reserved 'builtin_' prefix", attr) + }) }) } impl<'a> Iterator for Iter<'a> { - type Item = Result, name::Error>; + type Item = Result, gix_error::ValidationError>; fn next(&mut self) -> Option { let attr = self.attrs.find(|a| !a.is_empty())?; @@ -82,7 +79,7 @@ impl<'a> Lines<'a> { } impl<'a> Iterator for Lines<'a> { - type Item = Result<(Kind, Iter<'a>, usize), Error>; + type Item = Result<(Kind, Iter<'a>, usize), gix_error::Exn>; fn next(&mut self) -> Option { fn skip_blanks(line: &BStr) -> &BStr { @@ -103,7 +100,10 @@ impl<'a> Iterator for Lines<'a> { } } -fn parse_line(line: &BStr, line_number: usize) -> Option, usize), Error>> { +fn parse_line( + line: &BStr, + line_number: usize, +) -> Option, usize), gix_error::Exn>> { if line.is_empty() { return None; } diff --git a/gix-attributes/src/search/attributes.rs b/gix-attributes/src/search/attributes.rs index 32671ddca25..c59a728d8ff 100644 --- a/gix-attributes/src/search/attributes.rs +++ b/gix-attributes/src/search/attributes.rs @@ -128,7 +128,7 @@ impl Pattern for Attributes { fn bytes_to_patterns(&self, bytes: &[u8], _source: &std::path::Path) -> Vec> { fn into_owned_assignments<'a>( - attrs: impl Iterator, crate::name::Error>>, + attrs: impl Iterator, gix_error::ValidationError>>, ) -> Option { let res = attrs .map(|res| { diff --git a/gix-attributes/tests/attributes/main.rs b/gix-attributes/tests/attributes/main.rs index fad4b9a5011..9deb66ed8ba 100644 --- a/gix-attributes/tests/attributes/main.rs +++ b/gix-attributes/tests/attributes/main.rs @@ -1,4 +1,3 @@ -pub use gix_testtools::TestResult as Result; mod assignment; mod parse; mod search; diff --git a/gix-attributes/tests/attributes/parse.rs b/gix-attributes/tests/attributes/parse.rs index 610a1054bea..69188c844b4 100644 --- a/gix-attributes/tests/attributes/parse.rs +++ b/gix-attributes/tests/attributes/parse.rs @@ -483,11 +483,11 @@ fn pattern(name: &str, flags: gix_glob::pattern::Mode, first_wildcard_pos: Optio }) } -fn has_validation_message(result: Result, expected: &str) -> bool { +fn has_validation_message(result: Result>, expected: &str) -> bool { result.is_err_and(|err| err.message == expected) } -fn try_line(input: &str) -> Result, parse::Error> { +fn try_line(input: &str) -> Result, gix_error::Exn> { let mut lines = gix_attributes::parse(input.as_bytes()); let res = expand(lines.next().unwrap())?; assert!(lines.next().is_none(), "expected only one line"); @@ -502,7 +502,7 @@ fn byte_line(input: &[u8]) -> ExpandedAttribute<'_> { try_byte_line(input).unwrap() } -fn try_byte_line(input: &[u8]) -> Result, parse::Error> { +fn try_byte_line(input: &[u8]) -> Result, gix_error::Exn> { let mut lines = gix_attributes::parse(input); let res = expand(lines.next().unwrap())?; assert!(lines.next().is_none(), "expected only one line"); @@ -516,13 +516,13 @@ fn lenient_lines(input: &str) -> Vec> { .collect() } -fn try_lines(input: &str) -> Result>, parse::Error> { +fn try_lines(input: &str) -> Result>, gix_error::Exn> { gix_attributes::parse(input.as_bytes()).map(expand).collect() } fn expand( - input: Result<(parse::Kind, parse::Iter<'_>, usize), parse::Error>, -) -> Result, parse::Error> { + input: Result<(parse::Kind, parse::Iter<'_>, usize), gix_error::Exn>, +) -> Result, gix_error::Exn> { let (pattern, attrs, line_no) = input?; let attrs = attrs .map(|r| r.map(|attr| (attr.name.as_str().into(), attr.state))) diff --git a/gix-attributes/tests/attributes/search.rs b/gix-attributes/tests/attributes/search.rs index 9a17ca7621e..e56acd9e571 100644 --- a/gix-attributes/tests/attributes/search.rs +++ b/gix-attributes/tests/attributes/search.rs @@ -63,7 +63,7 @@ mod specials { } #[test] -fn baseline() -> crate::Result { +fn baseline() -> gix_error::TestResult { let mut buf = Vec::new(); // Due to the way our setup differs from gits dynamic stack (which involves trying to read files from disk // by path) we can only test one case baseline, so we require multiple platforms (or filesystems) to run this. @@ -129,7 +129,7 @@ fn assert_references(out: &Outcome) { } #[test] -fn all_attributes_are_listed_in_declaration_order() -> crate::Result { +fn all_attributes_are_listed_in_declaration_order() -> gix_error::TestResult { let (mut group, mut collection, base, input) = baseline::user_attributes("lookup-order")?; let mut buf = Vec::new(); @@ -226,7 +226,7 @@ fn all_attributes_are_listed_in_declaration_order() -> crate::Result { } #[test] -fn given_attributes_are_made_available_in_given_order() -> crate::Result { +fn given_attributes_are_made_available_in_given_order() -> gix_error::TestResult { let (mut group, mut collection, base, input) = baseline::user_attributes_named_baseline("lookup-order", "baseline.selected")?; @@ -268,13 +268,13 @@ fn given_attributes_are_made_available_in_given_order() -> crate::Result { } #[test] -fn macro_attributes_expand_only_when_macro_is_set() -> crate::Result { +fn macro_attributes_expand_only_when_macro_is_set() -> gix_error::TestResult { assert_baseline("macro-expansion")?; Ok(()) } #[test] -fn attribute_tokenisation_matches_git() -> crate::Result { +fn attribute_tokenisation_matches_git() -> gix_error::TestResult { assert_baseline("tokenisation")?; Ok(()) } diff --git a/gix-bitmap/src/ewah.rs b/gix-bitmap/src/ewah.rs index f8b308cfd4a..090b83eb3d4 100644 --- a/gix-bitmap/src/ewah.rs +++ b/gix-bitmap/src/ewah.rs @@ -1,11 +1,5 @@ -/// -pub mod decode { - /// The error returned by [`decode()`](super::decode()). - pub type Error = gix_error::Exn; -} - /// Decode `data` as EWAH bitmap. -pub fn decode(data: &[u8]) -> Result<(Vec, &[u8]), decode::Error> { +pub fn decode(data: &[u8]) -> Result<(Vec, &[u8]), gix_error::Exn> { use crate::decode; use gix_error::{OptionExt, message}; diff --git a/gix-blame/src/error.rs b/gix-blame/src/error.rs deleted file mode 100644 index 3950deb2d43..00000000000 --- a/gix-blame/src/error.rs +++ /dev/null @@ -1,2 +0,0 @@ -/// The error returned by [file()](crate::file()). -pub type Error = gix_error::Exn; diff --git a/gix-blame/src/file/function.rs b/gix-blame/src/file/function.rs index 34cd1436ab1..2a790631bd4 100644 --- a/gix-blame/src/file/function.rs +++ b/gix-blame/src/file/function.rs @@ -12,7 +12,7 @@ use smallvec::SmallVec; use super::{Change, UnblamedHunk, process_changes}; use crate::{ - BlameEntry, Error, Options, Outcome, Statistics, + BlameEntry, Options, Outcome, Statistics, types::{BlamePathEntry, Start}, }; @@ -72,7 +72,7 @@ pub fn file( resource_cache: &mut gix_diff::blob::Platform, file_path: &BStr, options: Options, -) -> Result { +) -> Result { let _span = gix_trace::coarse!("gix_blame::file()", ?file_path, ?start); let mut stats = Statistics::default(); @@ -580,7 +580,7 @@ fn tree_diff_at_file_path( lhs_tree_buf: &mut Vec, rhs_tree_buf: &mut Vec, rewrites: Option, -) -> Result, Error> { +) -> Result, gix_error::Exn> { let parent_tree_id = find_commit(cache, &odb, &parent_id, commit_buf) .or_raise_erased(|| message("Could not find existing iterator over a tree"))? .tree_id() @@ -637,7 +637,7 @@ fn tree_diff_without_rewrites_at_file_path( state: &mut gix_diff::tree::State, parent_tree_iter: gix_object::TreeRefIter<'_>, tree_iter: gix_object::TreeRefIter<'_>, -) -> Result, Error> { +) -> Result, gix_error::Exn> { struct FindChangeToPath { inner: gix_diff::tree::Recorder, interesting_path: BString, @@ -740,7 +740,7 @@ fn tree_diff_with_rewrites_at_file_path( parent_tree_iter: gix_object::TreeRefIter<'_>, tree_iter: gix_object::TreeRefIter<'_>, rewrites: gix_diff::Rewrites, -) -> Result, Error> { +) -> Result, gix_error::Exn> { let mut change: Option = None; let options: gix_diff::tree_with_rewrites::Options = gix_diff::tree_with_rewrites::Options { @@ -781,7 +781,7 @@ fn blob_changes( previous_file_path: &BStr, diff_algorithm: gix_diff::blob::Algorithm, stats: &mut Statistics, -) -> Result, Error> { +) -> Result, gix_error::Exn> { resource_cache .set_resource( previous_oid, @@ -869,7 +869,7 @@ fn find_path_entry_in_commit( buf: &mut Vec, buf2: &mut Vec, stats: &mut Statistics, -) -> Result, Error> { +) -> Result, gix_error::Exn> { let tree_id = find_commit(cache, odb, commit, buf) .or_raise_erased(|| message("Could not find existing iterator over a tree"))? .tree_id() @@ -897,7 +897,7 @@ fn collect_parents( odb: &impl gix_object::Find, cache: Option<&gix_commitgraph::Graph>, buf: &mut Vec, -) -> Result { +) -> Result { let mut parent_ids: ParentIds = Default::default(); match commit { gix_traverse::commit::Either::CachedCommit(commit) => { @@ -952,7 +952,7 @@ fn initial_state( buf: &mut Vec, buf2: &mut Vec, stats: &mut Statistics, -) -> Result { +) -> Result { match start { Start::Commit(suspect) => { let blamed_file_entry_id = find_path_entry_in_commit(&odb, &suspect, file_path, cache, buf, buf2, stats)? diff --git a/gix-blame/src/lib.rs b/gix-blame/src/lib.rs index 7495189e6fc..94109e0a4ee 100644 --- a/gix-blame/src/lib.rs +++ b/gix-blame/src/lib.rs @@ -14,8 +14,6 @@ #![deny(missing_docs)] #![forbid(unsafe_code)] -mod error; -pub use error::Error; mod types; pub use types::{BlameEntry, BlamePathEntry, BlameRanges, Options, Outcome, Start, Statistics}; diff --git a/gix-blame/tests/blame.rs b/gix-blame/tests/blame.rs index 99b14c9c34d..83e65a6d6fe 100644 --- a/gix-blame/tests/blame.rs +++ b/gix-blame/tests/blame.rs @@ -218,7 +218,7 @@ impl Fixture { &mut self, source_file_name: &bstr::BStr, options: gix_blame::Options, - ) -> Result { + ) -> Result { gix_blame::file( &self.odb, gix_blame::Start::Commit(self.suspect), @@ -234,7 +234,7 @@ impl Fixture { source_file_name: &bstr::BStr, contents: Vec, options: gix_blame::Options, - ) -> Result { + ) -> Result { gix_blame::file( &self.odb, gix_blame::Start::Contents { diff --git a/gix-config-value/src/boolean.rs b/gix-config-value/src/boolean.rs index 4e0d440e1a9..bb8ae1b5db1 100644 --- a/gix-config-value/src/boolean.rs +++ b/gix-config-value/src/boolean.rs @@ -3,7 +3,7 @@ use std::{borrow::Cow, ffi::OsString, fmt::Display}; use bstr::{BStr, BString}; use gix_error::{ErrorExt, ResultExt, ValidationError}; -use crate::{Boolean, Error, Integer}; +use crate::{Boolean, Integer}; fn bool_err(input: impl Into) -> ValidationError { ValidationError::new_with_input( @@ -13,7 +13,7 @@ fn bool_err(input: impl Into) -> ValidationError { } impl TryFrom for Boolean { - type Error = Error; + type Error = gix_error::Exn; fn try_from(value: OsString) -> Result { let value = gix_path::os_str_into_bstr(&value).or_raise(|| { @@ -38,7 +38,7 @@ impl TryFrom for Boolean { /// Instead of this, obtain booleans with `config.boolean(…)`, which handles the case were no separator is /// present correctly. impl TryFrom<&BStr> for Boolean { - type Error = Error; + type Error = gix_error::Exn; fn try_from(value: &BStr) -> Result { if parse_true(value) { @@ -54,7 +54,7 @@ impl TryFrom<&BStr> for Boolean { } impl TryFrom<&str> for Boolean { - type Error = Error; + type Error = gix_error::Exn; fn try_from(value: &str) -> Result { Self::try_from(BStr::new(value)) @@ -71,14 +71,14 @@ impl Boolean { } impl TryFrom> for Boolean { - type Error = Error; + type Error = gix_error::Exn; fn try_from(c: Cow<'_, BStr>) -> Result { Self::try_from(c.as_ref()) } } impl TryFrom for Boolean { - type Error = Error; + type Error = gix_error::Exn; fn try_from(value: BString) -> Result { Self::try_from(BStr::new(&value)) } diff --git a/gix-config-value/src/color.rs b/gix-config-value/src/color.rs index c6255f9f581..dfd8423d514 100644 --- a/gix-config-value/src/color.rs +++ b/gix-config-value/src/color.rs @@ -3,7 +3,7 @@ use std::{borrow::Cow, fmt::Display, str::FromStr}; use bstr::{BStr, BString}; use gix_error::{ErrorExt, ResultExt, ValidationError}; -use crate::{Color, Error}; +use crate::Color; impl Display for Color { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { @@ -39,7 +39,7 @@ fn color_err(input: impl Into) -> ValidationError { } impl TryFrom<&BStr> for Color { - type Error = Error; + type Error = gix_error::Exn; fn try_from(s: &BStr) -> Result { let s = std::str::from_utf8(s).or_raise(|| color_err(s))?; @@ -90,7 +90,7 @@ impl TryFrom<&BStr> for Color { } impl TryFrom<&str> for Color { - type Error = Error; + type Error = gix_error::Exn; fn try_from(value: &str) -> Result { Self::try_from(BStr::new(value)) @@ -98,7 +98,7 @@ impl TryFrom<&str> for Color { } impl TryFrom> for Color { - type Error = Error; + type Error = gix_error::Exn; fn try_from(c: Cow<'_, BStr>) -> Result { Self::try_from(c.as_ref()) @@ -106,7 +106,7 @@ impl TryFrom> for Color { } impl TryFrom for Color { - type Error = Error; + type Error = gix_error::Exn; fn try_from(value: BString) -> Result { Self::try_from(BStr::new(&value)) @@ -231,7 +231,7 @@ fn parse_hex(hex: &[u8]) -> Option<(u8, u8, u8)> { } impl FromStr for Name { - type Err = Error; + type Err = gix_error::Exn; fn from_str(s: &str) -> Result { const BASIC: &[(&str, Name, Name)] = &[ @@ -287,7 +287,7 @@ impl FromStr for Name { } impl TryFrom<&BStr> for Name { - type Error = Error; + type Error = gix_error::Exn; fn try_from(s: &BStr) -> Result { Self::from_str(std::str::from_utf8(s).or_raise(|| color_err(s))?) @@ -384,7 +384,7 @@ impl serde::Serialize for Attribute { } impl FromStr for Attribute { - type Err = Error; + type Err = gix_error::Exn; fn from_str(mut s: &str) -> Result { let inverted = if let Some(rest) = s.strip_prefix("no-").or_else(|| s.strip_prefix("no")) { @@ -423,7 +423,7 @@ impl FromStr for Attribute { } impl TryFrom<&BStr> for Attribute { - type Error = Error; + type Error = gix_error::Exn; fn try_from(s: &BStr) -> Result { Self::from_str(std::str::from_utf8(s).or_raise(|| color_err(s))?) diff --git a/gix-config-value/src/integer.rs b/gix-config-value/src/integer.rs index 0d0cd265163..82346391e37 100644 --- a/gix-config-value/src/integer.rs +++ b/gix-config-value/src/integer.rs @@ -3,7 +3,7 @@ use std::{borrow::Cow, fmt::Display, str::FromStr}; use bstr::{BStr, BString}; use gix_error::{ErrorExt, ResultExt, ValidationError}; -use crate::{Error, Integer}; +use crate::Integer; impl Integer { /// Canonicalize values as simple decimal numbers. @@ -86,7 +86,7 @@ fn parse_like_git(input: &str) -> Option { } impl TryFrom<&BStr> for Integer { - type Error = Error; + type Error = gix_error::Exn; fn try_from(s: &BStr) -> Result { let s = std::str::from_utf8(s).or_raise(|| int_err(s))?; @@ -116,7 +116,7 @@ impl TryFrom<&BStr> for Integer { } impl TryFrom<&str> for Integer { - type Error = Error; + type Error = gix_error::Exn; fn try_from(value: &str) -> Result { Self::try_from(BStr::new(value)) @@ -124,7 +124,7 @@ impl TryFrom<&str> for Integer { } impl TryFrom> for Integer { - type Error = Error; + type Error = gix_error::Exn; fn try_from(c: Cow<'_, BStr>) -> Result { Self::try_from(c.as_ref()) @@ -132,7 +132,7 @@ impl TryFrom> for Integer { } impl TryFrom for Integer { - type Error = Error; + type Error = gix_error::Exn; fn try_from(value: BString) -> Result { Self::try_from(BStr::new(&value)) diff --git a/gix-config-value/src/lib.rs b/gix-config-value/src/lib.rs index 5c3193eff53..4073a457652 100644 --- a/gix-config-value/src/lib.rs +++ b/gix-config-value/src/lib.rs @@ -25,9 +25,6 @@ #![cfg_attr(all(doc, feature = "document-features"), feature(doc_cfg))] #![deny(missing_docs, unsafe_code)] -/// The error returned when any config value couldn't be instantiated due to malformed input. -pub type Error = gix_error::Exn; - mod boolean; /// Color value parsing and the supported color names and attributes. pub mod color; diff --git a/gix-config-value/src/path.rs b/gix-config-value/src/path.rs index d3d1003b9bd..6c18dad8bb6 100644 --- a/gix-config-value/src/path.rs +++ b/gix-config-value/src/path.rs @@ -31,9 +31,6 @@ pub mod interpolate { } } - /// The error returned by [`Path::interpolate()`][crate::Path::interpolate()]. - pub type Error = gix_error::Exn; - /// Obtain the home directory for the given user `name` or return `None` if the user wasn't found /// or any other error occurred. /// It can be used as `home_for_user` parameter in [`Path::interpolate()`][crate::Path::interpolate()]. @@ -154,7 +151,7 @@ impl Path { home_dir, home_for_user, }: interpolate::Context<'_>, - ) -> Result { + ) -> Result { if self.is_empty() { return Err(NotFoundError::new("path is missing").raise_erased()); } @@ -207,7 +204,7 @@ impl Path { fn home_for_username( username: &[u8], home_for_user: fn(&str) -> Option, - ) -> Result { + ) -> Result { let username = std::str::from_utf8(username) .or_raise_erased(|| ValidationError::new_with_input("Ill-formed UTF-8 in username", username))?; home_for_user(username).ok_or_raise_erased(|| NotFoundError::new("pwd user info is missing")) diff --git a/gix-config-value/tests/value/boolean.rs b/gix-config-value/tests/value/boolean.rs index d51ea991931..c5e845fd930 100644 --- a/gix-config-value/tests/value/boolean.rs +++ b/gix-config-value/tests/value/boolean.rs @@ -1,7 +1,7 @@ use gix_config_value::Boolean; #[test] -fn from_utf8_str() -> crate::Result { +fn from_utf8_str() -> gix_error::Result { assert_eq!( Boolean::try_from("yes")?, Boolean(true), @@ -11,7 +11,7 @@ fn from_utf8_str() -> crate::Result { } #[test] -fn from_str_false() -> crate::Result { +fn from_str_false() -> gix_error::Result { assert!(!Boolean::try_from("no")?.0); assert!(!Boolean::try_from("off")?.0); assert!(!Boolean::try_from("false")?.0); @@ -21,7 +21,7 @@ fn from_str_false() -> crate::Result { } #[test] -fn from_str_true() -> crate::Result { +fn from_str_true() -> gix_error::Result { assert!(Boolean::try_from("yes")?.0); assert!(Boolean::try_from("on")?.0); assert!(Boolean::try_from("true")?.0); @@ -44,7 +44,7 @@ fn ignores_case() { } #[test] -fn numbers_are_parsed_as_integers() -> crate::Result { +fn numbers_are_parsed_as_integers() -> gix_error::Result { // Use the same bases, suffixes, and full `i64` range as `Integer`. for (input, expected) in [ ("0x10", true), diff --git a/gix-config-value/tests/value/color.rs b/gix-config-value/tests/value/color.rs index eace46a7f5b..7f3a6b81b9a 100644 --- a/gix-config-value/tests/value/color.rs +++ b/gix-config-value/tests/value/color.rs @@ -1,7 +1,7 @@ use gix_config_value::Color; #[test] -fn from_utf8_str() -> crate::Result { +fn from_utf8_str() -> gix_error::Result { assert_eq!( Color::try_from("red bold")?.to_string(), "red bold", @@ -288,7 +288,7 @@ mod from_git { try_color(name).expect("input color is expected to be valid") } - fn try_color<'a>(name: impl Into<&'a BStr>) -> crate::Result { + fn try_color<'a>(name: impl Into<&'a BStr>) -> gix_error::Result { Ok(Color::try_from(name.into())?.to_string()) } } diff --git a/gix-config-value/tests/value/integer.rs b/gix-config-value/tests/value/integer.rs index 4423ff70e4c..ce3f76f7d88 100644 --- a/gix-config-value/tests/value/integer.rs +++ b/gix-config-value/tests/value/integer.rs @@ -1,7 +1,7 @@ use gix_config_value::{Integer, integer::Suffix}; #[test] -fn from_utf8_str() -> crate::Result { +fn from_utf8_str() -> gix_error::Result { assert_eq!( Integer::try_from("1k")?, Integer { diff --git a/gix-config-value/tests/value/main.rs b/gix-config-value/tests/value/main.rs index d554c601782..3d23b8e5374 100644 --- a/gix-config-value/tests/value/main.rs +++ b/gix-config-value/tests/value/main.rs @@ -1,5 +1,3 @@ -type Result = gix_error::Result; - mod boolean; mod color; mod integer; diff --git a/gix-config-value/tests/value/path.rs b/gix-config-value/tests/value/path.rs index fd7b34590fe..b8ce08b2477 100644 --- a/gix-config-value/tests/value/path.rs +++ b/gix-config-value/tests/value/path.rs @@ -6,7 +6,7 @@ mod interpolate { use gix_error::{NotFoundError, ValidationError}; #[test] - fn backslash_is_not_special_and_they_are_not_escaping_anything() -> crate::Result { + fn backslash_is_not_special_and_they_are_not_escaping_anything() -> gix_error::Result { for path in [r"C:\foo\bar", "/foo/bar"] { let actual = gix_config_value::Path::from(path).interpolate(Default::default())?; assert_eq!(actual, Path::new(path)); @@ -56,7 +56,7 @@ mod interpolate { } #[test] - fn tilde_alone_substitutes_current_user() -> crate::Result { + fn tilde_alone_substitutes_current_user() -> gix_error::Result { let home = std::env::current_dir().expect("current directory is available"); assert_eq!( gix_config_value::Path::from("~").interpolate(path::interpolate::Context { @@ -77,7 +77,7 @@ mod interpolate { } #[test] - fn tilde_slash_substitutes_current_user() -> crate::Result { + fn tilde_slash_substitutes_current_user() -> gix_error::Result { let home = std::env::current_dir().expect("current directory is available"); for suffix in ["", "user/bar", r"user\bar", "/user/bar"] { let actual = gix_config_value::Path::from(format!("~/{suffix}").as_str()).interpolate( @@ -97,7 +97,7 @@ mod interpolate { } #[test] - fn tilde_with_given_user() -> crate::Result { + fn tilde_with_given_user() -> gix_error::Result { let home = std::env::current_dir().expect("current directory is available"); for path_suffix in &["foo/bar", r"foo\bar", ""] { @@ -141,9 +141,7 @@ mod interpolate { assert!(err.downcast_any_ref::().is_some()); } - fn interpolate_without_context( - path: impl AsRef, - ) -> Result { + fn interpolate_without_context(path: impl AsRef) -> Result { gix_config_value::Path::from(path.as_ref()).interpolate(path::interpolate::Context { home_for_user: Some(home_for_user), ..Default::default() diff --git a/gix-config/src/file/access/comfort.rs b/gix-config/src/file/access/comfort.rs index 1066d54e9ee..59f09d690af 100644 --- a/gix-config/src/file/access/comfort.rs +++ b/gix-config/src/file/access/comfort.rs @@ -1,7 +1,7 @@ use bstr::{BStr, BString}; use gix_error::{ErrorExt, ValidationError}; -use crate::{AsBStrOpt, AsKey, File, file::Metadata, value}; +use crate::{AsBStrOpt, AsKey, File, file::Metadata}; /// Comfortable API for accessing values impl File { @@ -86,7 +86,7 @@ impl File { } /// Like [`boolean_by()`](File::boolean_by()), but suitable for statically known `key`s like `remote.origin.url`. - pub fn boolean(&self, key: impl AsKey) -> Result, value::Error> { + pub fn boolean(&self, key: impl AsKey) -> Result, gix_error::Exn> { self.boolean_filter(key, |_| true) } @@ -96,7 +96,7 @@ impl File { section_name: impl AsRef, subsection_name: impl AsBStrOpt, value_name: impl AsRef, - ) -> Result, value::Error> { + ) -> Result, gix_error::Exn> { self.boolean_filter_by(section_name, subsection_name, value_name, |_| true) } @@ -105,7 +105,7 @@ impl File { &self, key: impl AsKey, filter: impl FnMut(&Metadata) -> bool, - ) -> Result, value::Error> { + ) -> Result, gix_error::Exn> { let Some(key) = key.try_as_key() else { return Ok(None); }; @@ -119,7 +119,7 @@ impl File { subsection_name: impl AsBStrOpt, value_name: impl AsRef, mut filter: impl FnMut(&Metadata) -> bool, - ) -> Result, value::Error> { + ) -> Result, gix_error::Exn> { let section_name = section_name.as_ref(); let section_ids = self .section_ids_by_name_and_subname(section_name, subsection_name.as_bstr_opt()) @@ -143,7 +143,7 @@ impl File { } /// Like [`integer_by()`](File::integer_by()), but suitable for statically known `key`s like `remote.origin.url`. - pub fn integer(&self, key: impl AsKey) -> Result, value::Error> { + pub fn integer(&self, key: impl AsKey) -> Result, gix_error::Exn> { self.integer_filter(key, |_| true) } @@ -153,7 +153,7 @@ impl File { section_name: impl AsRef, subsection_name: impl AsBStrOpt, value_name: impl AsRef, - ) -> Result, value::Error> { + ) -> Result, gix_error::Exn> { self.integer_filter_by(section_name, subsection_name, value_name, |_| true) } @@ -162,7 +162,7 @@ impl File { &self, key: impl AsKey, filter: impl FnMut(&Metadata) -> bool, - ) -> Result, value::Error> { + ) -> Result, gix_error::Exn> { let Some(key) = key.try_as_key() else { return Ok(None); }; @@ -176,7 +176,7 @@ impl File { subsection_name: impl AsBStrOpt, value_name: impl AsRef, filter: impl FnMut(&Metadata) -> bool, - ) -> Result, value::Error> { + ) -> Result, gix_error::Exn> { let Some(int) = self .raw_value_filter_by(section_name, subsection_name, value_name, filter) .ok() @@ -226,7 +226,7 @@ impl File { } /// Like [`integers()`](File::integers()), but suitable for statically known `key`s like `remote.origin.url`. - pub fn integers(&self, key: impl AsKey) -> Result>, value::Error> { + pub fn integers(&self, key: impl AsKey) -> Result>, gix_error::Exn> { self.integers_filter(key, |_| true) } @@ -237,7 +237,7 @@ impl File { section_name: impl AsRef, subsection_name: impl AsBStrOpt, value_name: impl AsRef, - ) -> Result>, value::Error> { + ) -> Result>, gix_error::Exn> { self.integers_filter_by(section_name, subsection_name, value_name, |_| true) } @@ -246,7 +246,7 @@ impl File { &self, key: impl AsKey, filter: impl FnMut(&Metadata) -> bool, - ) -> Result>, value::Error> { + ) -> Result>, gix_error::Exn> { let Some(key) = key.try_as_key() else { return Ok(None); }; @@ -261,7 +261,7 @@ impl File { subsection_name: impl AsBStrOpt, value_name: impl AsRef, filter: impl FnMut(&Metadata) -> bool, - ) -> Result>, value::Error> { + ) -> Result>, gix_error::Exn> { let Some(values) = self .raw_values_filter_by(section_name, subsection_name, value_name, filter) .ok() diff --git a/gix-config/src/file/access/mutate.rs b/gix-config/src/file/access/mutate.rs index 54908f9ad22..3c9823202a5 100644 --- a/gix-config/src/file/access/mutate.rs +++ b/gix-config/src/file/access/mutate.rs @@ -3,7 +3,7 @@ use gix_features::threading::OwnShared; use crate::{ AsBStrOpt, File, - file::{self, IntoBStringOpt, Metadata, SectionId, SectionMut, rename_section, write::ends_with_newline}, + file::{self, IntoBStringOpt, Metadata, SectionId, SectionMut, write::ends_with_newline}, lookup, parse::{Event, FrontMatterEvents, Span, section}, }; @@ -51,7 +51,7 @@ impl File { &mut self, name: impl AsRef, subsection_name: impl AsBStrOpt, - ) -> Result, lookup::existing::Error> { + ) -> Result, gix_error::Exn> { self.section_mut_inner(name.as_ref(), subsection_name.as_bstr_opt()) } @@ -59,7 +59,7 @@ impl File { &'a mut self, name: &str, subsection_name: Option<&BStr>, - ) -> Result, lookup::existing::Error> { + ) -> Result, gix_error::Exn> { let id = self .section_ids_by_name_and_subname(name, subsection_name)? .next_back() @@ -71,7 +71,7 @@ impl File { } /// Returns the last found mutable section with a given `key`, identifying the name and subsection name like `core` or `remote.origin`. - pub fn section_mut_by_key(&mut self, key: impl crate::AsBStr) -> Result, lookup::existing::Error> { + pub fn section_mut_by_key(&mut self, key: impl crate::AsBStr) -> Result, gix_error::Exn> { let key = section::unvalidated::KeyRef::parse(&key).ok_or_else(lookup::existing::key_missing)?; self.section_mut_inner(key.section_name, key.subsection_name) } @@ -89,7 +89,7 @@ impl File { &mut self, name: impl AsRef, subsection_name: impl AsBStrOpt, - ) -> Result, section::header::Error> { + ) -> Result, gix_error::ValidationError> { self.section_mut_or_create_new_inner(name.as_ref(), subsection_name.as_bstr_opt()) } @@ -97,7 +97,7 @@ impl File { &'a mut self, name: &str, subsection_name: Option<&BStr>, - ) -> Result, section::header::Error> { + ) -> Result, gix_error::ValidationError> { self.section_mut_or_create_new_filter_inner(name, subsection_name, |_| true) } @@ -108,7 +108,7 @@ impl File { name: impl AsRef, subsection_name: impl AsBStrOpt, filter: impl FnMut(&Metadata) -> bool, - ) -> Result, section::header::Error> { + ) -> Result, gix_error::ValidationError> { self.section_mut_or_create_new_filter_inner(name.as_ref(), subsection_name.as_bstr_opt(), filter) } @@ -117,7 +117,7 @@ impl File { name: &str, subsection_name: Option<&BStr>, mut filter: impl FnMut(&Metadata) -> bool, - ) -> Result, section::header::Error> { + ) -> Result, gix_error::ValidationError> { match self .section_ids_by_name_and_subname(name, subsection_name) .ok() @@ -144,7 +144,7 @@ impl File { name: impl AsRef, subsection_name: impl AsBStrOpt, filter: impl FnMut(&Metadata) -> bool, - ) -> Result>, lookup::existing::Error> { + ) -> Result>, gix_error::Exn> { self.section_mut_filter_inner(name.as_ref(), subsection_name.as_bstr_opt(), filter) } @@ -153,7 +153,7 @@ impl File { name: &str, subsection_name: Option<&BStr>, mut filter: impl FnMut(&Metadata) -> bool, - ) -> Result>, lookup::existing::Error> { + ) -> Result>, gix_error::Exn> { let id = self .section_ids_by_name_and_subname(name, subsection_name)? .rev() @@ -171,7 +171,7 @@ impl File { &mut self, key: impl crate::AsBStr, filter: impl FnMut(&Metadata) -> bool, - ) -> Result>, lookup::existing::Error> { + ) -> Result>, gix_error::Exn> { let key = section::unvalidated::KeyRef::parse(&key).ok_or_else(lookup::existing::key_missing)?; self.section_mut_filter_inner(key.section_name, key.subsection_name, filter) } @@ -214,7 +214,7 @@ impl File { &mut self, name: impl AsRef, subsection: impl IntoBStringOpt, - ) -> Result, section::header::Error> { + ) -> Result, gix_error::ValidationError> { self.new_section_inner(name.as_ref(), subsection.into_bstring_opt()) } @@ -222,7 +222,7 @@ impl File { &mut self, name: &str, subsection: Option, - ) -> Result, section::header::Error> { + ) -> Result, gix_error::ValidationError> { let section = file::SectionData::new(name, subsection, OwnShared::clone(&self.meta), &mut self.backing)?; let id = self.push_section_internal(section); let nl = self.detect_newline_style_smallvec(); @@ -332,7 +332,7 @@ impl File { /// Adds the provided `section` to the config, returning a mutable reference to it for immediate editing. /// Note that its meta-data will remain as is. - pub fn push_section(&mut self, section: file::Section) -> Result, crate::parse::span::Error> { + pub fn push_section(&mut self, section: file::Section) -> Result, gix_error::ValidationError> { let section = section.into_data(&mut self.backing)?; let id = self.push_section_internal(section); let nl = self.detect_newline_style_smallvec(); @@ -349,7 +349,7 @@ impl File { subsection_name: impl AsBStrOpt, new_name: impl AsRef, new_subsection_name: impl IntoBStringOpt, - ) -> Result<(), rename_section::Error> { + ) -> Result<(), gix_error::Exn> { self.rename_section_filter(name, subsection_name, new_name, new_subsection_name, |_| true) } @@ -366,7 +366,7 @@ impl File { new_name: impl AsRef, new_subsection_name: impl IntoBStringOpt, mut filter: impl FnMut(&Metadata) -> bool, - ) -> Result<(), rename_section::Error> { + ) -> Result<(), gix_error::Exn> { let ids: Vec<_> = self .section_ids_by_name_and_subname(name.as_ref(), subsection_name.as_bstr_opt())? .filter(|id| filter(&self.sections.get(id).expect("each id has a section").meta)) @@ -392,7 +392,7 @@ impl File { } /// Append another File to the end of ourselves, without losing any information. - pub fn append(&mut self, other: Self) -> Result<&mut Self, crate::parse::span::Error> { + pub fn append(&mut self, other: Self) -> Result<&mut Self, gix_error::ValidationError> { self.append_or_insert(other, None) } @@ -401,7 +401,7 @@ impl File { &mut self, mut other: Self, mut insert_after: Option, - ) -> Result<&mut Self, crate::parse::span::Error> { + ) -> Result<&mut Self, gix_error::ValidationError> { let nl = self.detect_newline_style_smallvec(); let our_last_section_before_append = insert_after.or_else(|| (self.next_section_id != 0).then(|| SectionId(self.next_section_id - 1))); @@ -465,7 +465,7 @@ impl File { Ok(self) } - fn rebase_events(&mut self, offset: usize) -> Result<(), crate::parse::span::Error> { + fn rebase_events(&mut self, offset: usize) -> Result<(), gix_error::ValidationError> { for event in &mut self.frontmatter_events { event.rebase(offset)?; } diff --git a/gix-config/src/file/access/raw.rs b/gix-config/src/file/access/raw.rs index d5bf64e7683..581aa34aeb1 100644 --- a/gix-config/src/file/access/raw.rs +++ b/gix-config/src/file/access/raw.rs @@ -19,7 +19,7 @@ impl File { /// /// Consider [`Self::raw_values()`] if you want to get all values of /// a multivar instead. - pub fn raw_value(&self, key: impl AsKey) -> Result { + pub fn raw_value(&self, key: impl AsKey) -> Result { let key = key.as_key(); self.raw_value_filter_by(key.section_name, key.subsection_name, key.value_name, |_| true) } @@ -34,7 +34,7 @@ impl File { section_name: impl AsRef, subsection_name: impl AsBStrOpt, value_name: impl AsRef, - ) -> Result { + ) -> Result { self.raw_value_filter_by(section_name, subsection_name, value_name, |_| true) } @@ -42,10 +42,7 @@ impl File { /// /// Resolution is identical to [`raw_value()`][Self::raw_value()]: the last explicit value wins, even across /// multiple matching sections. - pub fn raw_value_with_section( - &self, - key: impl AsKey, - ) -> Result<(BString, file::SectionRef<'_>), lookup::existing::Error> { + pub fn raw_value_with_section(&self, key: impl AsKey) -> Result<(BString, file::SectionRef<'_>), gix_error::Exn> { let key = key.as_key(); self.raw_value_with_section_by(key.section_name, key.subsection_name, key.value_name) } @@ -59,7 +56,7 @@ impl File { section_name: impl AsRef, subsection_name: impl AsBStrOpt, value_name: impl AsRef, - ) -> Result<(BString, file::SectionRef<'_>), lookup::existing::Error> { + ) -> Result<(BString, file::SectionRef<'_>), gix_error::Exn> { self.raw_value_with_section_filter_by(section_name, subsection_name, value_name, |_| true) } @@ -71,7 +68,7 @@ impl File { &self, key: impl AsKey, filter: impl FnMut(&Metadata) -> bool, - ) -> Result<(BString, file::SectionRef<'_>), lookup::existing::Error> { + ) -> Result<(BString, file::SectionRef<'_>), gix_error::Exn> { let key = key.as_key(); self.raw_value_with_section_filter_by(key.section_name, key.subsection_name, key.value_name, filter) } @@ -84,7 +81,7 @@ impl File { subsection_name: impl AsBStrOpt, value_name: impl AsRef, filter: impl FnMut(&Metadata) -> bool, - ) -> Result<(BString, file::SectionRef<'_>), lookup::existing::Error> { + ) -> Result<(BString, file::SectionRef<'_>), gix_error::Exn> { self.raw_value_with_section_filter_inner( section_name.as_ref(), subsection_name.as_bstr_opt(), @@ -101,7 +98,7 @@ impl File { &self, key: impl AsKey, filter: impl FnMut(&Metadata) -> bool, - ) -> Result { + ) -> Result { let key = key.as_key(); self.raw_value_filter_by(key.section_name, key.subsection_name, key.value_name, filter) } @@ -117,7 +114,7 @@ impl File { subsection_name: impl AsBStrOpt, value_name: impl AsRef, filter: impl FnMut(&Metadata) -> bool, - ) -> Result { + ) -> Result { self.raw_value_filter_inner( section_name.as_ref(), subsection_name.as_bstr_opt(), @@ -132,7 +129,7 @@ impl File { subsection_name: Option<&BStr>, value_name: &str, filter: impl FnMut(&Metadata) -> bool, - ) -> Result { + ) -> Result { self.raw_value_with_section_filter_inner(section_name, subsection_name, value_name, filter) .map(|(value, _section)| value) } @@ -143,7 +140,7 @@ impl File { subsection_name: Option<&BStr>, value_name: &str, mut filter: impl FnMut(&Metadata) -> bool, - ) -> Result<(BString, file::SectionRef<'_>), lookup::existing::Error> { + ) -> Result<(BString, file::SectionRef<'_>), gix_error::Exn> { let section_ids = self.section_ids_by_name_and_subname(section_name, subsection_name)?; for section_id in section_ids.rev() { let section = self.sections.get(§ion_id).expect("known section id"); @@ -162,7 +159,7 @@ impl File { /// /// Consider [`Self::raw_values_mut`] if you want to get mutable /// references to all values of a multivar instead. - pub fn raw_value_mut(&mut self, key: impl AsKey) -> Result, lookup::existing::Error> { + pub fn raw_value_mut(&mut self, key: impl AsKey) -> Result, gix_error::Exn> { let key = key.as_key(); self.raw_value_mut_filter_inner(key.section_name, key.subsection_name, key.value_name, |_| true) } @@ -177,7 +174,7 @@ impl File { section_name: impl AsRef, subsection_name: impl AsBStrOpt, value_name: impl AsRef, - ) -> Result, lookup::existing::Error> { + ) -> Result, gix_error::Exn> { self.raw_value_mut_filter_by(section_name, subsection_name, value_name, |_| true) } @@ -189,7 +186,7 @@ impl File { &mut self, key: impl AsKey, filter: impl FnMut(&Metadata) -> bool, - ) -> Result, lookup::existing::Error> { + ) -> Result, gix_error::Exn> { let key = key.as_key(); self.raw_value_mut_filter_inner(key.section_name, key.subsection_name, key.value_name, filter) } @@ -204,7 +201,7 @@ impl File { subsection_name: impl AsBStrOpt, value_name: impl AsRef, filter: impl FnMut(&Metadata) -> bool, - ) -> Result, lookup::existing::Error> { + ) -> Result, gix_error::Exn> { self.raw_value_mut_filter_inner( section_name.as_ref(), subsection_name.as_bstr_opt(), @@ -219,7 +216,7 @@ impl File { subsection_name: Option<&BStr>, value_name: &str, mut filter: impl FnMut(&Metadata) -> bool, - ) -> Result, lookup::existing::Error> { + ) -> Result, gix_error::Exn> { let mut section_ids = self .section_ids_by_name_and_subname(section_name, subsection_name)? .rev(); @@ -310,7 +307,7 @@ impl File { /// /// Consider [`Self::raw_value`] if you want to get the resolved single /// value for a given key, if your value does not support multi-valued values. - pub fn raw_values(&self, key: impl AsKey) -> Result, lookup::existing::Error> { + pub fn raw_values(&self, key: impl AsKey) -> Result, gix_error::Exn> { let key = key.as_key(); self.raw_values_by(key.section_name, key.subsection_name, key.value_name) } @@ -356,7 +353,7 @@ impl File { section_name: impl AsRef, subsection_name: impl AsBStrOpt, value_name: impl AsRef, - ) -> Result, lookup::existing::Error> { + ) -> Result, gix_error::Exn> { self.raw_values_filter_by(section_name, subsection_name, value_name, |_| true) } @@ -364,7 +361,7 @@ impl File { pub fn raw_values_with_sections( &self, key: impl AsKey, - ) -> Result)>, lookup::existing::Error> { + ) -> Result)>, gix_error::Exn> { let key = key.as_key(); self.raw_values_with_sections_by(key.section_name, key.subsection_name, key.value_name) } @@ -376,7 +373,7 @@ impl File { section_name: impl AsRef, subsection_name: impl AsBStrOpt, value_name: impl AsRef, - ) -> Result)>, lookup::existing::Error> { + ) -> Result)>, gix_error::Exn> { self.raw_values_with_sections_filter_by(section_name, subsection_name, value_name, |_| true) } @@ -386,7 +383,7 @@ impl File { &self, key: impl AsKey, filter: impl FnMut(&Metadata) -> bool, - ) -> Result)>, lookup::existing::Error> { + ) -> Result)>, gix_error::Exn> { let key = key.as_key(); self.raw_values_with_sections_filter_by(key.section_name, key.subsection_name, key.value_name, filter) } @@ -399,7 +396,7 @@ impl File { subsection_name: impl AsBStrOpt, value_name: impl AsRef, filter: impl FnMut(&Metadata) -> bool, - ) -> Result)>, lookup::existing::Error> { + ) -> Result)>, gix_error::Exn> { self.raw_values_with_sections_filter_inner( section_name.as_ref(), subsection_name.as_bstr_opt(), @@ -416,7 +413,7 @@ impl File { &self, key: impl AsKey, filter: impl FnMut(&Metadata) -> bool, - ) -> Result, lookup::existing::Error> { + ) -> Result, gix_error::Exn> { let key = key.as_key(); self.raw_values_filter_by(key.section_name, key.subsection_name, key.value_name, filter) } @@ -432,7 +429,7 @@ impl File { subsection_name: impl AsBStrOpt, value_name: impl AsRef, filter: impl FnMut(&Metadata) -> bool, - ) -> Result, lookup::existing::Error> { + ) -> Result, gix_error::Exn> { self.raw_values_filter_inner( section_name.as_ref(), subsection_name.as_bstr_opt(), @@ -447,7 +444,7 @@ impl File { subsection_name: Option<&BStr>, value_name: &str, filter: impl FnMut(&Metadata) -> bool, - ) -> Result, lookup::existing::Error> { + ) -> Result, gix_error::Exn> { self.raw_values_with_sections_filter_inner(section_name, subsection_name, value_name, filter) .map(|values| values.into_iter().map(|(value, _section)| value).collect()) } @@ -458,7 +455,7 @@ impl File { subsection_name: Option<&BStr>, value_name: &str, mut filter: impl FnMut(&Metadata) -> bool, - ) -> Result)>, lookup::existing::Error> { + ) -> Result)>, gix_error::Exn> { let mut values = Vec::new(); let section_ids = self.section_ids_by_name_and_subname(section_name, subsection_name)?; for section_id in section_ids { @@ -530,7 +527,7 @@ impl File { /// /// Note that this operation is relatively expensive, requiring a full /// traversal of the config. - pub fn raw_values_mut(&mut self, key: impl AsKey) -> Result, lookup::existing::Error> { + pub fn raw_values_mut(&mut self, key: impl AsKey) -> Result, gix_error::Exn> { let key = key.as_key(); self.raw_values_mut_filter_inner(key.section_name, key.subsection_name, key.value_name, |_| true) } @@ -588,7 +585,7 @@ impl File { section_name: impl AsRef, subsection_name: impl AsBStrOpt, value_name: impl AsRef, - ) -> Result, lookup::existing::Error> { + ) -> Result, gix_error::Exn> { self.raw_values_mut_filter_by(section_name, subsection_name, value_name, |_| true) } @@ -598,7 +595,7 @@ impl File { &mut self, key: impl AsKey, filter: impl FnMut(&Metadata) -> bool, - ) -> Result, lookup::existing::Error> { + ) -> Result, gix_error::Exn> { let key = key.as_key(); self.raw_values_mut_filter_inner(key.section_name, key.subsection_name, key.value_name, filter) } @@ -611,7 +608,7 @@ impl File { subsection_name: impl AsBStrOpt, value_name: impl AsRef, filter: impl FnMut(&Metadata) -> bool, - ) -> Result, lookup::existing::Error> { + ) -> Result, gix_error::Exn> { self.raw_values_mut_filter_inner( section_name.as_ref(), subsection_name.as_bstr_opt(), @@ -626,7 +623,7 @@ impl File { subsection_name: Option<&BStr>, value_name: &str, mut filter: impl FnMut(&Metadata) -> bool, - ) -> Result, lookup::existing::Error> { + ) -> Result, gix_error::Exn> { let section_ids = self.section_ids_by_name_and_subname(section_name, subsection_name)?; let key = section::ValueName::try_from(value_name).or_erased()?; @@ -722,7 +719,7 @@ impl File { &mut self, key: impl AsKey, new_value: impl crate::AsBStr, - ) -> Result<(), crate::file::set_raw_value::Error> { + ) -> Result<(), gix_error::Exn> { let key = key.as_key(); self.raw_value_mut_filter_inner(key.section_name, key.subsection_name, key.value_name, |_| true)? .set(new_value) @@ -770,7 +767,7 @@ impl File { subsection_name: impl AsBStrOpt, value_name: impl AsRef, new_value: impl crate::AsBStr, - ) -> Result<(), crate::file::set_raw_value::Error> { + ) -> Result<(), gix_error::Exn> { self.raw_value_mut_by(section_name, subsection_name, value_name)? .set(new_value) .or_erased()?; @@ -805,7 +802,7 @@ impl File { &mut self, key: impl AsKey, new_value: impl crate::AsBStr, - ) -> Result, crate::file::set_raw_value::Error> { + ) -> Result, gix_error::Exn> { self.set_raw_value_filter(key, new_value, |_| true) } @@ -839,7 +836,7 @@ impl File { subsection_name: impl AsBStrOpt, value_name: impl AsRef, new_value: impl crate::AsBStr, - ) -> Result, crate::file::set_raw_value::Error> { + ) -> Result, gix_error::Exn> { self.set_raw_value_filter_by(section_name, subsection_name, value_name, new_value, |_| true) } @@ -850,7 +847,7 @@ impl File { key: impl AsKey, new_value: impl crate::AsBStr, filter: impl FnMut(&Metadata) -> bool, - ) -> Result, crate::file::set_raw_value::Error> { + ) -> Result, gix_error::Exn> { let key = key.as_key(); self.set_raw_value_filter_by_inner(key.section_name, key.subsection_name, key.value_name, new_value, filter) } @@ -864,7 +861,7 @@ impl File { value_name: impl AsRef, new_value: impl crate::AsBStr, filter: impl FnMut(&Metadata) -> bool, - ) -> Result, crate::file::set_raw_value::Error> { + ) -> Result, gix_error::Exn> { self.set_raw_value_filter_by_inner( section_name.as_ref(), subsection_name.as_bstr_opt(), @@ -881,7 +878,7 @@ impl File { value_name: &str, new_value: impl crate::AsBStr, filter: impl FnMut(&Metadata) -> bool, - ) -> Result, crate::file::set_raw_value::Error> { + ) -> Result, gix_error::Exn> { let key = section::ValueName::try_from(value_name).or_erased()?; let mut section = self .section_mut_or_create_new_filter_inner(section_name, subsection_name, filter) @@ -971,7 +968,7 @@ impl File { &mut self, key: impl AsKey, new_values: Iter, - ) -> Result<(), crate::file::set_raw_value::Error> + ) -> Result<(), gix_error::Exn> where Iter: IntoIterator, Item: crate::AsBStr, @@ -1067,7 +1064,7 @@ impl File { subsection_name: impl AsBStrOpt, value_name: impl AsRef, new_values: Iter, - ) -> Result<(), crate::file::set_raw_value::Error> + ) -> Result<(), gix_error::Exn> where Iter: IntoIterator, Item: crate::AsBStr, diff --git a/gix-config/src/file/access/read_only.rs b/gix-config/src/file/access/read_only.rs index f6a1085ad14..55f4629cbce 100644 --- a/gix-config/src/file/access/read_only.rs +++ b/gix-config/src/file/access/read_only.rs @@ -176,7 +176,7 @@ impl File { /// // ... or explicitly declare the type to avoid the turbofish /// let c_value: Vec = git_config.values("core.c")?; /// assert_eq!(c_value, vec![Boolean(false)]); - /// # Ok::<(), gix_config::lookup::Error>(()) + /// # Ok::<(), gix_config::lookup::Error>>(()) /// ``` /// /// [`value`]: crate::value @@ -230,7 +230,7 @@ impl File { /// // ... or explicitly declare the type to avoid the turbofish /// let c_value: Vec = git_config.values_by("core", None, "c")?; /// assert_eq!(c_value, vec![Boolean(false)]); - /// # Ok::<(), gix_config::lookup::Error>(()) + /// # Ok::<(), gix_config::lookup::Error>>(()) /// ``` /// /// [`value`]: crate::value @@ -277,17 +277,14 @@ impl File { &self, name: impl AsRef, subsection_name: impl AsBStrOpt, - ) -> Result, lookup::existing::Error> { + ) -> Result, gix_error::Exn> { self.section_filter(name, subsection_name, |_| true)? .ok_or_else(lookup::existing::section_missing) } /// Returns the last found immutable section with a given `section_key`, identifying the name and subsection name like `core` /// or `remote.origin`. - pub fn section_by_key( - &self, - section_key: impl crate::AsBStr, - ) -> Result, lookup::existing::Error> { + pub fn section_by_key(&self, section_key: impl crate::AsBStr) -> Result, gix_error::Exn> { let key = crate::parse::section::unvalidated::KeyRef::parse(section_key.as_bstr()) .ok_or_else(lookup::existing::key_missing)?; self.section(key.section_name, key.subsection_name) @@ -302,7 +299,7 @@ impl File { name: impl AsRef, subsection_name: impl AsBStrOpt, mut filter: impl FnMut(&Metadata) -> bool, - ) -> Result>, lookup::existing::Error> { + ) -> Result>, gix_error::Exn> { Ok(self .section_ids_by_name_and_subname(name.as_ref(), subsection_name.as_bstr_opt())? .rev() @@ -320,7 +317,7 @@ impl File { &self, section_key: impl crate::AsBStr, filter: impl FnMut(&Metadata) -> bool, - ) -> Result>, lookup::existing::Error> { + ) -> Result>, gix_error::Exn> { let key = crate::parse::section::unvalidated::KeyRef::parse(section_key.as_bstr()) .ok_or_else(lookup::existing::key_missing)?; self.section_filter(key.section_name, key.subsection_name, filter) diff --git a/gix-config/src/file/includes/mod.rs b/gix-config/src/file/includes/mod.rs index 1af131bb943..4e142dbd534 100644 --- a/gix-config/src/file/includes/mod.rs +++ b/gix-config/src/file/includes/mod.rs @@ -8,7 +8,6 @@ use gix_ref::Category; use crate::{ File, file, file::{Metadata, SectionId, includes, init}, - path, }; impl File { @@ -31,7 +30,7 @@ impl File { /// We can fix this by 'splitting' the include section if needed so the included sections are put into the right place. /// - `hasconfig:remote.*.url` will not prevent itself to include files with `[remote "name"]\nurl = x` values, but it also /// won't match them, i.e. one cannot include something that will cause the condition to match or to always be true. - pub fn resolve_includes(&mut self, options: init::Options<'_>) -> Result<(), Error> { + pub fn resolve_includes(&mut self, options: init::Options<'_>) -> Result<(), gix_error::Exn> { if options.includes.max_depth == 0 { return Ok(()); } @@ -40,7 +39,7 @@ impl File { } } -pub(crate) fn resolve(config: &mut File, buf: &mut Vec, options: init::Options<'_>) -> Result<(), Error> { +pub(crate) fn resolve(config: &mut File, buf: &mut Vec, options: init::Options<'_>) -> Result<(), gix_error::Exn> { resolve_includes_recursive(None, config, 0, buf, options) } @@ -50,7 +49,7 @@ fn resolve_includes_recursive( depth: u8, buf: &mut Vec, options: init::Options<'_>, -) -> Result<(), Error> { +) -> Result<(), gix_error::Exn> { if depth == options.includes.max_depth { return if options.includes.err_on_max_depth_exceeded { Err(ValidationError::new(format!( @@ -97,7 +96,7 @@ fn insert_includes_recursively( depth: u8, options: init::Options<'_>, buf: &mut Vec, -) -> Result<(), Error> { +) -> Result<(), gix_error::Exn> { for (section_id, config_path) in section_ids_and_include_paths { let meta = OwnShared::clone(&target_config.sections[§ion_id].meta); let target_config_path = meta.path.as_deref(); @@ -156,7 +155,7 @@ fn include_condition_match( target_config_path: Option<&Path>, search_config: &File, options: Options<'_>, -) -> Result { +) -> Result { let mut tokens = condition.splitn(2, |b| *b == b':'); let (prefix, condition) = match (tokens.next(), tokens.next()) { (Some(a), Some(b)) => (a, b), @@ -243,7 +242,7 @@ fn gitdir_matches( .. }: Options<'_>, wildmatch_mode: gix_glob::wildmatch::Mode, -) -> Result { +) -> Result { if !err_on_interpolation_failure && git_dir.is_none() { return Ok(false); } @@ -313,8 +312,8 @@ fn gitdir_matches( fn check_interpolation_result( disable: bool, - res: Result, path::interpolate::Error>, -) -> Result, path::interpolate::Error> { + res: Result, gix_error::Exn>, +) -> Result, gix_error::Exn> { if disable { return res.map(|path| Some(path.into())); } @@ -334,7 +333,7 @@ fn resolve_path( err_on_missing_config_path, .. }: includes::Options<'_>, -) -> Result, Error> { +) -> Result, gix_error::Exn> { let path = match check_interpolation_result(err_on_interpolation_failure, path.interpolate(context)) .or_raise_erased(|| message("Could not interpolate include path"))? { @@ -361,4 +360,4 @@ fn resolve_path( } mod types; -pub use types::{Error, Options, conditional}; +pub use types::{Options, conditional}; diff --git a/gix-config/src/file/includes/types.rs b/gix-config/src/file/includes/types.rs index 9b9530ea4d7..7f8aa5fcbe3 100644 --- a/gix-config/src/file/includes/types.rs +++ b/gix-config/src/file/includes/types.rs @@ -1,8 +1,5 @@ use crate::path::interpolate; -/// The error returned when following includes. -pub type Error = gix_error::Exn; - /// Options to handle includes, like `include.path` or `includeIf..path`, #[derive(Clone, Copy)] pub struct Options<'a> { diff --git a/gix-config/src/file/init/comfort.rs b/gix-config/src/file/init/comfort.rs index 808a1f76f9c..f98923568df 100644 --- a/gix-config/src/file/init/comfort.rs +++ b/gix-config/src/file/init/comfort.rs @@ -23,7 +23,7 @@ impl File { /// which excludes repository local configuration, as well as override-configuration from environment variables. /// /// Note that the file might [be empty][File::is_void()] in case no configuration file was found. - pub fn from_globals() -> Result { + pub fn from_globals() -> Result> { let metas = [ source::Kind::GitInstallation, source::Kind::System, @@ -60,7 +60,7 @@ impl File { /// See [`git-config`'s documentation] for more information on the environment variables in question. /// /// [`git-config`'s documentation]: https://git-scm.com/docs/git-config#Documentation/git-config.txt-GITCONFIGCOUNT - pub fn from_environment_overrides() -> Result { + pub fn from_environment_overrides() -> Result { let home = gix_path::env::home_dir(); let options = init::Options { includes: init::includes::Options::follow_without_conditional(home.as_deref()), @@ -84,7 +84,7 @@ impl File { /// /// Includes will be resolved within limits as some information like the git installation directory is missing to interpolate /// paths with as well as git repository information like the branch name. - pub fn from_git_dir(dir: std::path::PathBuf) -> Result { + pub fn from_git_dir(dir: std::path::PathBuf) -> Result> { use gix_error::{ResultExt, message}; let (mut local, git_dir) = { @@ -159,9 +159,3 @@ impl File { Ok(globals) } } - -/// -pub mod from_git_dir { - /// The error returned by [`File::from_git_dir()`][crate::File::from_git_dir()]. - pub type Error = gix_error::Exn; -} diff --git a/gix-config/src/file/init/from_env.rs b/gix-config/src/file/init/from_env.rs index 6b123c1e2b0..d0610b8c2e6 100644 --- a/gix-config/src/file/init/from_env.rs +++ b/gix-config/src/file/init/from_env.rs @@ -2,9 +2,6 @@ use bstr::ByteSlice; use crate::{File, KeyRef, file, file::init}; -/// Represents the errors that may occur when calling [`File::from_env()`]. -pub type Error = gix_error::Exn; - /// Instantiation from environment variables impl File { /// Generates a config from `GIT_CONFIG_*` environment variables or returns `Ok(None)` if no configuration was found. @@ -13,7 +10,7 @@ impl File { /// With `options` configured, it's possible to resolve `include.path` or `includeIf..path` directives as well. /// /// [`git-config`'s documentation]: https://git-scm.com/docs/git-config#Documentation/git-config.txt-GITCONFIGCOUNT - pub fn from_env(options: init::Options<'_>) -> Result, Error> { + pub fn from_env(options: init::Options<'_>) -> Result, gix_error::Exn> { use gix_error::{ErrorExt, NotFoundError, OptionExt, ResultExt, ValidationError, message}; use std::env; let count: usize = match env::var("GIT_CONFIG_COUNT") { diff --git a/gix-config/src/file/init/from_paths.rs b/gix-config/src/file/init/from_paths.rs index 4f3088df7c0..fa33f8c2a14 100644 --- a/gix-config/src/file/init/from_paths.rs +++ b/gix-config/src/file/init/from_paths.rs @@ -5,15 +5,15 @@ use crate::{ file::{Metadata, init::Options}, }; -/// The error returned by [`File::from_paths_metadata()`] and [`File::from_path_no_includes()`]. -pub type Error = gix_error::Exn; - /// Instantiation from one or more paths impl File { /// Load the single file at `path` with `source` without following include directives. /// /// Note that the path will be checked for ownership to derive trust. - pub fn from_path_no_includes(path: std::path::PathBuf, source: crate::Source) -> Result { + pub fn from_path_no_includes( + path: std::path::PathBuf, + source: crate::Source, + ) -> Result> { use gix_error::{ResultExt, message}; let trust = gix_sec::Trust::from_path_ownership(&path).or_raise(|| { message!( @@ -44,7 +44,7 @@ impl File { pub fn from_paths_metadata( path_meta: impl IntoIterator>, options: Options<'_>, - ) -> Result, Error> { + ) -> Result, gix_error::Exn> { let mut buf = Vec::with_capacity(512); let err_on_nonexisting_paths = true; Self::from_paths_metadata_buf( @@ -64,7 +64,7 @@ impl File { buf: &mut Vec, err_on_non_existing_paths: bool, options: Options<'_>, - ) -> Result, Error> { + ) -> Result, gix_error::Exn> { use gix_error::{ErrorExt, ResultExt, message}; let mut target = None; let mut seen = BTreeSet::default(); diff --git a/gix-config/src/file/init/mod.rs b/gix-config/src/file/init/mod.rs index 5f9b70c25c7..26314660f53 100644 --- a/gix-config/src/file/init/mod.rs +++ b/gix-config/src/file/init/mod.rs @@ -7,7 +7,7 @@ use crate::{ }; mod types; -pub use types::{Error, Options}; +pub use types::Options; mod comfort; /// @@ -36,7 +36,7 @@ impl File { input: &[u8], meta: impl Into>, options: Options<'_>, - ) -> Result { + ) -> Result { use gix_error::{ResultExt, message}; let meta = meta.into(); Ok(Self::from_parse_events_no_includes( @@ -84,7 +84,7 @@ impl File { input_and_buf: &mut Vec, meta: impl Into>, options: Options<'_>, - ) -> Result { + ) -> Result { use gix_error::{ResultExt, message}; let mut config = Self::from_parse_events_no_includes( parse::Events::from_bytes(input_and_buf, options.to_event_filter()) diff --git a/gix-config/src/file/init/types.rs b/gix-config/src/file/init/types.rs index 36186a1899f..ca397605f0e 100644 --- a/gix-config/src/file/init/types.rs +++ b/gix-config/src/file/init/types.rs @@ -1,8 +1,5 @@ use crate::{file::init, parse::EventRef}; -/// The error returned by [`File::from_bytes_no_includes()`][crate::File::from_bytes_no_includes()]. -pub type Error = gix_error::Exn; - /// Options when loading git config using [`File::from_paths_metadata()`][crate::File::from_paths_metadata()]. #[derive(Clone, Copy, Default)] pub struct Options<'a> { diff --git a/gix-config/src/file/mod.rs b/gix-config/src/file/mod.rs index 68f916c1960..fc6a00bbade 100644 --- a/gix-config/src/file/mod.rs +++ b/gix-config/src/file/mod.rs @@ -24,18 +24,6 @@ mod util; /// pub mod section; -/// -pub mod rename_section { - /// The error returned by [`File::rename_section(…)`][crate::File::rename_section()]. - pub type Error = gix_error::Exn; -} - -/// -pub mod set_raw_value { - /// The error returned by [`File::set_raw_value(…)`][crate::File::set_raw_value()]. - pub type Error = gix_error::Exn; -} - /// Convert ergonomic subsection inputs into an optional owned name. pub trait IntoBStringOpt { /// Convert into an optional owned subsection name. diff --git a/gix-config/src/file/mutable/mod.rs b/gix-config/src/file/mutable/mod.rs index e6e5ad9fa5b..ae9b2861732 100644 --- a/gix-config/src/file/mutable/mod.rs +++ b/gix-config/src/file/mutable/mod.rs @@ -53,7 +53,7 @@ impl Default for Whitespace { } impl Whitespace { - fn key_value_separators(&self, backing: &mut Vec) -> Result, crate::parse::span::Error> { + fn key_value_separators(&self, backing: &mut Vec) -> Result, gix_error::ValidationError> { let mut out = Vec::with_capacity(3); if let Some(ws) = &self.pre_sep { out.push(Event::Whitespace(crate::parse::Span::append(backing, ws)?)); diff --git a/gix-config/src/file/mutable/multi_value.rs b/gix-config/src/file/mutable/multi_value.rs index 91eb99cb726..72da048dc7f 100644 --- a/gix-config/src/file/mutable/multi_value.rs +++ b/gix-config/src/file/mutable/multi_value.rs @@ -37,7 +37,7 @@ pub struct MultiValueMut<'borrow> { impl MultiValueMut<'_> { /// Returns the actual values. - pub fn get(&self) -> Result, lookup::existing::Error> { + pub fn get(&self) -> Result, gix_error::Exn> { let mut expect_value = false; let mut values = Vec::new(); let mut concatenated_value = BString::default(); @@ -98,7 +98,7 @@ impl MultiValueMut<'_> { /// # Safety /// /// This will panic if the index is out of range. - pub fn set_string_at(&mut self, index: usize, value: impl AsRef) -> Result<(), crate::parse::span::Error> { + pub fn set_string_at(&mut self, index: usize, value: impl AsRef) -> Result<(), gix_error::ValidationError> { self.set_at(index, value.as_ref()) } @@ -107,7 +107,7 @@ impl MultiValueMut<'_> { /// # Safety /// /// This will panic if the index is out of range. - pub fn set_at(&mut self, index: usize, value: impl crate::AsBStr) -> Result<(), crate::parse::span::Error> { + pub fn set_at(&mut self, index: usize, value: impl crate::AsBStr) -> Result<(), gix_error::ValidationError> { let EntryData { section_id, offset_index, @@ -130,7 +130,7 @@ impl MultiValueMut<'_> { /// remaining values are ignored. /// /// [`zip`]: std::iter::Iterator::zip - pub fn set_values(&mut self, values: Iter) -> Result<(), crate::parse::span::Error> + pub fn set_values(&mut self, values: Iter) -> Result<(), gix_error::ValidationError> where Iter: IntoIterator, Item: crate::AsBStr, @@ -158,7 +158,7 @@ impl MultiValueMut<'_> { /// Sets all values in this multivar to the provided one without owning the /// provided input. - pub fn set_all(&mut self, input: impl crate::AsBStr) -> Result<(), crate::parse::span::Error> { + pub fn set_all(&mut self, input: impl crate::AsBStr) -> Result<(), gix_error::ValidationError> { let input = input.as_bstr(); for EntryData { section_id, @@ -186,7 +186,7 @@ impl MultiValueMut<'_> { section_id: SectionId, offset_index: usize, value: &BStr, - ) -> Result<(), crate::parse::span::Error> { + ) -> Result<(), gix_error::ValidationError> { let (offset, size) = MultiValueMut::index_and_size(offsets, section_id, offset_index); let whitespace = Whitespace::from_body(section, backing); let value = crate::parse::Span::append(backing, &escape_value(value))?; diff --git a/gix-config/src/file/mutable/section.rs b/gix-config/src/file/mutable/section.rs index e3c8a4c6123..eff9e60e304 100644 --- a/gix-config/src/file/mutable/section.rs +++ b/gix-config/src/file/mutable/section.rs @@ -42,7 +42,7 @@ impl SectionMut<'_> { &mut self, name: impl AsRef, subsection_name: impl IntoBStringOpt, - ) -> Result<&mut Self, parse::section::header::Error> { + ) -> Result<&mut Self, gix_error::ValidationError> { let header = parse::section::HeaderData::new_in(name, subsection_name.into_bstring_opt(), self.backing)?; self.set_header(header); Ok(self) @@ -54,7 +54,7 @@ impl SectionMut<'_> { &mut self, value_name: impl AsRef, value: impl AsBStrOpt, - ) -> Result<&mut Self, file::section::value::Error> { + ) -> Result<&mut Self, gix_error::ValidationError> { let value_name = ValueName::try_from(value_name.as_ref())?; self.push_with_comment_inner(value_name, value.as_bstr_opt(), None)?; Ok(self) @@ -69,7 +69,7 @@ impl SectionMut<'_> { value_name: impl AsRef, value: impl AsBStrOpt, comment: impl crate::AsBStr, - ) -> Result<&mut Self, file::section::value::Error> { + ) -> Result<&mut Self, gix_error::ValidationError> { let value_name = ValueName::try_from(value_name.as_ref())?; self.push_with_comment_inner(value_name, value.as_bstr_opt(), Some(comment.as_bstr()))?; Ok(self) @@ -80,7 +80,7 @@ impl SectionMut<'_> { value_name: ValueName, value: Option<&BStr>, comment: Option<&BStr>, - ) -> Result<(), parse::span::Error> { + ) -> Result<(), gix_error::ValidationError> { let mut events = Vec::new(); if let Some(ws) = &self.whitespace.pre_key { events.push(Event::Whitespace(Span::append(self.backing, ws)?)); @@ -173,7 +173,7 @@ impl SectionMut<'_> { &mut self, value_name: impl AsRef, value: impl crate::AsBStr, - ) -> Result, file::section::value::Error> { + ) -> Result, gix_error::ValidationError> { let value_name = ValueName::try_from(value_name.as_ref())?; self.set_inner(value_name, value.as_bstr()) } @@ -182,7 +182,7 @@ impl SectionMut<'_> { &mut self, value_name: ValueName, value: &BStr, - ) -> Result, parse::span::Error> { + ) -> Result, gix_error::ValidationError> { match self.section.body.key_and_value_range_by_in(self.backing, &value_name) { None => { self.push_with_comment_inner(value_name, Some(value), None)?; @@ -216,7 +216,7 @@ impl SectionMut<'_> { /// Adds a new line event. Note that you don't need to call this unless /// you've disabled implicit newlines. - pub fn push_newline(&mut self) -> Result<&mut Self, parse::span::Error> { + pub fn push_newline(&mut self) -> Result<&mut Self, gix_error::ValidationError> { let newline = Span::append(self.backing, &self.newline)?; self.section.body.0.push(Event::Newline(newline)); Ok(self) @@ -394,7 +394,7 @@ impl<'a> SectionMut<'a> { } } - pub(crate) fn get(&self, key: &ValueName, start: Index, end: Index) -> Result { + pub(crate) fn get(&self, key: &ValueName, start: Index, end: Index) -> Result { let mut expect_value = false; let mut concatenated_value = BString::default(); @@ -459,7 +459,7 @@ impl<'a> SectionMut<'a> { index: Index, key: ValueName, value: &BStr, - ) -> Result { + ) -> Result { let mut size = 0; let value = Span::append(self.backing, &escape_value(value))?; let sep_events = self.whitespace.key_value_separators(self.backing)?; diff --git a/gix-config/src/file/mutable/value.rs b/gix-config/src/file/mutable/value.rs index 00da62e8b25..ab8076dc8a6 100644 --- a/gix-config/src/file/mutable/value.rs +++ b/gix-config/src/file/mutable/value.rs @@ -3,7 +3,6 @@ use bstr::BString; use crate::{ file, file::{Index, Size, mutable::section::SectionMut}, - lookup, parse::section, }; @@ -19,21 +18,21 @@ pub struct ValueMut<'borrow> { impl<'borrow> ValueMut<'borrow> { /// Returns the actual value. This is computed each time this is called /// requiring an allocation for multi-line values. - pub fn get(&self) -> Result { + pub fn get(&self) -> Result { 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. - pub fn set_string(&mut self, input: impl AsRef) -> Result<(), crate::parse::span::Error> { + pub fn set_string(&mut self, input: impl AsRef) -> Result<(), gix_error::ValidationError> { self.set(input.as_ref()) } /// 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. - pub fn set(&mut self, input: impl crate::AsBStr) -> Result<(), crate::parse::span::Error> { + pub fn set(&mut self, input: impl crate::AsBStr) -> Result<(), gix_error::ValidationError> { let new_size = self .section .set_internal(self.index, self.key.to_owned(), input.as_bstr())?; diff --git a/gix-config/src/file/section/body.rs b/gix-config/src/file/section/body.rs index 8b33aa4014b..3bbd2ed8731 100644 --- a/gix-config/src/file/section/body.rs +++ b/gix-config/src/file/section/body.rs @@ -225,7 +225,7 @@ impl BodyData { &self, source: &[u8], target: &mut Vec, - ) -> Result { + ) -> Result { Ok(BodyData( self.0 .iter() diff --git a/gix-config/src/file/section/mod.rs b/gix-config/src/file/section/mod.rs index 21cb46eab9f..8a99a89212e 100644 --- a/gix-config/src/file/section/mod.rs +++ b/gix-config/src/file/section/mod.rs @@ -15,12 +15,6 @@ use gix_features::threading::OwnShared; use crate::file::{SectionId, write::platform_newline}; -/// Errors related to changing values in a section. -pub mod value { - /// The error returned when adding or changing a value in a section. - pub type Error = gix_error::ValidationError; -} - impl std::ops::Deref for SectionData { type Target = BodyData; @@ -82,7 +76,7 @@ impl Section { name: impl AsRef, subsection: impl IntoBStringOpt, meta: impl Into>, - ) -> Result { + ) -> Result { let mut backing = Vec::new(); let data = SectionData::new(name, subsection.into_bstring_opt(), meta, &mut backing)?; Ok(Section { backing, data }) @@ -113,7 +107,7 @@ impl Section { Section { backing, data } } - pub(crate) fn into_data(self, target: &mut Vec) -> Result { + pub(crate) fn into_data(self, target: &mut Vec) -> Result { self.data.copy_to_backing_in(&self.backing, target) } } @@ -125,7 +119,7 @@ impl SectionData { subsection: impl Into>, meta: impl Into>, backing: &mut Vec, - ) -> Result { + ) -> Result { Ok(SectionData { header: parse::section::HeaderData::new_in(name, subsection, backing)?, body: Default::default(), @@ -148,7 +142,11 @@ impl SectionData { &self.meta } - pub(crate) fn copy_to_backing_in(&self, source: &[u8], target: &mut Vec) -> Result { + pub(crate) fn copy_to_backing_in( + &self, + source: &[u8], + target: &mut Vec, + ) -> Result { Ok(SectionData { header: self.header.copy_to_backing_in(source, target)?, body: self.body.copy_to_backing_in(source, target)?, diff --git a/gix-config/src/file/util.rs b/gix-config/src/file/util.rs index 912edb091b1..bc30c2d40fc 100644 --- a/gix-config/src/file/util.rs +++ b/gix-config/src/file/util.rs @@ -62,7 +62,7 @@ impl File { &'a self, section_name: &'a str, subsection_name: Option<&BStr>, - ) -> Result + DoubleEndedIterator + 'a, lookup::existing::Error> { + ) -> Result + DoubleEndedIterator + 'a, gix_error::Exn> { let section_name = section::Name::from_str_unchecked(section_name); let lookup = self .section_lookup_tree @@ -79,7 +79,7 @@ impl File { pub(crate) fn section_ids_by_name<'a>( &'a self, section_name: &str, - ) -> Result + 'a + use<'a>, lookup::existing::Error> { + ) -> Result + 'a + use<'a>, gix_error::Exn> { let lookup_name = section::Name::from_str_unchecked(section_name); let lookup = self .section_lookup_tree diff --git a/gix-config/src/lookup.rs b/gix-config/src/lookup.rs index e54ac502696..0535e780599 100644 --- a/gix-config/src/lookup.rs +++ b/gix-config/src/lookup.rs @@ -34,30 +34,28 @@ impl std::error::Error for Error { } } -impl From for Error { - fn from(err: existing::Error) -> Self { +impl From for Error { + fn from(err: gix_error::Exn) -> Self { Error::ValueMissing(err.into_error()) } } /// pub mod existing { - /// The error when looking up a value that doesn't exist, for example via [`File::value()`][crate::File::value()]. - pub type Error = gix_error::Exn; - pub(crate) fn section_missing() -> Error { + pub(crate) fn section_missing() -> gix_error::Exn { not_found("The requested section does not exist") } - pub(crate) fn subsection_missing() -> Error { + pub(crate) fn subsection_missing() -> gix_error::Exn { not_found("The requested subsection does not exist") } - pub(crate) fn key_missing() -> Error { + pub(crate) fn key_missing() -> gix_error::Exn { not_found("The key does not exist in the requested section") } - fn not_found(message: &'static str) -> Error { + fn not_found(message: &'static str) -> gix_error::Exn { use gix_error::ErrorExt; gix_error::NotFoundError::new(message).raise_erased() } diff --git a/gix-config/src/parse/comment.rs b/gix-config/src/parse/comment.rs index 3f8a26bec15..4089fb9fab1 100644 --- a/gix-config/src/parse/comment.rs +++ b/gix-config/src/parse/comment.rs @@ -5,7 +5,7 @@ impl Comment { &self, source: &[u8], target: &mut Vec, - ) -> Result { + ) -> Result { Ok(Comment { tag: self.tag, text: self.text.copy_to_backing_in(source, target)?, diff --git a/gix-config/src/parse/event.rs b/gix-config/src/parse/event.rs index be328595048..0610b5ab6ea 100644 --- a/gix-config/src/parse/event.rs +++ b/gix-config/src/parse/event.rs @@ -6,7 +6,7 @@ use crate::parse::{Event, EventRef}; impl Event { /// Shift all backing-buffer spans in this event forward by `offset` bytes. - pub(crate) fn rebase(&mut self, offset: usize) -> Result<(), crate::parse::span::Error> { + pub(crate) fn rebase(&mut self, offset: usize) -> Result<(), gix_error::ValidationError> { match self { Event::Comment(comment) => comment.text.rebase(offset), Event::SectionHeader(header) => header.rebase(offset), @@ -24,7 +24,7 @@ impl Event { &self, source: &[u8], target: &mut Vec, - ) -> Result { + ) -> Result { Ok(match self { Event::Comment(comment) => Event::Comment(comment.copy_to_backing_in(source, target)?), Event::SectionHeader(header) => Event::SectionHeader(header.copy_to_backing_in(source, target)?), diff --git a/gix-config/src/parse/format.rs b/gix-config/src/parse/format.rs index 85b0ab0bc6d..97cae17b8ea 100644 --- a/gix-config/src/parse/format.rs +++ b/gix-config/src/parse/format.rs @@ -76,8 +76,8 @@ impl Default for Options { /// /// # Errors /// -/// Returns a [`parse::Error`] if `input` is not a syntactically valid git-config file. -pub fn normalize(input: &[u8], options: &Options) -> Result { +/// Returns a [`gix_error::ValidationError`] if `input` is not a syntactically valid git-config file. +pub fn normalize(input: &[u8], options: &Options) -> Result { let parsed = parse::Events::from_bytes(input, None)?; let events: Vec<_> = parsed.iter().collect(); Ok(normalize_events(&events, options)) diff --git a/gix-config/src/parse/mod.rs b/gix-config/src/parse/mod.rs index 46a2187a44e..898cc1d12c1 100644 --- a/gix-config/src/parse/mod.rs +++ b/gix-config/src/parse/mod.rs @@ -36,11 +36,9 @@ pub(crate) struct Span { /// Errors produced when a span cannot be represented. pub mod span { - /// A span offset or length exceeded the supported 32-bit representation. - pub type Error = gix_error::ValidationError; - pub(crate) fn error() -> Error { - Error::new(format!( + pub(crate) fn error() -> gix_error::ValidationError { + gix_error::ValidationError::new(format!( "configuration data exceeds the supported span size of {} bytes", u32::MAX )) @@ -79,11 +77,15 @@ impl MaybeDecoded { .map_or_else(|| self.raw.as_bstr_in(backing), |value| value.as_bstr()) } - pub(crate) fn rebase(&mut self, offset: usize) -> Result<(), span::Error> { + pub(crate) fn rebase(&mut self, offset: usize) -> Result<(), gix_error::ValidationError> { self.raw.rebase(offset) } - pub(crate) fn copy_to_backing_in(&self, source: &[u8], target: &mut Vec) -> Result { + pub(crate) fn copy_to_backing_in( + &self, + source: &[u8], + target: &mut Vec, + ) -> Result { Ok(Self { raw: self.raw.copy_to_backing_in(source, target)?, decoded: self.decoded.clone(), @@ -92,7 +94,7 @@ impl MaybeDecoded { } impl Span { - pub(crate) fn append(backing: &mut Vec, bytes: &[u8]) -> Result { + pub(crate) fn append(backing: &mut Vec, bytes: &[u8]) -> Result { let start = backing.len(); let span = Self::range(start, bytes.len())?; backing.len().checked_add(bytes.len()).ok_or_else(span::error)?; @@ -100,7 +102,7 @@ impl Span { Ok(span) } - pub(crate) fn range(start: usize, len: usize) -> Result { + pub(crate) fn range(start: usize, len: usize) -> Result { Ok(Span { start: start.try_into().map_err(|_| span::error())?, len: len.try_into().map_err(|_| span::error())?, @@ -137,11 +139,15 @@ impl Span { self.as_slice_in(backing).into() } - pub(crate) fn copy_to_backing_in(&self, source: &[u8], target: &mut Vec) -> Result { + pub(crate) fn copy_to_backing_in( + &self, + source: &[u8], + target: &mut Vec, + ) -> Result { Span::append(target, self.as_slice_in(source)) } - pub(crate) fn rebase(&mut self, offset: usize) -> Result<(), span::Error> { + pub(crate) fn rebase(&mut self, offset: usize) -> Result<(), gix_error::ValidationError> { self.start = (self.start as usize) .checked_add(offset) .and_then(|start| start.try_into().ok()) diff --git a/gix-config/src/parse/section/header.rs b/gix-config/src/parse/section/header.rs index 5b2e53dcbd5..4916ac5d45d 100644 --- a/gix-config/src/parse/section/header.rs +++ b/gix-config/src/parse/section/header.rs @@ -2,15 +2,12 @@ use bstr::{BStr, BString, ByteSlice}; use crate::parse::{Span, section::HeaderData}; -/// The error returned when creating a section header. -pub type Error = gix_error::ValidationError; - impl HeaderData { pub(crate) fn new_in( name: impl AsRef, subsection: impl Into>, backing: &mut Vec, - ) -> Result { + ) -> Result { let name = validated_name(name.as_ref().as_bytes().as_bstr())?; let name = Span::append(backing, &name)?; let (separator, subsection_name) = match subsection.into() { @@ -41,21 +38,21 @@ pub fn is_valid_subsection(name: impl crate::AsBStr) -> bool { name.as_bstr().find_byteset(b"\n\0").is_none() } -fn validated_subsection(name: &BStr) -> Result { - is_valid_subsection(name) - .then(|| name.into()) - .ok_or_else(|| Error::new_with_input("sub-section names must not contain newlines or null bytes", name)) +fn validated_subsection(name: &BStr) -> Result { + is_valid_subsection(name).then(|| name.into()).ok_or_else(|| { + gix_error::ValidationError::new_with_input("sub-section names must not contain newlines or null bytes", name) + }) } -fn validated_name(name: &BStr) -> Result { +fn validated_name(name: &BStr) -> Result { name.iter() .all(|b| b.is_ascii_alphanumeric() || *b == b'-') .then(|| name.into()) - .ok_or_else(|| Error::new_with_input("section names can only be ascii, '-'", name)) + .ok_or_else(|| gix_error::ValidationError::new_with_input("section names can only be ascii, '-'", name)) } impl HeaderData { - pub(crate) fn rebase(&mut self, offset: usize) -> Result<(), crate::parse::span::Error> { + pub(crate) fn rebase(&mut self, offset: usize) -> Result<(), gix_error::ValidationError> { self.name.rebase(offset)?; if let Some(separator) = &mut self.separator { separator.rebase(offset)?; @@ -70,7 +67,7 @@ impl HeaderData { &self, source: &[u8], target: &mut Vec, - ) -> Result { + ) -> Result { Ok(HeaderData { name: self.name.copy_to_backing_in(source, target)?, separator: self diff --git a/gix-config/src/parse/section/mod.rs b/gix-config/src/parse/section/mod.rs index a6c1e5e91e0..af29b8812b6 100644 --- a/gix-config/src/parse/section/mod.rs +++ b/gix-config/src/parse/section/mod.rs @@ -23,13 +23,7 @@ mod types { use bstr::ByteSlice; macro_rules! generate_case_insensitive { - ($name:ident, $module:ident, $err_doc:literal, $validate:ident, $cow_inner_type:ty, $comment:literal) => { - /// - pub mod $module { - /// The error returned when `TryFrom` is invoked to create an instance. - pub type Error = gix_error::ValidationError; - } - + ($name:ident, $err_doc:literal, $validate:ident, $cow_inner_type:ty, $comment:literal) => { #[doc = $comment] #[derive(Clone, Eq, Debug, Default)] pub struct $name(pub(crate) bstr::BString); @@ -80,7 +74,7 @@ mod types { } impl std::convert::TryFrom<&str> for $name { - type Error = $module::Error; + type Error = gix_error::ValidationError; fn try_from(s: &str) -> Result { Self::try_from(bstr::ByteSlice::as_bstr(s.as_bytes())) @@ -88,7 +82,7 @@ mod types { } impl std::convert::TryFrom for $name { - type Error = $module::Error; + type Error = gix_error::ValidationError; fn try_from(s: String) -> Result { Self::try_from(bstr::BString::from(s)) @@ -96,7 +90,7 @@ mod types { } impl std::convert::TryFrom for $name { - type Error = $module::Error; + type Error = gix_error::ValidationError; fn try_from(s: bstr::BString) -> Result { if $validate(s.as_slice().as_bstr()) { @@ -108,7 +102,7 @@ mod types { } impl std::convert::TryFrom<&bstr::BStr> for $name { - type Error = $module::Error; + type Error = gix_error::ValidationError; fn try_from(s: &bstr::BStr) -> Result { if $validate(s) { @@ -144,7 +138,6 @@ mod types { generate_case_insensitive!( Name, - name, "Valid names consist of alphanumeric characters or dashes.", is_valid_name, bstr::BStr, @@ -153,15 +146,14 @@ mod types { generate_case_insensitive!( ValueName, - value_name, "Valid value names consist of alphanumeric characters or dashes, starting with an alphabetic character.", is_valid_value_name, bstr::BStr, "Wrapper struct for value names, like `path` in `include.path`, since keys are case-insensitive." ); } +pub use types::Name; pub(crate) use types::ValueName; -pub use types::{Name, name, value_name}; #[cfg(test)] mod tests { diff --git a/gix-config/src/value/mod.rs b/gix-config/src/value/mod.rs index 56d02a94981..61a9857ad4d 100644 --- a/gix-config/src/value/mod.rs +++ b/gix-config/src/value/mod.rs @@ -1,4 +1,2 @@ -pub use gix_config_value::Error; - mod normalize; pub use normalize::normalize; diff --git a/gix-config/tests/config/file/init/from_paths/includes/unconditional.rs b/gix-config/tests/config/file/init/from_paths/includes/unconditional.rs index 7b909674024..1814c4fb199 100644 --- a/gix-config/tests/config/file/init/from_paths/includes/unconditional.rs +++ b/gix-config/tests/config/file/init/from_paths/includes/unconditional.rs @@ -2,7 +2,7 @@ use std::fs; use gix_config::{ File, - file::{includes, init, init::from_paths}, + file::{includes, init}, }; use gix_testtools::tempfile::tempdir; @@ -18,7 +18,7 @@ fn follow_options() -> init::Options<'static> { } } -fn assert_include_depth(err: from_paths::Error, max_depth: u8) { +fn assert_include_depth(err: gix_error::Exn, max_depth: u8) { let err = err .downcast_any_ref::() .expect("exceeding the include depth is a validation error"); diff --git a/gix-config/tests/config/parse/section.rs b/gix-config/tests/config/parse/section.rs index 844b2ee5ccc..d98a7cc2735 100644 --- a/gix-config/tests/config/parse/section.rs +++ b/gix-config/tests/config/parse/section.rs @@ -12,10 +12,7 @@ pub fn header_event(name: &'static str, subsection: impl Into Result { + fn serialized(name: &str, subsection: impl IntoBStringOpt) -> Result { let mut config = gix_config::File::default(); let section = config.new_section(name, subsection.into_bstring_opt())?; Ok(section.header().to_bstring()) diff --git a/gix-credentials/examples/custom-helper.rs b/gix-credentials/examples/custom-helper.rs index c148feeaacb..16c0f6b6ca7 100644 --- a/gix-credentials/examples/custom-helper.rs +++ b/gix-credentials/examples/custom-helper.rs @@ -2,7 +2,7 @@ use gix_credentials::{program, protocol}; use gix_error::ErrorExt; /// Run like this `echo url=https://example.com | cargo run --example custom-helper -- get` -pub fn main() -> Result<(), gix_credentials::program::main::Error> { +pub fn main() -> Result<(), gix_error::Exn> { gix_credentials::program::main( std::env::args_os().skip(1), std::io::stdin(), diff --git a/gix-credentials/examples/git-credential-lite.rs b/gix-credentials/examples/git-credential-lite.rs index 8c7c2fb2b7b..613743cc586 100644 --- a/gix-credentials/examples/git-credential-lite.rs +++ b/gix-credentials/examples/git-credential-lite.rs @@ -1,5 +1,5 @@ /// Run like this `echo url=https://example.com | cargo run --example git-credential-light -- fill` -pub fn main() -> Result<(), gix_credentials::program::main::Error> { +pub fn main() -> Result<(), gix_error::Exn> { gix_credentials::program::main( std::env::args_os().skip(1), std::io::stdin(), diff --git a/gix-credentials/src/helper/invoke.rs b/gix-credentials/src/helper/invoke.rs index f8d3790e016..ff41ec0cb86 100644 --- a/gix-credentials/src/helper/invoke.rs +++ b/gix-credentials/src/helper/invoke.rs @@ -2,7 +2,7 @@ use std::io::Read; use gix_error::{ErrorExt, ResultExt, RetryableError, message}; -use crate::helper::{Action, Context, Error, NextAction, Outcome, Result}; +use crate::helper::{Action, Context, NextAction, Outcome, Result}; impl Action { /// Send ourselves to the given `write` which is expected to be credentials-helper compatible @@ -44,7 +44,10 @@ pub fn invoke(helper: &mut crate::Program, action: &Action) -> Result { } } -pub(crate) fn raw(helper: &mut crate::Program, action: &Action) -> std::result::Result>, Error> { +pub(crate) fn raw( + helper: &mut crate::Program, + action: &Action, +) -> std::result::Result>, gix_error::Exn> { let communication_error = || message("An IO error occurred while communicating to the credentials helper"); let (mut stdin, stdout) = helper.start(action).or_raise_erased(communication_error)?; if let (Action::Get(_), None) = (&action, &stdout) { diff --git a/gix-credentials/src/helper/mod.rs b/gix-credentials/src/helper/mod.rs index f14a29df239..32e2ead5e59 100644 --- a/gix-credentials/src/helper/mod.rs +++ b/gix-credentials/src/helper/mod.rs @@ -54,10 +54,7 @@ impl Outcome { } /// The Result type used in [`invoke()`][crate::helper::invoke()]. -pub type Result = std::result::Result, Error>; - -/// The error used in the [credentials helper invocation][crate::helper::invoke()]. -pub type Error = gix_error::Exn; +pub type Result = std::result::Result, gix_error::Exn>; /// The action to perform by the credentials [helper][`crate::helper::invoke()`]. #[derive(Clone, Debug)] @@ -138,7 +135,7 @@ pub struct NextAction { } impl TryFrom<&NextAction> for Context { - type Error = protocol::context::decode::Error; + type Error = gix_error::ValidationError; fn try_from(value: &NextAction) -> std::result::Result { Context::from_bytes(value.previous_output.as_ref(), value.options) diff --git a/gix-credentials/src/program/main.rs b/gix-credentials/src/program/main.rs index 359f77e9d87..d3b625f9db5 100644 --- a/gix-credentials/src/program/main.rs +++ b/gix-credentials/src/program/main.rs @@ -42,16 +42,13 @@ impl Action { } } -/// The error of [`main()`][crate::program::main()]. -pub type Error = gix_error::Exn; - pub(crate) mod function { use std::ffi::OsString; use gix_error::{ErrorExt, ResultExt, ValidationError}; use crate::{ - program::main::{Action, Error}, + program::main::Action, protocol::{Context, ContextOptions}, }; @@ -69,7 +66,7 @@ pub(crate) mod function { stdout: impl std::io::Write, options: ContextOptions, credentials: CredentialsFn, - ) -> Result<(), Error> + ) -> Result<(), gix_error::Exn> where CredentialsFn: FnOnce(Action, Context) -> Result, gix_error::Exn>, { diff --git a/gix-credentials/src/protocol/context/mod.rs b/gix-credentials/src/protocol/context/mod.rs index 72651a0a44f..e076acfcd5c 100644 --- a/gix-credentials/src/protocol/context/mod.rs +++ b/gix-credentials/src/protocol/context/mod.rs @@ -2,9 +2,6 @@ use bstr::BString; use crate::protocol::{Context, ContextOptions}; -/// Indicates key or values contain errors that can't be encoded. -pub type Error = gix_error::ValidationError; - impl Context { /// Create a context containing `url`, encoded and decoded according to `options`. pub fn from_url(url: impl Into, options: ContextOptions) -> Self { @@ -96,14 +93,14 @@ mod mutate { use bstr::ByteSlice; use gix_error::{OptionExt, ResultExt, ValidationError}; - use crate::{protocol, protocol::Context}; + use crate::protocol::Context; /// In-place mutation impl Context { /// Destructure the url at our `url` field into parts like protocol, host, username and path and store /// them in our respective fields. If `use_http_path` is set, http paths are significant even though /// normally this isn't the case. - pub fn destructure_url_in_place(&mut self, use_http_path: bool) -> Result<&mut Self, protocol::Error> { + pub fn destructure_url_in_place(&mut self, use_http_path: bool) -> Result<&mut Self, gix_error::Exn> { if self.url.is_none() { self.url = Some(self.to_url().ok_or_raise_erased(|| { ValidationError::new("Either 'url' field or both 'protocol' and 'host' fields must be provided") diff --git a/gix-credentials/src/protocol/context/serde.rs b/gix-credentials/src/protocol/context/serde.rs index d42e7b794ce..494c897645a 100644 --- a/gix-credentials/src/protocol/context/serde.rs +++ b/gix-credentials/src/protocol/context/serde.rs @@ -1,7 +1,5 @@ use bstr::BStr; -use crate::protocol::context::Error; - mod write { use bstr::{BStr, BString}; @@ -80,13 +78,10 @@ pub mod decode { use crate::protocol::{Context, ContextOptions, context::serde::validate}; - /// The error returned by [`from_bytes()`][Context::from_bytes()]. - pub type Error = ValidationError; - impl Context { /// Decode ourselves from `input` which is the format written by [`write_to()`][Self::write_to()]. /// `options` control what to support during deserialization. - pub fn from_bytes(input: &[u8], options: ContextOptions) -> Result { + pub fn from_bytes(input: &[u8], options: ContextOptions) -> Result { let mut ctx = Context { options, ..Context::default() @@ -157,7 +152,7 @@ pub mod decode { } } -fn validate(key: &str, value: &BStr, protect_protocol: bool) -> Result<(), Error> { +fn validate(key: &str, value: &BStr, protect_protocol: bool) -> Result<(), gix_error::ValidationError> { if key.contains('\0') || key.contains('\n') || key.contains('\r') diff --git a/gix-credentials/src/protocol/mod.rs b/gix-credentials/src/protocol/mod.rs index 9fecf0e4a9c..397ea574c44 100644 --- a/gix-credentials/src/protocol/mod.rs +++ b/gix-credentials/src/protocol/mod.rs @@ -13,10 +13,7 @@ pub struct Outcome { } /// The Result type used in credentials top-level functions to obtain a complete identity. -pub type Result = std::result::Result, Error>; - -/// The error returned top-level credential functions. -pub type Error = gix_error::Exn; +pub type Result = std::result::Result, gix_error::Exn>; /// Additional context to be passed to the credentials helper. #[derive(Debug, Default, Clone, Eq, PartialEq)] @@ -85,7 +82,7 @@ pub fn helper_outcome_to_result(outcome: Option, action: helper } } -fn identity_missing(context: Context) -> Error { +fn identity_missing(context: Context) -> gix_error::Exn { let mut buf = Vec::new(); // Invalid protocol values must not prevent reporting the missing identity. context.redacted().write_to(&mut buf).ok(); diff --git a/gix-date/src/lib.rs b/gix-date/src/lib.rs index d61a94dd1ab..22de767aa63 100644 --- a/gix-date/src/lib.rs +++ b/gix-date/src/lib.rs @@ -34,8 +34,6 @@ pub mod parse; pub use jiff::Zoned; pub use parse::function::{parse, parse_header}; -pub use gix_error::ValidationError as Error; - /// A timestamp with timezone. #[derive(Default, PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] diff --git a/gix-date/src/parse/function.rs b/gix-date/src/parse/function.rs index bf803adf533..c4dd8a56f3d 100644 --- a/gix-date/src/parse/function.rs +++ b/gix-date/src/parse/function.rs @@ -5,7 +5,7 @@ use jiff::{Zoned, civil::Date, fmt::rfc2822, tz::TimeZone}; use crate::parse::git::parse_git_date_format; use crate::parse::raw::parse_raw; use crate::{ - Error, OffsetInSeconds, SecondsSinceUnixEpoch, Time, + OffsetInSeconds, SecondsSinceUnixEpoch, Time, parse::relative, time::format::{DEFAULT, GITOXIDE, ISO8601, ISO8601_STRICT, SHORT}, }; @@ -101,7 +101,7 @@ const MAX_OFFSET_IN_SECONDS: i32 = 23 * 3600 + 59 * 60; /// /// In any of these formats, a timezone offset wider than `±23:59` is not a timezone to Git, so it /// is not accepted here either. -pub fn parse(input: &str, now: Option) -> Result> { +pub fn parse(input: &str, now: Option) -> Result> { // A leading `@` explicitly names epoch seconds, including small and negative values. if let Some(rest) = input.strip_prefix('@') { if let Some(val) = parse_raw(rest) { @@ -114,7 +114,7 @@ pub fn parse(input: &str, now: Option) -> Result> { let time = if let Ok(val) = Date::strptime(SHORT.0, input) { let val = val .to_zoned(TimeZone::UTC) - .or_raise(|| Error::new_with_input("Timezone conversion failed", input))?; + .or_raise(|| gix_error::ValidationError::new_with_input("Timezone conversion failed", input))?; Time::new(val.timestamp().as_second(), val.offset().seconds()) } else if let Ok(val) = rfc2822_relaxed(input) { Time::new(val.timestamp().as_second(), val.offset().seconds()) @@ -140,12 +140,12 @@ pub fn parse(input: &str, now: Option) -> Result> { // Format::Raw val } else { - return Err(Error::new_with_input("Unknown date format", input))?; + return Err(gix_error::ValidationError::new_with_input("Unknown date format", input))?; }; // Jiff parses textual offsets up to 25:59:59, beyond Git's accepted range. if time.offset.abs() > MAX_OFFSET_IN_SECONDS { - Err(Error::new_with_input("Unknown date format", input))?; + Err(gix_error::ValidationError::new_with_input("Unknown date format", input))?; } Ok(time) } diff --git a/gix-date/src/parse/mod.rs b/gix-date/src/parse/mod.rs index ee40ae27ade..3be9c0a5b64 100644 --- a/gix-date/src/parse/mod.rs +++ b/gix-date/src/parse/mod.rs @@ -1,7 +1,7 @@ use std::io; use std::str::FromStr; -use crate::{Error, Time}; +use crate::Time; /// A container for just enough bytes to hold the largest possible serialization of a [`Time`]. #[derive(Default, Clone)] @@ -64,10 +64,10 @@ impl Time { } impl FromStr for Time { - type Err = Error; + type Err = gix_error::ValidationError; fn from_str(s: &str) -> Result { - crate::parse_header(s).ok_or_else(|| Error::new_with_input("invalid time", s)) + crate::parse_header(s).ok_or_else(|| gix_error::ValidationError::new_with_input("invalid time", s)) } } diff --git a/gix-date/src/parse/relative.rs b/gix-date/src/parse/relative.rs index 272bff7546a..8abd0d5f812 100644 --- a/gix-date/src/parse/relative.rs +++ b/gix-date/src/parse/relative.rs @@ -1,10 +1,9 @@ use std::str::FromStr; -use crate::Error; use gix_error::{Exn, ResultExt, ValidationError}; use jiff::{SignedDuration, Zoned, civil, tz::TimeZone}; -pub fn parse(input: &str, now: Option) -> Option>> { +pub fn parse(input: &str, now: Option) -> Option>> { // First try named dates if let Some(result) = parse_named(input, now.as_ref()) { return Some(result); @@ -15,7 +14,7 @@ pub fn parse(input: &str, now: Option) -> Option } /// Parse named relative dates like "now", "today", "yesterday". -fn parse_named(input: &str, now: Option<&Zoned>) -> Option>> { +fn parse_named(input: &str, now: Option<&Zoned>) -> Option>> { let input = input.trim(); let duration = if input.eq_ignore_ascii_case("now") { SignedDuration::ZERO @@ -140,7 +139,7 @@ fn unit(period: &str, ago: bool) -> Option<(&str, Unit)> { /// Seconds-based units subtract from the timestamp, while months and years only step down the /// respective fields and normalize later, so that repeated units accumulate and a day beyond /// the end of the target month rolls over. -fn subtract_pairs(now: Option, pairs: &[Pair<'_>]) -> Result> { +fn subtract_pairs(now: Option, pairs: &[Pair<'_>]) -> Result> { /// The calendar and clock fields for subtraction. struct Fields { year: i16, @@ -165,23 +164,24 @@ fn subtract_pairs(now: Option, pairs: &[Pair<'_>]) -> Result Result> { - let first_of_month = civil::Date::new(self.year, self.month, 1) - .or_raise(|| Error::new(format!("Date lies out of range: {}-{:02}", self.year, self.month)))?; + fn normalize(&self) -> Result> { + let first_of_month = civil::Date::new(self.year, self.month, 1).or_raise(|| { + gix_error::ValidationError::new(format!("Date lies out of range: {}-{:02}", self.year, self.month)) + })?; let days_beyond_first = SignedDuration::from_secs((i64::from(self.day) - 1) * 24 * 60 * 60); first_of_month .checked_add(days_beyond_first) - .or_raise(|| Error::new(format!("Day {} lies out of range", self.day)))? + .or_raise(|| gix_error::ValidationError::new(format!("Day {} lies out of range", self.day)))? .to_datetime(self.time) .to_zoned(self.timezone.clone()) - .or_raise(|| Error::new("Could not convert date to a point in time")) + .or_raise(|| gix_error::ValidationError::new("Could not convert date to a point in time")) } } let now = now.ok_or(ValidationError::new("Missing current time"))?; let mut fields = Fields::from(now); for Pair { period, count, unit } in pairs { - let err = || Error::new(format!("Couldn't parse span from '{period} {count}'")); + let err = || gix_error::ValidationError::new(format!("Couldn't parse span from '{period} {count}'")); match unit { Unit::Seconds(factor) => { let seconds = count @@ -210,5 +210,5 @@ fn subtract_duration(now: Option<&Zoned>, duration: SignedDuration) -> Result Result<(), Exn> { +fn raw() -> Result<(), Exn> { assert_eq!( gix_date::parse("1660874655 +0800", None)?, Time { diff --git a/gix-diff/src/blob/pipeline.rs b/gix-diff/src/blob/pipeline.rs index 135d8b9602d..d2665fa08cf 100644 --- a/gix-diff/src/blob/pipeline.rs +++ b/gix-diff/src/blob/pipeline.rs @@ -123,12 +123,6 @@ impl Mode { } } -/// -pub mod convert_to_diffable { - /// The error returned by [Pipeline::convert_to_diffable()](super::Pipeline::convert_to_diffable()). - pub type Error = gix_error::Exn; -} - /// Lifecycle impl Pipeline { /// Create a new instance of a pipeline which produces blobs suitable for diffing. `roots` allow to read worktree files directly, otherwise @@ -203,7 +197,7 @@ impl Pipeline { objects: &dyn gix_object::FindObjectOrHeader, convert: Mode, out: &mut Vec, - ) -> Result { + ) -> Result> { let is_symlink = match mode { EntryKind::Link => true, EntryKind::Blob | EntryKind::BlobExecutable => false, @@ -506,7 +500,7 @@ fn none_if_missing(res: std::io::Result) -> std::io::Result> { } } -fn run_cmd(rela_path: &BStr, mut cmd: Command, out: &mut Vec) -> Result<(), convert_to_diffable::Error> { +fn run_cmd(rela_path: &BStr, mut cmd: Command, out: &mut Vec) -> Result<(), gix_error::Exn> { gix_trace::debug!(cmd = ?cmd, "Running binary-to-text command"); let mut res = cmd .output() diff --git a/gix-diff/src/blob/platform.rs b/gix-diff/src/blob/platform.rs index 4fb67f87305..90b6c4e4ab7 100644 --- a/gix-diff/src/blob/platform.rs +++ b/gix-diff/src/blob/platform.rs @@ -239,12 +239,6 @@ pub mod resource { } } -/// -pub mod set_resource { - /// The error returned by [Platform::set_resource](super::Platform::set_resource). - pub type Error = gix_error::Exn; -} - /// pub mod prepare_diff { use bstr::BStr; @@ -311,18 +305,12 @@ pub mod prepare_diff { ) } } - - /// The error returned by [Platform::prepare_diff()](super::Platform::prepare_diff()). - pub type Error = gix_error::ValidationError; } /// pub mod prepare_diff_command { use std::ops::{Deref, DerefMut}; - /// The error returned by [Platform::prepare_diff_command()](super::Platform::prepare_diff_command()). - pub type Error = gix_error::Exn; - /// The outcome of a [`prepare_diff_command`](super::Platform::prepare_diff_command()) operation. /// /// This type acts like [`std::process::Command`], ready to run, with `stdin`, `stdout` and `stderr` set to *inherit* @@ -416,7 +404,7 @@ impl Platform { rela_path: &BStr, kind: ResourceKind, objects: &impl gix_object::FindObjectOrHeader, // TODO: make this `dyn` once https://github.com/rust-lang/rust/issues/65991 is stable, then also make tracker.rs `objects` dyn - ) -> Result<(), set_resource::Error> { + ) -> Result<(), gix_error::Exn> { let res = self.set_resource_inner(id, mode, rela_path, kind, objects); if res.is_err() { *match kind { @@ -447,11 +435,12 @@ impl Platform { context: gix_command::Context, count: usize, total: usize, - ) -> Result { + ) -> Result> { fn add_resource( cmd: &mut std::process::Command, res: Resource<'_>, - ) -> Result>, prepare_diff_command::Error> { + ) -> Result>, gix_error::Exn> + { let tmpfile = match res.data { resource::Data::Missing => { cmd.args(["/dev/null", ".", "."]); @@ -562,7 +551,7 @@ impl Platform { /// /// The returned outcome allows to easily perform diff operations, based on the [`prepare_diff::Outcome::operation`] field, /// which hints at what should be done. - pub fn prepare_diff(&mut self) -> Result, prepare_diff::Error> { + pub fn prepare_diff(&mut self) -> Result, gix_error::ValidationError> { let old_key = &self.old.as_ref().ok_or_else(|| { ValidationError::new("Either the source or the destination of the diff operation were not set") })?; @@ -672,7 +661,7 @@ impl Platform { rela_path: &BStr, kind: ResourceKind, objects: &impl gix_object::FindObjectOrHeader, - ) -> Result<(), set_resource::Error> { + ) -> Result<(), gix_error::Exn> { if matches!( mode, gix_object::tree::EntryKind::Commit | gix_object::tree::EntryKind::Tree diff --git a/gix-diff/src/index/function.rs b/gix-diff/src/index/function.rs index df364eb3225..2164338cf6a 100644 --- a/gix-diff/src/index/function.rs +++ b/gix-diff/src/index/function.rs @@ -4,7 +4,7 @@ use bstr::BStr; use gix_error::{ErrorExt, message}; use gix_filter::attributes::glob::pattern::Case; -use super::{Action, ChangeRef, Error, RewriteOptions}; +use super::{Action, ChangeRef, RewriteOptions}; use crate::rewrites; /// Produce an entry-by-entry diff between `lhs` and `rhs`, sending changes to `cb(change) -> Action` for consumption, @@ -34,7 +34,7 @@ pub fn diff<'rhs, 'lhs: 'rhs, Find>( rewrite_options: Option>, pathspec: &mut gix_pathspec::Search, pathspec_attributes: &mut dyn FnMut(&BStr, Case, bool, &mut gix_attributes::search::Outcome) -> bool, -) -> Result, Error> +) -> Result, gix_error::Exn> where Find: gix_object::FindObjectOrHeader, { @@ -226,7 +226,7 @@ fn emit_deletion<'rhs, 'lhs: 'rhs>( (idx, path, entry): (usize, &'lhs BStr, &'lhs gix_index::Entry), mut cb: impl FnMut(ChangeRef<'lhs, 'rhs>) -> Result, tracker: Option<&mut rewrites::Tracker>>, -) -> Result { +) -> Result> { let change = ChangeRef::Deletion { location: Cow::Borrowed(path), index: idx, @@ -249,7 +249,7 @@ fn emit_addition<'rhs, 'lhs: 'rhs>( (idx, path, entry): (usize, &'rhs BStr, &'rhs gix_index::Entry), mut cb: impl FnMut(ChangeRef<'lhs, 'rhs>) -> Result, tracker: Option<&mut rewrites::Tracker>>, -) -> Result { +) -> Result> { if ignore_unmerged_and_intent_to_add((idx, path, entry)) { return Ok(std::ops::ControlFlow::Continue(())); } @@ -272,7 +272,7 @@ fn emit_addition<'rhs, 'lhs: 'rhs>( cb(change).map_err(callback_error) } -fn callback_error(err: gix_error::Exn) -> Error { +fn callback_error(err: gix_error::Exn) -> gix_error::Exn { err.raise(message("The callback indicated failure")) } diff --git a/gix-diff/src/index/mod.rs b/gix-diff/src/index/mod.rs index 2028175787b..d7e621e19a2 100644 --- a/gix-diff/src/index/mod.rs +++ b/gix-diff/src/index/mod.rs @@ -2,9 +2,6 @@ use std::borrow::Cow; use bstr::BStr; -/// The error returned by [`index()`](crate::index()). -pub type Error = gix_error::Exn; - /// What to do after a [ChangeRef] was passed ot the callback of [`index()`](crate::index()). /// /// Use [`std::ops::ControlFlow::Continue`] to continue the operation. diff --git a/gix-diff/src/rewrites/tracker.rs b/gix-diff/src/rewrites/tracker.rs index e7091bd36e5..aa14ae4b17f 100644 --- a/gix-diff/src/rewrites/tracker.rs +++ b/gix-diff/src/rewrites/tracker.rs @@ -134,12 +134,6 @@ pub mod visit { } } -/// -pub mod emit { - /// The error returned by [Tracker::emit()](super::Tracker::emit()). - pub type Error = gix_error::Exn; -} - /// Lifecycle impl Tracker { /// Create a new instance with `rewrites` configuration. @@ -210,7 +204,7 @@ impl Tracker { diff_cache: &mut crate::blob::Platform, objects: &impl gix_object::FindObjectOrHeader, mut push_source_tree: PushSourceTreeFn, - ) -> Result + ) -> Result> where PushSourceTreeFn: FnMut(&mut dyn FnMut(T, &BStr)) -> Result<(), E>, E: std::error::Error + Send + Sync + 'static, @@ -367,7 +361,7 @@ impl Tracker { diff_cache: &mut crate::blob::Platform, objects: &impl gix_object::FindObjectOrHeader, filter: Option bool>, - ) -> Result<(), emit::Error> { + ) -> Result<(), gix_error::Exn> { // we try to cheaply reduce the set of possibilities first, before possibly looking more exhaustively. let needs_second_pass = !needs_exact_match(percentage); @@ -418,7 +412,7 @@ impl Tracker { diff_cache: &mut crate::blob::Platform, objects: &impl gix_object::FindObjectOrHeader, filter: Option bool>, - ) -> Result { + ) -> Result> { let mut dest_ofs = 0; let mut num_checks = 0; let max_checks = { @@ -529,7 +523,7 @@ impl Tracker { kind: visit::SourceKind, src_parent_id: ChangeId, dst_parent_id: ChangeId, - ) -> Result { + ) -> Result> { debug_assert_ne!( src_parent_id, dst_parent_id, "src and destination directories must be distinct" @@ -592,7 +586,7 @@ impl Tracker { fn match_renamed_directories( &mut self, cb: &mut impl FnMut(visit::Destination<'_, T>, Option>) -> Action, - ) -> Result<(), emit::Error> { + ) -> Result<(), gix_error::Exn> { fn unemitted_directory_matching_relation_id(items: &[Item], child_id: ChangeId) -> Option { items.iter().position(|i| { !i.emitted && matches!(i.change.relation(), Some(Relation::Parent(pid)) if pid == child_id) @@ -698,7 +692,7 @@ fn find_match<'a, T: Change>( diff_cache: &mut crate::blob::Platform, path_backing: &[u8], num_checks: &mut usize, -) -> Result>, emit::Error> { +) -> Result>, gix_error::Exn> { let (item_id, item_mode) = item.change.id_and_entry_mode(); // Symlinks and gitlinks only participate in exact-ID matching; neither has meaningful blob similarity here. if needs_exact_match(percentage) || item_mode.is_link() || item_mode.is_commit() { diff --git a/gix-diff/src/tree/mod.rs b/gix-diff/src/tree/mod.rs index 865849c801d..51143133873 100644 --- a/gix-diff/src/tree/mod.rs +++ b/gix-diff/src/tree/mod.rs @@ -41,8 +41,8 @@ where } } -impl From for Error { - fn from(err: gix_object::decode::Error) -> Self { +impl From for Error { + fn from(err: gix_error::ValidationError) -> Self { Error::Failure(gix_error::Error::from_error(err)) } } diff --git a/gix-diff/src/tree_with_rewrites/change.rs b/gix-diff/src/tree_with_rewrites/change.rs index b613e203942..ea103715012 100644 --- a/gix-diff/src/tree_with_rewrites/change.rs +++ b/gix-diff/src/tree_with_rewrites/change.rs @@ -341,7 +341,7 @@ impl crate::blob::Platform { &mut self, change: ChangeRef<'_>, objects: &impl gix_object::FindObjectOrHeader, - ) -> Result<&mut Self, crate::blob::platform::set_resource::Error> { + ) -> Result<&mut Self, gix_error::Exn> { match change { ChangeRef::Addition { location, diff --git a/gix-diff/tests/diff/blob/platform.rs b/gix-diff/tests/diff/blob/platform.rs index 0d7775bcb84..1e9733782cc 100644 --- a/gix-diff/tests/diff/blob/platform.rs +++ b/gix-diff/tests/diff/blob/platform.rs @@ -179,10 +179,7 @@ fn resources_of_worktree_and_odb_and_check_link() -> crate::Result { } fn comparable_ext_diff( - cmd: Result< - gix_diff::blob::platform::prepare_diff_command::Command, - gix_diff::blob::platform::prepare_diff_command::Error, - >, + cmd: Result>, ) -> String { let cmd = cmd.expect("no error"); let command = format!("{:?}", *cmd); diff --git a/gix-diff/tests/diff/main.rs b/gix-diff/tests/diff/main.rs index 89b7b4a2a48..37466d9396d 100644 --- a/gix-diff/tests/diff/main.rs +++ b/gix-diff/tests/diff/main.rs @@ -74,7 +74,7 @@ mod tree; mod tree_with_rewrites; mod util { - use gix_object::{Write, find::Error}; + use gix_object::Write; pub type ObjectDb = gix_odb::memory::Proxy; @@ -83,7 +83,7 @@ mod util { } /// Insert `data` and return its hash. That can be used to find it again. - pub fn insert(db: &ObjectDb, data: &str) -> Result { + pub fn insert(db: &ObjectDb, data: &str) -> Result { db.write_buf(gix_object::Kind::Blob, data.as_bytes()) } } diff --git a/gix-dir/src/walk/classify.rs b/gix-dir/src/walk/classify.rs index e25c9fb145f..d47657edb83 100644 --- a/gix-dir/src/walk/classify.rs +++ b/gix-dir/src/walk/classify.rs @@ -9,7 +9,7 @@ use gix_error::{ResultExt, message}; use crate::{ Entry, EntryRef, entry, entry::PathspecMatch, - walk::{Context, Error, ForDeletionMode, Options}, + walk::{Context, ForDeletionMode, Options}, }; /// Classify the `worktree_relative_root` path and return the first `PathKind` that indicates that @@ -21,7 +21,7 @@ pub fn root( worktree_relative_root: &Path, options: Options<'_>, ctx: &mut Context<'_>, -) -> Result<(Outcome, bool), Error> { +) -> Result<(Outcome, bool), gix_error::Exn> { buf.clear(); let mut last_length = None; let mut path_buf = worktree_root.to_owned(); @@ -147,7 +147,7 @@ pub fn path( .. }: Options<'_>, ctx: &mut Context<'_>, -) -> Result { +) -> Result { let mut out = Outcome { status: entry::Status::Pruned, property: None, diff --git a/gix-dir/src/walk/function.rs b/gix-dir/src/walk/function.rs index 6a9d1290c09..6a1f701001e 100644 --- a/gix-dir/src/walk/function.rs +++ b/gix-dir/src/walk/function.rs @@ -8,7 +8,7 @@ use gix_error::{ErrorExt, OptionExt, ResultExt, ValidationError}; use crate::{ EntryRef, entry, - walk::{Action, Context, Delegate, Error, ForDeletionMode, Options, Outcome, classify, readdir}, + walk::{Action, Context, Delegate, ForDeletionMode, Options, Outcome, classify, readdir}, }; /// A function to perform a git-style, unsorted, directory walk. @@ -49,7 +49,7 @@ pub fn walk( mut ctx: Context<'_>, options: Options<'_>, delegate: &mut dyn Delegate, -) -> Result<(Outcome, PathBuf), Error> { +) -> Result<(Outcome, PathBuf), gix_error::Exn> { let root = match ctx.explicit_traversal_root { Some(root) => root.to_owned(), None => ctx @@ -135,7 +135,7 @@ pub fn walk( fn assure_no_symlink_in_root<'root>( worktree_root: &Path, root: &'root Path, -) -> Result<(PathBuf, Cow<'root, Path>), Error> { +) -> Result<(PathBuf, Cow<'root, Path>), gix_error::Exn> { let mut current = worktree_root.to_owned(); let worktree_relative = root .strip_prefix(worktree_root) diff --git a/gix-dir/src/walk/mod.rs b/gix-dir/src/walk/mod.rs index 044df22d59c..1529ba8ddf3 100644 --- a/gix-dir/src/walk/mod.rs +++ b/gix-dir/src/walk/mod.rs @@ -273,9 +273,6 @@ pub struct Outcome { pub seen_entries: u32, } -/// The error returned by [`walk()`](function::walk()). -pub type Error = gix_error::Exn; - mod classify; pub(crate) mod function; mod readdir; diff --git a/gix-dir/src/walk/readdir.rs b/gix-dir/src/walk/readdir.rs index 7fa828f05f3..976f3d600bd 100644 --- a/gix-dir/src/walk/readdir.rs +++ b/gix-dir/src/walk/readdir.rs @@ -14,7 +14,7 @@ use crate::{ walk::{ Action, CollapsedEntriesEmissionMode, Context, Delegate, EmissionMode::CollapseDirectory, - Error, ForDeletionMode, Options, Outcome, classify, + ForDeletionMode, Options, Outcome, classify, function::{can_recurse, emit_entry}, }, }; @@ -33,7 +33,7 @@ pub(super) fn recursive( delegate: &mut dyn Delegate, out: &mut Outcome, state: &mut State, -) -> Result<(Action, bool), Error> { +) -> Result<(Action, bool), gix_error::Exn> { if ctx.should_interrupt.is_some_and(|flag| flag.load(Ordering::Relaxed)) { return Err(message("Interrupted").raise_erased()); } diff --git a/gix-dir/tests/walk_utils/mod.rs b/gix-dir/tests/walk_utils/mod.rs index 54f8b92b92f..024618702c7 100644 --- a/gix-dir/tests/walk_utils/mod.rs +++ b/gix-dir/tests/walk_utils/mod.rs @@ -183,7 +183,7 @@ impl EntryExt for (Entry, Option) { pub fn collect( worktree_root: &Path, root: Option<&Path>, - cb: impl FnOnce(&mut dyn walk::Delegate, walk::Context) -> Result<(walk::Outcome, PathBuf), walk::Error>, + cb: impl FnOnce(&mut dyn walk::Delegate, walk::Context) -> Result<(walk::Outcome, PathBuf), gix_error::Exn>, ) -> ((walk::Outcome, PathBuf), Entries) { try_collect(worktree_root, root, cb).unwrap() } @@ -191,7 +191,7 @@ pub fn collect( pub fn collect_filtered( worktree_root: &Path, root: Option<&Path>, - cb: impl FnOnce(&mut dyn walk::Delegate, walk::Context) -> Result<(walk::Outcome, PathBuf), walk::Error>, + cb: impl FnOnce(&mut dyn walk::Delegate, walk::Context) -> Result<(walk::Outcome, PathBuf), gix_error::Exn>, patterns: impl IntoIterator>, ) -> ((walk::Outcome, PathBuf), Entries) { try_collect_filtered(worktree_root, root, cb, patterns).unwrap() @@ -200,7 +200,7 @@ pub fn collect_filtered( pub fn try_collect( worktree_root: &Path, root: Option<&Path>, - cb: impl FnOnce(&mut dyn walk::Delegate, walk::Context) -> Result<(walk::Outcome, PathBuf), walk::Error>, + cb: impl FnOnce(&mut dyn walk::Delegate, walk::Context) -> Result<(walk::Outcome, PathBuf), gix_error::Exn>, ) -> Result<((walk::Outcome, PathBuf), Entries), gix_error::Error> { try_collect_filtered(worktree_root, root, cb, None::<&str>) } @@ -208,7 +208,7 @@ pub fn try_collect( pub fn try_collect_filtered( worktree_root: &Path, root: Option<&Path>, - cb: impl FnOnce(&mut dyn walk::Delegate, walk::Context) -> Result<(walk::Outcome, PathBuf), walk::Error>, + cb: impl FnOnce(&mut dyn walk::Delegate, walk::Context) -> Result<(walk::Outcome, PathBuf), gix_error::Exn>, patterns: impl IntoIterator>, ) -> Result<((walk::Outcome, PathBuf), Entries), gix_error::Error> { try_collect_filtered_opts_collect(worktree_root, root, cb, patterns, Default::default()) @@ -217,7 +217,7 @@ pub fn try_collect_filtered( pub fn try_collect_filtered_opts_collect( worktree_root: &Path, root: Option<&Path>, - cb: impl FnOnce(&mut dyn walk::Delegate, walk::Context) -> Result<(walk::Outcome, PathBuf), walk::Error>, + cb: impl FnOnce(&mut dyn walk::Delegate, walk::Context) -> Result<(walk::Outcome, PathBuf), gix_error::Exn>, patterns: impl IntoIterator>, options: Options<'_>, ) -> Result<((walk::Outcome, PathBuf), Entries), gix_error::Error> { @@ -230,7 +230,7 @@ pub fn try_collect_filtered_opts_collect_with_root( worktree_root: &Path, root: Option<&Path>, explicit_traversal_root: Option<&Path>, - cb: impl FnOnce(&mut dyn walk::Delegate, walk::Context) -> Result<(walk::Outcome, PathBuf), walk::Error>, + cb: impl FnOnce(&mut dyn walk::Delegate, walk::Context) -> Result<(walk::Outcome, PathBuf), gix_error::Exn>, patterns: impl IntoIterator>, options: Options<'_>, ) -> Result<((walk::Outcome, PathBuf), Entries), gix_error::Error> { @@ -252,7 +252,7 @@ pub fn collect_filtered_with_cwd( worktree_root: &Path, root: Option<&Path>, cwd_suffix: Option<&str>, - cb: impl FnOnce(&mut dyn walk::Delegate, walk::Context) -> Result<(walk::Outcome, PathBuf), walk::Error>, + cb: impl FnOnce(&mut dyn walk::Delegate, walk::Context) -> Result<(walk::Outcome, PathBuf), gix_error::Exn>, patterns: impl IntoIterator>, ) -> ((walk::Outcome, PathBuf), Entries) { let mut dlg = gix_dir::walk::delegate::Collect::default(); @@ -276,7 +276,7 @@ pub fn try_collect_filtered_opts( root: Option<&Path>, explicit_traversal_root: Option<&Path>, append_to_cwd: Option<&str>, - cb: impl FnOnce(&mut dyn walk::Delegate, walk::Context) -> Result<(walk::Outcome, PathBuf), walk::Error>, + cb: impl FnOnce(&mut dyn walk::Delegate, walk::Context) -> Result<(walk::Outcome, PathBuf), gix_error::Exn>, patterns: impl IntoIterator>, delegate: &mut dyn gix_dir::walk::Delegate, Options { diff --git a/gix-discover/src/is.rs b/gix-discover/src/is.rs index f16330716ad..cc3da0c6afa 100644 --- a/gix-discover/src/is.rs +++ b/gix-discover/src/is.rs @@ -30,7 +30,7 @@ pub fn submodule_git_dir(git_dir: &Path) -> bool { /// * …a refs directory /// /// This obtains filesystem metadata for `git_dir` before checking its repository layout. -pub fn git(git_dir: &Path) -> Result { +pub fn git(git_dir: &Path) -> Result { let git_dir_metadata = git_dir .metadata() .or_raise_erased(|| gix_error::message!("Could not retrieve metadata of \"{}\"", git_dir.display()))?; @@ -44,7 +44,7 @@ pub(crate) fn git_with_metadata( git_dir: &Path, git_dir_metadata: &std::fs::Metadata, cwd: &Path, -) -> Result { +) -> Result { #[derive(Eq, PartialEq)] enum Kind { MaybeRepo, diff --git a/gix-discover/src/lib.rs b/gix-discover/src/lib.rs index 9ece748da6d..bf1d19acea1 100644 --- a/gix-discover/src/lib.rs +++ b/gix-discover/src/lib.rs @@ -18,7 +18,7 @@ //! # let nested = dir.path().join("src").join("module"); //! # std::fs::create_dir_all(&nested)?; //! let (path, _trust) = -//! gix_discover::upwards(&nested).map_err(gix_discover::upwards::Error::into_error)?; +//! gix_discover::upwards(&nested).map_err(gix_error::Exn::into_error)?; //! let (repository_dir, worktree_dir) = path.into_repository_and_work_tree_directories(); //! //! assert_eq!(repository_dir, git_dir); @@ -38,12 +38,6 @@ pub const MODULES: &str = "modules"; /// pub mod repository; -/// -pub mod is_git { - /// The error returned by [`crate::is_git()`]. - pub type Error = gix_error::Exn; -} - mod is; #[expect( deprecated, diff --git a/gix-discover/src/parse.rs b/gix-discover/src/parse.rs index c2f635fdb6d..bb8b007206d 100644 --- a/gix-discover/src/parse.rs +++ b/gix-discover/src/parse.rs @@ -2,26 +2,20 @@ use std::path::PathBuf; use bstr::ByteSlice; -/// -pub mod gitdir { - /// The error returned by [`parse::gitdir()`][super::gitdir()]. - pub type Error = gix_error::ValidationError; -} - /// Parse typical `gitdir` files as seen in worktrees and submodules. -pub fn gitdir(input: &[u8]) -> Result { +pub fn gitdir(input: &[u8]) -> Result { let path = input .strip_prefix(b"gitdir: ") - .ok_or_else(|| gitdir::Error::new_with_input("Format should be 'gitdir: ', but got", input))? + .ok_or_else(|| gix_error::ValidationError::new_with_input("Format should be 'gitdir: ', but got", input))? .as_bstr(); let path = path.trim_end().as_bstr(); if path.is_empty() { - return Err(gitdir::Error::new_with_input( + return Err(gix_error::ValidationError::new_with_input( "Format should be 'gitdir: ', but got", input, )); } Ok(gix_path::try_from_bstr(path) - .map_err(|_| gitdir::Error::new_with_input("Couldn't decode input as UTF8", input))? + .map_err(|_| gix_error::ValidationError::new_with_input("Couldn't decode input as UTF8", input))? .into_owned()) } diff --git a/gix-discover/src/path.rs b/gix-discover/src/path.rs index 14eee428b5d..44b49651f50 100644 --- a/gix-discover/src/path.rs +++ b/gix-discover/src/path.rs @@ -15,12 +15,6 @@ pub enum RepositoryKind { Common, } -/// -pub mod from_gitdir_file { - /// The error returned by [`from_gitdir_file()`][crate::path::from_gitdir_file()]. - pub type Error = gix_error::Exn; -} - fn read_regular_file_content_with_size_limit(path: &std::path::Path) -> std::io::Result> { let mut file = std::fs::File::open(path)?; let max_file_size = 1024 * 64; // NOTE: git allows 1MB here @@ -125,7 +119,7 @@ pub fn from_plain_file_relative_to_file(path: &std::path::Path) -> Option Result { +pub fn from_gitdir_file(path: &std::path::Path) -> Result { let buf = read_regular_file_content_with_size_limit(path).or_erased()?; let mut gitdir = crate::parse::gitdir(&buf).or_erased()?; if let Some(parent) = path.parent() { diff --git a/gix-discover/src/upwards/mod.rs b/gix-discover/src/upwards/mod.rs index 1376fed11d0..12047b01414 100644 --- a/gix-discover/src/upwards/mod.rs +++ b/gix-discover/src/upwards/mod.rs @@ -1,5 +1,5 @@ mod types; -pub use types::{Error, Options, TrustPolicy}; +pub use types::{Options, TrustPolicy}; mod util; @@ -14,7 +14,7 @@ pub(crate) mod function { use gix_error::{ErrorExt, NotFoundError, OptionExt, ResultExt, ValidationError, message}; use gix_sec::Trust; - use super::{Error, Options, TrustPolicy}; + use super::{Options, TrustPolicy}; #[cfg(unix)] use crate::upwards::util::device_id; use crate::{ @@ -84,7 +84,7 @@ pub(crate) mod function { self.physical_parent_steps.is_some() } - pub fn metadata(&mut self) -> Result<&std::fs::Metadata, Error> { + pub fn metadata(&mut self) -> Result<&std::fs::Metadata, gix_error::Exn> { if self.current_metadata.is_none() { let path = if self.current.as_os_str().is_empty() { Path::new(".") @@ -212,7 +212,7 @@ pub(crate) mod function { current_dir, dot_git_only, }: Options<'_>, - ) -> Result<(crate::repository::Path, gix_sec::Trust), Error> { + ) -> Result<(crate::repository::Path, gix_sec::Trust), gix_error::Exn> { // Normalize the path so that `Path::parent()` _actually_ gives // us the parent directory. (`Path::parent` just strips off the last // path component, which means it will not do what you expect when @@ -261,7 +261,7 @@ pub(crate) mod function { let initial_device = device_id(&dir_metadata); let resolved = OnceCell::>::new(); let resolved = || resolved.get_or_init(|| resolved_directory_for_parent_traversal(directory, cwd.as_ref())); - let filter_by_trust = |dir: &Path| -> Result, Error> { + let filter_by_trust = |dir: &Path| -> Result, gix_error::Exn> { match trust { TrustPolicy::Required(required) => { let trust = Trust::from_path_ownership(dir).or_raise_erased(|| { @@ -395,7 +395,7 @@ pub(crate) mod function { /// the trust level derived from Path ownership. /// /// Fail if no valid-looking git repository could be found. - pub fn discover(directory: &Path) -> Result<(crate::repository::Path, gix_sec::Trust), Error> { + pub fn discover(directory: &Path) -> Result<(crate::repository::Path, gix_sec::Trust), gix_error::Exn> { discover_opts(directory, Default::default()) } } diff --git a/gix-discover/src/upwards/types.rs b/gix-discover/src/upwards/types.rs index 150d1e689c0..15fa10c6128 100644 --- a/gix-discover/src/upwards/types.rs +++ b/gix-discover/src/upwards/types.rs @@ -1,8 +1,5 @@ use std::{env, ffi::OsStr, path::PathBuf}; -/// The error returned by [`gix_discover::upwards()`][crate::upwards()]. -pub type Error = gix_error::Exn; - /// How to obtain the trust level for a discovered repository. #[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] pub enum TrustPolicy { diff --git a/gix-error/src/lib.rs b/gix-error/src/lib.rs index 3928822da9e..0765bf0dd06 100644 --- a/gix-error/src/lib.rs +++ b/gix-error/src/lib.rs @@ -130,6 +130,10 @@ //! while a function that wraps I/O errors during parsing could return `Result<_, Exn>`. //! When in doubt, [`Message`] is the default choice. //! +//! Use the chosen type directly in signatures, importing it under its canonical name where helpful. +//! Crate-specific and operation-specific forwarding aliases or renamed error exports are unnecessary. +//! Facades may re-export the canonical types, as `gix` does with `gix::Error` and `gix::Exn`. +//! //! ## Translating variants //! //! The translation depends on the chosen return type. When the function returns a plain error @@ -292,11 +296,11 @@ //! [`Exn`] also converts directly into `Box`, so `?` works //! without an explicit conversion when that is the receiving result's error type: //! ```rust,ignore -//! // In the porcelain crate's error module: -//! pub type Error = gix_error::Error; // not gix_archive::Error (which is Exn) -//! -//! // The conversion happens automatically via From> for Error, -//! // so `?` works without explicit .into_error() calls. +//! fn porcelain_operation() -> Result<(), gix_error::Error> { +//! // From> for Error converts the plumbing error at this boundary. +//! plumbing_operation()?; +//! Ok(()) +//! } //! ``` //! //! # Feature Flags diff --git a/gix-filter/src/driver/apply.rs b/gix-filter/src/driver/apply.rs index f2643a92967..a59c5c90ba4 100644 --- a/gix-filter/src/driver/apply.rs +++ b/gix-filter/src/driver/apply.rs @@ -23,9 +23,6 @@ pub enum Delay { Forbid, } -/// The error returned by [State::apply()][super::State::apply()]. -pub type Error = gix_error::Exn; - /// Additional information for use in the [`State::apply()`] method. #[derive(Debug, Copy, Clone)] pub struct Context<'a, 'b> { @@ -67,7 +64,7 @@ impl State { src: &mut impl std::io::Read, operation: Operation, ctx: Context<'_, '_>, - ) -> Result>, Error> { + ) -> Result>, gix_error::Exn> { match self.apply_delayed(driver, src, operation, Delay::Forbid, ctx)? { Some(MaybeDelayed::Delayed(_)) => { unreachable!("we forbid delaying the entry") @@ -88,7 +85,7 @@ impl State { operation: Operation, delay: Delay, ctx: Context<'_, '_>, - ) -> Result>, Error> { + ) -> Result>, gix_error::Exn> { use gix_error::{ErrorExt, ResultExt, message}; match self diff --git a/gix-filter/src/driver/delayed.rs b/gix-filter/src/driver/delayed.rs index fc38257cd77..cdd3dd0b4e7 100644 --- a/gix-filter/src/driver/delayed.rs +++ b/gix-filter/src/driver/delayed.rs @@ -5,18 +5,6 @@ use crate::{ driver::{Operation, State, apply::handle_io_err}, }; -/// -pub mod list { - /// The error returned by [State::list_delayed_paths()][super::State::list_delayed_paths()]. - pub type Error = gix_error::Exn; -} - -/// -pub mod fetch { - /// The error returned by [State::fetch_delayed()][super::State::fetch_delayed()]. - pub type Error = gix_error::Exn; -} - /// Operations related to delayed filtering. impl State { /// Return a list of delayed paths for `process` that can then be obtained with [`fetch_delayed()`][Self::fetch_delayed()]. @@ -29,7 +17,10 @@ impl State { /// /// Usually if the process sends the "abort" status, we will not use a certain capability again. Here it's unclear what capability /// that is and what to do, so we leave the process running and do nothing else (just like `git`). - pub fn list_delayed_paths(&mut self, process: &driver::Key) -> Result, list::Error> { + pub fn list_delayed_paths( + &mut self, + process: &driver::Key, + ) -> Result, gix_error::Exn> { use gix_error::{ErrorExt, OptionExt, message}; let client = self.running.get_mut(&process.0).ok_or_raise(|| { @@ -81,7 +72,7 @@ impl State { process: &driver::Key, path: &BStr, operation: Operation, - ) -> Result { + ) -> Result> { use gix_error::{ErrorExt, OptionExt, message}; let client = self.running.get_mut(&process.0).ok_or_raise(|| { diff --git a/gix-filter/src/driver/init.rs b/gix-filter/src/driver/init.rs index 176e87e9449..4ecd70fdedc 100644 --- a/gix-filter/src/driver/init.rs +++ b/gix-filter/src/driver/init.rs @@ -7,9 +7,6 @@ use crate::{ driver::{Operation, Process, State, process, substitute_f_parameter}, }; -/// The error returned by [State::maybe_launch_process()][super::State::maybe_launch_process()]. -pub type Error = gix_error::Exn; - /// Lifecycle impl State { /// Obtain a process as defined in `driver` suitable for a given `operation. `rela_path` may be used to substitute the current @@ -21,7 +18,7 @@ impl State { driver: &Driver, operation: Operation, rela_path: &BStr, - ) -> Result>, Error> { + ) -> Result>, gix_error::Exn> { match driver.process.as_ref() { Some(process) => { let client = match self.running.remove(process) { @@ -73,7 +70,7 @@ impl State { fn spawn_driver( cmd: BString, context: &gix_command::Context, -) -> Result<(std::process::Child, std::process::Command), Error> { +) -> Result<(std::process::Child, std::process::Command), gix_error::Exn> { let mut cmd: std::process::Command = gix_command::prepare(gix_path::from_bstr(cmd).into_owned()) .command_may_be_shell_script() .with_context(context.clone()) diff --git a/gix-filter/src/driver/process/client.rs b/gix-filter/src/driver/process/client.rs index 30bd593773f..62c2f8e2d93 100644 --- a/gix-filter/src/driver/process/client.rs +++ b/gix-filter/src/driver/process/client.rs @@ -8,24 +8,6 @@ use crate::driver::{ process::{Capabilities, Client, PacketlineReader}, }; -/// -pub mod handshake { - /// The error returned by [Client::handshake()][super::Client::handshake()]. - pub type Error = gix_error::Exn; -} - -/// -pub mod invoke { - /// The error returned by [Client::invoke()][super::Client::invoke()]. - pub type Error = gix_error::Exn; - - /// - pub mod without_content { - /// The error returned by [Client::invoke_without_content()][super::super::Client::invoke_without_content()]. - pub type Error = gix_error::Exn; - } -} - /// Protocol implementation impl Client { /// Given a spawned `process` as created from `cmd`, use the 'long-running-process' protocol to send `welcome-prefix` and supported @@ -36,7 +18,7 @@ impl Client { welcome_prefix: &str, versions: &[usize], desired_capabilities: &[&str], - ) -> Result { + ) -> Result> { use gix_error::{ErrorExt, ResultExt, message}; let mut out = Writer::new(process.stdin.take().expect("configured stdin when spawning")); @@ -143,7 +125,7 @@ impl Client { command: &str, meta: &mut dyn Iterator, content: &mut dyn std::io::Read, - ) -> Result { + ) -> Result> { use gix_error::{ResultExt, message}; self.send_command_and_meta(command, meta)?; @@ -166,7 +148,7 @@ impl Client { command: &str, meta: &mut dyn Iterator, inspect_line: &mut dyn FnMut(&BStr), - ) -> Result { + ) -> Result> { use gix_error::{ResultExt, message}; self.send_command_and_meta(command, meta)?; @@ -206,7 +188,7 @@ impl Client { &mut self, command: &str, meta: &mut dyn Iterator, - ) -> Result<(), invoke::Error> { + ) -> Result<(), gix_error::Exn> { use gix_error::{ResultExt, message}; self.input diff --git a/gix-filter/src/driver/process/server.rs b/gix-filter/src/driver/process/server.rs index c2eb296c9ef..93f52a74984 100644 --- a/gix-filter/src/driver/process/server.rs +++ b/gix-filter/src/driver/process/server.rs @@ -14,18 +14,6 @@ pub struct Request<'a> { pub meta: Vec<(String, BString)>, } -/// -pub mod next_request { - /// The error returned by [Server::next_request()][super::Server::next_request()]. - pub type Error = gix_error::Exn; -} - -/// -pub mod handshake { - /// The error returned by [Server::handshake()][super::Server::handshake()]. - pub type Error = gix_error::Exn; -} - impl Server { /// Perform a handshake with the client sending information to our `stdin` and receiving information through our `stdout` /// in packetline format. @@ -43,7 +31,7 @@ impl Server { welcome_prefix: &str, pick_version: &mut dyn FnMut(&[usize]) -> Option, available_capabilities: &[&str], - ) -> Result { + ) -> Result> { use gix_error::{ErrorExt, OptionExt, ResultExt, message}; let mut input = StreamingPeekableIter::new( @@ -146,7 +134,7 @@ impl Server { /// /// Note that the process is supposed to shut-down once there are no more requests, and `git` will wait /// until it has finished. - pub fn next_request(&mut self) -> Result>, next_request::Error> { + pub fn next_request(&mut self) -> Result>, gix_error::Exn> { use gix_error::{ErrorExt, OptionExt, ResultExt, message}; let mut buf = String::new(); diff --git a/gix-filter/src/driver/shutdown.rs b/gix-filter/src/driver/shutdown.rs index 0a4c0cbc1e4..e4c32c4fdbf 100644 --- a/gix-filter/src/driver/shutdown.rs +++ b/gix-filter/src/driver/shutdown.rs @@ -10,16 +10,13 @@ pub struct Outcome { pub processes: Vec<(BString, Option)>, } -/// A filter process that exited unsuccessfully during shutdown. -pub type Error = gix_error::Exn; - impl Outcome { /// Return this outcome if all observed processes exited successfully, or the first failure otherwise. /// /// This is stricter than Git, which ignores a long-running filter's exit status during shutdown after it has /// successfully converted all requested input. Callers that require Git-compatible behavior should inspect or /// discard the outcome instead. - pub fn into_result(self) -> Result { + pub fn into_result(self) -> Result> { if let Some((command, status)) = self.processes.iter().find_map(|(command, status)| { status .as_ref() diff --git a/gix-filter/src/eol/convert_to_git.rs b/gix-filter/src/eol/convert_to_git.rs index 328e9f38bda..94b7715c2c4 100644 --- a/gix-filter/src/eol/convert_to_git.rs +++ b/gix-filter/src/eol/convert_to_git.rs @@ -26,9 +26,6 @@ pub enum RoundTripCheck<'a> { }, } -/// The error returned by [convert_to_git()][super::convert_to_git()]. -pub type Error = gix_error::Exn; - /// A function that writes a buffer like `fn(&mut buf)` with by tes of an object in the index that is the one that should be converted. pub type IndexObjectFn<'a> = dyn FnMut(&mut Vec) -> Result, gix_error::Exn> + 'a; @@ -39,7 +36,7 @@ pub(crate) mod function { clear_and_set_capacity, eol::{ AttributesDigest, Stats, - convert_to_git::{Error, IndexObjectFn, Options, RoundTripCheck}, + convert_to_git::{IndexObjectFn, Options, RoundTripCheck}, }, }; @@ -61,7 +58,7 @@ pub(crate) mod function { round_trip_check, config, }: Options<'_>, - ) -> Result { + ) -> Result> { use gix_error::{ErrorExt, ResultExt, message}; if digest == AttributesDigest::Binary || src.is_empty() { diff --git a/gix-filter/src/eol/convert_to_worktree.rs b/gix-filter/src/eol/convert_to_worktree.rs index 3eb76a8c9aa..c0aeb774fc4 100644 --- a/gix-filter/src/eol/convert_to_worktree.rs +++ b/gix-filter/src/eol/convert_to_worktree.rs @@ -5,9 +5,6 @@ use crate::{ eol::{AttributesDigest, Configuration, Mode, Stats}, }; -/// The error produced by [`convert_to_worktree()`]. -pub type Error = gix_error::Exn; - /// Convert all `\n` in `src` to `crlf` if `digest` and `config` indicate it, returning `true` if `buf` holds the result, or `false` /// if no change was made after all. pub fn convert_to_worktree( @@ -15,7 +12,7 @@ pub fn convert_to_worktree( digest: AttributesDigest, buf: &mut Vec, config: Configuration, -) -> Result { +) -> Result> { use gix_error::{ResultExt, message}; if src.is_empty() || digest.to_eol(config) != Some(Mode::CrLf) { diff --git a/gix-filter/src/ident.rs b/gix-filter/src/ident.rs index 54cc75e8e94..79a7f7ddc7f 100644 --- a/gix-filter/src/ident.rs +++ b/gix-filter/src/ident.rs @@ -40,12 +40,6 @@ pub fn undo(src: &[u8], buf: &mut Vec) -> Result; -} - /// Substitute all occurrences of `$Id$` with `$Id: $` if present in `src` and write all changes to `buf`, /// with `object_hash` being used accordingly. Return `true` if `buf` was written to or `false` if no change was made /// (as there was nothing to do). @@ -54,7 +48,11 @@ pub mod apply { /// /// `Git` also tries to cleanup 'stray' substituted `$Id: $`, but we don't do that, sticking exactly to what ought to be done. /// The respective code is up to 16 years old and one might assume that `git` by now handles checking and checkout filters correctly. -pub fn apply(src: &[u8], object_hash: gix_hash::Kind, buf: &mut Vec) -> Result { +pub fn apply( + src: &[u8], + object_hash: gix_hash::Kind, + buf: &mut Vec, +) -> Result> { use gix_error::{ResultExt, message}; const HASH_LEN: usize = ": ".len() + gix_hash::Kind::longest().len_in_hex(); diff --git a/gix-filter/src/pipeline/convert.rs b/gix-filter/src/pipeline/convert.rs index e48fed9e231..b5cb6f13f42 100644 --- a/gix-filter/src/pipeline/convert.rs +++ b/gix-filter/src/pipeline/convert.rs @@ -4,19 +4,10 @@ use bstr::BStr; use crate::{Pipeline, driver, eol, ident, pipeline::util::Configuration, worktree}; -/// -pub mod configuration { - /// Errors related to the configuration of filter attributes. - pub type Error = gix_error::ValidationError; -} - /// pub mod to_git { /// A function that fills `buf` `fn(&mut buf)` with the data stored in the index of the file that should be converted. - pub type IndexObjectFn<'a> = dyn FnMut(&mut Vec) -> Result, gix_object::find::Error> + 'a; - - /// The error returned by [Pipeline::convert_to_git()][super::Pipeline::convert_to_git()]. - pub type Error = gix_error::Exn; + pub type IndexObjectFn<'a> = dyn FnMut(&mut Vec) -> Result, gix_error::Exn> + 'a; } /// @@ -41,9 +32,6 @@ pub mod to_worktree { /// Return an error. Fail, } - - /// The error returned by [Pipeline::convert_to_worktree()][super::Pipeline::convert_to_worktree()]. - pub type Error = gix_error::Exn; } /// Access @@ -58,7 +46,7 @@ impl Pipeline { rela_path: &Path, attributes: &mut dyn FnMut(&BStr, &mut gix_attributes::search::Outcome), index_object: &mut to_git::IndexObjectFn<'_>, - ) -> Result, to_git::Error> + ) -> Result, gix_error::Exn> where R: std::io::Read, { @@ -180,7 +168,7 @@ impl Pipeline { can_delay, unknown_encoding, }: to_worktree::Options, - ) -> Result, to_worktree::Error> { + ) -> Result, gix_error::Exn> { use gix_error::ResultExt; let Configuration { diff --git a/gix-filter/src/pipeline/util.rs b/gix-filter/src/pipeline/util.rs index a4ad362a255..800ef146142 100644 --- a/gix-filter/src/pipeline/util.rs +++ b/gix-filter/src/pipeline/util.rs @@ -7,7 +7,7 @@ use smallvec::SmallVec; use crate::{ Driver, driver, eol, eol::AttributesDigest, - pipeline::{Context, CrlfRoundTripCheck, convert::configuration}, + pipeline::{Context, CrlfRoundTripCheck}, }; pub(crate) struct Configuration<'a> { @@ -29,7 +29,7 @@ impl<'driver> Configuration<'driver> { attributes: &mut dyn FnMut(&BStr, &mut gix_attributes::search::Outcome), config: eol::Configuration, ignore_unknown_encoding: bool, - ) -> Result, configuration::Error> { + ) -> Result, gix_error::ValidationError> { fn extract_driver<'a>(drivers: &'a [Driver], attr: &gix_attributes::search::Match<'_>) -> Option<&'a Driver> { if let StateRef::Value(name) = attr.assignment.state { drivers.iter().find(|d| d.name == name.as_bstr()) @@ -41,7 +41,7 @@ impl<'driver> Configuration<'driver> { fn extract_encoding( attr: &gix_attributes::search::Match<'_>, ignore_unknown: bool, - ) -> Result, configuration::Error> { + ) -> Result, gix_error::ValidationError> { match attr.assignment.state { StateRef::Set | StateRef::Unset => Err(gix_error::ValidationError::new( "Encodings must be names, like UTF-16, and cannot be booleans.", diff --git a/gix-filter/src/worktree/encode_to_git.rs b/gix-filter/src/worktree/encode_to_git.rs index a4b4ae77e84..c10e7f64657 100644 --- a/gix-filter/src/worktree/encode_to_git.rs +++ b/gix-filter/src/worktree/encode_to_git.rs @@ -7,13 +7,10 @@ pub enum RoundTripCheck { Skip, } -/// The error returned by [`encode_to_git()][super::encode_to_git()]. -pub type Error = gix_error::ValidationError; - pub(crate) mod function { use encoding_rs::DecoderResult; - use super::{Error, RoundTripCheck}; + use super::RoundTripCheck; /// Decode `src` according to `src_encoding` to `UTF-8` for storage in git and place it in `buf`. /// Note that the encoding is always applied, there is no conditional even if `src_encoding` already is `UTF-8`. @@ -22,7 +19,7 @@ pub(crate) mod function { src_encoding: &'static encoding_rs::Encoding, buf: &mut Vec, round_trip: RoundTripCheck, - ) -> Result<(), Error> { + ) -> Result<(), gix_error::ValidationError> { let mut decoder = src_encoding.new_decoder_with_bom_removal(); let buf_len = decoder .max_utf8_buffer_length_without_replacement(src.len()) diff --git a/gix-filter/src/worktree/encode_to_worktree.rs b/gix-filter/src/worktree/encode_to_worktree.rs index a8bd0b88e46..fe0750381c5 100644 --- a/gix-filter/src/worktree/encode_to_worktree.rs +++ b/gix-filter/src/worktree/encode_to_worktree.rs @@ -1,11 +1,6 @@ -/// The error returned by [`encode_to_worktree()][super::encode_to_worktree()]. -pub type Error = gix_error::Exn; - pub(crate) mod function { use encoding_rs::EncoderResult; - use super::Error; - /// Encode `src_utf8`, which is assumed to be UTF-8 encoded, according to `worktree_encoding` for placement in the working directory, /// and write it to `buf`, possibly resizing it. /// Note that the encoding is always applied, there is no conditional even if `worktree_encoding` and the `src` encoding are the same. @@ -13,7 +8,7 @@ pub(crate) mod function { src_utf8: &[u8], worktree_encoding: &'static encoding_rs::Encoding, buf: &mut Vec, - ) -> Result<(), Error> { + ) -> Result<(), gix_error::Exn> { use gix_error::{ErrorExt, ResultExt}; let mut encoder = worktree_encoding.new_encoder(); diff --git a/gix-filter/src/worktree/encoding.rs b/gix-filter/src/worktree/encoding.rs index 9bf302d214a..70affb5e8d7 100644 --- a/gix-filter/src/worktree/encoding.rs +++ b/gix-filter/src/worktree/encoding.rs @@ -1,12 +1,6 @@ use bstr::BStr; use encoding_rs::Encoding; -/// -pub mod for_label { - /// The error returned by [for_label()][super::for_label()]. - pub type Error = gix_error::ValidationError; -} - /// Try to produce a new `Encoding` for `label` or report an error if it is not known. /// /// ### Deviation @@ -14,7 +8,7 @@ pub mod for_label { /// * There is no special handling of UTF-16LE/BE with checks if data contains a BOM or not, like `git` as we don't expect to have /// data available here. /// * Special `-BOM` suffixed versions of `UTF-16` encodings are not supported. -pub fn for_label<'a>(label: impl Into<&'a BStr>) -> Result<&'static Encoding, for_label::Error> { +pub fn for_label<'a>(label: impl Into<&'a BStr>) -> Result<&'static Encoding, gix_error::ValidationError> { let mut label = label.into(); if label == "latin-1" { label = "ISO-8859-1".into(); diff --git a/gix-filter/tests/filter/pipeline/convert_to_git.rs b/gix-filter/tests/filter/pipeline/convert_to_git.rs index fd56b134f25..d0f8ab886e7 100644 --- a/gix-filter/tests/filter/pipeline/convert_to_git.rs +++ b/gix-filter/tests/filter/pipeline/convert_to_git.rs @@ -145,10 +145,10 @@ fn no_filter_means_reader_is_returned_unchanged() -> gix_testtools::Result { Ok(()) } -fn no_call(_buf: &mut Vec) -> Result, gix_object::find::Error> { +fn no_call(_buf: &mut Vec) -> Result, gix_error::Exn> { unreachable!("index function will not be called") } -fn no_object_in_index(_buf: &mut Vec) -> Result, gix_object::find::Error> { +fn no_object_in_index(_buf: &mut Vec) -> Result, gix_error::Exn> { Ok(None) } diff --git a/gix-fs/src/stack.rs b/gix-fs/src/stack.rs index b84096d6333..80984feaee8 100644 --- a/gix-fs/src/stack.rs +++ b/gix-fs/src/stack.rs @@ -7,26 +7,20 @@ use bstr::{BStr, BString, ByteSlice}; use crate::Stack; -/// -pub mod to_normal_path_components { - /// The error used in [`ToNormalPathComponents::to_normal_path_components()`](super::ToNormalPathComponents::to_normal_path_components()). - pub type Error = gix_error::ValidationError; -} - /// Obtain an iterator over `OsStr`-components which are normal, none-relative and not absolute. pub trait ToNormalPathComponents { /// Return an iterator over the normal components of a path, without the separator. - fn to_normal_path_components(&self) -> impl Iterator>; + fn to_normal_path_components(&self) -> impl Iterator>; } impl ToNormalPathComponents for &Path { - fn to_normal_path_components(&self) -> impl Iterator> { + fn to_normal_path_components(&self) -> impl Iterator> { self.components().map(|c| component_to_os_str(c, self)) } } impl ToNormalPathComponents for PathBuf { - fn to_normal_path_components(&self) -> impl Iterator> { + fn to_normal_path_components(&self) -> impl Iterator> { self.components().map(|c| component_to_os_str(c, self)) } } @@ -34,10 +28,10 @@ impl ToNormalPathComponents for PathBuf { fn component_to_os_str<'a>( component: Component<'a>, path_with_component: &Path, -) -> Result<&'a OsStr, to_normal_path_components::Error> { +) -> Result<&'a OsStr, gix_error::ValidationError> { match component { Component::Normal(os_str) => Ok(os_str), - _ => Err(to_normal_path_components::Error::new(format!( + _ => Err(gix_error::ValidationError::new(format!( "Input path \"{}\" contains relative or absolute components", path_with_component.display() ))), @@ -45,21 +39,21 @@ fn component_to_os_str<'a>( } impl ToNormalPathComponents for &BStr { - fn to_normal_path_components(&self) -> impl Iterator> { + fn to_normal_path_components(&self) -> impl Iterator> { self.split(|b| *b == b'/') .filter_map(|c| bytes_component_to_os_str(c, self)) } } impl ToNormalPathComponents for &str { - fn to_normal_path_components(&self) -> impl Iterator> { + fn to_normal_path_components(&self) -> impl Iterator> { self.split('/') .filter_map(|c| bytes_component_to_os_str(c.as_bytes(), (*self).into())) } } impl ToNormalPathComponents for &BString { - fn to_normal_path_components(&self) -> impl Iterator> { + fn to_normal_path_components(&self) -> impl Iterator> { self.split(|b| *b == b'/') .filter_map(|c| bytes_component_to_os_str(c, self.as_bstr())) } @@ -68,7 +62,7 @@ impl ToNormalPathComponents for &BString { fn bytes_component_to_os_str<'a>( component: &'a [u8], path: &BStr, -) -> Option> { +) -> Option> { if component.is_empty() { return None; } diff --git a/gix-fsck/Cargo.toml b/gix-fsck/Cargo.toml index f3e00274026..6fe931427a4 100644 --- a/gix-fsck/Cargo.toml +++ b/gix-fsck/Cargo.toml @@ -21,6 +21,7 @@ sha1 = ["gix-hash/sha1"] sha256 = ["gix-hash/sha256"] [dependencies] +gix-error = { version = "^0.3.2", path = "../gix-error" } gix-hash = { version = "^0.26.2", path = "../gix-hash" } gix-hashtable = { version = "^0.16.0", path = "../gix-hashtable" } gix-object = { version = "^0.64.1", path = "../gix-object" } diff --git a/gix-fsck/src/lib.rs b/gix-fsck/src/lib.rs index 1317586b18c..7054982ebb2 100644 --- a/gix-fsck/src/lib.rs +++ b/gix-fsck/src/lib.rs @@ -47,7 +47,7 @@ where /// Any referenced blobs that are not present in the ODB will result in a call to the `missing_cb`. /// Missing commits or trees will cause an error to be returned. /// - TODO: consider how to handle a missing commit (invoke `missing_cb`, or possibly return a Result?) - pub fn check_commit(&mut self, oid: &ObjectId) -> Result<(), gix_object::find::existing_object::Error> { + pub fn check_commit(&mut self, oid: &ObjectId) -> Result<(), gix_error::Exn> { // Attempt to insert the commit ID in the set, and if already present, return immediately if !self.seen.insert(*oid) { return Ok(()); diff --git a/gix-hash/src/change_id.rs b/gix-hash/src/change_id.rs index 359ed4f3b71..47e532a675f 100644 --- a/gix-hash/src/change_id.rs +++ b/gix-hash/src/change_id.rs @@ -6,17 +6,17 @@ const REVERSE_HEX: &[u8; 16] = b"zyxwvutsrqponmlk"; impl ChangeId { /// Parse a complete SHA-1 or SHA-256 hash written with reverse-hex alphabet. - pub fn from_reverse_hex(buffer: &[u8]) -> Result { + pub fn from_reverse_hex(buffer: &[u8]) -> Result { let len = buffer.len(); if crate::Kind::from_hex_len(len).is_none_or(|kind| kind.len_in_hex() != len) { - return Err(crate::decode::Error::new(format!( + return Err(gix_error::ValidationError::new(format!( "A hash sized {len} hexadecimal characters is invalid" ))); } let mut hex = Kind::hex_buf(); reverse_hex_to_hex(buffer, &mut hex[..len]) - .map_err(|()| crate::decode::Error::new("Invalid character encountered"))?; + .map_err(|()| gix_error::ValidationError::new("Invalid character encountered"))?; ObjectId::from_hex(&hex[..len]).map(ChangeId) } @@ -39,7 +39,7 @@ pub struct ReverseHexDisplay<'a> { } impl FromStr for ChangeId { - type Err = crate::decode::Error; + type Err = gix_error::ValidationError; fn from_str(value: &str) -> Result { Self::from_reverse_hex(value.as_bytes()) diff --git a/gix-hash/src/hasher.rs b/gix-hash/src/hasher.rs index 0d0aae7448e..cb9dd951a3f 100644 --- a/gix-hash/src/hasher.rs +++ b/gix-hash/src/hasher.rs @@ -1,9 +1,4 @@ -/// The error returned by [`Hasher::try_finalize()`](crate::Hasher::try_finalize()). -pub type Error = gix_error::CorruptionError; - pub(super) mod _impl { - use crate::hasher::Error; - /// Hash implementations that can be used once. #[derive(Clone)] pub enum Hasher { @@ -48,17 +43,17 @@ pub(super) mod _impl { /// Finalize the hash and produce an object id. /// - /// Returns [`Error`] if a collision attack is detected. + /// Returns [`gix_error::CorruptionError`] if a collision attack is detected. // TODO: Since SHA-256 has an infallible `finalize`, it might be worth investigating // turning the return type into `Result` when this crate is // compiled with SHA-256 support only. #[inline] - pub fn try_finalize(self) -> Result { + pub fn try_finalize(self) -> Result { match self { #[cfg(feature = "sha1")] Hasher::Sha1(sha1) => match sha1.finalize() { Ok(digest) => Ok(crate::ObjectId::Sha1(digest.into())), - Err(collision) => Err(Error::new(format!( + Err(collision) => Err(gix_error::CorruptionError::new(format!( "Detected SHA-1 collision attack with digest {}", crate::ObjectId::Sha1(collision.digest().into()) ))), diff --git a/gix-hash/src/io.rs b/gix-hash/src/io.rs index ff584f76b23..7cfc96626d5 100644 --- a/gix-hash/src/io.rs +++ b/gix-hash/src/io.rs @@ -1,25 +1,21 @@ -use crate::hasher; use gix_error::{ErrorExt, message}; -/// The error type for I/O operations that compute hashes. -pub type Error = gix_error::Exn; - /// Convert an I/O error into this module's error type without changing its message. // TODO(gix-error): review and attempt to remove the need for this if possible. But don't stress it. -pub fn from_std_io(source: std::io::Error) -> Error { +pub fn from_std_io(source: std::io::Error) -> gix_error::Exn { source.raise_erased() } /// Convert a hashing error into this module's error type and add operation context. // TODO(gix-error): review and attempt to remove the need for this if possible. But don't stress it. -pub fn from_hasher(source: hasher::Error) -> Error { +pub fn from_hasher(source: gix_error::CorruptionError) -> gix_error::Exn { source.and_raise(message("Failed to hash data")).erased() } pub(super) mod _impl { use crate::{ Hasher, hasher, - io::{Error, from_hasher, from_std_io}, + io::{from_hasher, from_std_io}, }; /// Compute the hash of `kind` for the bytes in the file at `path`, hashing only the first `num_bytes_from_start` @@ -37,7 +33,7 @@ pub(super) mod _impl { kind: crate::Kind, progress: &mut dyn gix_features::progress::Progress, should_interrupt: &std::sync::atomic::AtomicBool, - ) -> Result { + ) -> Result { bytes( &mut std::fs::File::open(path).map_err(from_std_io)?, num_bytes_from_start, @@ -54,7 +50,7 @@ pub(super) mod _impl { kind: crate::Kind, progress: &mut dyn gix_features::progress::Progress, should_interrupt: &std::sync::atomic::AtomicBool, - ) -> Result { + ) -> Result { bytes_with_hasher(read, num_bytes_from_start, hasher(kind), progress, should_interrupt) } @@ -65,7 +61,7 @@ pub(super) mod _impl { mut hasher: Hasher, progress: &mut dyn gix_features::progress::Progress, should_interrupt: &std::sync::atomic::AtomicBool, - ) -> Result { + ) -> Result { let start = std::time::Instant::now(); // init progress before the possibility for failure, as convenience in case people want to recover progress.init( diff --git a/gix-hash/src/lib.rs b/gix-hash/src/lib.rs index 112b8aa7ed0..80d162018e2 100644 --- a/gix-hash/src/lib.rs +++ b/gix-hash/src/lib.rs @@ -79,7 +79,7 @@ macro_rules! impl_partial_eq_str { #[path = "oid.rs"] mod borrowed; -pub use borrowed::{Error, oid}; +pub use borrowed::oid; /// Hash functions and hash utilities pub mod hasher; diff --git a/gix-hash/src/object_id.rs b/gix-hash/src/object_id.rs index 1899d199e28..59d7fec2849 100644 --- a/gix-hash/src/object_id.rs +++ b/gix-hash/src/object_id.rs @@ -45,7 +45,6 @@ pub mod decode { use std::str::FromStr; use crate::object_id::ObjectId; - use gix_error::ValidationError; #[cfg(feature = "sha1")] use crate::{SIZE_OF_SHA1_DIGEST, SIZE_OF_SHA1_HEX_DIGEST}; @@ -53,9 +52,6 @@ pub mod decode { #[cfg(feature = "sha256")] use crate::{SIZE_OF_SHA256_DIGEST, SIZE_OF_SHA256_HEX_DIGEST}; - /// An error returned by [`ObjectId::from_hex()`][crate::ObjectId::from_hex()] - pub type Error = ValidationError; - /// Hash decoding impl ObjectId { /// Create an instance from a `buffer` of 40 bytes or 64 bytes encoded with hexadecimal @@ -63,7 +59,7 @@ pub mod decode { /// as SHA256 when it is enabled. /// /// Such a buffer can be obtained using [`oid::write_hex_to(buffer)`][super::oid::write_hex_to()] - pub fn from_hex(buffer: &[u8]) -> Result { + pub fn from_hex(buffer: &[u8]) -> Result { match buffer.len() { #[cfg(feature = "sha1")] SIZE_OF_SHA1_HEX_DIGEST => Ok({ @@ -71,7 +67,7 @@ pub mod decode { let mut buf = [0; SIZE_OF_SHA1_DIGEST]; faster_hex::hex_decode(buffer, &mut buf).map_err(|err| match err { faster_hex::Error::InvalidChar | faster_hex::Error::Overflow => { - Error::new("Invalid character encountered") + gix_error::ValidationError::new("Invalid character encountered") } faster_hex::Error::InvalidLength(_) => { unreachable!("BUG: This is already checked") @@ -86,7 +82,7 @@ pub mod decode { let mut buf = [0; SIZE_OF_SHA256_DIGEST]; faster_hex::hex_decode(buffer, &mut buf).map_err(|err| match err { faster_hex::Error::InvalidChar | faster_hex::Error::Overflow => { - Error::new("Invalid character encountered") + gix_error::ValidationError::new("Invalid character encountered") } faster_hex::Error::InvalidLength(_) => { unreachable!("BUG: This is already checked") @@ -95,7 +91,7 @@ pub mod decode { buf }) }), - len => Err(Error::new(format!( + len => Err(gix_error::ValidationError::new(format!( "A hash sized {len} hexadecimal characters is invalid" ))), } @@ -103,7 +99,7 @@ pub mod decode { } impl FromStr for ObjectId { - type Err = Error; + type Err = gix_error::ValidationError; fn from_str(s: &str) -> Result { Self::from_hex(s.as_bytes()) @@ -313,7 +309,7 @@ impl From<&oid> for ObjectId { } impl TryFrom<&[u8]> for ObjectId { - type Error = crate::Error; + type Error = gix_error::ValidationError; fn try_from(bytes: &[u8]) -> Result { Ok(oid::try_from_bytes(bytes)?.into()) diff --git a/gix-hash/src/oid.rs b/gix-hash/src/oid.rs index e607051cdc0..df6bdbcc884 100644 --- a/gix-hash/src/oid.rs +++ b/gix-hash/src/oid.rs @@ -82,14 +82,11 @@ impl std::fmt::Debug for oid { } } -/// The error returned when trying to convert a byte slice to an [`oid`] or [`ObjectId`] -pub type Error = gix_error::ValidationError; - /// Conversion impl oid { /// Try to create a shared object id from a slice of bytes representing a hash `digest` #[inline] - pub fn try_from_bytes(digest: &[u8]) -> Result<&Self, Error> { + pub fn try_from_bytes(digest: &[u8]) -> Result<&Self, gix_error::ValidationError> { match digest.len() { #[cfg(feature = "sha1")] SIZE_OF_SHA1_DIGEST => Ok( @@ -105,7 +102,7 @@ impl oid { &*(std::ptr::from_ref::<[u8]>(digest) as *const oid) }, ), - len => Err(Error::new(format!( + len => Err(gix_error::ValidationError::new(format!( "Cannot instantiate git hash from a digest of length {len}" ))), } @@ -296,7 +293,7 @@ impl AsRef for &oid { } impl<'a> TryFrom<&'a [u8]> for &'a oid { - type Error = Error; + type Error = gix_error::ValidationError; fn try_from(value: &'a [u8]) -> Result { oid::try_from_bytes(value) diff --git a/gix-hash/src/prefix.rs b/gix-hash/src/prefix.rs index 7ca3835f6f4..68d8b0cf64f 100644 --- a/gix-hash/src/prefix.rs +++ b/gix-hash/src/prefix.rs @@ -2,15 +2,6 @@ use std::cmp::Ordering; use crate::{ChangeId, ObjectId, Prefix, change_id::ReverseHexDisplay, oid}; -/// The error returned by [`Prefix::new()`]. -pub type Error = gix_error::ValidationError; - -/// -pub mod from_hex { - /// The error returned by [`Prefix::from_hex`][super::Prefix::from_hex()]. - pub type Error = gix_error::ValidationError; -} - impl Prefix { /// The smallest allowed prefix length below which chances for collisions are too high even in small repositories. pub const MIN_HEX_LEN: usize = 4; @@ -19,15 +10,15 @@ impl Prefix { /// /// For instance, with `hex_len` of 7 the resulting prefix is 3.5 bytes, or 3 bytes and 4 bits /// wide, with all other bytes and bits set to zero. - pub fn new(id: &oid, hex_len: usize) -> Result { + pub fn new(id: &oid, hex_len: usize) -> Result { if hex_len > id.kind().len_in_hex() { - Err(Error::new(format!( + Err(gix_error::ValidationError::new(format!( "An object of kind {} cannot be larger than {} in hex, but {hex_len} was requested", id.kind(), id.kind().len_in_hex() ))) } else if hex_len < Self::MIN_HEX_LEN { - Err(Error::new(format!( + Err(gix_error::ValidationError::new(format!( "The minimum hex length of a short object id is {}, got {hex_len}", Self::MIN_HEX_LEN ))) @@ -104,10 +95,10 @@ impl Prefix { /// Create an instance from the given hexadecimal prefix `value`, e.g. `35e77c16` would yield a `Prefix` with `hex_len()` = 8. /// Note that the minimum hex length is `4` - use [`Self::from_hex_nonempty()`]. - pub fn from_hex(value: &str) -> Result { + pub fn from_hex(value: &str) -> Result { let hex_len = value.len(); if hex_len < Self::MIN_HEX_LEN { - return Err(from_hex::Error::new(format!( + return Err(gix_error::ValidationError::new(format!( "The minimum hex length of a short object id is {}, got {hex_len}", Self::MIN_HEX_LEN ))); @@ -117,16 +108,16 @@ impl Prefix { /// Create an instance from the given hexadecimal prefix `value`, e.g. `35e` would yield a `Prefix` with `hex_len()` = 3. /// Note that this function supports all non-empty hex input - for a more typical implementation, use [`Self::from_hex()`]. - pub fn from_hex_nonempty(value: &str) -> Result { + pub fn from_hex_nonempty(value: &str) -> Result { let hex_len = value.len(); if hex_len > crate::Kind::longest().len_in_hex() { - return Err(from_hex::Error::new(format!( + return Err(gix_error::ValidationError::new(format!( "An id cannot be larger than {} chars in hex, but {hex_len} was requested", crate::Kind::longest().len_in_hex() ))); } else if hex_len == 0 { - return Err(from_hex::Error::new(format!( + return Err(gix_error::ValidationError::new(format!( "The minimum hex length of a short object id is {}, got {hex_len}", Self::MIN_HEX_LEN ))); @@ -145,7 +136,7 @@ impl Prefix { }; decode_result.map_err(|e| match e { faster_hex::Error::InvalidChar | faster_hex::Error::Overflow => { - from_hex::Error::new("Invalid hex character") + gix_error::ValidationError::new("Invalid hex character") } faster_hex::Error::InvalidLength(_) => panic!("This is already checked"), })?; @@ -154,10 +145,10 @@ impl Prefix { } /// Create an instance from a reverse-hex prefix, requiring at least [`Self::MIN_HEX_LEN`] characters. - pub fn from_reverse_hex(value: &str) -> Result { + pub fn from_reverse_hex(value: &str) -> Result { let hex_len = value.len(); if hex_len < Self::MIN_HEX_LEN { - return Err(from_hex::Error::new(format!( + return Err(gix_error::ValidationError::new(format!( "The minimum hex length of a short object id is {}, got {hex_len}", Self::MIN_HEX_LEN ))); @@ -166,15 +157,15 @@ impl Prefix { } /// Create an instance from a non-empty prefix written with JJ's reverse-hex alphabet. - pub fn from_reverse_hex_nonempty(value: &str) -> Result { + pub fn from_reverse_hex_nonempty(value: &str) -> Result { let hex_len = value.len(); if hex_len > crate::Kind::longest().len_in_hex() { - return Err(from_hex::Error::new(format!( + return Err(gix_error::ValidationError::new(format!( "An id cannot be larger than {} chars in hex, but {hex_len} was requested", crate::Kind::longest().len_in_hex() ))); } else if hex_len == 0 { - return Err(from_hex::Error::new(format!( + return Err(gix_error::ValidationError::new(format!( "The minimum hex length of a short object id is {}, got {hex_len}", Self::MIN_HEX_LEN ))); @@ -182,7 +173,7 @@ impl Prefix { let mut hex = crate::Kind::hex_buf(); crate::change_id::reverse_hex_to_hex(value.as_bytes(), &mut hex[..hex_len]) - .map_err(|()| from_hex::Error::new("Invalid hex character"))?; + .map_err(|()| gix_error::ValidationError::new("Invalid hex character"))?; let hex = std::str::from_utf8(&hex[..hex_len]).expect("translated reverse hex is always ASCII"); Self::from_hex_nonempty(hex) } @@ -196,7 +187,7 @@ impl Prefix { /// Create an instance from the given hexadecimal prefix, e.g. `35e77c16` would yield a `Prefix` /// with `hex_len()` = 8. impl TryFrom<&str> for Prefix { - type Error = from_hex::Error; + type Error = gix_error::ValidationError; fn try_from(value: &str) -> Result { Prefix::from_hex(value) diff --git a/gix-hash/src/verify.rs b/gix-hash/src/verify.rs index 1e13ef28df1..5e12f0d79a2 100644 --- a/gix-hash/src/verify.rs +++ b/gix-hash/src/verify.rs @@ -1,18 +1,17 @@ use crate::oid; -/// The error returned by [`oid::verify()`]. -pub type Error = gix_error::CorruptionError; - impl oid { /// Verify that `self` matches the `expected` object ID. /// - /// Returns an [`Error`] containing both object IDs if they differ. + /// Returns an [`gix_error::CorruptionError`] containing both object IDs if they differ. #[inline] - pub fn verify(&self, expected: &oid) -> Result<(), Error> { + pub fn verify(&self, expected: &oid) -> Result<(), gix_error::CorruptionError> { if self == expected { Ok(()) } else { - Err(Error::new(format!("Hash was {self}, but should have been {expected}"))) + Err(gix_error::CorruptionError::new(format!( + "Hash was {self}, but should have been {expected}" + ))) } } } diff --git a/gix-hash/tests/hash/hasher.rs b/gix-hash/tests/hash/hasher.rs index 15c64d58b6e..69cd22487e6 100644 --- a/gix-hash/tests/hash/hasher.rs +++ b/gix-hash/tests/hash/hasher.rs @@ -46,7 +46,7 @@ fn size_of_hasher_sha1_and_sha256() { #[cfg(all(not(feature = "sha256"), feature = "sha1"))] fn size_of_try_finalize_return_type_sha1_only() { assert_eq!( - std::mem::size_of::>(), + std::mem::size_of::>(), 32, "The size of the return value should remain compact" ); @@ -56,7 +56,7 @@ fn size_of_try_finalize_return_type_sha1_only() { #[cfg(all(feature = "sha256", feature = "sha1"))] fn size_of_try_finalize_return_type_sha1_and_sha256() { assert_eq!( - std::mem::size_of::>(), + std::mem::size_of::>(), 32 + std::mem::size_of::(), "The size of the return value should remain compact" ); diff --git a/gix-hash/tests/hash/object_id.rs b/gix-hash/tests/hash/object_id.rs index 0dbe3f78db9..ace7610c6af 100644 --- a/gix-hash/tests/hash/object_id.rs +++ b/gix-hash/tests/hash/object_id.rs @@ -68,7 +68,7 @@ mod sha1 { use gix_hash::{Kind, ObjectId, hasher}; - fn hash_contents(s: &[u8]) -> Result { + fn hash_contents(s: &[u8]) -> Result { let mut hasher = hasher(Kind::Sha1); hasher.update(s); hasher.try_finalize() @@ -139,7 +139,7 @@ mod sha1 { mod sha256 { use gix_hash::{Kind, ObjectId, hasher}; - fn hash_contents(s: &[u8]) -> Result { + fn hash_contents(s: &[u8]) -> Result { let mut hasher = hasher(Kind::Sha256); hasher.update(s); hasher.try_finalize() diff --git a/gix-index/src/decode/entries.rs b/gix-index/src/decode/entries.rs index f8bcd9350a3..20ffc7c5fe0 100644 --- a/gix-index/src/decode/entries.rs +++ b/gix-index/src/decode/entries.rs @@ -4,7 +4,7 @@ use gix_error::OptionExt; use crate::{ Entry, Version, - decode::{self, header}, + decode::header, entry, util::{read_u32, split_at_byte_exclusive, var_int}, }; @@ -99,7 +99,7 @@ pub fn chunk<'a>( num_entries: u32, object_hash: gix_hash::Kind, version: Version, -) -> Result<(Outcome, &'a [u8]), decode::Error> { +) -> Result<(Outcome, &'a [u8]), gix_error::Exn> { let mut is_sparse = false; let has_delta_paths = version == Version::V4; let mut prev_path = None; diff --git a/gix-index/src/decode/header.rs b/gix-index/src/decode/header.rs index bdacef74ec0..774a6c30012 100644 --- a/gix-index/src/decode/header.rs +++ b/gix-index/src/decode/header.rs @@ -4,13 +4,7 @@ use crate::{Version, util::from_be_u32}; pub(crate) const SIGNATURE: &[u8] = b"DIRC"; -mod error { - /// The error produced when failing to decode an index header. - pub type Error = gix_error::Exn; -} -pub use error::Error; - -pub(crate) fn decode(data: &[u8], object_hash: gix_hash::Kind) -> Result<(Version, u32, &[u8]), Error> { +pub(crate) fn decode(data: &[u8], object_hash: gix_hash::Kind) -> Result<(Version, u32, &[u8]), gix_error::Exn> { use gix_error::ErrorExt; if data.len() < (3 * 4) + object_hash.len_in_bytes() { diff --git a/gix-index/src/decode/mod.rs b/gix-index/src/decode/mod.rs index b1509ebb361..9dd8028b89a 100644 --- a/gix-index/src/decode/mod.rs +++ b/gix-index/src/decode/mod.rs @@ -6,8 +6,6 @@ mod entries; /// pub mod header; -/// The error returned by [`State::from_bytes()`][crate::State::from_bytes()]. -pub type Error = gix_error::Exn; use gix_error::{CorruptionError, ErrorExt, ResourceExhaustionError, ResourceExhaustionKind, ResultExt, message}; use gix_features::parallel::InOrderIter; @@ -48,7 +46,7 @@ impl State { expected_checksum, alloc_limit_bytes, }: Options, - ) -> Result<(Self, Option), Error> { + ) -> Result<(Self, Option), gix_error::Exn> { let _span = gix_features::trace::detail!("gix_index::State::from_bytes()", options = ?_options); let (version, num_entries, post_header_data) = header::decode(data, object_hash)?; let start_of_extensions = extension::end_of_index_entry::decode(data, object_hash) @@ -134,7 +132,7 @@ impl State { )?; is_sparse |= chunk_is_sparse; } - Ok::<_, Error>(( + Ok::<_, gix_error::Exn>(( id, EntriesOutcome { entries, @@ -267,7 +265,7 @@ struct EntriesOutcome { pub is_sparse: bool, } -fn vec_with_capacity(capacity: usize) -> Result, Error> { +fn vec_with_capacity(capacity: usize) -> Result, gix_error::Exn> { let mut vec = Vec::new(); vec.try_reserve(capacity) .or_raise_erased(|| message("Index data would require more memory than can be reserved"))?; @@ -280,7 +278,7 @@ fn entries( num_entries: u32, object_hash: gix_hash::Kind, version: Version, -) -> Result<(EntriesOutcome, &[u8]), Error> { +) -> Result<(EntriesOutcome, &[u8]), gix_error::Exn> { let mut entries = vec_with_capacity(num_entries as usize)?; let mut path_backing = vec_with_capacity(path_backing_buffer_size)?; entries::chunk( @@ -333,13 +331,13 @@ pub(crate) fn stat(data: &[u8]) -> Option<(entry::Stat, &[u8])> { )) } -fn ensure_in_alloc_limit(size: usize, alloc_limit_bytes: Option) -> Result<(), Error> { +fn ensure_in_alloc_limit(size: usize, alloc_limit_bytes: Option) -> Result<(), gix_error::Exn> { if alloc_limit_bytes.is_some_and(|limit| size > limit) { return Err(allocation_error(ResourceExhaustionKind::AllocationLimit)); } Ok(()) } -fn allocation_error(kind: ResourceExhaustionKind) -> Error { +fn allocation_error(kind: ResourceExhaustionKind) -> gix_error::Exn { ResourceExhaustionError::new(kind, "Index data would require more memory than can be reserved").raise_erased() } diff --git a/gix-index/src/extension/decode.rs b/gix-index/src/extension/decode.rs index 6773563ef81..a8c05a14c74 100644 --- a/gix-index/src/extension/decode.rs +++ b/gix-index/src/extension/decode.rs @@ -6,17 +6,11 @@ pub(crate) fn header(data: &[u8]) -> (Signature, u32, &[u8]) { (signature.try_into().unwrap(), from_be_u32(size), data) } -mod error { - /// The error returned when decoding extensions. - pub type Error = gix_error::Exn; -} -pub use error::Error; - pub(crate) fn all( maybe_beginning_of_extensions: &[u8], object_hash: gix_hash::Kind, alloc_limit_bytes: Option, -) -> Result<(Outcome, &[u8]), Error> { +) -> Result<(Outcome, &[u8]), gix_error::Exn> { use gix_error::{ErrorExt, ResultExt, message}; let mut ext_iter = match extension::Iter::new_without_checksum(maybe_beginning_of_extensions, object_hash) { diff --git a/gix-index/src/extension/end_of_index_entry/decode.rs b/gix-index/src/extension/end_of_index_entry/decode.rs index a827388f191..5a47af70dae 100644 --- a/gix-index/src/extension/end_of_index_entry/decode.rs +++ b/gix-index/src/extension/end_of_index_entry/decode.rs @@ -13,7 +13,7 @@ use crate::{ /// stored prior to this one to assure they are correct. /// /// If the checksum wasn't matched, we will ignore this extension entirely. -pub fn decode(data: &[u8], object_hash: gix_hash::Kind) -> Result, gix_hash::hasher::Error> { +pub fn decode(data: &[u8], object_hash: gix_hash::Kind) -> Result, gix_error::CorruptionError> { let hash_len = object_hash.len_in_bytes(); if data.len() < MIN_SIZE_WITH_HEADER + hash_len { return Ok(None); diff --git a/gix-index/src/extension/end_of_index_entry/write.rs b/gix-index/src/extension/end_of_index_entry/write.rs index 3adece3dc75..bf0dc9a9c18 100644 --- a/gix-index/src/extension/end_of_index_entry/write.rs +++ b/gix-index/src/extension/end_of_index_entry/write.rs @@ -11,7 +11,7 @@ pub fn write_to( object_hash: gix_hash::Kind, offset_to_extensions: u32, prior_extensions: impl IntoIterator, -) -> Result<(), gix_hash::io::Error> { +) -> Result<(), gix_error::Exn> { out.write_all(&SIGNATURE).map_err(gix_hash::io::from_std_io)?; let extension_size: u32 = 4 + object_hash.len_in_bytes() as u32; out.write_all(&extension_size.to_be_bytes()) diff --git a/gix-index/src/extension/link.rs b/gix-index/src/extension/link.rs index 8673efdf9af..aff9adeec20 100644 --- a/gix-index/src/extension/link.rs +++ b/gix-index/src/extension/link.rs @@ -12,13 +12,10 @@ pub struct Bitmaps { pub replace: gix_bitmap::ewah::Vec, } -/// -pub mod decode { - /// The error returned when decoding link extensions. - pub type Error = gix_error::Exn; -} - -pub(crate) fn decode(data: &[u8], object_hash: gix_hash::Kind) -> Result { +pub(crate) fn decode( + data: &[u8], + object_hash: gix_hash::Kind, +) -> Result> { use gix_error::{ErrorExt, OptionExt, ResultExt}; let (id, data) = data @@ -55,7 +52,7 @@ impl Link { object_hash: gix_hash::Kind, skip_hash: bool, options: crate::decode::Options, - ) -> Result<(), crate::file::init::Error> { + ) -> Result<(), gix_error::Exn> { use gix_error::ErrorExt; let corrupt = |message| gix_error::CorruptionError::new(message).raise_erased(); diff --git a/gix-index/src/extension/tree/verify.rs b/gix-index/src/extension/tree/verify.rs index f3935a63fe1..d1312dfcc26 100644 --- a/gix-index/src/extension/tree/verify.rs +++ b/gix-index/src/extension/tree/verify.rs @@ -6,18 +6,19 @@ use gix_object::FindExt; use crate::extension::Tree; -/// The error returned by [`Tree::verify()`][crate::extension::Tree::verify()]. -pub type Error = gix_error::Exn; - impl Tree { /// Validate the correctness of this instance. If `use_objects` is true, then `objects` will be used to access all objects. - pub fn verify(&self, use_objects: bool, objects: impl gix_object::Find) -> Result<(), Error> { + pub fn verify( + &self, + use_objects: bool, + objects: impl gix_object::Find, + ) -> Result<(), gix_error::Exn> { fn verify_recursive( parent_id: gix_hash::ObjectId, children: &[Tree], mut object_buf: Option<&mut Vec>, objects: &impl gix_object::Find, - ) -> Result, Error> { + ) -> Result, gix_error::Exn> { if children.is_empty() { return Ok(None); } @@ -115,7 +116,10 @@ impl Tree { /// /// This is a cheap heuristic: it doesn't prove each cached subtree count matches its actual path range, /// but no TREE node can describe more entries than the entire index contains. - pub(crate) fn verify_entries_count(&self, num_index_entries: usize) -> Result<(), Error> { + pub(crate) fn verify_entries_count( + &self, + num_index_entries: usize, + ) -> Result<(), gix_error::Exn> { if let Some(actual) = self.num_entries && actual as usize > num_index_entries { @@ -145,7 +149,7 @@ mod tests { &self, _id: &gix_hash::oid, _buffer: &'a mut Vec, - ) -> Result>, gix_object::find::Error> { + ) -> Result>, gix_error::Exn> { Ok(Some(gix_object::Data::new( b"40000 child\0", gix_object::Kind::Tree, @@ -171,6 +175,6 @@ mod tests { let err = tree.verify(true, MalformedTree).expect_err("malformed entry must fail"); assert_eq!(err.to_string(), format!("Could not decode an entry in tree {root_id}")); - assert!(err.downcast_any_ref::().is_some()); + assert!(err.downcast_any_ref::().is_some()); } } diff --git a/gix-index/src/file/init.rs b/gix-index/src/file/init.rs index aab4e4dc9f1..cdbaf8c3179 100644 --- a/gix-index/src/file/init.rs +++ b/gix-index/src/file/init.rs @@ -6,9 +6,6 @@ use gix_error::{ResultExt, message}; use crate::{File, State, decode, extension}; -/// The error returned by [File::at()][File::at()]. -pub type Error = gix_error::Exn; - /// A failure to open an index file, retaining its path to distinguish primary and shared indexes. #[derive(Debug)] pub struct OpenError { @@ -42,7 +39,7 @@ impl File { object_hash: gix_hash::Kind, skip_hash: bool, options: decode::Options, - ) -> Result { + ) -> Result { let path = path.into(); Ok(match Self::at(&path, object_hash, skip_hash, options) { Ok(f) => f, @@ -68,7 +65,7 @@ impl File { object_hash: gix_hash::Kind, skip_hash: bool, options: decode::Options, - ) -> Result { + ) -> Result { let _span = gix_features::trace::detail!("gix_index::File::at()"); let path = path.into(); let (data, mtime) = { diff --git a/gix-index/src/file/verify.rs b/gix-index/src/file/verify.rs index 9af3e54da41..c6608bf4a18 100644 --- a/gix-index/src/file/verify.rs +++ b/gix-index/src/file/verify.rs @@ -2,15 +2,9 @@ use std::sync::atomic::AtomicBool; use crate::File; -mod error { - /// The error returned by [File::verify_integrity()][super::File::verify_integrity()]. - pub type Error = gix_error::Exn; -} -pub use error::Error; - impl File { /// Verify the integrity of the index to assure its consistency. - pub fn verify_integrity(&self) -> Result<(), Error> { + pub fn verify_integrity(&self) -> Result<(), gix_error::Exn> { use gix_error::{ResultExt, message}; let _span = gix_features::trace::coarse!("gix_index::File::verify_integrity()"); diff --git a/gix-index/src/file/write.rs b/gix-index/src/file/write.rs index fc7a8a1bc33..b23ee337095 100644 --- a/gix-index/src/file/write.rs +++ b/gix-index/src/file/write.rs @@ -1,8 +1,5 @@ use crate::{File, Version, write}; -/// The error produced by [`File::write()`]. -pub type Error = gix_error::Exn; - impl File { /// Write the index to `out` with `options`, to be readable by [`File::at()`], returning the version that was actually written /// to retain all information of this index. @@ -14,7 +11,7 @@ impl File { &self, mut out: impl std::io::Write, options: write::Options, - ) -> Result<(Version, gix_hash::ObjectId), gix_hash::io::Error> { + ) -> Result<(Version, gix_hash::ObjectId), gix_error::Exn> { let _span = gix_features::trace::detail!("gix_index::File::write_to()", skip_hash = options.skip_hash); let (version, hash) = if options.skip_hash { let out: &mut dyn std::io::Write = &mut out; @@ -55,7 +52,7 @@ impl File { /// ``` /// /// [issue #2421]: https://github.com/GitoxideLabs/gitoxide/issues/2421 - pub fn write(&mut self, options: write::Options) -> Result<(), Error> { + pub fn write(&mut self, options: write::Options) -> Result<(), gix_error::Exn> { use gix_error::{ErrorExt, ResultExt, message}; let _span = gix_features::trace::detail!("gix_index::File::write()", path = ?self.path); diff --git a/gix-index/src/init.rs b/gix-index/src/init.rs index f5117f00e82..a6e42a459e2 100644 --- a/gix-index/src/init.rs +++ b/gix-index/src/init.rs @@ -12,9 +12,6 @@ pub mod from_tree { entry::{Flags, Mode, Stat}, }; - /// The error returned by [State::from_tree()]. - pub type Error = gix_error::Exn; - /// Initialization impl State { /// Return a new and empty in-memory index assuming the given `object_hash`. @@ -55,7 +52,7 @@ pub mod from_tree { tree: &gix_hash::oid, objects: Find, validate: gix_validate::path::component::Options, - ) -> Result + ) -> Result where Find: gix_object::Find, { diff --git a/gix-index/src/verify.rs b/gix-index/src/verify.rs index 2ca132663fe..f66ffbbd79b 100644 --- a/gix-index/src/verify.rs +++ b/gix-index/src/verify.rs @@ -2,21 +2,9 @@ use std::cmp::Ordering; use crate::State; -/// -pub mod entries { - /// The error returned by [`State::verify_entries()`][crate::State::verify_entries()]. - pub type Error = gix_error::Exn; -} - -/// -pub mod extensions { - /// The error returned by [`State::verify_extensions()`][crate::State::verify_extensions()]. - pub type Error = gix_error::Exn; -} - impl State { /// Assure our entries are consistent. - pub fn verify_entries(&self) -> Result<(), entries::Error> { + pub fn verify_entries(&self) -> Result<(), gix_error::Exn> { use gix_error::ErrorExt; let _span = gix_features::trace::coarse!("gix_index::File::verify_entries()"); @@ -40,7 +28,11 @@ impl State { } /// Note: `objects` cannot be `Option` as we can't call it with a closure then due to the indirection through `Some`. - pub fn verify_extensions(&self, use_find: bool, objects: impl gix_object::Find) -> Result<(), extensions::Error> { + pub fn verify_extensions( + &self, + use_find: bool, + objects: impl gix_object::Find, + ) -> Result<(), gix_error::Exn> { if let Some(tree) = self.tree() { tree.verify(use_find, objects)?; tree.verify_entries_count(self.entries.len())?; diff --git a/gix-index/src/write.rs b/gix-index/src/write.rs index 03c12981d15..0a8f1225480 100644 --- a/gix-index/src/write.rs +++ b/gix-index/src/write.rs @@ -71,7 +71,7 @@ impl State { extensions, skip_hash: _, }: Options, - ) -> Result { + ) -> Result { let _span = gix_features::trace::detail!("gix_index::State::write()"); let version = self.detect_required_version(); diff --git a/gix-index/tests/index/file/read.rs b/gix-index/tests/index/file/read.rs index 0eb5df783bd..901d2550f49 100644 --- a/gix-index/tests/index/file/read.rs +++ b/gix-index/tests/index/file/read.rs @@ -22,7 +22,7 @@ pub(crate) fn loose_file(name: &str) -> gix_index::File { verify(file) } -pub(crate) fn try_file(name: &str, needs_archive: bool) -> Result { +pub(crate) fn try_file(name: &str, needs_archive: bool) -> Result { let path = if needs_archive { crate::fixture_index_path_needs_archive(name) } else { diff --git a/gix-index/tests/index/fuzzed.rs b/gix-index/tests/index/fuzzed.rs index 58981e009f1..4f52d1480f4 100644 --- a/gix-index/tests/index/fuzzed.rs +++ b/gix-index/tests/index/fuzzed.rs @@ -1,7 +1,7 @@ use filetime::FileTime; use std::path::PathBuf; -fn decode_fuzzed(data: &[u8]) -> Result<(gix_index::State, Option), gix_index::decode::Error> { +fn decode_fuzzed(data: &[u8]) -> Result<(gix_index::State, Option), gix_error::Exn> { gix_index::State::from_bytes( data, FileTime::from_unix_time(0, 0), @@ -10,7 +10,7 @@ fn decode_fuzzed(data: &[u8]) -> Result<(gix_index::State, Option bool { +fn is_corruption_with(err: &gix_error::Exn, prefix: &str) -> bool { err.downcast_any_ref::() .is_some_and(|err| err.message.starts_with(prefix)) } diff --git a/gix-lock/src/acquire.rs b/gix-lock/src/acquire.rs index a860b66b4a3..609140c3cb9 100644 --- a/gix-lock/src/acquire.rs +++ b/gix-lock/src/acquire.rs @@ -41,9 +41,6 @@ impl From for Fail { } } -/// The error returned when acquiring a [`File`] or [`Marker`]. -pub type Error = gix_error::Exn; - impl File { /// Create a writable lock file with failure `mode` whose content will eventually overwrite the given resource `at_path`. /// @@ -71,7 +68,7 @@ impl File { boundary_directory: Option, resolve_resource: Option<&dyn Fn(&Path) -> PathBuf>, adjust_permissions: Option<&dyn Fn(std::fs::Permissions) -> std::fs::Permissions>, - ) -> Result { + ) -> Result { let resolve_resource = resolve_resource.unwrap_or(&keep_resource); let (resource_path, lock_path, handle) = lock_with_mode( at_path.as_ref(), @@ -106,7 +103,7 @@ impl File { at_path: impl AsRef, mode: Fail, boundary_directory: Option, - ) -> Result { + ) -> Result { Self::acquire(at_path, mode, boundary_directory, None, None) } @@ -116,7 +113,7 @@ impl File { mode: Fail, boundary_directory: Option, make_permissions: impl Fn() -> std::fs::Permissions, - ) -> Result { + ) -> Result { let (resource_path, lock_path, handle) = lock_with_mode( at_path.as_ref(), mode, @@ -137,7 +134,7 @@ impl File { at_path: impl AsRef, mode: Fail, boundary_directory: Option, - ) -> Result { + ) -> Result { Self::acquire(at_path, mode, boundary_directory, Some(&resolve_symlink), None) } @@ -148,7 +145,7 @@ impl File { mode: Fail, boundary_directory: Option, adjust_permissions: impl Fn(std::fs::Permissions) -> std::fs::Permissions, - ) -> Result { + ) -> Result { Self::acquire( at_path, mode, @@ -177,7 +174,7 @@ impl Marker { at_path: impl AsRef, mode: Fail, boundary_directory: Option, - ) -> Result { + ) -> Result { let (resource_path, lock_path, handle) = lock_with_mode( at_path.as_ref(), mode, @@ -205,7 +202,7 @@ impl Marker { mode: Fail, boundary_directory: Option, make_permissions: impl Fn() -> std::fs::Permissions, - ) -> Result { + ) -> Result { let (resource_path, lock_path, handle) = lock_with_mode( at_path.as_ref(), mode, @@ -261,7 +258,7 @@ fn lock_with_mode( boundary_directory: Option, resolve_resource: &dyn Fn(&Path) -> PathBuf, try_lock: &dyn Fn(&Path, ContainingDirectory, AutoRemove) -> std::io::Result, -) -> Result<(PathBuf, PathBuf, T), Error> { +) -> Result<(PathBuf, PathBuf, T), gix_error::Exn> { use std::io::ErrorKind::*; let io_error = |err: std::io::Error| { err.and_raise(message("Another IO error occurred while obtaining the lock")) diff --git a/gix-lock/tests/all.rs b/gix-lock/tests/all.rs index 6a4ac2180d7..4f8a6a16513 100644 --- a/gix-lock/tests/all.rs +++ b/gix-lock/tests/all.rs @@ -1,3 +1 @@ -type Result = std::result::Result; - mod lock; diff --git a/gix-lock/tests/lock/file.rs b/gix-lock/tests/lock/file.rs index 68d16502521..1fbabd1b2e2 100644 --- a/gix-lock/tests/lock/file.rs +++ b/gix-lock/tests/lock/file.rs @@ -5,7 +5,7 @@ mod close { use gix_lock::acquire::Fail; #[test] - fn acquire_close_commit_to_existing_file() -> crate::Result { + fn acquire_close_commit_to_existing_file() -> gix_error::TestResult { let dir = tempfile::tempdir()?; let resource = dir.path().join("resource-existing.ext"); std::fs::write(&resource, b"old state")?; @@ -31,7 +31,7 @@ mod commit { use gix_lock::acquire::Fail; #[test] - fn failure_to_commit_does_return_a_registered_marker() -> crate::Result { + fn failure_to_commit_does_return_a_registered_marker() -> gix_error::TestResult { let dir = tempfile::tempdir()?; let resource = dir.path().join("resource-existing.ext"); std::fs::create_dir(&resource)?; @@ -53,7 +53,7 @@ mod commit { } #[test] - fn failure_to_commit_does_return_a_registered_file() -> crate::Result { + fn failure_to_commit_does_return_a_registered_file() -> gix_error::TestResult { let dir = tempfile::tempdir()?; let resource = dir.path().join("resource-existing.ext"); std::fs::create_dir(&resource)?; @@ -96,7 +96,7 @@ mod acquire { } #[test] - fn lock_create_dir_write_commit() -> crate::Result { + fn lock_create_dir_write_commit() -> gix_error::TestResult { let dir = tempfile::tempdir()?; let resource = dir.path().join("a").join("resource-nonexisting"); let resource_lock = resource.with_extension("lock"); @@ -127,7 +127,7 @@ mod acquire { } #[test] - fn lock_write_drop() -> crate::Result { + fn lock_write_drop() -> gix_error::TestResult { let dir = tempfile::tempdir()?; let resource = dir.path().join("resource-nonexisting.ext"); { @@ -140,7 +140,7 @@ mod acquire { #[test] #[cfg(unix)] - fn lock_following_symlinks_writes_their_target() -> crate::Result { + fn lock_following_symlinks_writes_their_target() -> gix_error::TestResult { use std::os::unix::fs::symlink; let dir = tempfile::tempdir()?; @@ -164,7 +164,7 @@ mod acquire { #[test] #[cfg(unix)] - fn lock_permissions_can_be_adjusted_after_applying_the_umask() -> crate::Result { + fn lock_permissions_can_be_adjusted_after_applying_the_umask() -> gix_error::TestResult { use std::{cell::Cell, os::unix::fs::PermissionsExt}; let dir = tempfile::tempdir()?; @@ -192,7 +192,7 @@ mod acquire { } #[test] - fn resource_path_does_not_parse_the_lock_file_name() -> crate::Result { + fn resource_path_does_not_parse_the_lock_file_name() -> gix_error::TestResult { let dir = tempfile::tempdir()?; let resource_dir = dir.path().join("resource"); std::fs::create_dir(&resource_dir)?; @@ -208,7 +208,7 @@ mod acquire { } #[test] - fn lock_non_existing_dir_fails() -> crate::Result { + fn lock_non_existing_dir_fails() -> gix_error::TestResult { let dir = tempfile::tempdir()?; let resource = dir.path().join("a").join("resource.ext"); let err = gix_lock::File::acquire_to_update_resource(&resource, fail_immediately(), None) diff --git a/gix-lock/tests/lock/marker.rs b/gix-lock/tests/lock/marker.rs index 7da61a3bad3..7bc164a656b 100644 --- a/gix-lock/tests/lock/marker.rs +++ b/gix-lock/tests/lock/marker.rs @@ -4,7 +4,7 @@ mod acquire { use gix_lock::acquire::Fail; #[test] - fn fail_mode_immediately_produces_a_descriptive_error() -> crate::Result { + fn fail_mode_immediately_produces_a_descriptive_error() -> gix_error::TestResult { let dir = tempfile::tempdir()?; let resource = dir.path().join("the-resource"); let guard = gix_lock::Marker::acquire_to_hold_resource(&resource, Fail::Immediately, None)?; @@ -24,7 +24,7 @@ mod acquire { } #[test] - fn fail_mode_after_duration_fails_after_a_given_duration_or_more() -> crate::Result { + fn fail_mode_after_duration_fails_after_a_given_duration_or_more() -> gix_error::TestResult { let dir = tempfile::tempdir()?; let resource = dir.path().join("the-resource"); let _guard = gix_lock::Marker::acquire_to_hold_resource(&resource, Fail::Immediately, None)?; @@ -71,7 +71,7 @@ mod commit { } #[test] - fn fails_for_ordinary_marker_that_was_never_writable() -> crate::Result { + fn fails_for_ordinary_marker_that_was_never_writable() -> gix_error::TestResult { let dir = tempfile::tempdir()?; let resource = dir.path().join("the-resource"); let mark = gix_lock::Marker::acquire_to_hold_resource(resource, Fail::Immediately, None)?; diff --git a/gix-mailmap/src/parse.rs b/gix-mailmap/src/parse.rs index 8e8f029f896..75441abb382 100644 --- a/gix-mailmap/src/parse.rs +++ b/gix-mailmap/src/parse.rs @@ -1,6 +1,3 @@ -/// The error returned by [`parse()`](crate::parse()). -pub type Error = gix_error::Exn; - use bstr::{BStr, ByteSlice}; use gix_error::{ErrorExt, OptionExt, ValidationError}; @@ -22,7 +19,7 @@ impl<'a> Lines<'a> { } impl<'a> Iterator for Lines<'a> { - type Item = Result, Error>; + type Item = Result, gix_error::Exn>; fn next(&mut self) -> Option { for line in self.lines.by_ref() { @@ -42,7 +39,7 @@ impl<'a> Iterator for Lines<'a> { } } -fn parse_line(line: &BStr, line_number: usize) -> Result, Error> { +fn parse_line(line: &BStr, line_number: usize) -> Result, gix_error::Exn> { let (name1, email1, rest) = parse_name_and_email(line, line_number, false)?; let (name2, email2, _rest) = parse_name_and_email(rest, line_number, true).unwrap_or((None, None, rest)); if email1.is_none() { @@ -74,11 +71,12 @@ fn parse_line(line: &BStr, line_number: usize) -> Result, Error> { }) } +#[expect(clippy::type_complexity)] fn parse_name_and_email( line: &BStr, line_number: usize, allow_empty_email: bool, -) -> Result<(Option<&'_ BStr>, Option<&'_ BStr>, &'_ BStr), Error> { +) -> Result<(Option<&'_ BStr>, Option<&'_ BStr>, &'_ BStr), gix_error::Exn> { match line.find_byte(b'<') { Some(start_bracket) => { let email = &line[start_bracket + 1..]; diff --git a/gix-mailmap/tests/parse/mod.rs b/gix-mailmap/tests/parse/mod.rs index e822f93453d..e674ce2a5b6 100644 --- a/gix-mailmap/tests/parse/mod.rs +++ b/gix-mailmap/tests/parse/mod.rs @@ -1,4 +1,4 @@ -use gix_mailmap::{Entry, parse}; +use gix_mailmap::Entry; use gix_testtools::fixture_bytes; #[test] @@ -160,7 +160,7 @@ fn line(input: &str) -> Entry<'_> { try_line(input).unwrap() } -fn try_line(input: &str) -> Result, parse::Error> { +fn try_line(input: &str) -> Result, gix_error::Exn> { let mut lines = gix_mailmap::parse(input.as_bytes()); let res = lines.next().expect("single line"); assert!(lines.next().is_none(), "only one line provided"); diff --git a/gix-merge/src/blob/pipeline.rs b/gix-merge/src/blob/pipeline.rs index 34caf9db933..e8c89f0eb4c 100644 --- a/gix-merge/src/blob/pipeline.rs +++ b/gix-merge/src/blob/pipeline.rs @@ -110,12 +110,6 @@ pub enum Data { }, } -/// -pub mod convert_to_mergeable { - /// The error returned by [Pipeline::convert_to_mergeable()](super::Pipeline::convert_to_mergeable()). - pub type Error = gix_error::Exn; -} - /// Conversion impl Pipeline { /// Convert the object at `id`, `mode`, `rela_path` and `kind`, providing access to `attributes` and `objects`. @@ -150,7 +144,7 @@ impl Pipeline { objects: &dyn gix_object::FindObjectOrHeader, convert: Mode, out: &mut Vec, - ) -> Result, convert_to_mergeable::Error> { + ) -> Result, gix_error::Exn> { use gix_error::{ErrorExt, NotFoundError, OptionExt, ResultExt, message}; if !matches!(mode, EntryKind::Blob | EntryKind::BlobExecutable) { diff --git a/gix-merge/src/blob/platform/merge.rs b/gix-merge/src/blob/platform/merge.rs index 286c9ff26db..9aad5d1e348 100644 --- a/gix-merge/src/blob/platform/merge.rs +++ b/gix-merge/src/blob/platform/merge.rs @@ -15,9 +15,6 @@ pub struct Options { pub text: builtin_driver::text::Options, } -/// The error returned by [`PlatformRef::merge()`]. -pub type Error = gix_error::Exn; - /// The product of a [`PlatformRef::prepare_external_driver()`] operation. /// /// This type allows to creation of [`std::process::Command`], ready to run, with `stderr` and `stdout` set to *inherit*, @@ -61,9 +58,6 @@ pub(super) mod inner { platform::{DriverChoice, merge}, }; - /// The error returned by [PlatformRef::prepare_external_driver()](PlatformRef::prepare_external_driver()). - pub type Error = gix_error::Exn; - /// Plumbing impl<'parent> PlatformRef<'parent> { /// Given `merge_command` and `context`, typically obtained from git-configuration, and the currently set merge-resources, @@ -89,7 +83,7 @@ pub(super) mod inner { other, }: builtin_driver::text::Labels<'_>, context: gix_command::Context, - ) -> Result { + ) -> Result { use gix_error::{OptionExt, ResultExt, ValidationError, message}; fn write_data( @@ -397,7 +391,7 @@ impl<'parent> PlatformRef<'parent> { out: &mut Vec, labels: builtin_driver::text::Labels<'_>, context: &gix_command::Context, - ) -> Result<(inner::builtin_merge::Pick, Resolution), Error> { + ) -> Result<(inner::builtin_merge::Pick, Resolution), gix_error::Exn> { use gix_error::{ErrorExt, ResultExt, message}; match self.configured_driver() { diff --git a/gix-merge/src/blob/platform/prepare_merge.rs b/gix-merge/src/blob/platform/prepare_merge.rs index c3c79ca0a15..2f48509f939 100644 --- a/gix-merge/src/blob/platform/prepare_merge.rs +++ b/gix-merge/src/blob/platform/prepare_merge.rs @@ -10,9 +10,6 @@ use crate::blob::{ platform::{DriverChoice, ResourceRef, merge}, }; -/// The error returned by [Platform::prepare_merge_state()](Platform::prepare_merge()). -pub type Error = gix_error::Exn; - /// Preparation impl Platform { /// Prepare all state needed for performing a merge, using all [previously set](Self::set_resource()) resources. @@ -27,7 +24,7 @@ impl Platform { &mut self, objects: &impl gix_object::Find, mut options: merge::Options, - ) -> Result, Error> { + ) -> Result, gix_error::Exn> { let current = self.current.as_ref().ok_or_raise(|| { message("The 'current', 'ancestor' or 'other' resource for the merge operation were not set") })?; diff --git a/gix-merge/src/blob/platform/set_resource.rs b/gix-merge/src/blob/platform/set_resource.rs index de33539d132..aaac6788c10 100644 --- a/gix-merge/src/blob/platform/set_resource.rs +++ b/gix-merge/src/blob/platform/set_resource.rs @@ -3,9 +3,6 @@ use gix_error::{ErrorExt, ResultExt, message}; use crate::blob::{Platform, ResourceKind, platform::Resource}; -/// The error returned by [Platform::set_resource](Platform::set_resource). -pub type Error = gix_error::Exn; - /// Preparation impl Platform { /// Store enough information about a resource to eventually use it in a merge, where… @@ -26,7 +23,7 @@ impl Platform { rela_path: &BStr, kind: ResourceKind, objects: &impl gix_object::FindObjectOrHeader, - ) -> Result<(), Error> { + ) -> Result<(), gix_error::Exn> { if !matches!( mode, gix_object::tree::EntryKind::Blob | gix_object::tree::EntryKind::BlobExecutable diff --git a/gix-merge/src/commit/function.rs b/gix-merge/src/commit/function.rs index 31b738dfa27..cfd7335c0fe 100644 --- a/gix-merge/src/commit/function.rs +++ b/gix-merge/src/commit/function.rs @@ -3,10 +3,7 @@ use std::borrow::Cow; use gix_error::{ErrorExt, ResultExt, message}; use gix_object::FindExt; -use crate::{ - blob::builtin_driver, - commit::{Error, Options}, -}; +use crate::{blob::builtin_driver, commit::Options}; /// Like [`tree()`](crate::tree()), but it takes only two commits, `our_commit` and `their_commit` to automatically /// compute the merge-bases among them. @@ -53,7 +50,7 @@ pub fn commit<'objects>( objects: &'objects (impl gix_object::FindObjectOrHeader + gix_object::Write), abbreviate_hash: &mut dyn FnMut(&gix_hash::oid) -> String, options: Options, -) -> Result, Error> { +) -> Result, gix_error::Exn> { let merge_bases = gix_revision::merge_base(our_commit, &[their_commit], graph) .or_raise(|| message("Failed to obtain the merge base between the two commits to be merged"))?; let mut virtual_merge_bases = Vec::new(); diff --git a/gix-merge/src/commit/mod.rs b/gix-merge/src/commit/mod.rs index 5f288641f58..6ac396864eb 100644 --- a/gix-merge/src/commit/mod.rs +++ b/gix-merge/src/commit/mod.rs @@ -1,6 +1,3 @@ -/// The error returned by [`commit()`](crate::commit()). -pub type Error = gix_error::Exn; - /// A way to configure [`commit()`](crate::commit()). #[derive(Default, Debug, Clone)] pub struct Options { diff --git a/gix-merge/src/commit/virtual_merge_base.rs b/gix-merge/src/commit/virtual_merge_base.rs index 636d7a4321e..f8e15c8035d 100644 --- a/gix-merge/src/commit/virtual_merge_base.rs +++ b/gix-merge/src/commit/virtual_merge_base.rs @@ -11,14 +11,10 @@ pub struct Outcome { pub tree_id: gix_hash::ObjectId, } -/// The error returned by [`commit::merge_base()`](crate::commit::virtual_merge_base()). -pub type Error = gix_error::Exn; - pub(super) mod function { use gix_error::{ErrorExt, ResultExt, message}; use gix_object::FindExt; - use super::Error; use crate::{ blob::builtin_driver, tree::{TreatAsUnresolved, treat_as_unresolved}, @@ -42,7 +38,7 @@ pub(super) mod function { objects: &'objects (impl gix_object::FindObjectOrHeader + gix_object::Write), abbreviate_hash: &mut dyn FnMut(&gix_hash::oid) -> String, mut options: crate::tree::Options, - ) -> Result { + ) -> Result> { let mut merged_commit_id = first_commit; others.push(second_commit); @@ -121,7 +117,7 @@ pub(super) mod function { parent_a: gix_hash::ObjectId, parent_b: gix_hash::ObjectId, tree_id: gix_hash::ObjectId, - ) -> Result { + ) -> Result> { let mut buf = Vec::new(); let commit_ref = objects .find_commit(&parent_a, &mut buf) diff --git a/gix-merge/src/tree/function/change.rs b/gix-merge/src/tree/function/change.rs index 6311be9ca7f..bfb6893d301 100644 --- a/gix-merge/src/tree/function/change.rs +++ b/gix-merge/src/tree/function/change.rs @@ -13,10 +13,7 @@ use gix_diff::{tree::recorder::Location, tree_with_rewrites::Change}; use gix_error::ResultExt; use gix_object::FindExt; -use crate::tree::{ - Error, - utils::{ChangeList, ChangeListRef, PossibleConflict, TreeNodes, track}, -}; +use crate::tree::utils::{ChangeList, ChangeListRef, PossibleConflict, TreeNodes, track}; pub(super) struct SideState { changes: ChangeList, @@ -102,7 +99,7 @@ pub(super) fn collect( diff_resource_cache: &mut gix_diff::blob::Platform, diff_state: &mut gix_diff::tree::State, rewrites: Option, -) -> Result { +) -> Result { let mut changes = Vec::new(); if base_tree != side_tree { let side_tree = objects diff --git a/gix-merge/src/tree/function/resolve.rs b/gix-merge/src/tree/function/resolve.rs index 0214ef14f0c..80d29a5c67b 100644 --- a/gix-merge/src/tree/function/resolve.rs +++ b/gix-merge/src/tree/function/resolve.rs @@ -16,7 +16,7 @@ use gix_object::{ use crate::tree::{ Conflict, ConflictIndexEntry, ConflictIndexEntryPathHint, ConflictMapping, ConflictMapping::{Original, Swapped}, - ContentMerge, Error, Options, Outcome, Resolution, ResolutionFailure, ResolveWith, + ContentMerge, Options, Outcome, Resolution, ResolutionFailure, ResolveWith, utils::{ ChangeDisposition, ChangeList, PossibleConflict, TrackedChange, apply_change, perform_blob_merge, possibly_rewritten_location, rewrite_location_with_renamed_directory, to_components, unique_path_in_tree, @@ -99,7 +99,7 @@ pub fn tree<'objects>( diff_resource_cache: &mut gix_diff::blob::Platform, blob_merge: &mut crate::blob::Platform, options: Options, -) -> Result, Error> { +) -> Result, gix_error::Exn> { let _span = gix_trace::coarse!("gix_merge::tree", ?base_tree, ?our_tree, ?their_tree, ?labels); let (mut base_buf, mut side_buf) = (Vec::new(), Vec::new()); let mut editor = { @@ -2032,7 +2032,7 @@ fn apply_change_and_mark( editor: &mut tree::Editor<'_>, change: &Change, disposition: &mut ChangeDisposition, -) -> Result<(), Error> { +) -> Result<(), gix_error::Exn> { apply_change(editor, change, None)?; *disposition = ChangeDisposition::Applied; Ok(()) @@ -2045,7 +2045,7 @@ fn apply_our_resolution( editor: &mut gix_object::tree::Editor<'_>, local_ours_disposition: &mut ChangeDisposition, local_theirs_disposition: &mut ChangeDisposition, -) -> Result<(), Error> { +) -> Result<(), gix_error::Exn> { let (ours, disposition) = match outer_side { Original => (local_ours, local_ours_disposition), Swapped => (local_theirs, local_theirs_disposition), diff --git a/gix-merge/src/tree/mod.rs b/gix-merge/src/tree/mod.rs index fc72f4fe8e8..e2313c2ef5b 100644 --- a/gix-merge/src/tree/mod.rs +++ b/gix-merge/src/tree/mod.rs @@ -1,9 +1,6 @@ use bstr::BString; use gix_diff::{Rewrites, tree_with_rewrites::Change}; -/// The error returned by [`tree()`](crate::tree()). -pub type Error = gix_error::Exn; - /// The outcome produced by [`tree()`](crate::tree()). #[derive(Clone)] pub struct Outcome<'a> { diff --git a/gix-merge/src/tree/utils.rs b/gix-merge/src/tree/utils.rs index aa40bac7c5c..c60f14ef794 100644 --- a/gix-merge/src/tree/utils.rs +++ b/gix-merge/src/tree/utils.rs @@ -19,7 +19,7 @@ use gix_object::{ use crate::{ blob::{ResourceKind, builtin_driver::binary::Pick}, tree::{ - Conflict, ConflictIndexEntry, ConflictIndexEntryPathHint, ConflictMapping, Error, Options, Resolution, + Conflict, ConflictIndexEntry, ConflictIndexEntryPathHint, ConflictMapping, Options, Resolution, ResolutionFailure, }, }; @@ -77,7 +77,7 @@ pub fn unique_path_in_tree( editor: &tree::Editor<'_>, tree: &TreeNodes, side_name: &BStr, -) -> Result { +) -> Result { let mut qualifier = BString::from("~"); qualifier.extend( side_name @@ -131,7 +131,7 @@ pub fn perform_blob_merge( (previous_location, previous_id, previous_mode): (&BString, ObjectId, EntryMode), (extra_markers, outer_side): (u8, ConflictMapping), options: &Options, -) -> Result<(ObjectId, crate::blob::Resolution), Error> { +) -> Result<(ObjectId, crate::blob::Resolution), gix_error::Exn> { if our_id == their_id { // This can happen if the merge modes are different. debug_assert_ne!( @@ -387,7 +387,7 @@ pub fn apply_change( editor: &mut tree::Editor<'_>, change: &Change, alternative_location: Option<&BString>, -) -> Result<(), tree::editor::Error> { +) -> Result<(), gix_error::Exn> { use to_components_bstring_ref as to_components; if change.entry_mode().is_tree() { return Ok(()); @@ -860,7 +860,7 @@ mod tree_nodes_tests { } #[test] - fn unique_path_qualifies_a_non_tree_parent_instead_of_looping_over_child_names() -> Result<(), Error> { + fn unique_path_qualifies_a_non_tree_parent_instead_of_looping_over_child_names() -> Result<(), gix_error::Exn> { let mut tree = TreeNodes::new(); tree.track_change( &Change::Addition { diff --git a/gix-merge/tests/merge/blob/mod.rs b/gix-merge/tests/merge/blob/mod.rs index b6897671c48..c05d43a7bd5 100644 --- a/gix-merge/tests/merge/blob/mod.rs +++ b/gix-merge/tests/merge/blob/mod.rs @@ -3,7 +3,7 @@ mod pipeline; mod platform; mod util { - use gix_object::{Write, find::Error}; + use gix_object::Write; pub type ObjectDb = gix_odb::memory::Proxy; @@ -12,7 +12,7 @@ mod util { } /// Insert `data` and return its hash. That can be used to find it again. - pub fn insert(db: &ObjectDb, data: &str) -> Result { + pub fn insert(db: &ObjectDb, data: &str) -> Result { db.write_buf(gix_object::Kind::Blob, data.as_bytes()) } } diff --git a/gix-negotiate/Cargo.toml b/gix-negotiate/Cargo.toml index baaf5c641bc..1958c9168f0 100644 --- a/gix-negotiate/Cargo.toml +++ b/gix-negotiate/Cargo.toml @@ -22,6 +22,7 @@ sha1 = ["gix-hash/sha1"] sha256 = ["gix-hash/sha256"] [dependencies] +gix-error = { version = "^0.3.2", path = "../gix-error" } gix-hash = { version = "^0.26.2", path = "../gix-hash" } gix-object = { version = "^0.64.1", path = "../gix-object" } gix-date = { version = "^0.16.0", path = "../gix-date" } diff --git a/gix-negotiate/src/consecutive.rs b/gix-negotiate/src/consecutive.rs index 5fa4b15fae9..e02660db289 100644 --- a/gix-negotiate/src/consecutive.rs +++ b/gix-negotiate/src/consecutive.rs @@ -1,7 +1,7 @@ use gix_date::SecondsSinceUnixEpoch; use gix_hash::ObjectId; -use crate::{Error, Flags, Negotiator}; +use crate::{Flags, Negotiator}; pub(crate) struct Algorithm { revs: gix_revwalk::PriorityQueue, @@ -19,7 +19,12 @@ impl Default for Algorithm { impl Algorithm { /// Add `id` to our priority queue and *add* `flags` to it. - fn add_to_queue(&mut self, id: ObjectId, mark: Flags, graph: &mut crate::Graph<'_, '_>) -> Result<(), Error> { + fn add_to_queue( + &mut self, + id: ObjectId, + mark: Flags, + graph: &mut crate::Graph<'_, '_>, + ) -> Result<(), gix_error::Exn> { let mut is_common = false; let mut has_mark = false; if let Some(commit) = graph @@ -44,7 +49,7 @@ impl Algorithm { mode: Mark, ancestors: Ancestors, graph: &mut crate::Graph<'_, '_>, - ) -> Result<(), Error> { + ) -> Result<(), gix_error::Exn> { let mut is_common = false; if let Some(commit) = graph .get_or_insert_commit(id, |data| is_common = data.flags.contains(Flags::COMMON))? @@ -89,7 +94,7 @@ impl Algorithm { } impl Negotiator for Algorithm { - fn known_common(&mut self, id: ObjectId, graph: &mut crate::Graph<'_, '_>) -> Result<(), Error> { + fn known_common(&mut self, id: ObjectId, graph: &mut crate::Graph<'_, '_>) -> Result<(), gix_error::Exn> { if graph .get(&id) .is_none_or(|commit| !commit.data.flags.contains(Flags::SEEN)) @@ -100,11 +105,11 @@ impl Negotiator for Algorithm { Ok(()) } - fn add_tip(&mut self, id: ObjectId, graph: &mut crate::Graph<'_, '_>) -> Result<(), Error> { + fn add_tip(&mut self, id: ObjectId, graph: &mut crate::Graph<'_, '_>) -> Result<(), gix_error::Exn> { self.add_to_queue(id, Flags::SEEN, graph) } - fn next_have(&mut self, graph: &mut crate::Graph<'_, '_>) -> Option> { + fn next_have(&mut self, graph: &mut crate::Graph<'_, '_>) -> Option> { loop { let id = self.revs.pop_value().filter(|_| self.non_common_revs != 0)?; let commit = graph.get_mut(&id).expect("it was added to the graph by now"); @@ -144,7 +149,11 @@ impl Negotiator for Algorithm { } } - fn in_common_with_remote(&mut self, id: ObjectId, graph: &mut crate::Graph<'_, '_>) -> Result { + fn in_common_with_remote( + &mut self, + id: ObjectId, + graph: &mut crate::Graph<'_, '_>, + ) -> Result { let known_to_be_common = graph .get(&id) .is_some_and(|commit| commit.data.flags.contains(Flags::COMMON)); diff --git a/gix-negotiate/src/lib.rs b/gix-negotiate/src/lib.rs index e4f189f4cb7..551ea9cea4c 100644 --- a/gix-negotiate/src/lib.rs +++ b/gix-negotiate/src/lib.rs @@ -125,23 +125,24 @@ pub trait Negotiator { /// Mark `id` as common between the remote and us. /// /// These ids are typically the local tips of remote tracking branches. - fn known_common(&mut self, id: gix_hash::ObjectId, graph: &mut Graph<'_, '_>) -> Result<(), Error>; + fn known_common(&mut self, id: gix_hash::ObjectId, graph: &mut Graph<'_, '_>) -> Result<(), gix_error::Exn>; /// Add `id` as starting point of a traversal across commits that aren't necessarily common between the remote and us. /// /// These tips are usually the commits of local references whose tips should lead to objects that we have in common with the remote. - fn add_tip(&mut self, id: gix_hash::ObjectId, graph: &mut Graph<'_, '_>) -> Result<(), Error>; + fn add_tip(&mut self, id: gix_hash::ObjectId, graph: &mut Graph<'_, '_>) -> Result<(), gix_error::Exn>; /// Produce the next id of an object that we want the server to know we have. It's an object we don't know we have in common or not. /// /// Returns `None` if we have exhausted all options, which might mean we have traversed the entire commit graph. - fn next_have(&mut self, graph: &mut Graph<'_, '_>) -> Option>; + fn next_have(&mut self, graph: &mut Graph<'_, '_>) -> Option>; /// Mark `id` as being common with the remote (as informed by the remote itself) and return `true` if we knew it was common already. /// /// We can assume to have already seen `id` as we were the one to inform the remote in a prior `have`. - fn in_common_with_remote(&mut self, id: gix_hash::ObjectId, graph: &mut Graph<'_, '_>) -> Result; + fn in_common_with_remote( + &mut self, + id: gix_hash::ObjectId, + graph: &mut Graph<'_, '_>, + ) -> Result; } - -/// An error that happened during any of the methods on a [`Negotiator`]. -pub type Error = gix_revwalk::graph::get_or_insert_default::Error; diff --git a/gix-negotiate/src/noop.rs b/gix-negotiate/src/noop.rs index 17140f36a25..4b2fc1e5047 100644 --- a/gix-negotiate/src/noop.rs +++ b/gix-negotiate/src/noop.rs @@ -1,23 +1,27 @@ use gix_hash::ObjectId; -use crate::{Error, Negotiator}; +use crate::Negotiator; pub(crate) struct Noop; impl Negotiator for Noop { - fn known_common(&mut self, _id: ObjectId, _graph: &mut crate::Graph<'_, '_>) -> Result<(), Error> { + fn known_common(&mut self, _id: ObjectId, _graph: &mut crate::Graph<'_, '_>) -> Result<(), gix_error::Exn> { Ok(()) } - fn add_tip(&mut self, _id: ObjectId, _graph: &mut crate::Graph<'_, '_>) -> Result<(), Error> { + fn add_tip(&mut self, _id: ObjectId, _graph: &mut crate::Graph<'_, '_>) -> Result<(), gix_error::Exn> { Ok(()) } - fn next_have(&mut self, _graph: &mut crate::Graph<'_, '_>) -> Option> { + fn next_have(&mut self, _graph: &mut crate::Graph<'_, '_>) -> Option> { None } - fn in_common_with_remote(&mut self, _id: ObjectId, _graph: &mut crate::Graph<'_, '_>) -> Result { + fn in_common_with_remote( + &mut self, + _id: ObjectId, + _graph: &mut crate::Graph<'_, '_>, + ) -> Result { Ok(false) } } diff --git a/gix-negotiate/src/skipping.rs b/gix-negotiate/src/skipping.rs index 91e1c3dffb1..421ab35aea9 100644 --- a/gix-negotiate/src/skipping.rs +++ b/gix-negotiate/src/skipping.rs @@ -1,7 +1,7 @@ use gix_date::SecondsSinceUnixEpoch; use gix_hash::ObjectId; -use crate::{Error, Flags, Metadata, Negotiator}; +use crate::{Flags, Metadata, Negotiator}; pub(crate) struct Algorithm { revs: gix_revwalk::PriorityQueue, @@ -19,7 +19,12 @@ impl Default for Algorithm { impl Algorithm { /// Add `id` to our priority queue and *add* `flags` to it. - fn add_to_queue(&mut self, id: ObjectId, mark: Flags, graph: &mut crate::Graph<'_, '_>) -> Result<(), Error> { + fn add_to_queue( + &mut self, + id: ObjectId, + mark: Flags, + graph: &mut crate::Graph<'_, '_>, + ) -> Result<(), gix_error::Exn> { let commit = graph.get_or_insert_commit(id, |entry| { entry.flags |= mark | Flags::SEEN; })?; @@ -32,7 +37,7 @@ impl Algorithm { Ok(()) } - fn mark_common(&mut self, id: ObjectId, graph: &mut crate::Graph<'_, '_>) -> Result<(), Error> { + fn mark_common(&mut self, id: ObjectId, graph: &mut crate::Graph<'_, '_>) -> Result<(), gix_error::Exn> { let mut is_common = false; if let Some(commit) = graph .get_or_insert_commit(id, |entry| { @@ -77,7 +82,7 @@ impl Algorithm { entry: Metadata, parent_id: ObjectId, graph: &mut crate::Graph<'_, '_>, - ) -> Result { + ) -> Result { let mut was_seen = false; if let Some(parent) = graph .get(&parent_id) @@ -112,7 +117,7 @@ impl Algorithm { } impl Negotiator for Algorithm { - fn known_common(&mut self, id: ObjectId, graph: &mut crate::Graph<'_, '_>) -> Result<(), Error> { + fn known_common(&mut self, id: ObjectId, graph: &mut crate::Graph<'_, '_>) -> Result<(), gix_error::Exn> { if graph .get(&id) .is_some_and(|commit| commit.data.flags.contains(Flags::SEEN)) @@ -122,7 +127,7 @@ impl Negotiator for Algorithm { self.add_to_queue(id, Flags::ADVERTISED, graph) } - fn add_tip(&mut self, id: ObjectId, graph: &mut crate::Graph<'_, '_>) -> Result<(), Error> { + fn add_tip(&mut self, id: ObjectId, graph: &mut crate::Graph<'_, '_>) -> Result<(), gix_error::Exn> { if graph .get(&id) .is_some_and(|commit| commit.data.flags.contains(Flags::SEEN)) @@ -132,7 +137,7 @@ impl Negotiator for Algorithm { self.add_to_queue(id, Flags::default(), graph) } - fn next_have(&mut self, graph: &mut crate::Graph<'_, '_>) -> Option> { + fn next_have(&mut self, graph: &mut crate::Graph<'_, '_>) -> Option> { loop { let id = self.revs.pop_value().filter(|_| self.non_common_revs != 0)?; let commit = graph.get_mut(&id).expect("it was added to the graph by now"); @@ -165,7 +170,11 @@ impl Negotiator for Algorithm { } } - fn in_common_with_remote(&mut self, id: ObjectId, graph: &mut crate::Graph<'_, '_>) -> Result { + fn in_common_with_remote( + &mut self, + id: ObjectId, + graph: &mut crate::Graph<'_, '_>, + ) -> Result { let mut was_seen = false; let known_to_be_common = graph.get(&id).is_some_and(|commit| { was_seen = commit.data.flags.contains(Flags::SEEN); diff --git a/gix-note/src/lib.rs b/gix-note/src/lib.rs index 2366b4da374..1c546d82d2e 100644 --- a/gix-note/src/lib.rs +++ b/gix-note/src/lib.rs @@ -2,7 +2,7 @@ #![forbid(unsafe_code)] #![deny(missing_docs)] -use gix_error::{CorruptionError, ErrorExt, ResultExt, ValidationError, message}; +use gix_error::{CorruptionError, ErrorExt, Exn, ResultExt, ValidationError, message}; use gix_hash::{ObjectId, oid}; use gix_object::{ Find, FindExt, Tree, Write, @@ -10,9 +10,6 @@ use gix_object::{ tree::{Editor, Entry, EntryKind, EntryMode}, }; -/// The type-erased error returned by note operations. -pub type Error = gix_error::Exn; - /// The result of changing one note mapping. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct Edit { @@ -48,7 +45,7 @@ pub struct State { impl State { /// Initialize state from `root_tree_id`, loading only its root tree from `objects`. - pub fn new(root_tree_id: ObjectId, objects: &impl Find) -> Result { + pub fn new(root_tree_id: ObjectId, objects: &impl Find) -> Result { let mut root = InternalNode::default(); let mut non_notes = Vec::new(); load_subtree( @@ -90,7 +87,7 @@ impl State { /// Fanout subtrees on the lookup path are materialized once and retained for /// subsequent operations. Entries that do not conform to Git's notes layout /// are ignored. - pub fn get(&mut self, annotated_object_id: &oid, objects: &impl Find) -> Result, Error> { + pub fn get(&mut self, annotated_object_id: &oid, objects: &impl Find) -> Result, Exn> { validate_annotated_hash_kind(self.root_tree_id.kind(), annotated_object_id)?; self.reset_on_error(|state| state.root.get(annotated_object_id, 0, objects, &mut state.non_notes)) } @@ -110,7 +107,7 @@ impl State { annotated_object_id: ObjectId, note_blob_id: ObjectId, objects: &(impl Find + Write), - ) -> Result { + ) -> Result { let previous = self.edit(annotated_object_id, Some(note_blob_id), objects)?; Ok(Edit { tree: self.write(objects)?, @@ -124,7 +121,7 @@ impl State { /// removed note. This also writes changes staged by [`Self::edit()`]. /// /// If there is no such note and no staged changes, the root is returned unchanged. - pub fn remove(&mut self, annotated_object_id: ObjectId, objects: &(impl Find + Write)) -> Result { + pub fn remove(&mut self, annotated_object_id: ObjectId, objects: &(impl Find + Write)) -> Result { let previous = self.edit(annotated_object_id, None, objects)?; Ok(Edit { tree: self.write(objects)?, @@ -145,7 +142,7 @@ impl State { annotated_object_id: ObjectId, note_blob_id: Option, objects: &impl Find, - ) -> Result, Error> { + ) -> Result, Exn> { if let Some(note_blob_id) = note_blob_id { validate_replace_hash_kinds( self.root_tree_id.kind(), @@ -181,7 +178,7 @@ impl State { /// progressive fanout and non-note preservation as [`Self::replace()`]. If there are no staged /// changes, return the current root without writing any objects. On failure, discard staged edits /// and recover from the last successfully written root. - pub fn write(&mut self, objects: &(impl Find + Write)) -> Result { + pub fn write(&mut self, objects: &(impl Find + Write)) -> Result { if !self.dirty { return Ok(self.root_tree_id); } @@ -194,7 +191,7 @@ impl State { }) } - fn reset_on_error(&mut self, operation: impl FnOnce(&mut Self) -> Result) -> Result { + fn reset_on_error(&mut self, operation: impl FnOnce(&mut Self) -> Result) -> Result { let result = operation(self); if result.is_err() { let root_tree_id = self.root_tree_id; @@ -216,7 +213,7 @@ fn validate_replace_hash_kinds( root: gix_hash::Kind, annotated_object: gix_hash::Kind, note_blob: gix_hash::Kind, -) -> Result<(), Error> { +) -> Result<(), Exn> { if annotated_object != root || note_blob != root { return Err( ValidationError::from("Notes, annotated objects, and their root tree must use the same hash kind") @@ -226,7 +223,7 @@ fn validate_replace_hash_kinds( Ok(()) } -fn validate_annotated_hash_kind(root: gix_hash::Kind, annotated_object_id: &oid) -> Result<(), Error> { +fn validate_annotated_hash_kind(root: gix_hash::Kind, annotated_object_id: &oid) -> Result<(), Exn> { if annotated_object_id.kind() != root { return Err( ValidationError::from("The annotated object and notes root tree must use the same hash kind") @@ -305,7 +302,7 @@ impl InternalNode { nibble: usize, objects: &impl Find, non_notes: &mut Vec, - ) -> Result, Error> { + ) -> Result, Exn> { if self.load_matching_subtree(annotated_object_id, nibble, objects, non_notes)? { return self.get(annotated_object_id, nibble, objects, non_notes); } @@ -335,7 +332,7 @@ impl InternalNode { nibble: usize, objects: &impl Find, non_notes: &mut Vec, - ) -> Result<(), Error> { + ) -> Result<(), Exn> { if self.load_matching_subtree(entry.key(), nibble, objects, non_notes)? { return self.insert(entry, nibble, objects, non_notes); } @@ -387,7 +384,7 @@ impl InternalNode { nibble: usize, objects: &impl Find, non_notes: &mut Vec, - ) -> Result<(), Error> { + ) -> Result<(), Exn> { let mut child = InternalNode::default(); child.insert(existing, nibble + 1, objects, non_notes)?; child.insert(entry, nibble + 1, objects, non_notes)?; @@ -401,7 +398,7 @@ impl InternalNode { nibble: usize, objects: &impl Find, non_notes: &mut Vec, - ) -> Result, Error> { + ) -> Result, Exn> { if self.load_matching_subtree(annotated_object_id, nibble, objects, non_notes)? { return self.remove(annotated_object_id, nibble, objects, non_notes); } @@ -442,7 +439,7 @@ impl InternalNode { nibble: usize, objects: &impl Find, non_notes: &mut Vec, - ) -> Result { + ) -> Result { let is_match = self.children[0] .as_deref() .is_some_and(|node| matches!(node, Node::Subtree(subtree) if subtree.contains(key))); @@ -486,7 +483,7 @@ fn load_subtree( nibble: usize, objects: &impl Find, non_notes: &mut Vec, -) -> Result<(), Error> { +) -> Result<(), Exn> { let mut buf = Vec::new(); let tree = objects .find_tree(&subtree.tree_id, &mut buf) @@ -551,7 +548,7 @@ impl InternalNode { non_notes: &mut Vec, hash: gix_hash::Kind, objects: &(impl Find + Write), - ) -> Result { + ) -> Result { let mut notes = Vec::new(); self.collect_for_write(0, 0, objects, non_notes, &mut notes)?; if non_notes.is_empty() { @@ -585,7 +582,7 @@ impl InternalNode { objects: &impl Find, non_notes: &mut Vec, notes: &mut Vec, - ) -> Result<(), Error> { + ) -> Result<(), Exn> { let fanout = if nibble.is_multiple_of(2) && nibble <= 2 * fanout && self @@ -636,7 +633,7 @@ impl InternalNode { /// This is faster than going through the tree editor, whose characteristics are useful enough /// to bear worse performance in the uncommon case where non-note entries are present. -fn write_note_entries(notes: &[TreeEntry], level: usize, objects: &impl Write) -> Result { +fn write_note_entries(notes: &[TreeEntry], level: usize, objects: &impl Write) -> Result { let mut entries = Vec::new(); let mut start = 0; while start < notes.len() { diff --git a/gix-note/tests/note.rs b/gix-note/tests/note.rs index eb04c7af958..b2936341f72 100644 --- a/gix-note/tests/note.rs +++ b/gix-note/tests/note.rs @@ -4,7 +4,7 @@ use std::{ io::{self, Read}, }; -use gix_error::ErrorExt; +use gix_error::{ErrorExt, Exn}; use gix_hash::{Kind, ObjectId, oid}; use gix_object::{ FindExt, Tree, Write, @@ -21,7 +21,7 @@ mod one_shot { root_tree_id: ObjectId, annotated_object_id: &oid, objects: &impl gix_object::Find, - ) -> Result, gix_note::Error> { + ) -> Result, Exn> { let mut state = gix_note::State::new(root_tree_id, objects)?; state.get(annotated_object_id, objects) } @@ -31,7 +31,7 @@ mod one_shot { annotated_object_id: ObjectId, note_blob_id: ObjectId, objects: &(impl gix_object::Find + Write), - ) -> Result { + ) -> Result { let mut state = gix_note::State::new(root_tree_id, objects)?; state.replace(annotated_object_id, note_blob_id, objects) } @@ -40,7 +40,7 @@ mod one_shot { root_tree_id: ObjectId, annotated_object_id: ObjectId, objects: &(impl gix_object::Find + Write), - ) -> Result { + ) -> Result { let mut state = gix_note::State::new(root_tree_id, objects)?; state.remove(annotated_object_id, objects) } @@ -65,7 +65,7 @@ impl CountingObjectDb { } } - fn maybe_fail_write(&self) -> Result<(), gix_object::write::Error> { + fn maybe_fail_write(&self) -> Result<(), Exn> { if self.fail_next_write.replace(false) { return Err(io::Error::other("injected write failure").raise_erased()); } @@ -74,11 +74,7 @@ impl CountingObjectDb { } impl gix_object::Find for CountingObjectDb { - fn try_find<'a>( - &self, - id: &gix_hash::oid, - buffer: &'a mut Vec, - ) -> Result>, gix_object::find::Error> { + fn try_find<'a>(&self, id: &gix_hash::oid, buffer: &'a mut Vec) -> Result>, Exn> { self.reads.set(self.reads.get() + 1); if self.fail_next_read.replace(false) { return Err(io::Error::other("injected read failure").raise_erased()); @@ -88,23 +84,13 @@ impl gix_object::Find for CountingObjectDb { } impl gix_object::Write for CountingObjectDb { - fn write_buf_with_known_id( - &self, - kind: gix_object::Kind, - from: &[u8], - id: ObjectId, - ) -> Result { + fn write_buf_with_known_id(&self, kind: gix_object::Kind, from: &[u8], id: ObjectId) -> Result { self.writes.set(self.writes.get() + 1); self.maybe_fail_write()?; self.inner.write_buf_with_known_id(kind, from, id) } - fn write_stream( - &self, - kind: gix_object::Kind, - size: u64, - from: &mut dyn Read, - ) -> Result { + fn write_stream(&self, kind: gix_object::Kind, size: u64, from: &mut dyn Read) -> Result { self.writes.set(self.writes.get() + 1); self.maybe_fail_write()?; self.inner.write_stream(kind, size, from) @@ -116,7 +102,7 @@ impl gix_object::Write for CountingObjectDb { size: u64, from: &mut dyn Read, id: ObjectId, - ) -> Result { + ) -> Result { self.writes.set(self.writes.get() + 1); self.maybe_fail_write()?; self.inner.write_stream_with_known_id(kind, size, from, id) diff --git a/gix-object/benches/edit_tree.rs b/gix-object/benches/edit_tree.rs index a721b1b80a2..659cd72d9fd 100644 --- a/gix-object/benches/edit_tree.rs +++ b/gix-object/benches/edit_tree.rs @@ -133,10 +133,7 @@ criterion_main!(benches); type ObjectDb = Rc>; -fn new_inmemory_writes() -> ( - ObjectDb, - impl FnMut(&Tree) -> Result, -) { +fn new_inmemory_writes() -> (ObjectDb, impl FnMut(&Tree) -> Result) { let odb = Rc::new(gix_odb::memory::Proxy::new( gix_object::find::Never, gix_hash::Kind::Sha1, diff --git a/gix-object/src/commit/mod.rs b/gix-object/src/commit/mod.rs index 43f2df6360d..1962a6ab0b6 100644 --- a/gix-object/src/commit/mod.rs +++ b/gix-object/src/commit/mod.rs @@ -47,7 +47,10 @@ mod write; impl<'a> CommitRef<'a> { /// Deserialize a commit from the given `data` bytes while avoiding most allocations, using `object_hash` to know /// what kind of hash to expect for validation. - pub fn from_bytes(mut data: &'a [u8], object_hash: gix_hash::Kind) -> Result, crate::decode::Error> { + pub fn from_bytes( + mut data: &'a [u8], + object_hash: gix_hash::Kind, + ) -> Result, gix_error::ValidationError> { let input = &mut data; match decode::commit(input, object_hash) { Ok(tag) => Ok(tag), @@ -81,14 +84,14 @@ impl<'a> CommitRef<'a> { /// Return the author, with whitespace trimmed. /// /// This is different from the `author` field which may contain whitespace. - pub fn author(&self) -> Result, crate::decode::Error> { + pub fn author(&self) -> Result, gix_error::ValidationError> { parse_signature(self.author).map(|signature| signature.trim()) } /// Return the committer, with whitespace trimmed. /// /// This is different from the `committer` field which may contain whitespace. - pub fn committer(&self) -> Result, crate::decode::Error> { + pub fn committer(&self) -> Result, gix_error::ValidationError> { parse_signature(self.committer).map(|signature| signature.trim()) } @@ -98,7 +101,7 @@ impl<'a> CommitRef<'a> { } /// Returns the time at which this commit was created, or a default time if it could not be parsed. - pub fn time(&self) -> Result { + pub fn time(&self) -> Result { parse_signature(self.committer).map(|signature| signature.time().unwrap_or_default()) } } @@ -106,12 +109,12 @@ impl<'a> CommitRef<'a> { /// Conversion impl CommitRef<'_> { /// Copy all fields of this instance into a fully owned commit, consuming this instance. - pub fn into_owned(self) -> Result { + pub fn into_owned(self) -> Result { self.try_into() } /// Copy all fields of this instance into a fully owned commit, internally cloning this instance. - pub fn to_owned(self) -> Result { + pub fn to_owned(self) -> Result { self.try_into() } } @@ -165,7 +168,7 @@ where /// /// A merge tag is a tag object embedded within the respective header field of a commit, making /// it a child object of sorts. - pub fn mergetags(self) -> impl Iterator, crate::decode::Error>> { + pub fn mergetags(self) -> impl Iterator, gix_error::ValidationError>> { let hash_kind = self.hash_kind; self.find_all("mergetag").map(move |b| TagRef::from_bytes(b, hash_kind)) } diff --git a/gix-object/src/commit/ref_iter.rs b/gix-object/src/commit/ref_iter.rs index 9132e473ada..2b34f214a5c 100644 --- a/gix-object/src/commit/ref_iter.rs +++ b/gix-object/src/commit/ref_iter.rs @@ -53,7 +53,7 @@ impl<'a> CommitRefIter<'a> { pub fn signature( data: &'a [u8], hash_kind: gix_hash::Kind, - ) -> Result, SignedData<'a>)>, crate::decode::Error> { + ) -> Result, SignedData<'a>)>, gix_error::ValidationError> { let mut signature_and_range = None; let raw_tokens = CommitRefIterRaw { @@ -84,7 +84,7 @@ impl<'a> CommitRefIter<'a> { /// Errors are coerced into options, hiding whether there was an error or not. The caller should assume an error if they /// call the method as intended. Such a squelched error cannot be recovered unless the objects data is retrieved and parsed again. /// `next()`. - pub fn tree_id(&mut self) -> Result { + pub fn tree_id(&mut self) -> Result { let tree_id = self.next().ok_or_else(missing_field)??; Token::try_into_id(tree_id).ok_or_else(missing_field) } @@ -112,7 +112,7 @@ impl<'a> CommitRefIter<'a> { } /// Returns the committer signature if there is no decoding error. - pub fn committer(mut self) -> Result, crate::decode::Error> { + pub fn committer(mut self) -> Result, gix_error::ValidationError> { self.find_map(|t| match t { Ok(Token::Committer { signature }) => Some(Ok(signature)), Err(err) => Some(Err(err)), @@ -124,7 +124,7 @@ impl<'a> CommitRefIter<'a> { /// Returns the author signature if there is no decoding error. /// /// It may contain white space surrounding it, and is exactly as parsed. - pub fn author(mut self) -> Result, crate::decode::Error> { + pub fn author(mut self) -> Result, gix_error::ValidationError> { self.find_map(|t| match t { Ok(Token::Author { signature }) => Some(Ok(signature)), Err(err) => Some(Err(err)), @@ -137,7 +137,7 @@ impl<'a> CommitRefIter<'a> { /// /// It may contain white space surrounding it, and is exactly as // parsed. - pub fn message(mut self) -> Result<&'a BStr, crate::decode::Error> { + pub fn message(mut self) -> Result<&'a BStr, gix_error::ValidationError> { self.find_map(|t| match t { Ok(Token::Message(msg)) => Some(Ok(msg)), Err(err) => Some(Err(err)), @@ -148,7 +148,7 @@ impl<'a> CommitRefIter<'a> { } } -fn missing_field() -> crate::decode::Error { +fn missing_field() -> gix_error::ValidationError { crate::decode::empty_error() } @@ -158,7 +158,7 @@ impl<'a> CommitRefIter<'a> { mut i: &'a [u8], state: &mut State, hash_kind: gix_hash::Kind, - ) -> Result<(&'a [u8], Token<'a>), crate::decode::Error> { + ) -> Result<(&'a [u8], Token<'a>), gix_error::ValidationError> { let input = &mut i; match Self::next_inner_(input, state, hash_kind) { Ok(token) => Ok((*input, token)), @@ -170,7 +170,7 @@ impl<'a> CommitRefIter<'a> { input: &mut &'a [u8], state: &mut State, hash_kind: gix_hash::Kind, - ) -> Result, crate::decode::Error> { + ) -> Result, gix_error::ValidationError> { use State::*; Ok(match state { Tree => { @@ -250,7 +250,7 @@ impl<'a> CommitRefIter<'a> { } impl<'a> Iterator for CommitRefIter<'a> { - type Item = Result, crate::decode::Error>; + type Item = Result, gix_error::ValidationError>; fn next(&mut self) -> Option { if self.data.is_empty() { @@ -278,7 +278,7 @@ struct CommitRefIterRaw<'a> { } impl<'a> Iterator for CommitRefIterRaw<'a> { - type Item = Result, crate::decode::Error>; + type Item = Result, gix_error::ValidationError>; fn next(&mut self) -> Option { if self.data.is_empty() { diff --git a/gix-object/src/data.rs b/gix-object/src/data.rs index 9559f3b0b91..2ad52857eb7 100644 --- a/gix-object/src/data.rs +++ b/gix-object/src/data.rs @@ -16,7 +16,7 @@ impl<'a> Data<'a> { /// /// **Note** that [mutable, decoded objects][crate::Object] can be created from [`Data`] /// using [`crate::ObjectRef::into_owned()`]. - pub fn decode(&self) -> Result, crate::decode::Error> { + pub fn decode(&self) -> Result, gix_error::ValidationError> { Ok(match self.kind { Kind::Tree => ObjectRef::Tree(TreeRef::from_bytes(self.data, self.object_hash)?), Kind::Blob => ObjectRef::Blob(BlobRef { data: self.data }), @@ -57,14 +57,14 @@ impl<'a> Data<'a> { pub mod verify { use gix_error::{CorruptionError, ErrorExt, ResultExt}; - /// Returned by [`crate::Data::verify_checksum()`] - pub type Error = gix_error::Exn; - impl crate::Data<'_> { /// Compute the checksum of `self` and compare it with the `expected` hash. - /// If the hashes do not match, an [`Error`] is returned, containing the actual + /// If the hashes do not match, an [`gix_error::Exn`] is returned, containing the actual /// hash of `self`. - pub fn verify_checksum(&self, expected: &gix_hash::oid) -> Result { + pub fn verify_checksum( + &self, + expected: &gix_hash::oid, + ) -> Result> { let actual = crate::compute_hash(expected.kind(), self.kind, self.data) .or_raise(|| CorruptionError::new("Failed to hash object"))?; actual.verify(expected).map_err(ErrorExt::raise)?; diff --git a/gix-object/src/encode.rs b/gix-object/src/encode.rs index 68b0958b38a..1fb05c2ff6a 100644 --- a/gix-object/src/encode.rs +++ b/gix-object/src/encode.rs @@ -3,9 +3,6 @@ use std::io::{self, Write}; use bstr::ByteSlice; -/// An error returned when object encoding fails. -pub type Error = gix_error::ValidationError; - macro_rules! check { ($e: expr) => { $e.expect("Writing to a Vec should never fail.") @@ -69,10 +66,12 @@ pub(crate) fn trusted_header_id( pub(crate) fn header_field(name: &[u8], value: &[u8], out: &mut dyn io::Write) -> io::Result<()> { if value.is_empty() { - return Err(io::Error::other(Error::new("Header values must not be empty"))); + return Err(io::Error::other(gix_error::ValidationError::new( + "Header values must not be empty", + ))); } if value.find(NL).is_some() { - return Err(io::Error::other(Error::new_with_input( + return Err(io::Error::other(gix_error::ValidationError::new_with_input( "Newlines are not allowed in header values", value, ))); diff --git a/gix-object/src/find.rs b/gix-object/src/find.rs index 74750d77e90..e24069c6e85 100644 --- a/gix-object/src/find.rs +++ b/gix-object/src/find.rs @@ -1,25 +1,5 @@ use gix_error::ResultExt; -/// The error type returned by the [`Find`](crate::Find) trait. -pub type Error = gix_error::Exn; -/// -pub mod existing { - /// The error returned by the [`find(…)`][crate::FindExt::find()] trait methods. - pub type Error = gix_error::Exn; -} - -/// -pub mod existing_object { - /// The error returned by the various [`find_*()`][crate::FindExt::find_commit()] trait methods. - pub type Error = gix_error::Exn; -} - -/// -pub mod existing_iter { - /// The error returned by the various [`find_*_iter()`][crate::FindExt::find_commit_iter()] trait methods. - pub type Error = gix_error::Exn; -} - /// An implementation of object access traits that stores nothing and finds nothing. /// Use [`Never::panic_on_access()`] to panic on object access instead. #[derive(Debug, Copy, Clone)] @@ -39,13 +19,17 @@ impl Never { pub struct PanicAlways; impl super::FindHeader for PanicAlways { - fn try_header(&self, _id: &gix_hash::oid) -> Result, Error> { + fn try_header(&self, _id: &gix_hash::oid) -> Result, gix_error::Exn> { panic!("object header lookups are forbidden"); } } impl super::Find for PanicAlways { - fn try_find<'a>(&self, _id: &gix_hash::oid, _buffer: &'a mut Vec) -> Result>, Error> { + fn try_find<'a>( + &self, + _id: &gix_hash::oid, + _buffer: &'a mut Vec, + ) -> Result>, gix_error::Exn> { panic!("object lookups are forbidden"); } } @@ -57,7 +41,7 @@ impl super::Exists for PanicAlways { } impl super::Write for PanicAlways { - fn write(&self, _object: &dyn crate::WriteTo) -> Result { + fn write(&self, _object: &dyn crate::WriteTo) -> Result { panic!("object writes are forbidden"); } @@ -66,7 +50,7 @@ impl super::Write for PanicAlways { _object: crate::Kind, _from: &[u8], _id: gix_hash::ObjectId, - ) -> Result { + ) -> Result { panic!("object writes are forbidden"); } @@ -75,7 +59,7 @@ impl super::Write for PanicAlways { _kind: crate::Kind, _size: u64, _from: &mut dyn std::io::Read, - ) -> Result { + ) -> Result { panic!("object writes are forbidden"); } @@ -85,19 +69,23 @@ impl super::Write for PanicAlways { _size: u64, _from: &mut dyn std::io::Read, _id: gix_hash::ObjectId, - ) -> Result { + ) -> Result { panic!("object writes are forbidden"); } } impl super::FindHeader for Never { - fn try_header(&self, _id: &gix_hash::oid) -> Result, Error> { + fn try_header(&self, _id: &gix_hash::oid) -> Result, gix_error::Exn> { Ok(None) } } impl super::Find for Never { - fn try_find<'a>(&self, _id: &gix_hash::oid, _buffer: &'a mut Vec) -> Result>, Error> { + fn try_find<'a>( + &self, + _id: &gix_hash::oid, + _buffer: &'a mut Vec, + ) -> Result>, gix_error::Exn> { Ok(None) } } @@ -109,7 +97,7 @@ impl super::Exists for Never { } impl super::Write for Never { - fn write_buf(&self, object: crate::Kind, from: &[u8]) -> Result { + fn write_buf(&self, object: crate::Kind, from: &[u8]) -> Result { crate::compute_hash(gix_hash::Kind::default(), object, from).or_erased() } @@ -118,7 +106,7 @@ impl super::Write for Never { _object: crate::Kind, _from: &[u8], id: gix_hash::ObjectId, - ) -> Result { + ) -> Result { Ok(id) } @@ -127,7 +115,7 @@ impl super::Write for Never { kind: crate::Kind, size: u64, from: &mut dyn std::io::Read, - ) -> Result { + ) -> Result { crate::compute_stream_hash( gix_hash::Kind::default(), kind, @@ -145,7 +133,7 @@ impl super::Write for Never { mut size: u64, from: &mut dyn std::io::Read, id: gix_hash::ObjectId, - ) -> Result { + ) -> Result { let mut buf = [0u8; u16::MAX as usize]; while size != 0 { let bytes = (size as usize).min(buf.len()); diff --git a/gix-object/src/kind.rs b/gix-object/src/kind.rs index 2b1baa06f3a..a71a29d1dbd 100644 --- a/gix-object/src/kind.rs +++ b/gix-object/src/kind.rs @@ -1,18 +1,15 @@ use crate::Kind; -/// The Error used in [`Kind::from_bytes()`]. -pub type Error = gix_error::ValidationError; - /// Initialization impl Kind { /// Parse a `Kind` from its serialized loose git objects. - pub fn from_bytes(s: &[u8]) -> Result { + pub fn from_bytes(s: &[u8]) -> Result { Ok(match s { b"tree" => Kind::Tree, b"blob" => Kind::Blob, b"commit" => Kind::Commit, b"tag" => Kind::Tag, - _ => return Err(Error::new_with_input("Unknown object kind", s)), + _ => return Err(gix_error::ValidationError::new_with_input("Unknown object kind", s)), }) } } diff --git a/gix-object/src/lib.rs b/gix-object/src/lib.rs index 08f89412a54..e6d48178142 100644 --- a/gix-object/src/lib.rs +++ b/gix-object/src/lib.rs @@ -63,12 +63,6 @@ pub mod data; /// pub mod find; -/// -pub mod write { - /// The error type returned by the [`Write`](crate::Write) trait. - pub type Error = gix_error::Exn; -} - mod traits; pub use traits::{Exists, Find, FindExt, FindObjectOrHeader, Header as FindHeader, HeaderExt, Write, WriteTo}; @@ -317,18 +311,12 @@ pub struct Header { /// pub mod decode { mod error { - pub(crate) fn empty_error() -> Error { - Error::new("object parsing failed") + pub(crate) fn empty_error() -> gix_error::ValidationError { + gix_error::ValidationError::new("object parsing failed") } - - /// A type to indicate any error occurred during parsing. - pub type Error = gix_error::ValidationError; } - pub use error::Error; - pub(crate) use error::empty_error; - /// Returned by [`loose_header()`] - pub type LooseHeaderDecodeError = gix_error::Exn; + pub(crate) use error::empty_error; use bstr::ByteSlice; use gix_error::{ErrorExt, ResultExt, ValidationError}; @@ -336,7 +324,7 @@ pub mod decode { /// ([`kind`](super::Kind), `size`, `consumed bytes`). /// /// `size` is the uncompressed size of the payload in bytes. - pub fn loose_header(input: &[u8]) -> Result<(super::Kind, u64, usize), LooseHeaderDecodeError> { + pub fn loose_header(input: &[u8]) -> Result<(super::Kind, u64, usize), gix_error::Exn> { let kind_end = input .find_byte(0x20) .ok_or_else(|| ValidationError::new("Expected ' '").raise())?; @@ -364,7 +352,7 @@ pub fn compute_hash( hash_kind: gix_hash::Kind, object_kind: Kind, data: &[u8], -) -> Result { +) -> Result { let mut hasher = object_hasher(hash_kind, object_kind, data.len() as u64); hasher.update(data); hasher.try_finalize() @@ -382,7 +370,7 @@ pub fn compute_stream_hash( stream_len: u64, progress: &mut dyn gix_features::progress::Progress, should_interrupt: &std::sync::atomic::AtomicBool, -) -> Result { +) -> Result { let hasher = object_hasher(hash_kind, object_kind, stream_len); gix_hash::bytes_with_hasher(stream, stream_len, hasher, progress, should_interrupt) } diff --git a/gix-object/src/object/convert.rs b/gix-object/src/object/convert.rs index 8ee7a1ce0b6..48db0a5e0e3 100644 --- a/gix-object/src/object/convert.rs +++ b/gix-object/src/object/convert.rs @@ -4,7 +4,7 @@ use crate::parse::parse_signature; use crate::{Blob, BlobRef, Commit, CommitRef, Object, ObjectRef, Tag, TagRef, Tree, TreeRef, tree}; impl TryFrom> for Tag { - type Error = crate::decode::Error; + type Error = gix_error::ValidationError; fn try_from(other: TagRef<'_>) -> Result { let TagRef { @@ -28,7 +28,7 @@ impl TryFrom> for Tag { } impl TryFrom> for Commit { - type Error = crate::decode::Error; + type Error = gix_error::ValidationError; fn try_from(other: CommitRef<'_>) -> Result { let CommitRef { @@ -101,7 +101,7 @@ impl<'a> From<&'a tree::Entry> for tree::EntryRef<'a> { } impl TryFrom> for Object { - type Error = crate::decode::Error; + type Error = gix_error::ValidationError; fn try_from(v: ObjectRef<'_>) -> Result { Ok(match v { diff --git a/gix-object/src/object/mod.rs b/gix-object/src/object/mod.rs index 67c815b6303..7501c657e72 100644 --- a/gix-object/src/object/mod.rs +++ b/gix-object/src/object/mod.rs @@ -188,11 +188,12 @@ impl Object { use crate::{BlobRef, CommitRef, Kind, ObjectRef, TagRef, TreeRef, decode::loose_header}; use gix_error::{ErrorExt, ResultExt, ValidationError}; -pub type LooseDecodeError = gix_error::Exn; - impl<'a> ObjectRef<'a> { /// Deserialize an object from a loose serialisation given `data`, parsing with the provided `object_hash`. - pub fn from_loose(data: &'a [u8], hash_kind: gix_hash::Kind) -> Result, LooseDecodeError> { + pub fn from_loose( + data: &'a [u8], + hash_kind: gix_hash::Kind, + ) -> Result, gix_error::Exn> { let (kind, size, offset) = loose_header(data)?; let size = usize::try_from(size).or_raise(|| { @@ -212,7 +213,7 @@ impl<'a> ObjectRef<'a> { data: &'a [u8], kind: Kind, hash_kind: gix_hash::Kind, - ) -> Result, crate::decode::Error> { + ) -> Result, gix_error::ValidationError> { Ok(match kind { Kind::Tree => ObjectRef::Tree(TreeRef::from_bytes(data, hash_kind)?), Kind::Blob => ObjectRef::Blob(BlobRef { data }), @@ -224,14 +225,14 @@ impl<'a> ObjectRef<'a> { /// Convert the immutable object into a mutable version, consuming the source in the process. /// /// Note that this is an expensive operation. - pub fn into_owned(self) -> Result { + pub fn into_owned(self) -> Result { self.try_into() } /// Convert this immutable object into its mutable counterpart. /// /// Note that this is an expensive operation. - pub fn to_owned(&self) -> Result { + pub fn to_owned(&self) -> Result { self.clone().try_into() } } diff --git a/gix-object/src/parse.rs b/gix-object/src/parse.rs index e8c3c54d807..f3b743ad2ff 100644 --- a/gix-object/src/parse.rs +++ b/gix-object/src/parse.rs @@ -5,7 +5,7 @@ pub(crate) const SPACE: &[u8] = b" "; const SPACE_OR_NL: &[u8] = b" \n"; /// The result type shared by object parsers. -pub(crate) type ParseResult = Result; +pub(crate) type ParseResult = Result; /// Parse any multi-line object header field. /// @@ -147,6 +147,6 @@ pub(crate) fn signature_raw(i: &[u8]) -> ParseResult<&BStr> { /// /// This is a convenience wrapper around [`signature`] for callers that already /// hold byte-string data. -pub(crate) fn parse_signature(raw: &BStr) -> Result, crate::decode::Error> { +pub(crate) fn parse_signature(raw: &BStr) -> Result, gix_error::ValidationError> { signature(raw.as_ref()) } diff --git a/gix-object/src/signature/sign.rs b/gix-object/src/signature/sign.rs index 6bdc8e68610..21e58e136e9 100644 --- a/gix-object/src/signature/sign.rs +++ b/gix-object/src/signature/sign.rs @@ -29,19 +29,16 @@ pub struct Options { pub environment: Vec<(OsString, OsString)>, } -/// The error returned when signing an object. -pub type Error = gix_error::Exn; - impl CommitRef<'_> { /// Return an owned copy of this commit with its active signature replaced by a newly created one. - pub fn sign(self, options: Options) -> Result { + pub fn sign(self, options: Options) -> Result { self.into_owned().or_erased()?.sign(options) } } impl Commit { /// Return this commit with its active signature replaced by a newly created one according to `options`. - pub fn sign(mut self, options: Options) -> Result { + pub fn sign(mut self, options: Options) -> Result { let signature_field = crate::commit::signature_field_name(self.tree.kind()); self.extra_headers.retain(|(name, _)| name != signature_field); let mut payload = Vec::new(); @@ -55,14 +52,14 @@ impl Commit { impl TagRef<'_> { /// Return an owned copy of this annotated tag with its in-body signature replaced by a newly created one /// according to `options`. - pub fn sign(self, options: Options) -> Result { + pub fn sign(self, options: Options) -> Result { self.into_owned().or_erased()?.sign(options) } } impl Tag { /// Return this annotated tag with its in-body signature replaced by a newly created one according to `options`. - pub fn sign(mut self, options: Options) -> Result { + pub fn sign(mut self, options: Options) -> Result { self.signature = None; let mut payload = Vec::new(); self.write_to(&mut payload).or_erased()?; @@ -74,7 +71,7 @@ impl Tag { } } -fn sign(payload: &[u8], options: &Options) -> Result { +fn sign(payload: &[u8], options: &Options) -> Result { match options.format { Format::OpenPgp | Format::X509 => sign_gpg(payload, options), Format::Ssh => sign_ssh(payload, options), @@ -88,7 +85,7 @@ fn command(options: &Options) -> gix_command::Prepare { ) } -fn sign_gpg(payload: &[u8], options: &Options) -> Result { +fn sign_gpg(payload: &[u8], options: &Options) -> Result { if options.signing_key.is_empty() { return Err(ValidationError::new("A signing key is required").raise_erased()); } @@ -120,7 +117,7 @@ fn sign_gpg(payload: &[u8], options: &Options) -> Result { Ok(strip_cr_before_lf(output.stdout).into()) } -fn sign_ssh(payload: &[u8], options: &Options) -> Result { +fn sign_ssh(payload: &[u8], options: &Options) -> Result { if options.signing_key.is_empty() { return Err(ValidationError::new("A signing key is required").raise_erased()); } @@ -185,7 +182,7 @@ pub fn is_literal_ssh_key(key: &[u8]) -> Option<&[u8]> { } /// On Unix, creates a file with 0o600 just like Git. -fn secure_temporary_file() -> Result, Error> { +fn secure_temporary_file() -> Result, gix_error::Exn> { gix_tempfile::new( std::env::temp_dir(), gix_tempfile::ContainingDirectory::Exists, @@ -194,18 +191,21 @@ fn secure_temporary_file() -> Result, data: &[u8]) -> Result<(), Error> { +fn write_temporary( + file: &mut gix_tempfile::Handle, + data: &[u8], +) -> Result<(), gix_error::Exn> { file.with_mut(|file| file.write_all(data)) .or_raise_erased(|| message("Could not create or write a temporary signing file"))? .or_raise_erased(|| message("Could not create or write a temporary signing file")) } -fn temporary_path(file: &mut gix_tempfile::Handle) -> Result { +fn temporary_path(file: &mut gix_tempfile::Handle) -> Result { file.with_mut(|file| file.path().to_owned()) .or_raise_erased(|| message("Could not create or write a temporary signing file")) } -fn run(command: gix_command::Prepare, program: &OsStr, input: &[u8]) -> Result { +fn run(command: gix_command::Prepare, program: &OsStr, input: &[u8]) -> Result { let mut child = command .spawn() .or_raise_erased(|| message!("Could not execute signing program {program:?}"))?; diff --git a/gix-object/src/signature/verify.rs b/gix-object/src/signature/verify.rs index 8472b132688..640507f28f7 100644 --- a/gix-object/src/signature/verify.rs +++ b/gix-object/src/signature/verify.rs @@ -161,12 +161,9 @@ impl Outcome { } } -/// The error returned when verifying an object signature. -pub type Error = gix_error::Exn; - impl SignedData<'_> { /// Verify `signature` over these exact object bytes with fully resolved `options`. - pub fn verify(&self, signature: &BStr, options: Options) -> Result { + pub fn verify(&self, signature: &BStr, options: Options) -> Result { let format = Format::from_signature(signature) .ok_or_raise_erased(|| CorruptionError::new("The signature format is unsupported"))?; match options { @@ -240,7 +237,7 @@ impl SignedData<'_> { program_arguments: Vec, environment: Vec<(OsString, OsString)>, minimum_trust: TrustLevel, - ) -> Result { + ) -> Result { let mut signature_file = signature_file(signature)?; let path = signature_path(&mut signature_file)?; let mut command = prepare(&program, program_arguments, &environment); @@ -286,7 +283,7 @@ impl SignedData<'_> { revocation_file: Option, verify_time: gix_date::Time, minimum_trust: TrustLevel, - ) -> Result { + ) -> Result { let verify_time = verify_time .format(gix_date::time::CustomFormat::new("%Y%m%d%H%M%S")) .or_raise_erased(|| message("Signature time could not be formatted for SSH verification"))?; @@ -379,7 +376,7 @@ impl SignedData<'_> { Ok(outcome) } - fn run(&self, command: gix_command::Prepare, program: &OsStr) -> Result { + fn run(&self, command: gix_command::Prepare, program: &OsStr) -> Result { let mut child = command .spawn() .or_raise_erased(|| message!("Could not execute signature verifier {program:?}"))?; @@ -404,7 +401,7 @@ impl SignedData<'_> { &self, common: (&OsStr, &[OsString], &[(OsString, OsString)]), args: impl IntoIterator, - ) -> Result { + ) -> Result { let (program, program_arguments, environment) = common; self.run( prepare(program, program_arguments.iter().cloned(), environment) @@ -432,7 +429,7 @@ fn run_prepared( common: (&OsStr, &[OsString], &[(OsString, OsString)]), args: impl IntoIterator, input: &[u8], -) -> Result { +) -> Result { let (program, program_arguments, environment) = common; let command = prepare(program, program_arguments.iter().cloned(), environment) .args(args) @@ -453,13 +450,13 @@ fn run_prepared( .or_raise_erased(|| message!("Could not communicate with signature verifier {program:?}")) } -fn signature_file(signature: &BStr) -> Result, Error> { +fn signature_file(signature: &BStr) -> Result, gix_error::Exn> { temporary_file([signature.as_ref()]) } fn temporary_file<'a>( data: impl IntoIterator, -) -> Result, Error> { +) -> Result, gix_error::Exn> { let mut file = gix_tempfile::new( std::env::temp_dir(), gix_tempfile::ContainingDirectory::Exists, @@ -477,12 +474,12 @@ fn temporary_file<'a>( Ok(file) } -fn signature_path(file: &mut gix_tempfile::Handle) -> Result { +fn signature_path(file: &mut gix_tempfile::Handle) -> Result { file.with_mut(|file| file.path().to_owned()) .or_raise_erased(|| message("Could not create or write the temporary signature file")) } -fn run_without_input(command: gix_command::Prepare, program: &OsStr) -> Result { +fn run_without_input(command: gix_command::Prepare, program: &OsStr) -> Result { command .spawn() .or_raise_erased(|| message!("Could not execute signature verifier {program:?}"))? @@ -654,7 +651,7 @@ mod tests { } #[test] - fn rejects_good_ssh_output_from_a_failed_verifier() -> Result<(), Error> { + fn rejects_good_ssh_output_from_a_failed_verifier() -> Result<(), gix_error::Exn> { let signed = SignedData::new(b"payloadsignature", 7..16); let outcome = signed.verify( BStr::new(b"-----BEGIN SSH SIGNATURE-----\n"), diff --git a/gix-object/src/tag/mod.rs b/gix-object/src/tag/mod.rs index 84da2abd316..ecf7b3d76c1 100644 --- a/gix-object/src/tag/mod.rs +++ b/gix-object/src/tag/mod.rs @@ -13,7 +13,7 @@ pub mod ref_iter; impl<'a> TagRef<'a> { /// Deserialize a tag from `data`. - pub fn from_bytes(mut data: &'a [u8], hash_kind: gix_hash::Kind) -> Result, crate::decode::Error> { + pub fn from_bytes(mut data: &'a [u8], hash_kind: gix_hash::Kind) -> Result, gix_error::ValidationError> { let input = &mut data; match decode::git_tag(input, hash_kind) { Ok(tag) => Ok(tag), @@ -26,7 +26,7 @@ impl<'a> TagRef<'a> { } /// Return the tagger, if present. - pub fn tagger(&self) -> Result>, crate::decode::Error> { + pub fn tagger(&self) -> Result>, gix_error::ValidationError> { Ok(self .tagger .map(parse_signature) @@ -42,7 +42,7 @@ impl<'a> TagRef<'a> { } /// Copy all data into a fully-owned instance. - pub fn into_owned(self) -> Result { + pub fn into_owned(self) -> Result { self.try_into() } } diff --git a/gix-object/src/tag/ref_iter.rs b/gix-object/src/tag/ref_iter.rs index 1e1cdc04b34..8ffba805b0c 100644 --- a/gix-object/src/tag/ref_iter.rs +++ b/gix-object/src/tag/ref_iter.rs @@ -47,14 +47,14 @@ impl<'a> TagRefIter<'a> { /// Errors are coerced into options, hiding whether there was an error or not. The caller should assume an error if they /// call the method as intended. Such a squelched error cannot be recovered unless the objects data is retrieved and parsed again. /// `next()`. - pub fn target_id(mut self) -> Result { + pub fn target_id(mut self) -> Result { let token = self.next().ok_or_else(missing_field)??; Token::into_id(token).ok_or_else(missing_field) } /// Returns the taggers signature if there is no decoding error, and if this field exists. /// Errors are coerced into options, hiding whether there was an error or not. The caller knows if there was an error or not. - pub fn tagger(mut self) -> Result>, crate::decode::Error> { + pub fn tagger(mut self) -> Result>, gix_error::ValidationError> { self.find_map(|t| match t { Ok(Token::Tagger(signature)) => Some(Ok(signature)), Err(err) => Some(Err(err)), @@ -64,7 +64,7 @@ impl<'a> TagRefIter<'a> { } } -fn missing_field() -> crate::decode::Error { +fn missing_field() -> gix_error::ValidationError { crate::decode::empty_error() } @@ -74,7 +74,7 @@ impl<'a> TagRefIter<'a> { mut i: &'a [u8], state: &mut State, hash_kind: gix_hash::Kind, - ) -> Result<(&'a [u8], Token<'a>), crate::decode::Error> { + ) -> Result<(&'a [u8], Token<'a>), gix_error::ValidationError> { let input = &mut i; match Self::next_inner_(input, state, hash_kind) { Ok(token) => Ok((*input, token)), @@ -86,7 +86,7 @@ impl<'a> TagRefIter<'a> { input: &mut &'a [u8], state: &mut State, hash_kind: gix_hash::Kind, - ) -> Result, crate::decode::Error> { + ) -> Result, gix_error::ValidationError> { use State::*; Ok(match state { Target => { @@ -124,7 +124,7 @@ impl<'a> TagRefIter<'a> { } impl<'a> Iterator for TagRefIter<'a> { - type Item = Result, crate::decode::Error>; + type Item = Result, gix_error::ValidationError>; fn next(&mut self) -> Option { if self.data.is_empty() { diff --git a/gix-object/src/tag/write.rs b/gix-object/src/tag/write.rs index e5f842d25cb..d53a055dc4a 100644 --- a/gix-object/src/tag/write.rs +++ b/gix-object/src/tag/write.rs @@ -6,9 +6,6 @@ use gix_error::{ErrorExt, ResultExt, ValidationError}; use crate::{Kind, Tag, TagRef, encode, encode::NL}; -/// An Error used in [`Tag::write_to()`][crate::WriteTo::write_to()]. -pub type Error = gix_error::Exn; - impl crate::WriteTo for Tag { fn write_to(&self, out: &mut dyn io::Write) -> io::Result<()> { encode::trusted_header_id(b"object", &self.target, out)?; @@ -91,7 +88,7 @@ impl crate::WriteTo for TagRef<'_> { } } -fn validated_name(name: &BStr) -> Result<&BStr, Error> { +fn validated_name(name: &BStr) -> Result<&BStr, gix_error::Exn> { gix_validate::tag::name(name).or_raise(|| ValidationError::new("The tag name was no valid reference name"))?; if name[0] == b'-' { return Err(ValidationError::new("Tags must not start with a dash: '-'").raise()); diff --git a/gix-object/src/traits/_impls.rs b/gix-object/src/traits/_impls.rs index e887b4e8bd1..2b3410ebbc4 100644 --- a/gix-object/src/traits/_impls.rs +++ b/gix-object/src/traits/_impls.rs @@ -8,24 +8,19 @@ impl crate::Write for &T where T: crate::Write, { - fn write(&self, object: &dyn WriteTo) -> Result { + fn write(&self, object: &dyn WriteTo) -> Result { (*self).write(object) } - fn write_buf(&self, object: Kind, from: &[u8]) -> Result { + fn write_buf(&self, object: Kind, from: &[u8]) -> Result { (*self).write_buf(object, from) } - fn write_buf_with_known_id( - &self, - object: Kind, - from: &[u8], - id: ObjectId, - ) -> Result { + fn write_buf_with_known_id(&self, object: Kind, from: &[u8], id: ObjectId) -> Result { (*self).write_buf_with_known_id(object, from, id) } - fn write_stream(&self, kind: Kind, size: u64, from: &mut dyn Read) -> Result { + fn write_stream(&self, kind: Kind, size: u64, from: &mut dyn Read) -> Result { (*self).write_stream(kind, size, from) } @@ -35,7 +30,7 @@ where size: u64, from: &mut dyn Read, id: ObjectId, - ) -> Result { + ) -> Result { (*self).write_stream_with_known_id(kind, size, from, id) } } @@ -44,24 +39,19 @@ impl crate::Write for Arc where T: crate::Write, { - fn write(&self, object: &dyn WriteTo) -> Result { + fn write(&self, object: &dyn WriteTo) -> Result { self.deref().write(object) } - fn write_buf(&self, object: Kind, from: &[u8]) -> Result { + fn write_buf(&self, object: Kind, from: &[u8]) -> Result { self.deref().write_buf(object, from) } - fn write_buf_with_known_id( - &self, - object: Kind, - from: &[u8], - id: ObjectId, - ) -> Result { + fn write_buf_with_known_id(&self, object: Kind, from: &[u8], id: ObjectId) -> Result { self.deref().write_buf_with_known_id(object, from, id) } - fn write_stream(&self, kind: Kind, size: u64, from: &mut dyn Read) -> Result { + fn write_stream(&self, kind: Kind, size: u64, from: &mut dyn Read) -> Result { self.deref().write_stream(kind, size, from) } @@ -71,7 +61,7 @@ where size: u64, from: &mut dyn Read, id: ObjectId, - ) -> Result { + ) -> Result { self.deref().write_stream_with_known_id(kind, size, from, id) } } @@ -80,24 +70,19 @@ impl crate::Write for Rc where T: crate::Write, { - fn write(&self, object: &dyn WriteTo) -> Result { + fn write(&self, object: &dyn WriteTo) -> Result { self.deref().write(object) } - fn write_buf(&self, object: Kind, from: &[u8]) -> Result { + fn write_buf(&self, object: Kind, from: &[u8]) -> Result { self.deref().write_buf(object, from) } - fn write_buf_with_known_id( - &self, - object: Kind, - from: &[u8], - id: ObjectId, - ) -> Result { + fn write_buf_with_known_id(&self, object: Kind, from: &[u8], id: ObjectId) -> Result { self.deref().write_buf_with_known_id(object, from, id) } - fn write_stream(&self, kind: Kind, size: u64, from: &mut dyn Read) -> Result { + fn write_stream(&self, kind: Kind, size: u64, from: &mut dyn Read) -> Result { self.deref().write_stream(kind, size, from) } @@ -107,7 +92,7 @@ where size: u64, from: &mut dyn Read, id: ObjectId, - ) -> Result { + ) -> Result { self.deref().write_stream_with_known_id(kind, size, from, id) } } diff --git a/gix-object/src/traits/find.rs b/gix-object/src/traits/find.rs index a338e4c05ea..b413febd94a 100644 --- a/gix-object/src/traits/find.rs +++ b/gix-object/src/traits/find.rs @@ -1,5 +1,3 @@ -use crate::find; - /// Check if an object is present in an object store. pub trait Exists { /// Returns `true` if the object exists in the database. @@ -19,8 +17,11 @@ pub trait Find { /// /// Returns `Some` object if it was present in the database, or the error that occurred during lookup or object /// retrieval. - fn try_find<'a>(&self, id: &gix_hash::oid, buffer: &'a mut Vec) - -> Result>, find::Error>; + fn try_find<'a>( + &self, + id: &gix_hash::oid, + buffer: &'a mut Vec, + ) -> Result>, gix_error::Exn>; } /// Find the header of an object in the object store. @@ -28,7 +29,7 @@ pub trait Header { /// Find the header of the object matching `id` in the database. /// /// Returns `Some` header if it was present, or the error that occurred during lookup. - fn try_header(&self, id: &gix_hash::oid) -> Result, find::Error>; + fn try_header(&self, id: &gix_hash::oid) -> Result, gix_error::Exn>; } /// A combination of [`Find`] and [`Header`] traits to help with `dyn` trait objects. @@ -56,7 +57,7 @@ mod _impls { where T: crate::Find, { - fn try_find<'a>(&self, id: &oid, buffer: &'a mut Vec) -> Result>, crate::find::Error> { + fn try_find<'a>(&self, id: &oid, buffer: &'a mut Vec) -> Result>, gix_error::Exn> { (*self).try_find(id, buffer) } } @@ -65,7 +66,7 @@ mod _impls { where T: crate::FindHeader, { - fn try_header(&self, id: &gix_hash::oid) -> Result, crate::find::Error> { + fn try_header(&self, id: &gix_hash::oid) -> Result, gix_error::Exn> { (*self).try_header(id) } } @@ -92,7 +93,7 @@ mod _impls { where T: crate::Find, { - fn try_find<'a>(&self, id: &oid, buffer: &'a mut Vec) -> Result>, crate::find::Error> { + fn try_find<'a>(&self, id: &oid, buffer: &'a mut Vec) -> Result>, gix_error::Exn> { self.deref().try_find(id, buffer) } } @@ -101,7 +102,7 @@ mod _impls { where T: crate::FindHeader, { - fn try_header(&self, id: &gix_hash::oid) -> Result, crate::find::Error> { + fn try_header(&self, id: &gix_hash::oid) -> Result, gix_error::Exn> { self.deref().try_header(id) } } @@ -110,7 +111,7 @@ mod _impls { where T: crate::Find, { - fn try_find<'a>(&self, id: &oid, buffer: &'a mut Vec) -> Result>, crate::find::Error> { + fn try_find<'a>(&self, id: &oid, buffer: &'a mut Vec) -> Result>, gix_error::Exn> { self.deref().try_find(id, buffer) } } @@ -119,7 +120,7 @@ mod _impls { where T: crate::FindHeader, { - fn try_header(&self, id: &gix_hash::oid) -> Result, crate::find::Error> { + fn try_header(&self, id: &gix_hash::oid) -> Result, gix_error::Exn> { self.deref().try_header(id) } } @@ -137,7 +138,7 @@ mod _impls { where T: crate::Find, { - fn try_find<'a>(&self, id: &oid, buffer: &'a mut Vec) -> Result>, crate::find::Error> { + fn try_find<'a>(&self, id: &oid, buffer: &'a mut Vec) -> Result>, gix_error::Exn> { self.deref().try_find(id, buffer) } } @@ -146,7 +147,7 @@ mod _impls { where T: crate::FindHeader, { - fn try_header(&self, id: &gix_hash::oid) -> Result, crate::find::Error> { + fn try_header(&self, id: &gix_hash::oid) -> Result, gix_error::Exn> { self.deref().try_header(id) } } @@ -155,7 +156,7 @@ mod _impls { mod ext { use gix_error::{CorruptionError, ErrorExt, NotFoundError, ResultExt, ValidationError}; - use crate::{BlobRef, CommitRef, CommitRefIter, Kind, ObjectRef, TagRef, TagRefIter, TreeRef, TreeRefIter, find}; + use crate::{BlobRef, CommitRef, CommitRefIter, Kind, ObjectRef, TagRef, TagRefIter, TreeRef, TreeRefIter}; fn not_found(id: &gix_hash::oid) -> gix_error::Exn { NotFoundError::new(format!("An object with id {id} could not be found")).raise_erased() @@ -169,11 +170,7 @@ mod ext { ($method:ident, $object_variant:path, $object_kind:path, $object_type:ty) => { /// Like [`find(…)`][Self::find()], but flattens the `Result>` into a single `Result` making a non-existing object an error /// while returning the desired object type. - fn $method<'a>( - &self, - id: &gix_hash::oid, - buffer: &'a mut Vec, - ) -> Result<$object_type, find::existing_object::Error> { + fn $method<'a>(&self, id: &gix_hash::oid, buffer: &'a mut Vec) -> Result<$object_type, gix_error::Exn> { self.try_find(id, buffer)? .ok_or_else(|| not_found(id)) .and_then(|o| { @@ -192,11 +189,7 @@ mod ext { ($method:ident, $object_kind:path, $object_type:ty, $into_iter:tt) => { /// Like [`find(…)`][Self::find()], but flattens the `Result>` into a single `Result` making a non-existing object an error /// while returning the desired iterator type. - fn $method<'a>( - &self, - id: &gix_hash::oid, - buffer: &'a mut Vec, - ) -> Result<$object_type, find::existing_iter::Error> { + fn $method<'a>(&self, id: &gix_hash::oid, buffer: &'a mut Vec) -> Result<$object_type, gix_error::Exn> { self.try_find(id, buffer)? .ok_or_else(|| not_found(id)) .and_then(|o| o.$into_iter().ok_or_else(|| wrong_kind(id, o.kind, $object_kind))) @@ -207,7 +200,7 @@ mod ext { /// An extension trait with convenience functions. pub trait HeaderExt: super::Header { /// Like [`try_header(…)`](super::Header::try_header()), but flattens the `Result>` into a single `Result` making a non-existing header an error. - fn header(&self, id: &gix_hash::oid) -> Result { + fn header(&self, id: &gix_hash::oid) -> Result { self.try_header(id)?.ok_or_else(|| not_found(id)) } } @@ -215,21 +208,13 @@ mod ext { /// An extension trait with convenience functions. pub trait FindExt: super::Find { /// Like [`try_find(…)`](super::Find::try_find()), but flattens the `Result>` into a single `Result` making a non-existing object an error. - fn find<'a>( - &self, - id: &gix_hash::oid, - buffer: &'a mut Vec, - ) -> Result, find::existing::Error> { + fn find<'a>(&self, id: &gix_hash::oid, buffer: &'a mut Vec) -> Result, gix_error::Exn> { self.try_find(id, buffer)?.ok_or_else(|| not_found(id)) } /// Like [`find(…)`][Self::find()], but flattens the `Result>` into a single `Result` making a non-existing object an error /// while returning the desired object type. - fn find_blob<'a>( - &self, - id: &gix_hash::oid, - buffer: &'a mut Vec, - ) -> Result, find::existing_object::Error> { + fn find_blob<'a>(&self, id: &gix_hash::oid, buffer: &'a mut Vec) -> Result, gix_error::Exn> { if id == gix_hash::ObjectId::empty_blob(id.kind()) { return Ok(BlobRef { data: &[] }); } @@ -247,11 +232,7 @@ mod ext { /// Like [`find(…)`][Self::find()], but flattens the `Result>` into a single `Result` making a non-existing object an error /// while returning the desired object type. - fn find_tree<'a>( - &self, - id: &gix_hash::oid, - buffer: &'a mut Vec, - ) -> Result, find::existing_object::Error> { + fn find_tree<'a>(&self, id: &gix_hash::oid, buffer: &'a mut Vec) -> Result, gix_error::Exn> { if id == gix_hash::ObjectId::empty_tree(id.kind()) { return Ok(TreeRef { entries: Vec::new() }); } diff --git a/gix-object/src/traits/mod.rs b/gix-object/src/traits/mod.rs index 15e67b4e685..2894f20b4d6 100644 --- a/gix-object/src/traits/mod.rs +++ b/gix-object/src/traits/mod.rs @@ -8,13 +8,13 @@ use crate::Kind; pub trait Write { /// Write objects using the intrinsic kind of [`hash`](gix_hash::Kind) into the database, /// returning id to reference it in subsequent reads. - fn write(&self, object: &dyn WriteTo) -> Result { + fn write(&self, object: &dyn WriteTo) -> Result { let mut buf = Vec::with_capacity(2048); object.write_to(&mut buf).or_erased()?; self.write_stream(object.kind(), buf.len() as u64, &mut buf.as_slice()) } /// As [`write`](Write::write), but takes an [`object` kind](Kind) along with its encoded bytes. - fn write_buf(&self, object: crate::Kind, mut from: &[u8]) -> Result { + fn write_buf(&self, object: crate::Kind, mut from: &[u8]) -> Result { self.write_stream(object, from.len() as u64, &mut from) } /// As [`write_buf`](Write::write_buf), but the object `id` has already been computed by the caller. @@ -26,7 +26,7 @@ pub trait Write { object: crate::Kind, from: &[u8], id: gix_hash::ObjectId, - ) -> Result; + ) -> Result; /// As [`write`](Write::write), but takes an input stream. /// This is commonly used for writing blobs directly without reading them to memory first. fn write_stream( @@ -34,7 +34,7 @@ pub trait Write { kind: crate::Kind, size: u64, from: &mut dyn io::Read, - ) -> Result; + ) -> Result; /// As [`write_stream`](Write::write_stream), but the object `id` has already been computed by the caller. /// /// Implementations may trust the given `id` and avoid computing it again. Callers must make sure `id` matches @@ -45,7 +45,7 @@ pub trait Write { size: u64, from: &mut dyn io::Read, id: gix_hash::ObjectId, - ) -> Result; + ) -> Result; } /// Writing of objects to a `Write` implementation diff --git a/gix-object/src/tree/editor.rs b/gix-object/src/tree/editor.rs index 5b967347b0f..2056f87de93 100644 --- a/gix-object/src/tree/editor.rs +++ b/gix-object/src/tree/editor.rs @@ -32,9 +32,6 @@ impl std::fmt::Debug for Editor<'_> { } } -/// The error returned by [Editor] or [Cursor] edit operation. -pub type Error = gix_error::Exn; - /// Lifecycle impl<'a> Editor<'a> { /// Create a new editor that uses `root` as base for all edits. Use `find` to lookup existing @@ -79,7 +76,7 @@ impl Editor<'_> { /// Remove the entry at `rela_path`, loading all trees on the path accordingly. /// It's no error if the entry doesn't exist, or if `rela_path` doesn't lead to an existing entry at all. - pub fn remove(&mut self, rela_path: I) -> Result<&mut Self, Error> + pub fn remove(&mut self, rela_path: I) -> Result<&mut Self, gix_error::Exn> where I: IntoIterator, C: AsRef, @@ -94,7 +91,7 @@ impl Editor<'_> { /// Return an error if the entry exists and is a tree, as that would otherwise also remove all entries below it. /// Empty path components are rejected if reached while loading the path. This is useful to not unintentionally /// remove a directory. - pub fn remove_leaf(&mut self, rela_path: I) -> Result<&mut Self, Error> + pub fn remove_leaf(&mut self, rela_path: I) -> Result<&mut Self, gix_error::Exn> where I: IntoIterator, C: AsRef, @@ -131,7 +128,7 @@ impl Editor<'_> { /// /// `id` can also be an empty tree, along with [the respective `kind`](EntryKind::Tree), even though that's normally not allowed /// in Git trees. - pub fn upsert(&mut self, rela_path: I, kind: EntryKind, id: ObjectId) -> Result<&mut Self, Error> + pub fn upsert(&mut self, rela_path: I, kind: EntryKind, id: ObjectId) -> Result<&mut Self, gix_error::Exn> where I: IntoIterator, C: AsRef, @@ -258,7 +255,7 @@ impl Editor<'_> { unreachable!("we exit as soon as everything is consumed") } - fn upsert_or_remove_at_pathbuf(&mut self, rela_path: I, edit: EditMode) -> Result<&mut Self, Error> + fn upsert_or_remove_at_pathbuf(&mut self, rela_path: I, edit: EditMode) -> Result<&mut Self, gix_error::Exn> where I: IntoIterator, C: AsRef, @@ -414,7 +411,7 @@ mod cursor { /// /// The returned cursor will then allow applying edits to the tree at `rela_path` as root. /// If `rela_path` is a single empty string, it is equivalent to using the current instance itself. - pub fn cursor_at(&mut self, rela_path: I) -> Result, super::Error> + pub fn cursor_at(&mut self, rela_path: I) -> Result, gix_error::Exn> where I: IntoIterator, C: AsRef, @@ -448,7 +445,7 @@ mod cursor { } /// Like [`Editor::upsert()`], but with the constraint of only editing in this cursor's tree. - pub fn upsert(&mut self, rela_path: I, kind: EntryKind, id: ObjectId) -> Result<&mut Self, super::Error> + pub fn upsert(&mut self, rela_path: I, kind: EntryKind, id: ObjectId) -> Result<&mut Self, gix_error::Exn> where I: IntoIterator, C: AsRef, @@ -460,7 +457,7 @@ mod cursor { } /// Like [`Editor::remove()`], but with the constraint of only editing in this cursor's tree. - pub fn remove(&mut self, rela_path: I) -> Result<&mut Self, super::Error> + pub fn remove(&mut self, rela_path: I) -> Result<&mut Self, gix_error::Exn> where I: IntoIterator, C: AsRef, @@ -472,7 +469,7 @@ mod cursor { } /// Like [`Editor::remove_leaf()`], but with the constraint of only editing in this cursor's tree. - pub fn remove_leaf(&mut self, rela_path: I) -> Result<&mut Self, super::Error> + pub fn remove_leaf(&mut self, rela_path: I) -> Result<&mut Self, gix_error::Exn> where I: IntoIterator, C: AsRef, diff --git a/gix-object/src/tree/ref_iter.rs b/gix-object/src/tree/ref_iter.rs index 0e24f7ea8a5..8e0c39a38b3 100644 --- a/gix-object/src/tree/ref_iter.rs +++ b/gix-object/src/tree/ref_iter.rs @@ -71,7 +71,7 @@ impl<'a> TreeRefIter<'a> { odb: impl crate::Find, buffer: &'a mut Vec, path: I, - ) -> Result, crate::find::Error> + ) -> Result, gix_error::Exn> where I: IntoIterator, P: PartialEq, @@ -108,7 +108,7 @@ impl<'a> TreeRefIter<'a> { odb: impl crate::Find, buffer: &'a mut Vec, relative_path: impl AsRef, - ) -> Result, crate::find::Error> { + ) -> Result, gix_error::Exn> { self.lookup_entry( odb, buffer, @@ -122,7 +122,7 @@ impl<'a> TreeRefIter<'a> { impl<'a> TreeRef<'a> { /// Deserialize a Tree from `data`, assuming `object_hash` to determine how the object ids are encoded in this particular tree. - pub fn from_bytes(data: &'a [u8], hash_kind: gix_hash::Kind) -> Result, crate::decode::Error> { + pub fn from_bytes(data: &'a [u8], hash_kind: gix_hash::Kind) -> Result, gix_error::ValidationError> { decode::tree(data, hash_kind.len_in_bytes()) } @@ -146,7 +146,7 @@ impl<'a> TreeRef<'a> { impl<'a> TreeRefIter<'a> { /// Consume self and return all parsed entries. - pub fn entries(self) -> Result>, crate::decode::Error> { + pub fn entries(self) -> Result>, gix_error::ValidationError> { self.collect() } @@ -167,7 +167,7 @@ impl<'a> TreeRefIter<'a> { } impl<'a> Iterator for TreeRefIter<'a> { - type Item = Result, crate::decode::Error>; + type Item = Result, gix_error::ValidationError>; fn next(&mut self) -> Option { if self.data.is_empty() { @@ -218,7 +218,7 @@ mod decode { )) } - pub fn tree(data: &[u8], hash_len: usize) -> Result, crate::decode::Error> { + pub fn tree(data: &[u8], hash_len: usize) -> Result, gix_error::ValidationError> { let mut i = data; // Calculate an estimate of the amount of entries to reduce diff --git a/gix-object/src/tree/write.rs b/gix-object/src/tree/write.rs index d5e41519cd9..fa71e0b271e 100644 --- a/gix-object/src/tree/write.rs +++ b/gix-object/src/tree/write.rs @@ -8,9 +8,6 @@ use crate::{ tree::{Entry, EntryRef}, }; -/// The Error used in [`Tree::write_to()`][crate::WriteTo::write_to()]. -pub type Error = gix_error::ValidationError; - /// Serialization impl crate::WriteTo for Tree { /// Serialize this tree to `out` in the git internal format. @@ -30,7 +27,7 @@ impl crate::WriteTo for Tree { out.write_all(SPACE)?; if filename.find_byte(0).is_some() { - return Err(io::Error::other(Error::new_with_input( + return Err(io::Error::other(gix_error::ValidationError::new_with_input( "Nullbytes are invalid in file paths as they are separators", filename.clone(), ))); @@ -77,7 +74,7 @@ impl crate::WriteTo for TreeRef<'_> { out.write_all(SPACE)?; if filename.find_byte(0).is_some() { - return Err(io::Error::other(Error::new_with_input( + return Err(io::Error::other(gix_error::ValidationError::new_with_input( "Nullbytes are invalid in file paths as they are separators", (*filename).to_owned(), ))); diff --git a/gix-object/tests/object/tree/editor.rs b/gix-object/tests/object/tree/editor.rs index 597f22e5115..103ed142697 100644 --- a/gix-object/tests/object/tree/editor.rs +++ b/gix-object/tests/object/tree/editor.rs @@ -936,7 +936,7 @@ mod utils { &self, id: &gix_hash::oid, buffer: &'a mut Vec, - ) -> Result>, gix_object::find::Error> { + ) -> Result>, gix_error::Exn> { let borrow = self.0.borrow(); let old = self.2.get(); self.2.set(old + 1); diff --git a/gix-odb/src/alternate/mod.rs b/gix-odb/src/alternate/mod.rs index 7d22a2df25f..eba8e0c9f94 100644 --- a/gix-odb/src/alternate/mod.rs +++ b/gix-odb/src/alternate/mod.rs @@ -71,8 +71,8 @@ impl From for Error { } } -impl From for Error { - fn from(err: gix_path::realpath::Error) -> Self { +impl From for Error { + fn from(err: gix_error::Exn) -> Self { Error::Realpath(err.into_error()) } } diff --git a/gix-odb/src/cache.rs b/gix-odb/src/cache.rs index ac2d90e8631..10281567ab7 100644 --- a/gix-odb/src/cache.rs +++ b/gix-odb/src/cache.rs @@ -145,21 +145,11 @@ mod impls { where S: gix_object::Write, { - fn write_stream( - &self, - kind: Kind, - size: u64, - from: &mut dyn Read, - ) -> Result { + fn write_stream(&self, kind: Kind, size: u64, from: &mut dyn Read) -> Result { self.inner.write_stream(kind, size, from) } - fn write_buf_with_known_id( - &self, - kind: Kind, - from: &[u8], - id: ObjectId, - ) -> Result { + fn write_buf_with_known_id(&self, kind: Kind, from: &[u8], id: ObjectId) -> Result { self.inner.write_buf_with_known_id(kind, from, id) } @@ -169,7 +159,7 @@ mod impls { size: u64, from: &mut dyn Read, id: ObjectId, - ) -> Result { + ) -> Result { self.inner.write_stream_with_known_id(kind, size, from, id) } } @@ -178,7 +168,7 @@ mod impls { where S: gix_pack::Find, { - fn try_find<'a>(&self, id: &oid, buffer: &'a mut Vec) -> Result>, gix_object::find::Error> { + fn try_find<'a>(&self, id: &oid, buffer: &'a mut Vec) -> Result>, gix_error::Exn> { gix_pack::Find::try_find(self, id, buffer).map(|t| t.map(|t| t.0)) } } @@ -196,7 +186,7 @@ mod impls { where S: crate::Header, { - fn try_header(&self, id: &oid) -> Result, gix_object::find::Error> { + fn try_header(&self, id: &oid) -> Result, gix_error::Exn> { self.inner.try_header(id) } } @@ -205,7 +195,7 @@ mod impls { where S: gix_object::FindHeader, { - fn try_header(&self, id: &oid) -> Result, gix_object::find::Error> { + fn try_header(&self, id: &oid) -> Result, gix_error::Exn> { self.inner.try_header(id) } } @@ -222,7 +212,7 @@ mod impls { &self, id: &oid, buffer: &'a mut Vec, - ) -> Result, Option)>, gix_object::find::Error> { + ) -> Result, Option)>, gix_error::Exn> { match self.pack_cache.as_ref().map(RefCell::borrow_mut) { Some(mut pack_cache) => self.try_find_cached(id, buffer, pack_cache.deref_mut()), None => self.try_find_cached(id, buffer, &mut gix_pack::cache::Never), @@ -234,7 +224,7 @@ mod impls { id: &oid, buffer: &'a mut Vec, pack_cache: &mut dyn gix_pack::cache::DecodeEntry, - ) -> Result, Option)>, gix_object::find::Error> { + ) -> Result, Option)>, gix_error::Exn> { if let Some(mut obj_cache) = self.object_cache.as_ref().map(RefCell::borrow_mut) && let Some(kind) = obj_cache.get(&id.as_ref().to_owned(), buffer) { diff --git a/gix-odb/src/memory.rs b/gix-odb/src/memory.rs index 4883930a0bd..211c1e5d4a1 100644 --- a/gix-odb/src/memory.rs +++ b/gix-odb/src/memory.rs @@ -141,11 +141,7 @@ impl gix_object::Find for Proxy where T: gix_object::Find, { - fn try_find<'a>( - &self, - id: &gix_hash::oid, - buffer: &'a mut Vec, - ) -> Result>, gix_object::find::Error> { + fn try_find<'a>(&self, id: &gix_hash::oid, buffer: &'a mut Vec) -> Result>, gix_error::Exn> { if let Some(map) = self.memory.as_ref() { let map = map.borrow(); if let Some((kind, data)) = map.get(id) { @@ -175,7 +171,7 @@ impl crate::Header for Proxy where T: crate::Header, { - fn try_header(&self, id: &gix_hash::oid) -> Result, gix_object::find::Error> { + fn try_header(&self, id: &gix_hash::oid) -> Result, gix_error::Exn> { if let Some(map) = self.memory.as_ref() { let map = map.borrow(); if let Some((kind, data)) = map.get(id) { @@ -193,7 +189,7 @@ impl gix_object::FindHeader for Proxy where T: gix_object::FindHeader, { - fn try_header(&self, id: &gix_hash::oid) -> Result, gix_object::find::Error> { + fn try_header(&self, id: &gix_hash::oid) -> Result, gix_error::Exn> { if let Some(map) = self.memory.as_ref() { let map = map.borrow(); if let Some((kind, data)) = map.get(id) { @@ -211,7 +207,7 @@ impl gix_object::Write for Proxy where T: gix_object::Write, { - fn write(&self, object: &dyn gix_object::WriteTo) -> Result { + fn write(&self, object: &dyn gix_object::WriteTo) -> Result { let Some(map) = self.memory.as_ref() else { return self.inner.write(object); }; @@ -229,7 +225,7 @@ where kind: gix_object::Kind, size: u64, from: &mut dyn std::io::Read, - ) -> Result { + ) -> Result { let Some(map) = self.memory.as_ref() else { return self.inner.write_stream(kind, size, from); }; @@ -247,7 +243,7 @@ where kind: gix_object::Kind, from: &[u8], id: gix_hash::ObjectId, - ) -> Result { + ) -> Result { let Some(map) = self.memory.as_ref() else { return self.inner.write_buf_with_known_id(kind, from, id); }; @@ -262,7 +258,7 @@ where size: u64, from: &mut dyn std::io::Read, id: gix_hash::ObjectId, - ) -> Result { + ) -> Result { let Some(map) = self.memory.as_ref() else { return self.inner.write_stream_with_known_id(kind, size, from, id); }; diff --git a/gix-odb/src/sink.rs b/gix-odb/src/sink.rs index 2e4cf1e74e8..d5596e00791 100644 --- a/gix-odb/src/sink.rs +++ b/gix-odb/src/sink.rs @@ -22,7 +22,7 @@ impl gix_object::Write for Sink { kind: gix_object::Kind, mut from: &[u8], id: gix_hash::ObjectId, - ) -> Result { + ) -> Result { self.write_stream_with_known_id(kind, from.len() as u64, &mut from, id) } @@ -31,7 +31,7 @@ impl gix_object::Write for Sink { kind: gix_object::Kind, mut size: u64, from: &mut dyn io::Read, - ) -> Result { + ) -> Result { let mut buf = [0u8; u16::MAX as usize]; let header = gix_object::encode::loose_header(kind, size); @@ -68,7 +68,7 @@ impl gix_object::Write for Sink { mut size: u64, from: &mut dyn io::Read, id: gix_hash::ObjectId, - ) -> Result { + ) -> Result { let mut buf = [0u8; u16::MAX as usize]; let header = gix_object::encode::loose_header(kind, size); diff --git a/gix-odb/src/store_impls/dynamic/find.rs b/gix-odb/src/store_impls/dynamic/find.rs index 2a51a52a226..516a9047df3 100644 --- a/gix-odb/src/store_impls/dynamic/find.rs +++ b/gix-odb/src/store_impls/dynamic/find.rs @@ -6,19 +6,18 @@ use gix_pack::cache::DecodeEntry; use crate::store::{handle, load_index}; pub(crate) mod error { - use gix_error::Error as GixError; - use crate::{loose, pack}; + use crate::loose; /// Returned by [`Handle::try_find()`][gix_pack::Find::try_find()] #[derive(Debug)] #[allow(missing_docs)] pub enum Error { Loose(loose::find::Error), - Pack(GixError), + Pack(gix_error::Error), LoadIndex(crate::store::load_index::Error), LoadPack(std::io::Error), - EntryType(gix_pack::data::entry::decode::Error), + EntryType(gix_error::CorruptionError), DeltaBaseRecursionLimit { /// the maximum recursion depth we encountered. max_depth: usize, @@ -93,8 +92,8 @@ pub(crate) mod error { } } - impl From for Error { - fn from(err: pack::data::decode::Error) -> Self { + impl From for Error { + fn from(err: gix_error::Exn) -> Self { Error::Pack(err.into_error()) } } @@ -111,8 +110,8 @@ pub(crate) mod error { } } - impl From for Error { - fn from(err: gix_pack::data::entry::decode::Error) -> Self { + impl From for Error { + fn from(err: gix_error::CorruptionError) -> Self { Error::EntryType(err) } } @@ -426,7 +425,7 @@ where id: &gix_hash::oid, buffer: &'a mut Vec, pack_cache: &mut dyn DecodeEntry, - ) -> Result, Option)>, gix_object::find::Error> { + ) -> Result, Option)>, gix_error::Exn> { let mut snapshot = self.snapshot.borrow_mut(); let mut inflate = self.inflate.borrow_mut(); self.try_find_cached_inner(id, buffer, &mut inflate, pack_cache, &mut snapshot, None) @@ -591,7 +590,7 @@ where &self, id: &gix_hash::oid, buffer: &'a mut Vec, - ) -> Result>, gix_object::find::Error> { + ) -> Result>, gix_error::Exn> { gix_pack::Find::try_find(self, id, buffer).map(|t| t.map(|t| t.0)) } } @@ -600,7 +599,7 @@ impl gix_object::FindHeader for super::Handle where S: Deref + Clone, { - fn try_header(&self, id: &gix_hash::oid) -> Result, gix_object::find::Error> { + fn try_header(&self, id: &gix_hash::oid) -> Result, gix_error::Exn> { let mut snapshot = self.snapshot.borrow_mut(); let mut inflate = self.inflate.borrow_mut(); self.try_header_inner(id, &mut inflate, &mut snapshot, None) diff --git a/gix-odb/src/store_impls/dynamic/header.rs b/gix-odb/src/store_impls/dynamic/header.rs index 39695af906a..d9d0d1995a3 100644 --- a/gix-odb/src/store_impls/dynamic/header.rs +++ b/gix-odb/src/store_impls/dynamic/header.rs @@ -188,7 +188,7 @@ impl crate::Header for super::Handle where S: Deref + Clone, { - fn try_header(&self, id: &oid) -> Result, gix_object::find::Error> { + fn try_header(&self, id: &oid) -> Result, gix_error::Exn> { let mut snapshot = self.snapshot.borrow_mut(); let mut inflate = self.inflate.borrow_mut(); self.try_header_inner(id, &mut inflate, &mut snapshot, None).or_erased() diff --git a/gix-odb/src/store_impls/dynamic/prefix.rs b/gix-odb/src/store_impls/dynamic/prefix.rs index 0b43d133b55..7594f883d29 100644 --- a/gix-odb/src/store_impls/dynamic/prefix.rs +++ b/gix-odb/src/store_impls/dynamic/prefix.rs @@ -65,7 +65,7 @@ pub mod disambiguate { /// /// It is considered ambiguous until it's disambiguated by validating that there is only a single object /// matching this prefix. - pub fn new(id: impl Into, hex_len: usize) -> Result { + pub fn new(id: impl Into, hex_len: usize) -> Result { let id = id.into(); gix_hash::Prefix::new(&id, hex_len)?; Ok(Candidate { id, hex_len }) diff --git a/gix-odb/src/store_impls/dynamic/write.rs b/gix-odb/src/store_impls/dynamic/write.rs index e1ff8a33c2e..1a6559f43de 100644 --- a/gix-odb/src/store_impls/dynamic/write.rs +++ b/gix-odb/src/store_impls/dynamic/write.rs @@ -64,7 +64,7 @@ impl gix_object::Write for store::Handle where S: Deref + Clone, { - fn write_stream(&self, kind: Kind, size: u64, from: &mut dyn Read) -> Result { + fn write_stream(&self, kind: Kind, size: u64, from: &mut dyn Read) -> Result { let mut snapshot = self.snapshot.borrow_mut(); Ok(match snapshot.loose_dbs.first() { Some(ldb) => ldb.write_stream(kind, size, from)?, @@ -80,12 +80,7 @@ where }) } - fn write_buf_with_known_id( - &self, - kind: Kind, - from: &[u8], - id: ObjectId, - ) -> Result { + fn write_buf_with_known_id(&self, kind: Kind, from: &[u8], id: ObjectId) -> Result { let mut snapshot = self.snapshot.borrow_mut(); Ok(match snapshot.loose_dbs.first() { Some(ldb) => ldb.write_buf_with_known_id(kind, from, id)?, @@ -107,7 +102,7 @@ where size: u64, from: &mut dyn Read, id: ObjectId, - ) -> Result { + ) -> Result { let mut snapshot = self.snapshot.borrow_mut(); Ok(match snapshot.loose_dbs.first() { Some(ldb) => ldb.write_stream_with_known_id(kind, size, from, id)?, diff --git a/gix-odb/src/store_impls/loose/find.rs b/gix-odb/src/store_impls/loose/find.rs index e2bac290088..3a1cec490b7 100644 --- a/gix-odb/src/store_impls/loose/find.rs +++ b/gix-odb/src/store_impls/loose/find.rs @@ -1,6 +1,6 @@ use std::{cmp::Ordering, collections::HashSet, io, path::PathBuf}; -use gix_error::{CorruptionError, Error as GixError, ResourceExhaustionError, ResourceExhaustionKind}; +use gix_error::{CorruptionError, ResourceExhaustionError, ResourceExhaustionKind}; use crate::store_impls::loose::{HEADER_MAX_SIZE, Store, hash_path}; @@ -9,7 +9,7 @@ use crate::store_impls::loose::{HEADER_MAX_SIZE, Store, hash_path}; #[allow(missing_docs)] pub enum Error { DecompressFile { - source: GixError, + source: gix_error::Error, path: PathBuf, }, SizeMismatch { @@ -17,10 +17,10 @@ pub enum Error { expected: u64, path: PathBuf, }, - Decode(GixError), + Decode(gix_error::Error), OutOfMemory { size: u64, - source: GixError, + source: gix_error::Error, }, Io { source: std::io::Error, @@ -59,8 +59,8 @@ impl std::error::Error for Error { } } -impl From for Error { - fn from(err: gix_object::decode::LooseHeaderDecodeError) -> Self { +impl From> for Error { + fn from(err: gix_error::Exn) -> Self { Error::Decode(err.into_error()) } } @@ -170,7 +170,7 @@ impl Store { if status == gix_zlib::Status::BufError { return Err(Error::DecompressFile { - source: GixError::from_error(CorruptionError::new( + source: gix_error::Error::from_error(CorruptionError::new( "The zlib status indicated an error, status was 'BufError'", )), path, @@ -196,7 +196,7 @@ impl Store { })?; if status == gix_zlib::Status::BufError { return Err(Error::DecompressFile { - source: GixError::from_error(CorruptionError::new( + source: gix_error::Error::from_error(CorruptionError::new( "The zlib status indicated an error, status was 'BufError'", )), path, @@ -207,7 +207,7 @@ impl Store { self.ensure_in_alloc_limit(size)?; let size_usize = usize::try_from(size).map_err(|_| Error::OutOfMemory { size, - source: GixError::from_error(ResourceExhaustionError::new( + source: gix_error::Error::from_error(ResourceExhaustionError::new( ResourceExhaustionKind::AllocationFailure, "The object size cannot be represented in memory", )), @@ -233,7 +233,7 @@ impl Store { out.clear(); out.try_reserve(size_usize).map_err(|err| Error::OutOfMemory { size, - source: GixError::from_error(err), + source: gix_error::Error::from_error(err), })?; if status == gix_zlib::Status::StreamEnd { out.extend_from_slice(&header[header_size..consumed_out]); @@ -272,7 +272,7 @@ impl Store { if self.alloc_limit_bytes.is_some_and(|limit| size > limit as u64) { return Err(Error::OutOfMemory { size, - source: GixError::from_error(ResourceExhaustionError::new( + source: gix_error::Error::from_error(ResourceExhaustionError::new( ResourceExhaustionKind::AllocationLimit, "The object exceeds the configured allocation limit", )), diff --git a/gix-odb/src/store_impls/loose/verify.rs b/gix-odb/src/store_impls/loose/verify.rs index 8c08c01cecb..b98c34ed272 100644 --- a/gix-odb/src/store_impls/loose/verify.rs +++ b/gix-odb/src/store_impls/loose/verify.rs @@ -14,17 +14,17 @@ pub mod integrity { #[allow(missing_docs)] pub enum Error { ObjectDecode { - source: gix_object::decode::Error, + source: gix_error::ValidationError, kind: gix_object::Kind, id: gix_hash::ObjectId, }, ObjectHasher { - source: gix_hash::hasher::Error, + source: gix_error::CorruptionError, kind: gix_object::Kind, expected: gix_hash::ObjectId, }, ObjectEncodeMismatch { - source: gix_hash::verify::Error, + source: gix_error::CorruptionError, kind: gix_object::Kind, }, Retry, diff --git a/gix-odb/src/store_impls/loose/write.rs b/gix-odb/src/store_impls/loose/write.rs index e3d0fbcc859..e17c7580fb5 100644 --- a/gix-odb/src/store_impls/loose/write.rs +++ b/gix-odb/src/store_impls/loose/write.rs @@ -57,7 +57,7 @@ impl From for Error { } impl gix_object::Write for Store { - fn write(&self, object: &dyn WriteTo) -> Result { + fn write(&self, object: &dyn WriteTo) -> Result { let mut to = self.dest().or_erased()?; to.write_all(&object.loose_header()) .map_err(|err| Error::Io { @@ -81,7 +81,7 @@ impl gix_object::Write for Store { /// Write the given buffer in `from` to disk in one syscall at best. /// /// This will cost at least 4 IO operations. - fn write_buf(&self, kind: gix_object::Kind, from: &[u8]) -> Result { + fn write_buf(&self, kind: gix_object::Kind, from: &[u8]) -> Result { let mut to = self.dest().or_erased()?; to.write_all(&gix_object::encode::loose_header(kind, from.len() as u64)) .map_err(|err| Error::Io { @@ -107,7 +107,7 @@ impl gix_object::Write for Store { kind: gix_object::Kind, from: &[u8], id: gix_hash::ObjectId, - ) -> Result { + ) -> Result { let mut to = self.compressed_tempfile().or_erased()?; to.write_all(&gix_object::encode::loose_header(kind, from.len() as u64)) .map_err(|err| Error::Io { @@ -136,7 +136,7 @@ impl gix_object::Write for Store { kind: gix_object::Kind, size: u64, mut from: &mut dyn io::Read, - ) -> Result { + ) -> Result { let mut to = self.dest().or_erased()?; to.write_all(&gix_object::encode::loose_header(kind, size)) .map_err(|err| Error::Io { @@ -163,7 +163,7 @@ impl gix_object::Write for Store { size: u64, mut from: &mut dyn io::Read, id: gix_hash::ObjectId, - ) -> Result { + ) -> Result { let mut to = self.compressed_tempfile().or_erased()?; to.write_all(&gix_object::encode::loose_header(kind, size)) .map_err(|err| Error::Io { diff --git a/gix-odb/src/traits.rs b/gix-odb/src/traits.rs index bebc3ee8ad6..732a23e9325 100644 --- a/gix-odb/src/traits.rs +++ b/gix-odb/src/traits.rs @@ -3,7 +3,7 @@ use crate::find; /// A way to obtain object properties without fully decoding it. pub trait Header { /// Try to read the header of the object associated with `id` or return `None` if it could not be found. - fn try_header(&self, id: &gix_hash::oid) -> Result, gix_object::find::Error>; + fn try_header(&self, id: &gix_hash::oid) -> Result, gix_error::Exn>; } mod _impls { @@ -17,7 +17,7 @@ mod _impls { where T: crate::Header, { - fn try_header(&self, id: &oid) -> Result, gix_object::find::Error> { + fn try_header(&self, id: &oid) -> Result, gix_error::Exn> { (*self).try_header(id) } } @@ -26,7 +26,7 @@ mod _impls { where T: crate::Header, { - fn try_header(&self, id: &oid) -> Result, gix_object::find::Error> { + fn try_header(&self, id: &oid) -> Result, gix_error::Exn> { self.deref().try_header(id) } } @@ -35,7 +35,7 @@ mod _impls { where T: crate::Header, { - fn try_header(&self, id: &oid) -> Result, gix_object::find::Error> { + fn try_header(&self, id: &oid) -> Result, gix_error::Exn> { self.deref().try_header(id) } } @@ -48,7 +48,7 @@ mod ext { /// An extension trait with convenience functions. pub trait HeaderExt: super::Header { /// Like [`try_header(…)`][super::Header::try_header()], but flattens the `Result>` into a single `Result` making a non-existing object an error. - fn header(&self, id: impl AsRef) -> Result { + fn header(&self, id: impl AsRef) -> Result { let id = id.as_ref(); self.try_header(id)? .ok_or_else(|| NotFoundError::new(format!("An object with id {id} could not be found")).raise_erased()) diff --git a/gix-pack/src/bundle/find.rs b/gix-pack/src/bundle/find.rs index 48fd5637238..a0b2393c914 100644 --- a/gix-pack/src/bundle/find.rs +++ b/gix-pack/src/bundle/find.rs @@ -14,7 +14,7 @@ impl crate::Bundle { out: &'a mut Vec, inflate: &mut gix_zlib::Inflate, cache: &mut dyn crate::cache::DecodeEntry, - ) -> Result, crate::data::entry::Location)>, crate::data::decode::Error> { + ) -> Result, crate::data::entry::Location)>, gix_error::Exn> { let idx = match self.index.lookup(id) { Some(idx) => idx, None => return Ok(None), @@ -35,7 +35,7 @@ impl crate::Bundle { out: &'a mut Vec, inflate: &mut gix_zlib::Inflate, cache: &mut dyn crate::cache::DecodeEntry, - ) -> Result<(gix_object::Data<'a>, crate::data::entry::Location), crate::data::decode::Error> { + ) -> Result<(gix_object::Data<'a>, crate::data::entry::Location), gix_error::Exn> { let ofs = self.index.pack_offset_at_index(idx); let pack_entry = self.pack.entry(ofs).or_erased()?; let header_size = pack_entry.header_size(); diff --git a/gix-pack/src/bundle/init.rs b/gix-pack/src/bundle/init.rs index 8da30def0f6..3777ade9a2d 100644 --- a/gix-pack/src/bundle/init.rs +++ b/gix-pack/src/bundle/init.rs @@ -4,9 +4,6 @@ use gix_error::{ErrorExt, OptionExt, ValidationError}; use crate::Bundle; -/// Returned by [`Bundle::at()`] -pub type Error = gix_error::Exn; - /// Initialization impl Bundle { /// Create a `Bundle` from `path`, which is either a pack file _(*.pack)_ or an index file _(*.idx)_. @@ -15,11 +12,11 @@ impl Bundle { /// /// The `object_hash` is a way to read (and write) the same file format with different hashes, as the hash kind /// isn't stored within the file format itself. - pub fn at(path: impl AsRef, object_hash: gix_hash::Kind) -> Result { + pub fn at(path: impl AsRef, object_hash: gix_hash::Kind) -> Result { Self::at_inner(path.as_ref(), object_hash) } - fn at_inner(path: &Path, object_hash: gix_hash::Kind) -> Result { + fn at_inner(path: &Path, object_hash: gix_hash::Kind) -> Result { let ext = path .extension() .and_then(std::ffi::OsStr::to_str) diff --git a/gix-pack/src/bundle/mod.rs b/gix-pack/src/bundle/mod.rs index 278ca94496b..fdeb35f2c2e 100644 --- a/gix-pack/src/bundle/mod.rs +++ b/gix-pack/src/bundle/mod.rs @@ -33,7 +33,7 @@ pub mod verify { progress: &mut dyn DynNestedProgress, should_interrupt: &AtomicBool, options: crate::index::verify::integrity::Options, - ) -> Result + ) -> Result where C: crate::cache::DecodeEntry, F: Fn() -> C + Send + Clone, diff --git a/gix-pack/src/bundle/write/error.rs b/gix-pack/src/bundle/write/error.rs deleted file mode 100644 index 9ef6d75fb15..00000000000 --- a/gix-pack/src/bundle/write/error.rs +++ /dev/null @@ -1,2 +0,0 @@ -/// The error returned by [`Bundle::write_to_directory()`][crate::Bundle::write_to_directory()] -pub type Error = gix_error::Exn; diff --git a/gix-pack/src/bundle/write/mod.rs b/gix-pack/src/bundle/write/mod.rs index dca3e79d1d4..c9576a4d6ce 100644 --- a/gix-pack/src/bundle/write/mod.rs +++ b/gix-pack/src/bundle/write/mod.rs @@ -12,8 +12,6 @@ use gix_tempfile::{AutoRemove, ContainingDirectory}; use crate::data; -mod error; -pub use error::Error; use gix_features::progress::prodash::DynNestedProgress; mod types; @@ -69,7 +67,7 @@ impl crate::Bundle { thin_pack_base_object_lookup: Option, object_hash: gix_hash::Kind, options: Options, - ) -> Result { + ) -> Result { let _span = gix_features::trace::coarse!("gix_pack::Bundle::write_to_directory()"); let mut read_progress = progress.add_child_with_id("read pack".into(), ProgressId::ReadPackBytes.into()); read_progress.init(None, progress::bytes()); @@ -88,7 +86,7 @@ impl crate::Bundle { }, ))); let (pack_entries_iter, pack_version): ( - Box>>, + Box>>, _, ) = match thin_pack_base_object_lookup { Some(thin_pack_lookup) => { @@ -184,7 +182,7 @@ impl crate::Bundle { thin_pack_base_object_lookup: Option, object_hash: gix_hash::Kind, options: Options, - ) -> Result { + ) -> Result { let _span = gix_features::trace::coarse!("gix_pack::Bundle::write_to_directory_eagerly()"); let mut read_progress = progress.add_child_with_id("read pack".into(), ProgressId::ReadPackBytes.into()); /* Bundle Write Read pack Bytes*/ read_progress.init(pack_size.map(|s| s as usize), progress::bytes()); @@ -201,7 +199,7 @@ impl crate::Bundle { }))); let eight_pages = 4096 * 8; let (pack_entries_iter, pack_version): ( - Box> + Send + 'static>, + Box> + Send + 'static>, _, ) = match thin_pack_base_object_lookup { Some(thin_pack_lookup) => { @@ -285,10 +283,10 @@ impl crate::Bundle { compression: _, }: Options, data_file: SharedTempFile, - mut pack_entries_iter: Box> + 'a>, + mut pack_entries_iter: Box> + 'a>, should_interrupt: &AtomicBool, pack_version: data::Version, - ) -> Result { + ) -> Result { let mut indexing_progress = progress.add_child_with_id( "create index file".into(), ProgressId::IndexingSteps(Default::default()).into(), diff --git a/gix-pack/src/bundle/write/types.rs b/gix-pack/src/bundle/write/types.rs index 0f9ac285489..f227899e00f 100644 --- a/gix-pack/src/bundle/write/types.rs +++ b/gix-pack/src/bundle/write/types.rs @@ -64,7 +64,7 @@ pub struct Outcome { impl Outcome { /// Instantiate a bundle from the newly written index and data file that are represented by this `Outcome` - pub fn to_bundle(&self) -> Option> { + pub fn to_bundle(&self) -> Option> { self.index_path .as_ref() .map(|path| crate::Bundle::at(path, self.object_hash)) diff --git a/gix-pack/src/cache/delta/from_offsets.rs b/gix-pack/src/cache/delta/from_offsets.rs index 6633d1c99ed..daf764bc0a4 100644 --- a/gix-pack/src/cache/delta/from_offsets.rs +++ b/gix-pack/src/cache/delta/from_offsets.rs @@ -10,9 +10,6 @@ use gix_features::progress::{self, Progress}; use crate::{cache::delta::Tree, data}; -/// Returned by [`Tree::from_offsets_in_pack()`] -pub type Error = gix_error::Exn; - /// Generate tree from certain input impl Tree { /// Create a new `Tree` from any data sorted by offset, ascending as returned by the `data_sorted_by_offsets` iterator. @@ -34,7 +31,7 @@ impl Tree { progress: &mut dyn Progress, should_interrupt: &AtomicBool, object_hash: gix_hash::Kind, - ) -> Result { + ) -> Result { let mut r = io::BufReader::with_capacity( 8192 * 8, // this value directly corresponds to performance, 8k (default) is about 4x slower than 64k fs::File::open(pack_path).or_raise_erased(|| message("open pack path"))?, @@ -112,7 +109,7 @@ impl Tree { r: &mut io::BufReader, pack_offset: u64, previous_offset: u64, - ) -> Result<(), Error> { + ) -> Result<(), gix_error::Exn> { let bytes_to_skip: u64 = pack_offset .checked_sub(previous_offset) .expect("continuously ascending pack offsets"); diff --git a/gix-pack/src/cache/delta/mod.rs b/gix-pack/src/cache/delta/mod.rs index d3f3a90475a..ddff48657a2 100644 --- a/gix-pack/src/cache/delta/mod.rs +++ b/gix-pack/src/cache/delta/mod.rs @@ -1,5 +1,3 @@ -/// Returned when using various methods on a [`Tree`] -pub type Error = gix_error::Exn; /// /// A tree that allows one-time iteration over all nodes and their children, consuming it in the process, /// while being shareable among threads without a lock. diff --git a/gix-pack/src/cache/delta/traverse/mod.rs b/gix-pack/src/cache/delta/traverse/mod.rs index 80cb5d76156..93d06db52c2 100644 --- a/gix-pack/src/cache/delta/traverse/mod.rs +++ b/gix-pack/src/cache/delta/traverse/mod.rs @@ -18,15 +18,12 @@ pub(crate) mod util; /// Shared access to ref-delta child indices awaiting a resolved base, keyed by its object ID. pub(super) type SharedRefDeltaChildren = OwnShared>; -/// Returned by [`Tree::traverse()`] -pub type Error = gix_error::Exn; - #[cold] -pub(super) fn allocation_error(kind: gix_error::ResourceExhaustionKind) -> Error { +pub(super) fn allocation_error(kind: gix_error::ResourceExhaustionKind) -> gix_error::Exn { gix_error::ResourceExhaustionError::new(kind, "Entry too large to fit in memory").raise_erased() } -pub(super) fn interrupted() -> Error { +pub(super) fn interrupted() -> gix_error::Exn { RetryableError::new(message("Interrupted")).raise_erased() } @@ -105,7 +102,7 @@ where object_hash, alloc_limit_bytes, }: Options<'_, '_>, - ) -> Result, Error> + ) -> Result, gix_error::Exn> where F: for<'r> Fn(EntryRange, &'r R) -> Option<&'r [u8]> + Send + Clone, R: Send + Sync, diff --git a/gix-pack/src/cache/delta/traverse/resolve.rs b/gix-pack/src/cache/delta/traverse/resolve.rs index 605f623ac04..bc3c53d1ace 100644 --- a/gix-pack/src/cache/delta/traverse/resolve.rs +++ b/gix-pack/src/cache/delta/traverse/resolve.rs @@ -8,7 +8,7 @@ use gix_features::{ use crate::{ cache::delta::{ - traverse::{Context, Error, allocation_error, interrupted, util::ItemSliceSync}, + traverse::{Context, allocation_error, interrupted, util::ItemSliceSync}, tree::Item, }, data, @@ -75,7 +75,7 @@ fn attach_ref_delta_children( decompressed: &[u8], ref_delta_children: Option<&super::SharedRefDeltaChildren>, object_hash: gix_hash::Kind, -) -> Result<(), Error> { +) -> Result<(), gix_error::Exn> { let Some(ref_delta_children) = ref_delta_children else { return Ok(()); }; @@ -144,7 +144,7 @@ pub(super) unsafe fn all( object_hash: gix_hash::Kind, alloc_limit_bytes: Option, should_interrupt: &AtomicBool, -) -> Result<(), Error> +) -> Result<(), gix_error::Exn> where T: Send, R: Send + Sync, @@ -222,7 +222,7 @@ fn resolve_serial( object_hash: gix_hash::Kind, alloc_limit_bytes: Option, should_interrupt: &AtomicBool, -) -> Result<(), Error> +) -> Result<(), gix_error::Exn> where T: Send, R: Send + Sync, @@ -279,7 +279,7 @@ fn resolve_parallel( object_hash: gix_hash::Kind, alloc_limit_bytes: Option, should_interrupt: &AtomicBool, -) -> Result<(), Error> +) -> Result<(), gix_error::Exn> where T: Send, R: Send + Sync, @@ -462,7 +462,7 @@ fn resolve_task<'a, T, F, MBFN, R>( objects: &gix_features::progress::StepShared, size: &gix_features::progress::StepShared, mut push: impl FnMut(WorkItem<'a, T>), -) -> Result<(), Error> +) -> Result<(), gix_error::Exn> where T: Send, R: Send + Sync, @@ -486,7 +486,7 @@ where let (base_size, consumed) = data::delta::decode_header_size(delta_bytes).or_erased()?; let base_size = decoded_size_limited(base_size, alloc_limit_bytes)?; if parent.bytes.len() != base_size { - return Err(data::delta::apply::Error::new( + return Err(gix_error::CorruptionError::new( "Corrupt delta data: delta base size does not match base object size", ) .raise_erased()); @@ -579,7 +579,7 @@ fn inspect( modify_base: &mut MBFN, objects: &gix_features::progress::StepShared, size: &gix_features::progress::StepShared, -) -> Result<(), Error> +) -> Result<(), gix_error::Exn> where T: Send, MBFN: FnMut(&mut T, &dyn Progress, Context<'_>) -> Result<(), gix_error::Exn> + Send, @@ -612,7 +612,7 @@ fn decompress_from_resolver( resolve_data: &R, object_hash: gix_hash::Kind, alloc_limit_bytes: Option, -) -> Result<(data::Entry, u64), Error> +) -> Result<(data::Entry, u64), gix_error::Exn> where F: for<'r> Fn(EntryRange, &'r R) -> Option<&'r [u8]> + Send, { @@ -636,7 +636,7 @@ fn decompress_all_at_once_with( decompressed_len: usize, out: &mut Vec, alloc_limit_bytes: Option, -) -> Result<(), Error> { +) -> Result<(), gix_error::Exn> { resize_with_limit(out, decompressed_len, alloc_limit_bytes)?; inflate.reset(); inflate @@ -645,7 +645,7 @@ fn decompress_all_at_once_with( Ok(()) } -fn decoded_size_limited(size: u64, alloc_limit_bytes: Option) -> Result { +fn decoded_size_limited(size: u64, alloc_limit_bytes: Option) -> Result { let size: usize = size .try_into() .map_err(|_| allocation_error(ResourceExhaustionKind::AllocationFailure))?; @@ -655,7 +655,7 @@ fn decoded_size_limited(size: u64, alloc_limit_bytes: Option) -> Result, len: usize, alloc_limit_bytes: Option) -> Result<(), Error> { +fn resize_with_limit(out: &mut Vec, len: usize, alloc_limit_bytes: Option) -> Result<(), gix_error::Exn> { if alloc_limit_bytes.is_some_and(|limit| len > limit) { return Err(allocation_error(ResourceExhaustionKind::AllocationLimit)); } @@ -845,7 +845,7 @@ mod tests { ); } - fn traverse_with_limit(tree: Tree<()>, pack: &Vec) -> Result<(), traverse::Error> { + fn traverse_with_limit(tree: Tree<()>, pack: &Vec) -> Result<(), gix_error::Exn> { traverse( tree, pack, @@ -863,7 +863,7 @@ mod tests { alloc_limit_bytes: Option, resolve: F, inspect: MBFN, - ) -> Result<(), traverse::Error> + ) -> Result<(), gix_error::Exn> where F: for<'r> Fn(data::EntryRange, &'r Vec) -> Option<&'r [u8]> + Send + Clone, MBFN: diff --git a/gix-pack/src/cache/delta/tree.rs b/gix-pack/src/cache/delta/tree.rs index 9544de3f7b9..30679aa6e2e 100644 --- a/gix-pack/src/cache/delta/tree.rs +++ b/gix-pack/src/cache/delta/tree.rs @@ -2,7 +2,7 @@ use std::collections::BTreeMap; use gix_error::{CorruptionError, ErrorExt, ResourceExhaustionError, ResourceExhaustionKind, ResultExt}; -use super::{Error, Tree, traverse}; +use super::Tree; /// Maps each referenced base object ID to indices in `Tree::child_items` of ref-deltas waiting for it. pub(super) type RefDeltaChildren = BTreeMap>; @@ -53,7 +53,7 @@ fn allocation_error(kind: ResourceExhaustionKind) -> ResourceExhaustionError { impl Tree { /// Instantiate a empty tree capable of storing `num_objects` amounts of items. - pub(crate) fn with_capacity(num_objects: usize, alloc_limit_bytes: Option) -> Result { + pub(crate) fn with_capacity(num_objects: usize, alloc_limit_bytes: Option) -> Result { let capacity = num_objects / 2; let allocation_bytes = capacity .checked_mul(std::mem::size_of::>()) @@ -93,7 +93,7 @@ impl Tree { pub(super) fn assert_is_incrementing_and_update_next_offset( &mut self, offset: crate::data::Offset, - ) -> Result<(), Error> { + ) -> Result<(), gix_error::Exn> { let items = match &self.last_seen { Some(NodeKind::Root) => &mut self.root_items, Some(NodeKind::Child) => &mut self.child_items, @@ -114,7 +114,7 @@ impl Tree { pub(super) fn set_pack_entries_end_and_resolve_ref_offsets( &mut self, pack_entries_end: crate::data::Offset, - ) -> Result<(), traverse::Error> { + ) -> Result<(), gix_error::Exn> { if !self.future_child_offsets.is_empty() { for (parent_offset, child_index) in self.future_child_offsets.drain(..) { // SAFETY invariants upheld: @@ -143,7 +143,7 @@ impl Tree { /// Add a new root node, one that only has children but is not a child itself, at the given pack `offset` and associate /// custom `data` with it. - pub(crate) fn add_root(&mut self, offset: crate::data::Offset, data: T) -> Result<(), Error> { + pub(crate) fn add_root(&mut self, offset: crate::data::Offset, data: T) -> Result<(), gix_error::Exn> { self.assert_is_incrementing_and_update_next_offset(offset)?; self.last_seen = NodeKind::Root.into(); self.root_items.push(Item { @@ -162,7 +162,7 @@ impl Tree { base_offset: crate::data::Offset, offset: crate::data::Offset, data: T, - ) -> Result<(), Error> { + ) -> Result<(), gix_error::Exn> { self.assert_is_incrementing_and_update_next_offset(offset)?; let next_child_index = self.child_items.len(); @@ -205,7 +205,7 @@ impl Tree { base_id: gix_hash::ObjectId, offset: crate::data::Offset, data: T, - ) -> Result<(), Error> { + ) -> Result<(), gix_error::Exn> { self.assert_is_incrementing_and_update_next_offset(offset)?; let child_index = self.child_items.len() as u32; diff --git a/gix-pack/src/cache/mod.rs b/gix-pack/src/cache/mod.rs index f6c443af3b9..9a61f06244d 100644 --- a/gix-pack/src/cache/mod.rs +++ b/gix-pack/src/cache/mod.rs @@ -66,7 +66,7 @@ pub mod object; /// # fn build_index_less( /// # pack_path: &std::path::Path, /// # offsets: Vec, -/// # ) -> Result<(), gix_pack::cache::delta::from_offsets::Error> { +/// # ) -> Result<(), gix_error::Exn> { /// // Build the delta tree straight from the pack — no `.idx` required: /// let tree = Tree::from_offsets_in_pack( /// pack_path, diff --git a/gix-pack/src/data/delta.rs b/gix-pack/src/data/delta.rs index d184db9e1c4..04782406be4 100644 --- a/gix-pack/src/data/delta.rs +++ b/gix-pack/src/data/delta.rs @@ -1,16 +1,10 @@ -/// -pub mod apply { - /// Returned when failing to apply deltas. - pub type Error = gix_error::CorruptionError; -} - -fn corrupt(message: &'static str) -> apply::Error { - apply::Error::new(format!("Corrupt delta data: {message}")) +fn corrupt(message: &'static str) -> gix_error::CorruptionError { + gix_error::CorruptionError::new(format!("Corrupt delta data: {message}")) } /// Given the decompressed pack delta `d`, decode a size in bytes (either the base object size or the result object size) /// Equivalent to [this canonical git function](https://github.com/git/git/blob/311531c9de557d25ac087c1637818bd2aad6eb3a/delta.h#L89) -pub(crate) fn decode_header_size(d: &[u8]) -> Result<(u64, usize), apply::Error> { +pub(crate) fn decode_header_size(d: &[u8]) -> Result<(u64, usize), gix_error::CorruptionError> { let mut shift = 0; let mut size = 0u64; let mut consumed = 0; @@ -28,8 +22,8 @@ pub(crate) fn decode_header_size(d: &[u8]) -> Result<(u64, usize), apply::Error> Err(corrupt("delta header size is truncated")) } -pub(crate) fn apply(base: &[u8], mut target: &mut [u8], data: &[u8]) -> Result<(), apply::Error> { - fn next_byte(data: &[u8], i: &mut usize) -> Result { +pub(crate) fn apply(base: &[u8], mut target: &mut [u8], data: &[u8]) -> Result<(), gix_error::CorruptionError> { + fn next_byte(data: &[u8], i: &mut usize) -> Result { let byte = *data .get(*i) .ok_or_else(|| corrupt("delta copy instruction is truncated"))?; @@ -76,7 +70,7 @@ pub(crate) fn apply(base: &[u8], mut target: &mut [u8], data: &[u8]) -> Result<( base.get(ofs..end) .ok_or_else(|| corrupt("delta copy range exceeds base object size"))?, ) - .map_err(|_| apply::Error::new("Delta copy from base: byte slices must match"))?; + .map_err(|_| gix_error::CorruptionError::new("Delta copy from base: byte slices must match"))?; } 0 => { return Err(corrupt("delta command 0 is reserved and invalid")); @@ -90,7 +84,7 @@ pub(crate) fn apply(base: &[u8], mut target: &mut [u8], data: &[u8]) -> Result<( data.get(i..end) .ok_or_else(|| corrupt("delta insert data is truncated"))?, ) - .map_err(|_| apply::Error::new("Delta copy data: byte slices must match"))?; + .map_err(|_| gix_error::CorruptionError::new("Delta copy data: byte slices must match"))?; i = end; } } diff --git a/gix-pack/src/data/entry/decode.rs b/gix-pack/src/data/entry/decode.rs index 8b1edbeda43..bc2b5459d7f 100644 --- a/gix-pack/src/data/entry/decode.rs +++ b/gix-pack/src/data/entry/decode.rs @@ -5,17 +5,18 @@ use gix_features::decode::leb64_from_read; use super::{BLOB, COMMIT, OFS_DELTA, REF_DELTA, TAG, TREE}; use crate::data; -/// The error returned by [data::Entry::from_bytes()]. -pub type Error = gix_error::CorruptionError; - -fn corrupt(message: &'static str) -> Error { - Error::new(format!("Pack entry is truncated: {message}")) +fn corrupt(message: &'static str) -> gix_error::CorruptionError { + gix_error::CorruptionError::new(format!("Pack entry is truncated: {message}")) } /// Decoding impl data::Entry { /// Decode an entry from the given entry data `d`, providing the `pack_offset` to allow tracking the start of the entry data section. - pub fn from_bytes(d: &[u8], pack_offset: data::Offset, object_hash: gix_hash::Kind) -> Result { + pub fn from_bytes( + d: &[u8], + pack_offset: data::Offset, + object_hash: gix_hash::Kind, + ) -> Result { let (type_id, size, mut consumed) = parse_header_info(d)?; let hash_len = object_hash.len_in_bytes(); @@ -45,7 +46,11 @@ impl data::Entry { TREE => Tree, COMMIT => Commit, TAG => Tag, - other => return Err(Error::new(format!("Object type {other} is unsupported"))), + other => { + return Err(gix_error::CorruptionError::new(format!( + "Object type {other} is unsupported" + ))); + } }; Ok(data::Entry { header: object, @@ -96,7 +101,7 @@ impl data::Entry { } } -fn encoded_header_size(consumed: usize) -> Result { +fn encoded_header_size(consumed: usize) -> Result { consumed .try_into() .map_err(|_| corrupt("entry header size does not fit into u16")) @@ -128,7 +133,7 @@ fn streaming_parse_header_info(read: &mut dyn io::Read) -> Result<(u8, u64, usiz /// Parses the header of a pack-entry, yielding object type id, decompressed object size, and consumed bytes #[inline] -fn parse_header_info(data: &[u8]) -> Result<(u8, u64, usize), Error> { +fn parse_header_info(data: &[u8]) -> Result<(u8, u64, usize), gix_error::CorruptionError> { let mut c = *data .first() .ok_or_else(|| corrupt("need a pack entry header, got empty input"))?; @@ -143,16 +148,16 @@ fn parse_header_info(data: &[u8]) -> Result<(u8, u64, usize), Error> { i += 1; let component = u64::from(c & 0b0111_1111) .checked_shl(shift) - .ok_or_else(|| Error::new("Pack entry header value overflowed while decoding"))?; + .ok_or_else(|| gix_error::CorruptionError::new("Pack entry header value overflowed while decoding"))?; size = size .checked_add(component) - .ok_or_else(|| Error::new("Pack entry header value overflowed while decoding"))?; + .ok_or_else(|| gix_error::CorruptionError::new("Pack entry header value overflowed while decoding"))?; shift += 7; } Ok((type_id, size, i)) } -fn parse_leb64(data: &[u8]) -> Result<(u64, usize), Error> { +fn parse_leb64(data: &[u8]) -> Result<(u64, usize), gix_error::CorruptionError> { let mut i = 0; let mut c = *data.first().ok_or_else(|| corrupt("an ofs-delta base distance"))?; i += 1; @@ -166,7 +171,7 @@ fn parse_leb64(data: &[u8]) -> Result<(u64, usize), Error> { .checked_add(1) .and_then(|value| value.checked_shl(7)) .and_then(|value| value.checked_add(u64::from(c) & 0x7f)) - .ok_or_else(|| Error::new("Pack entry header value overflowed while decoding"))?; + .ok_or_else(|| gix_error::CorruptionError::new("Pack entry header value overflowed while decoding"))?; } Ok((value, i)) } diff --git a/gix-pack/src/data/file/decode/entry.rs b/gix-pack/src/data/file/decode/entry.rs index c82e63d78d5..1577047adc5 100644 --- a/gix-pack/src/data/file/decode/entry.rs +++ b/gix-pack/src/data/file/decode/entry.rs @@ -7,7 +7,7 @@ use crate::{ cache, data, data::{ File, delta, - file::decode::{DeltaBaseUnresolved, Error, allocation_error}, + file::decode::{DeltaBaseUnresolved, allocation_error}, }, }; @@ -80,7 +80,7 @@ impl File where T: crate::FileData, { - fn decoded_object_size(&self, size: u64) -> Result { + fn decoded_object_size(&self, size: u64) -> Result { decoded_object_size(size, self.alloc_limit_bytes) } @@ -94,7 +94,7 @@ where entry: &data::Entry, inflate: &mut gix_zlib::Inflate, out: &mut [u8], - ) -> Result { + ) -> Result { let size: usize = entry .decompressed_size .try_into() @@ -108,10 +108,10 @@ where /// Obtain the [`Entry`][crate::data::Entry] at the given `offset` into the pack. /// /// The `offset` is typically obtained from the pack index file. - pub fn entry(&self, offset: data::Offset) -> Result { + pub fn entry(&self, offset: data::Offset) -> Result { let pack_offset: usize = offset.try_into().expect("offset representable by machine"); if pack_offset > self.data.len() { - return Err(data::entry::decode::Error::new( + return Err(gix_error::CorruptionError::new( "Pack entry is truncated: an entry offset pointing beyond pack data", )); } @@ -130,7 +130,7 @@ where data_offset: data::Offset, inflate: &mut gix_zlib::Inflate, out: &mut [u8], - ) -> Result { + ) -> Result { let (consumed_in, _consumed_out) = self.decompress_complete_entry_from_data_offset(data_offset, inflate, out)?; Ok(consumed_in) @@ -148,11 +148,11 @@ where data_offset: data::Offset, inflate: &mut gix_zlib::Inflate, out: &mut [u8], - ) -> Result<(usize, usize), Error> { + ) -> Result<(usize, usize), gix_error::Exn> { let (status, consumed_in, consumed_out) = self.decompress_entry_from_data_offset_unchecked(data_offset, inflate, out)?; if status != gix_zlib::Status::StreamEnd || consumed_out != out.len() { - return Err(data::entry::decode::Error::new( + return Err(gix_error::CorruptionError::new( "Pack entry is truncated: pack entry decompressed size does not match entry header", ) .raise_erased()); @@ -169,10 +169,10 @@ where data_offset: data::Offset, inflate: &mut gix_zlib::Inflate, out: &mut [u8], - ) -> Result<(gix_zlib::Status, usize, usize), Error> { + ) -> Result<(gix_zlib::Status, usize, usize), gix_error::Exn> { let offset: usize = data_offset.try_into().expect("offset representable by machine"); if offset >= self.data.len() { - return Err(data::entry::decode::Error::new( + return Err(gix_error::CorruptionError::new( "Pack entry is truncated: an entry data offset pointing beyond pack data", ) .raise_erased()); @@ -202,7 +202,7 @@ where inflate: &mut gix_zlib::Inflate, resolve: &dyn Fn(&gix_hash::oid, &mut Vec) -> Option, delta_cache: &mut dyn cache::DecodeEntry, - ) -> Result { + ) -> Result { use crate::data::entry::Header::*; match entry.header { Tree | Blob | Commit | Tag => { @@ -235,7 +235,7 @@ where inflate: &mut gix_zlib::Inflate, out: &mut Vec, cache: &mut dyn cache::DecodeEntry, - ) -> Result { + ) -> Result { // all deltas, from the one that produces the desired object (first) to the oldest at the end of the chain let mut chain = SmallVec::<[Delta; 10]>::default(); let first_entry = last.clone(); @@ -285,7 +285,7 @@ where let offset = cursor .checked_base_pack_offset(base_distance) .ok_or_else(|| { - crate::data::entry::decode::Error::new( + gix_error::CorruptionError::new( "Pack entry is truncated: an ofs-delta base distance pointing before pack start", ) }) @@ -360,7 +360,7 @@ where let mut bytes_consumed_by_header = offset; delta.base_size = self.decoded_object_size(base_size)?; if delta.base_size != expected_base_size { - return Err(delta::apply::Error::new( + return Err(gix_error::CorruptionError::new( "Corrupt delta data: delta base size does not match base object size", ) .raise_erased()); @@ -491,7 +491,7 @@ where } /// Convert user-controlled sizes from pack data into allocation sizes while enforcing the configured allocation cap. -fn decoded_object_size(size: u64, alloc_limit_bytes: Option) -> Result { +fn decoded_object_size(size: u64, alloc_limit_bytes: Option) -> Result { let size: usize = size .try_into() .map_err(|_| allocation_error(ResourceExhaustionKind::AllocationFailure))?; diff --git a/gix-pack/src/data/file/decode/header.rs b/gix-pack/src/data/file/decode/header.rs index db3b69dbe62..650abc163dd 100644 --- a/gix-pack/src/data/file/decode/header.rs +++ b/gix-pack/src/data/file/decode/header.rs @@ -2,10 +2,7 @@ use gix_error::{ErrorExt, ResultExt}; use crate::{ data, - data::{ - File, delta, - file::decode::{DeltaBaseUnresolved, Error}, - }, + data::{File, delta, file::decode::DeltaBaseUnresolved}, }; /// A return value of a resolve function, which given an [`ObjectId`][gix_hash::ObjectId] determines where an object can be found. @@ -60,7 +57,7 @@ where mut entry: data::Entry, inflate: &mut gix_zlib::Inflate, resolve: &dyn Fn(&gix_hash::oid) -> Option, - ) -> Result { + ) -> Result { use crate::data::entry::Header::*; let mut num_deltas = 0; let mut first_delta_decompressed_size = None::; @@ -81,7 +78,7 @@ where let offset = entry .checked_base_pack_offset(base_distance) .ok_or_else(|| { - crate::data::entry::decode::Error::new( + gix_error::CorruptionError::new( "Pack entry is truncated: an ofs-delta base distance pointing before pack start", ) }) @@ -122,20 +119,24 @@ where /// decompression through `decode_entry()` must still validate that the stream length matches /// the pack entry header. #[inline] - fn decode_delta_object_size(&self, inflate: &mut gix_zlib::Inflate, entry: &data::Entry) -> Result { + fn decode_delta_object_size( + &self, + inflate: &mut gix_zlib::Inflate, + entry: &data::Entry, + ) -> Result { let mut buf = [0_u8; 20]; let max_size = entry.decompressed_size.min(buf.len() as u64) as usize; let (status, _consumed_in, consumed_out) = self.decompress_entry_from_data_offset_unchecked(entry.data_offset, inflate, &mut buf[..max_size])?; if status == gix_zlib::Status::StreamEnd { if consumed_out as u64 != entry.decompressed_size { - return Err(data::entry::decode::Error::new( + return Err(gix_error::CorruptionError::new( "Pack entry is truncated: pack entry decompressed to fewer bytes than declared in the entry header", ) .raise_erased()); } } else if entry.decompressed_size == max_size as u64 { - return Err(data::entry::decode::Error::new( + return Err(gix_error::CorruptionError::new( "Pack entry is truncated: pack entry decompressed to more bytes than declared in the entry header", ) .raise_erased()); diff --git a/gix-pack/src/data/file/decode/mod.rs b/gix-pack/src/data/file/decode/mod.rs index 76654fb9cec..22e6e217831 100644 --- a/gix-pack/src/data/file/decode/mod.rs +++ b/gix-pack/src/data/file/decode/mod.rs @@ -31,13 +31,8 @@ impl std::error::Error for DeltaBaseUnresolved { } } -/// Returned by [`File::decode_header()`][crate::data::File::decode_header()], -/// [`File::decode_entry()`][crate::data::File::decode_entry()] and . -/// [`File::decompress_entry()`][crate::data::File::decompress_entry()] -pub type Error = gix_error::Exn; - #[cold] -pub(super) fn allocation_error(kind: gix_error::ResourceExhaustionKind) -> Error { +pub(super) fn allocation_error(kind: gix_error::ResourceExhaustionKind) -> gix_error::Exn { use gix_error::ErrorExt; gix_error::ResourceExhaustionError::new(kind, "Entry too large to fit in memory").raise_erased() } diff --git a/gix-pack/src/data/file/init.rs b/gix-pack/src/data/file/init.rs index 3ae11f9de11..96352fdca9c 100644 --- a/gix-pack/src/data/file/init.rs +++ b/gix-pack/src/data/file/init.rs @@ -14,11 +14,11 @@ impl data::File { /// This constructor leaves allocation limiting disabled, allowing allocations of any size dictated by pack data. /// Use [`Self::from_data()`] together with [`File::with_alloc_limit_bytes()`][crate::data::File::with_alloc_limit_bytes()] /// when working with untrusted input. - pub fn at(path: impl AsRef, object_hash: gix_hash::Kind) -> Result { + pub fn at(path: impl AsRef, object_hash: gix_hash::Kind) -> Result { Self::at_inner(path.as_ref(), object_hash) } - fn at_inner(path: &Path, object_hash: gix_hash::Kind) -> Result { + fn at_inner(path: &Path, object_hash: gix_hash::Kind) -> Result { let data = crate::mmap::read_only(path) .or_raise_erased(|| message!("Could not open pack data file at '{}'", path.display()))?; Self::from_data(data, path.to_owned(), object_hash) @@ -33,7 +33,7 @@ where /// /// This constructor leaves allocation limiting disabled, allowing allocations of any size dictated by pack data. /// Call [`File::with_alloc_limit_bytes()`][crate::data::File::with_alloc_limit_bytes()] before decoding entries from untrusted input. - pub fn from_data(data: T, path: PathBuf, object_hash: gix_hash::Kind) -> Result { + pub fn from_data(data: T, path: PathBuf, object_hash: gix_hash::Kind) -> Result { let hash_len = object_hash.len_in_bytes(); let pack_len = data.len(); let id = gix_features::hash::crc32(path.as_os_str().to_string_lossy().as_bytes()); diff --git a/gix-pack/src/data/file/verify.rs b/gix-pack/src/data/file/verify.rs index a01fa7110ce..0f0b0eea519 100644 --- a/gix-pack/src/data/file/verify.rs +++ b/gix-pack/src/data/file/verify.rs @@ -4,12 +4,6 @@ use gix_features::progress::Progress; use crate::data::File; -/// -pub mod checksum { - /// Returned by [`data::File::verify_checksum()`][crate::data::File::verify_checksum()]. - pub type Error = crate::verify::checksum::Error; -} - /// Checksums and verify checksums impl File where @@ -32,7 +26,7 @@ where &self, progress: &mut dyn Progress, should_interrupt: &AtomicBool, - ) -> Result { + ) -> Result { crate::verify::checksum_on_disk_or_mmap( self.path(), &self.data, diff --git a/gix-pack/src/data/header.rs b/gix-pack/src/data/header.rs index 6887feda4d4..bae75663dde 100644 --- a/gix-pack/src/data/header.rs +++ b/gix-pack/src/data/header.rs @@ -7,7 +7,7 @@ pub(crate) const N32_SIZE: usize = std::mem::size_of::(); pub const SIZE: usize = b"PACK".len() + N32_SIZE * 2; /// Parses the first 12 bytes of a pack file, returning the pack version as well as the number of objects contained in the pack. -pub fn decode(data: &[u8; SIZE]) -> Result<(data::Version, u32), decode::Error> { +pub fn decode(data: &[u8; SIZE]) -> Result<(data::Version, u32), gix_error::Exn> { let mut ofs = 0; if &data[ofs..ofs + b"PACK".len()] != b"PACK" { return Err(gix_error::CorruptionError::new("Pack data type not recognized").raise_erased()); @@ -39,9 +39,3 @@ pub fn encode(version: data::Version, num_objects: u32) -> [u8; SIZE] { buf[8..].copy_from_slice(&num_objects.to_be_bytes()[..]); buf } - -/// -pub mod decode { - /// Returned by [`decode()`][super::decode()]. - pub type Error = gix_error::Exn; -} diff --git a/gix-pack/src/data/input/bytes_to_entries.rs b/gix-pack/src/data/input/bytes_to_entries.rs index afe2bab79df..d19226874de 100644 --- a/gix-pack/src/data/input/bytes_to_entries.rs +++ b/gix-pack/src/data/input/bytes_to_entries.rs @@ -51,7 +51,7 @@ where mode: input::Mode, compressed: input::EntryDataMode, object_hash: gix_hash::Kind, - ) -> Result, input::Error> { + ) -> Result, gix_error::Exn> { let mut header_data = [0u8; crate::data::header::SIZE]; read.read_exact(&mut header_data).map_err(io_error)?; @@ -76,7 +76,7 @@ where }) } - fn next_inner(&mut self) -> Result { + fn next_inner(&mut self) -> Result { self.objects_left -= 1; // even an error counts as objects // Read header @@ -170,7 +170,7 @@ where }) } - fn try_read_trailer(&mut self) -> Result, input::Error> { + fn try_read_trailer(&mut self) -> Result, gix_error::Exn> { Ok(if self.objects_left == 0 { let mut id = gix_hash::ObjectId::null(self.object_hash); if let Err(err) = self.read.read_exact(id.as_mut_slice()) @@ -214,7 +214,7 @@ impl Iterator for BytesToEntriesIter where R: io::BufRead, { - type Item = Result; + type Item = Result; fn next(&mut self) -> Option { if self.had_error || self.objects_left == 0 { @@ -283,7 +283,7 @@ where T: crate::FileData, { /// Returns an iterator over [`Entries`][crate::data::input::Entry], without making use of the memory mapping. - pub fn streaming_iter(&self) -> Result, input::Error> { + pub fn streaming_iter(&self) -> Result, gix_error::Exn> { let reader = io::BufReader::with_capacity(4096 * 8, fs::File::open(&self.path).map_err(io_error)?); BytesToEntriesIter::new_from_header( reader, @@ -294,12 +294,12 @@ where } } -fn io_error(err: io::Error) -> input::Error { +fn io_error(err: io::Error) -> gix_error::Exn { err.and_raise(message("An IO operation failed while streaming an entry")) .erased() } -fn hash_io_error(err: gix_hash::io::Error) -> input::Error { +fn hash_io_error(err: gix_error::Exn) -> gix_error::Exn { err.raise(message("An IO operation failed while streaming an entry")) .erased() } diff --git a/gix-pack/src/data/input/entries_to_bytes.rs b/gix-pack/src/data/input/entries_to_bytes.rs index f448abe78f7..af3c25aae1a 100644 --- a/gix-pack/src/data/input/entries_to_bytes.rs +++ b/gix-pack/src/data/input/entries_to_bytes.rs @@ -29,7 +29,7 @@ pub struct EntriesToBytesIter { impl EntriesToBytesIter where - I: Iterator>, + I: Iterator>, W: std::io::Read + std::io::Write + std::io::Seek, { /// Create a new instance reading [entries][input::Entry] from an `input` iterator and write pack data bytes to @@ -54,7 +54,7 @@ where self.trailer } - fn next_inner(&mut self, entry: input::Entry) -> Result { + fn next_inner(&mut self, entry: input::Entry) -> Result { if self.num_entries == 0 { let header_bytes = crate::data::header::encode(self.data_version, 0); self.output @@ -77,7 +77,7 @@ where Ok(entry) } - fn write_header_and_digest(&mut self, last_entry: Option<&mut input::Entry>) -> Result<(), gix_hash::io::Error> { + fn write_header_and_digest(&mut self, last_entry: Option<&mut input::Entry>) -> Result<(), gix_error::Exn> { let header_bytes = crate::data::header::encode(self.data_version, self.num_entries); let num_bytes_written = if last_entry.is_some() { self.output.stream_position().map_err(gix_hash::io::from_std_io)? @@ -115,11 +115,11 @@ where impl Iterator for EntriesToBytesIter where - I: Iterator>, + I: Iterator>, W: std::io::Read + std::io::Write + std::io::Seek, { /// The amount of bytes written to `out` if `Ok` or the error `E` received from the input. - type Item = Result; + type Item = Result; fn next(&mut self) -> Option { if self.is_done { @@ -155,7 +155,7 @@ where } } -fn hash_io_error(err: gix_hash::io::Error) -> input::Error { +fn hash_io_error(err: gix_error::Exn) -> gix_error::Exn { err.raise(message("An IO operation failed while streaming an entry")) .erased() } diff --git a/gix-pack/src/data/input/entry.rs b/gix-pack/src/data/input/entry.rs index 43281f48b3b..2673bef4b6b 100644 --- a/gix-pack/src/data/input/entry.rs +++ b/gix-pack/src/data/input/entry.rs @@ -13,7 +13,7 @@ impl input::Entry { obj: &gix_object::Data<'_>, pack_offset: u64, compression: gix_zlib::Compression, - ) -> Result { + ) -> Result { let header = to_header(obj.kind); let compressed = compress_data(obj, compression)?; let compressed_size = compressed.len() as u64; @@ -57,7 +57,7 @@ fn to_header(kind: gix_object::Kind) -> Header { } } -fn compress_data(obj: &gix_object::Data<'_>, compression: gix_zlib::Compression) -> Result, input::Error> { +fn compress_data(obj: &gix_object::Data<'_>, compression: gix_zlib::Compression) -> Result, gix_error::Exn> { let mut out = gix_zlib::stream::deflate::Write::new(Vec::new(), compression); if let Err(err) = std::io::copy(&mut &*obj.data, &mut out) { match err.kind() { diff --git a/gix-pack/src/data/input/lookup_ref_delta_objects.rs b/gix-pack/src/data/input/lookup_ref_delta_objects.rs index c3ea3dd5f0b..1135f41f229 100644 --- a/gix-pack/src/data/input/lookup_ref_delta_objects.rs +++ b/gix-pack/src/data/input/lookup_ref_delta_objects.rs @@ -22,7 +22,7 @@ pub struct LookupRefDeltaObjectsIter { impl LookupRefDeltaObjectsIter where - I: Iterator>, + I: Iterator>, Find: gix_object::Find, { /// Create a new instance wrapping `iter` and using `lookup` as function to retrieve objects that will serve as bases @@ -76,10 +76,10 @@ where impl Iterator for LookupRefDeltaObjectsIter where - I: Iterator>, + I: Iterator>, Find: gix_object::Find, { - type Item = Result; + type Item = Result; fn next(&mut self) -> Option { if let Some(delta) = self.next_delta.take() { diff --git a/gix-pack/src/data/input/mod.rs b/gix-pack/src/data/input/mod.rs index a6476fd7a9a..dc9378901f9 100644 --- a/gix-pack/src/data/input/mod.rs +++ b/gix-pack/src/data/input/mod.rs @@ -29,7 +29,7 @@ pub struct Entry { mod entry; mod types; -pub use types::{EntryDataMode, Error, Mode}; +pub use types::{EntryDataMode, Mode}; mod bytes_to_entries; pub use bytes_to_entries::BytesToEntriesIter; diff --git a/gix-pack/src/data/input/types.rs b/gix-pack/src/data/input/types.rs index ba48b2f937d..35d4594a4d5 100644 --- a/gix-pack/src/data/input/types.rs +++ b/gix-pack/src/data/input/types.rs @@ -1,7 +1,3 @@ -/// Returned by [`BytesToEntriesIter::new_from_header()`][crate::data::input::BytesToEntriesIter::new_from_header()] and as part -/// of `Item` of [`BytesToEntriesIter`][crate::data::input::BytesToEntriesIter]. -pub type Error = gix_error::Exn; - /// Iteration Mode #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] diff --git a/gix-pack/src/data/mod.rs b/gix-pack/src/data/mod.rs index ffb37535a20..9268864da16 100644 --- a/gix-pack/src/data/mod.rs +++ b/gix-pack/src/data/mod.rs @@ -44,11 +44,6 @@ pub use file::{Header, decode, verify}; /// pub mod header; -/// -pub mod init { - pub use super::header::decode::Error; -} - /// pub mod entry; diff --git a/gix-pack/src/data/output/bytes.rs b/gix-pack/src/data/output/bytes.rs index fe66e52f318..91dfea44581 100644 --- a/gix-pack/src/data/output/bytes.rs +++ b/gix-pack/src/data/output/bytes.rs @@ -4,9 +4,6 @@ use gix_error::{ResultExt, message}; use crate::{data::output, exact_vec}; -/// The error returned by `next()` in the [`FromEntriesIter`] iterator. -pub type Error = gix_error::Exn; - /// An implementation of [`Iterator`] to write [encoded entries][output::Entry] to an inner implementation each time /// `next()` is called. pub struct FromEntriesIter { @@ -74,7 +71,7 @@ where self.trailer } - fn next_inner(&mut self) -> Result { + fn next_inner(&mut self) -> Result { let previous_written = self.written; if let Some((version, num_entries)) = self.header_info.take() { let header_bytes = crate::data::header::encode(version, num_entries); @@ -132,7 +129,7 @@ where W: std::io::Write, { /// The amount of bytes written to `out` if `Ok` or the error `E` received from the input. - type Item = Result; + type Item = Result; fn next(&mut self) -> Option { if self.is_done { diff --git a/gix-pack/src/data/output/count/mod.rs b/gix-pack/src/data/output/count/mod.rs index 481ff65d3c3..44cb1d572c7 100644 --- a/gix-pack/src/data/output/count/mod.rs +++ b/gix-pack/src/data/output/count/mod.rs @@ -45,5 +45,5 @@ pub use objects_impl::{objects, objects_unthreaded}; /// pub mod objects { - pub use super::objects_impl::{Error, ObjectExpansion, Options, Outcome}; + pub use super::objects_impl::{ObjectExpansion, Options, Outcome}; } diff --git a/gix-pack/src/data/output/count/objects/mod.rs b/gix-pack/src/data/output/count/objects/mod.rs index 369f6a2cdf5..d1d45e283f1 100644 --- a/gix-pack/src/data/output/count/objects/mod.rs +++ b/gix-pack/src/data/output/count/objects/mod.rs @@ -9,7 +9,7 @@ pub(in crate::data::output::count::objects_impl) mod reduce; mod util; mod types; -pub use types::{Error, ObjectExpansion, Options, Outcome}; +pub use types::{ObjectExpansion, Options, Outcome}; mod tree; @@ -39,7 +39,7 @@ pub fn objects( input_object_expansion, chunk_size, }: Options, -) -> Result<(Vec, Outcome), Error> +) -> Result<(Vec, Outcome), gix_error::Exn> where Find: crate::Find + Send + Clone, { @@ -96,7 +96,7 @@ pub fn objects_unthreaded( objects: &dyn gix_features::progress::Count, should_interrupt: &AtomicBool, input_object_expansion: ObjectExpansion, -) -> Result<(Vec, Outcome), Error> { +) -> Result<(Vec, Outcome), gix_error::Exn> { let seen_objs = RefCell::new(gix_hashtable::HashSet::default()); let (mut buf1, mut buf2) = (Vec::new(), Vec::new()); @@ -125,7 +125,7 @@ mod expand { use super::{ tree, - types::{Error, ObjectExpansion, Outcome}, + types::{ObjectExpansion, Outcome}, util, }; use crate::{ @@ -144,7 +144,7 @@ mod expand { objects: &gix_features::progress::AtomicStep, should_interrupt: &AtomicBool, allow_pack_lookups: bool, - ) -> Result<(Vec, Outcome), Error> { + ) -> Result<(Vec, Outcome), gix_error::Exn> { use ObjectExpansion::*; let mut out = Vec::new(); @@ -385,7 +385,7 @@ mod expand { } impl gix_object::Find for CountingObjects<'_> { - fn try_find<'a>(&self, id: &oid, buffer: &'a mut Vec) -> Result>, gix_object::find::Error> { + fn try_find<'a>(&self, id: &oid, buffer: &'a mut Vec) -> Result>, gix_error::Exn> { let res = Ok(self.objects.try_find(id, buffer)?.map(|t| t.0)); *self.decoded_objects.borrow_mut() += 1; res @@ -423,7 +423,7 @@ mod expand { } impl gix_object::Find for ExpandedCountingObjects<'_> { - fn try_find<'a>(&self, id: &oid, buffer: &'a mut Vec) -> Result>, gix_object::find::Error> { + fn try_find<'a>(&self, id: &oid, buffer: &'a mut Vec) -> Result>, gix_error::Exn> { let maybe_obj = self.objects.try_find(id, buffer)?; *self.decoded_objects.borrow_mut() += 1; match maybe_obj { diff --git a/gix-pack/src/data/output/count/objects/types.rs b/gix-pack/src/data/output/count/objects/types.rs index c96e4ce2faf..6e0ee712d7a 100644 --- a/gix-pack/src/data/output/count/objects/types.rs +++ b/gix-pack/src/data/output/count/objects/types.rs @@ -76,6 +76,3 @@ impl Default for Options { } } } - -/// The error returned by the pack generation iterator [`bytes::FromEntriesIter`][crate::data::output::bytes::FromEntriesIter]. -pub type Error = gix_error::Exn; 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 d500d5ef6b2..6449a3462b5 100644 --- a/gix-pack/src/data/output/entry/iter_from_counts.rs +++ b/gix-pack/src/data/output/entry/iter_from_counts.rs @@ -11,7 +11,7 @@ pub(crate) mod function { }, }; - use super::{Error, Mode, Options, Outcome, ProgressId, reduce, util}; + use super::{Mode, Options, Outcome, ProgressId, reduce, util}; use crate::data::output; /// Given a known list of object `counts`, calculate entries ready to be put into a data pack. @@ -55,8 +55,8 @@ pub(crate) mod function { chunk_size, compression, }: Options, - ) -> impl Iterator), Error>> - + parallel::reduce::Finalize> + ) -> impl Iterator), gix_error::Exn>> + + parallel::reduce::Finalize> where Find: crate::Find + Send + Clone + 'static, { @@ -411,9 +411,6 @@ mod types { } } - /// The error returned by the pack generation function [`iter_from_counts()`][crate::data::output::entry::iter_from_counts()]. - pub type Error = gix_error::Exn; - /// The progress ids used in [`write_to_directory()`][crate::Bundle::write_to_directory()]. /// /// Use this information to selectively extract the progress of interest in case the parent application has custom visualization. @@ -434,4 +431,4 @@ mod types { } } } -pub use types::{Error, Mode, Options, Outcome, ProgressId}; +pub use types::{Mode, Options, Outcome, ProgressId}; diff --git a/gix-pack/src/data/output/entry/mod.rs b/gix-pack/src/data/output/entry/mod.rs index edb3a9ecb59..17e8f21f6ec 100644 --- a/gix-pack/src/data/output/entry/mod.rs +++ b/gix-pack/src/data/output/entry/mod.rs @@ -32,9 +32,6 @@ pub enum Kind { }, } -/// The error returned by [`output::Entry::from_data()`]. -pub type Error = gix_error::Exn; - impl output::Entry { /// An object which can be identified as invalid easily which happens if objects didn't exist even if they were referred to. pub fn invalid() -> output::Entry { @@ -64,7 +61,7 @@ impl output::Entry { bases_index_offset: usize, pack_offset_to_oid: Option Option>, target_version: data::Version, - ) -> Option> { + ) -> Option> { if entry.version != target_version { return None; } @@ -138,7 +135,7 @@ impl output::Entry { count: &output::Count, obj: &gix_object::Data<'_>, compression: gix_zlib::Compression, - ) -> Result { + ) -> Result { Ok(output::Entry { id: count.id.to_owned(), kind: Kind::Base(obj.kind), diff --git a/gix-pack/src/find_traits.rs b/gix-pack/src/find_traits.rs index 6d3b474b73d..40a352d7e29 100644 --- a/gix-pack/src/find_traits.rs +++ b/gix-pack/src/find_traits.rs @@ -25,7 +25,7 @@ pub trait Find { &self, id: &gix_hash::oid, buffer: &'a mut Vec, - ) -> Result, Option)>, gix_object::find::Error> { + ) -> Result, Option)>, gix_error::Exn> { self.try_find_cached(id, buffer, &mut crate::cache::Never) } @@ -40,7 +40,7 @@ pub trait Find { id: &gix_hash::oid, buffer: &'a mut Vec, pack_cache: &mut dyn crate::cache::DecodeEntry, - ) -> Result, Option)>, gix_object::find::Error>; + ) -> Result, Option)>, gix_error::Exn>; /// Find the packs location where an object with `id` can be found in the database, or `None` if there is no pack /// holding the object. @@ -83,8 +83,7 @@ mod ext { &self, id: &gix_hash::oid, buffer: &'a mut Vec, - ) -> Result<($object_type, Option), gix_object::find::existing_object::Error> - { + ) -> Result<($object_type, Option), gix_error::Exn> { let id = id.as_ref(); self.try_find(id, buffer)? .ok_or_else(|| not_found(id)) @@ -109,7 +108,7 @@ mod ext { &self, id: &gix_hash::oid, buffer: &'a mut Vec, - ) -> Result<($object_type, Option), gix_object::find::existing_iter::Error> { + ) -> Result<($object_type, Option), gix_error::Exn> { let id = id.as_ref(); self.try_find(id, buffer)? .ok_or_else(|| not_found(id)) @@ -129,8 +128,7 @@ mod ext { &self, id: &gix_hash::oid, buffer: &'a mut Vec, - ) -> Result<(gix_object::Data<'a>, Option), gix_object::find::existing::Error> - { + ) -> Result<(gix_object::Data<'a>, Option), gix_error::Exn> { self.try_find(id, buffer)?.ok_or_else(|| not_found(id)) } @@ -167,7 +165,7 @@ mod find_impls { id: &oid, buffer: &'a mut Vec, pack_cache: &mut dyn crate::cache::DecodeEntry, - ) -> Result, Option)>, gix_object::find::Error> { + ) -> Result, Option)>, gix_error::Exn> { (*self).try_find_cached(id, buffer, pack_cache) } @@ -197,7 +195,7 @@ mod find_impls { id: &oid, buffer: &'a mut Vec, pack_cache: &mut dyn crate::cache::DecodeEntry, - ) -> Result, Option)>, gix_object::find::Error> { + ) -> Result, Option)>, gix_error::Exn> { self.deref().try_find_cached(id, buffer, pack_cache) } @@ -227,7 +225,7 @@ mod find_impls { id: &oid, buffer: &'a mut Vec, pack_cache: &mut dyn crate::cache::DecodeEntry, - ) -> Result, Option)>, gix_object::find::Error> { + ) -> Result, Option)>, gix_error::Exn> { self.deref().try_find_cached(id, buffer, pack_cache) } @@ -257,7 +255,7 @@ mod find_impls { id: &oid, buffer: &'a mut Vec, pack_cache: &mut dyn crate::cache::DecodeEntry, - ) -> Result, Option)>, gix_object::find::Error> { + ) -> Result, Option)>, gix_error::Exn> { self.deref().try_find_cached(id, buffer, pack_cache) } diff --git a/gix-pack/src/index/encode.rs b/gix-pack/src/index/encode.rs index 583c1818664..4cd3065202f 100644 --- a/gix-pack/src/index/encode.rs +++ b/gix-pack/src/index/encode.rs @@ -74,7 +74,7 @@ mod function { kind: crate::index::Version, object_hash: gix_hash::Kind, progress: &mut dyn DynNestedProgress, - ) -> Result { + ) -> Result { use io::Write; assert_eq!(kind, crate::index::Version::V2, "Can only write V2 packs right now"); assert!( diff --git a/gix-pack/src/index/init.rs b/gix-pack/src/index/init.rs index 26104ce7f5c..ae58643dd6a 100644 --- a/gix-pack/src/index/init.rs +++ b/gix-pack/src/index/init.rs @@ -8,10 +8,7 @@ use gix_error::{ErrorExt, ResultExt, ValidationError, message}; use crate::index::{self, FAN_LEN, V2_SIGNATURE, Version}; -/// Returned by [`index::File::at()`]. -pub type Error = gix_error::Exn; - -fn corrupt(message: impl Into>) -> Error { +fn corrupt(message: impl Into>) -> gix_error::Exn { gix_error::CorruptionError::new(message).raise_erased() } @@ -23,11 +20,11 @@ impl index::File { /// /// The `object_hash` is a way to read (and write) the same file format with different hashes, as the hash kind /// isn't stored within the file format itself. - pub fn at(path: impl AsRef, object_hash: gix_hash::Kind) -> Result { + pub fn at(path: impl AsRef, object_hash: gix_hash::Kind) -> Result { Self::at_inner(path.as_ref(), object_hash) } - fn at_inner(path: &Path, object_hash: gix_hash::Kind) -> Result { + fn at_inner(path: &Path, object_hash: gix_hash::Kind) -> Result { let data = crate::mmap::read_only(path) .or_raise_erased(|| message!("Could not open pack index file at '{}'", path.display()))?; Self::from_data(data, path.to_owned(), object_hash) @@ -39,7 +36,7 @@ where T: crate::FileData, { /// Instantiate an index file from `data` as assumed to be read or memory-mapped from `path`. - pub fn from_data(data: T, path: PathBuf, object_hash: gix_hash::Kind) -> Result { + pub fn from_data(data: T, path: PathBuf, object_hash: gix_hash::Kind) -> Result { let idx_len = data.len(); let hash_len = object_hash.len_in_bytes(); @@ -102,14 +99,14 @@ fn read_fan(d: &[u8]) -> ([u32; FAN_LEN], usize) { (fan, FAN_LEN * N32_SIZE) } -fn validate_fan(fan: &[u32; FAN_LEN]) -> Result<(), Error> { +fn validate_fan(fan: &[u32; FAN_LEN]) -> Result<(), gix_error::Exn> { if !crate::fan_is_monotonically_increasing(fan) { return Err(corrupt("Pack index fan-out table must be monotonically increasing")); } Ok(()) } -fn validate_size(data: &[u8], kind: Version, num_objects: u32, hash_len: usize) -> Result<(), Error> { +fn validate_size(data: &[u8], kind: Version, num_objects: u32, hash_len: usize) -> Result<(), gix_error::Exn> { let num_objects = num_objects as usize; let footer_size = hash_len * 2; let expected_size = match kind { diff --git a/gix-pack/src/index/traverse/error.rs b/gix-pack/src/index/traverse/error.rs deleted file mode 100644 index d828a6458c6..00000000000 --- a/gix-pack/src/index/traverse/error.rs +++ /dev/null @@ -1,2 +0,0 @@ -/// Returned by [`crate::index::File::traverse_with_index()`] and [`crate::index::File::traverse_with_lookup`] -pub type Error = gix_error::Exn; diff --git a/gix-pack/src/index/traverse/mod.rs b/gix-pack/src/index/traverse/mod.rs index 11c81448773..8bb7270c0b1 100644 --- a/gix-pack/src/index/traverse/mod.rs +++ b/gix-pack/src/index/traverse/mod.rs @@ -12,8 +12,6 @@ pub mod with_index; pub mod with_lookup; use reduce::Reducer; -mod error; -pub use error::Error; use gix_features::progress::DynNestedProgress; mod types; @@ -94,7 +92,7 @@ where alloc_limit_bytes, make_pack_lookup_cache, }: Options, - ) -> Result + ) -> Result where C: crate::cache::DecodeEntry, Processor: @@ -135,7 +133,7 @@ where pack_progress: &mut dyn Progress, index_progress: &mut dyn Progress, should_interrupt: &AtomicBool, - ) -> Result + ) -> Result where D: crate::FileData + Send + Sync, { @@ -165,7 +163,7 @@ where progress: &mut dyn Progress, index_entry: &index::Entry, processor: &mut impl FnMut(gix_object::Kind, &[u8], &index::Entry, &dyn Progress) -> Result<(), gix_error::Exn>, - ) -> Result, Error> + ) -> Result, gix_error::Exn> where C: crate::cache::DecodeEntry, D: crate::FileData + Send + Sync, @@ -227,7 +225,7 @@ fn process_entry( pack_entry_crc32: impl FnOnce() -> u32, progress: &dyn Progress, processor: &mut impl FnMut(gix_object::Kind, &[u8], &index::Entry, &dyn Progress) -> Result<(), gix_error::Exn>, -) -> Result<(), Error> { +) -> Result<(), gix_error::Exn> { if check.object_checksum() { gix_object::Data::new(decompressed, object_kind, index_entry.oid.kind()) .verify_checksum(&index_entry.oid) diff --git a/gix-pack/src/index/traverse/reduce.rs b/gix-pack/src/index/traverse/reduce.rs index 411c95657d1..ca0006ccb1f 100644 --- a/gix-pack/src/index/traverse/reduce.rs +++ b/gix-pack/src/index/traverse/reduce.rs @@ -63,10 +63,10 @@ impl

parallel::Reduce for Reducer<'_, P> where P: Progress, { - type Input = Result, traverse::Error>; + type Input = Result, gix_error::Exn>; type FeedProduce = (); type Output = traverse::Statistics; - type Error = traverse::Error; + type Error = gix_error::Exn; fn feed(&mut self, input: Self::Input) -> Result<(), Self::Error> { let chunk_stats = input?; diff --git a/gix-pack/src/index/traverse/with_index.rs b/gix-pack/src/index/traverse/with_index.rs index 9ae913e7e1c..33a27ce8d70 100644 --- a/gix-pack/src/index/traverse/with_index.rs +++ b/gix-pack/src/index/traverse/with_index.rs @@ -2,7 +2,6 @@ use std::sync::atomic::{AtomicBool, Ordering}; use gix_features::{parallel, progress::DynNestedProgress}; -use super::Error; use crate::{ cache::delta::traverse, index::{self, traverse::Outcome, util::index_entries_sorted_by_offset_ascending}, @@ -73,7 +72,7 @@ where thread_limit, alloc_limit_bytes, }: Options, - ) -> Result + ) -> Result where Processor: FnMut( gix_object::Kind, @@ -104,7 +103,7 @@ where res } }, - || -> Result<_, Error> { + || -> Result<_, gix_error::Exn> { let sorted_entries = index_entries_sorted_by_offset_ascending( self, &mut progress.add_child_with_id( diff --git a/gix-pack/src/index/traverse/with_lookup.rs b/gix-pack/src/index/traverse/with_lookup.rs index a2f04ce98c0..99447be65c0 100644 --- a/gix-pack/src/index/traverse/with_lookup.rs +++ b/gix-pack/src/index/traverse/with_lookup.rs @@ -6,7 +6,7 @@ use gix_features::{ threading::{Mutable, OwnShared, lock}, }; -use super::{Error, Reducer}; +use super::Reducer; use crate::{ data, exact_vec, index, index::{traverse::Outcome, util}, @@ -79,7 +79,7 @@ where check, make_pack_lookup_cache, }: Options, - ) -> Result + ) -> Result where C: crate::cache::DecodeEntry, Processor: @@ -144,7 +144,7 @@ where state_per_thread, move |entries: &[index::Entry], (cache, buf, inflate, progress)| - -> Result, Error> { + -> Result, gix_error::Exn> { progress.init( Some(entries.len()), gix_features::progress::count_with_decimals("objects", 2), diff --git a/gix-pack/src/index/verify.rs b/gix-pack/src/index/verify.rs index ef963809d87..c93f33bdce0 100644 --- a/gix-pack/src/index/verify.rs +++ b/gix-pack/src/index/verify.rs @@ -11,9 +11,6 @@ use crate::index; pub mod integrity { use std::marker::PhantomData; - /// Returned by [`index::File::verify_integrity()`][crate::index::File::verify_integrity()]. - pub type Error = gix_error::Exn; - /// Returned by [`index::File::verify_integrity()`][crate::index::File::verify_integrity()]. pub struct Outcome { /// The computed checksum of the index which matched the stored one. @@ -67,12 +64,6 @@ pub mod integrity { } } -/// -pub mod checksum { - /// Returned by [`index::File::verify_checksum()`][crate::index::File::verify_checksum()]. - pub type Error = crate::verify::checksum::Error; -} - /// Various ways in which a pack and index can be verified #[derive(Default, Debug, Eq, PartialEq, Hash, Clone, Copy)] pub enum Mode { @@ -121,7 +112,7 @@ where &self, progress: &mut dyn Progress, should_interrupt: &AtomicBool, - ) -> Result { + ) -> Result { crate::verify::checksum_on_disk_or_mmap( self.path(), &self.data, @@ -154,7 +145,7 @@ where pack: Option>, progress: &mut dyn DynNestedProgress, should_interrupt: &AtomicBool, - ) -> Result + ) -> Result where C: crate::cache::DecodeEntry, F: Fn() -> C + Send + Clone, @@ -221,7 +212,7 @@ where buf: &[u8], index_entry: &index::Entry, _progress: &dyn gix_features::progress::Progress, - ) -> Result<(), integrity::Error> { + ) -> Result<(), gix_error::Exn> { if let Mode::HashCrc32Decode | Mode::HashCrc32DecodeEncode = verify_mode { use gix_object::Kind::*; match object_kind { diff --git a/gix-pack/src/index/write/error.rs b/gix-pack/src/index/write/error.rs deleted file mode 100644 index 2eb27b90242..00000000000 --- a/gix-pack/src/index/write/error.rs +++ /dev/null @@ -1,2 +0,0 @@ -/// Returned by [`crate::index::write_data_iter_to_stream()`] -pub type Error = gix_error::Exn; diff --git a/gix-pack/src/index/write/mod.rs b/gix-pack/src/index/write/mod.rs index 69b5f69b225..9b442fec03e 100644 --- a/gix-pack/src/index/write/mod.rs +++ b/gix-pack/src/index/write/mod.rs @@ -1,7 +1,3 @@ -pub use error::Error; - -mod error; - pub(crate) struct TreeEntry { pub id: gix_hash::ObjectId, pub crc32: u32, @@ -63,7 +59,7 @@ pub(super) mod function { use crate::cache::delta::{Tree, traverse}; - use super::{Error, Outcome, ProgressId, TreeEntry, modify_base}; + use super::{Outcome, ProgressId, TreeEntry, modify_base}; /// Write information about `entries` as obtained from a pack data file into a pack index file via the `out` stream. /// The resolver produced by `make_resolver` must resolve pack entries from the same pack data file that produced the @@ -100,7 +96,7 @@ pub(super) mod function { pub fn write_data_iter_to_stream( version: crate::index::Version, make_resolver: F, - entries: &mut dyn Iterator>, + entries: &mut dyn Iterator>, thread_limit: Option, root_progress: &mut dyn DynNestedProgress, out: &mut dyn io::Write, @@ -108,7 +104,7 @@ pub(super) mod function { object_hash: gix_hash::Kind, alloc_limit_bytes: Option, pack_version: crate::data::Version, - ) -> Result + ) -> Result where F: FnOnce() -> io::Result<(F2, R)>, R: Send + Sync, diff --git a/gix-pack/src/multi_index/chunk.rs b/gix-pack/src/multi_index/chunk.rs index 42c9deaa4e4..3c3ac8f1416 100644 --- a/gix-pack/src/multi_index/chunk.rs +++ b/gix-pack/src/multi_index/chunk.rs @@ -7,12 +7,6 @@ pub mod index_names { /// The ID used for the index-names chunk. pub const ID: gix_chunk::Id = *b"PNAM"; - /// - pub mod decode { - /// The error returned by [`from_bytes()`][super::from_bytes()]. - pub type Error = gix_error::Exn; - } - /// Parse null-separated index names from the given `chunk` of bytes. /// /// `chunk` @@ -30,7 +24,7 @@ pub mod index_names { mut chunk: &[u8], num_packs: u32, alloc_limit_bytes: Option, - ) -> Result, decode::Error> { + ) -> Result, gix_error::Exn> { use gix_error::{ CorruptionError, ErrorExt, OptionExt, ResourceExhaustionError, ResourceExhaustionKind, ResultExt, }; diff --git a/gix-pack/src/multi_index/init.rs b/gix-pack/src/multi_index/init.rs index b22b938a450..1a75326a9d7 100644 --- a/gix-pack/src/multi_index/init.rs +++ b/gix-pack/src/multi_index/init.rs @@ -7,10 +7,7 @@ use gix_error::{CorruptionError, ErrorExt, ResultExt, ValidationError, message}; use crate::multi_index::{File, Version, chunk}; -/// The error returned by [File::at()]. -pub type Error = gix_error::Exn; - -fn corrupt(message: impl Into>) -> Error { +fn corrupt(message: impl Into>) -> gix_error::Exn { CorruptionError::new(message).raise_erased() } @@ -20,11 +17,11 @@ impl File { /// /// `alloc_limit_bytes` bounds each allocation caused by user-controlled on-disk data, useful for untrusted input. /// Use `None` to disable the limit. - pub fn at(path: impl AsRef, alloc_limit_bytes: Option) -> Result { + pub fn at(path: impl AsRef, alloc_limit_bytes: Option) -> Result { Self::at_inner(path.as_ref(), alloc_limit_bytes) } - fn at_inner(path: &Path, alloc_limit_bytes: Option) -> Result { + fn at_inner(path: &Path, alloc_limit_bytes: Option) -> Result { let data = crate::mmap::read_only(path) .or_raise_erased(|| message!("Could not open multi-index file at '{}'", path.display()))?; Self::from_data(data, path.to_owned(), alloc_limit_bytes) @@ -42,7 +39,7 @@ where /// /// It is used to reject reserving the output `Vec` if its capacity estimate exceeds the limit, /// and to reject any single path entry whose byte length exceeds the limit before turning it into a `PathBuf`. - pub fn from_data(data: T, path: PathBuf, alloc_limit_bytes: Option) -> Result { + pub fn from_data(data: T, path: PathBuf, alloc_limit_bytes: Option) -> Result { const TRAILER_LEN: usize = gix_hash::Kind::shortest().len_in_bytes(); /* trailing hash */ if data.len() < Self::HEADER_LEN @@ -149,7 +146,7 @@ where } } -fn validate_fan(fan: &[u32; 256]) -> Result<(), Error> { +fn validate_fan(fan: &[u32; 256]) -> Result<(), gix_error::Exn> { if !crate::fan_is_monotonically_increasing(fan) { return Err(corrupt("multi-index fan-out table must be monotonically increasing")); } diff --git a/gix-pack/src/multi_index/verify.rs b/gix-pack/src/multi_index/verify.rs index 88ed108d6e5..f1a663c5d82 100644 --- a/gix-pack/src/multi_index/verify.rs +++ b/gix-pack/src/multi_index/verify.rs @@ -7,8 +7,6 @@ use crate::{exact_vec, index, multi_index::File}; /// pub mod integrity { - /// Returned by [`multi_index::File::verify_integrity()`][crate::multi_index::File::verify_integrity()]. - pub type Error = gix_error::Exn; /// Returned by [`multi_index::File::verify_integrity()`][crate::multi_index::File::verify_integrity()]. pub struct Outcome { @@ -39,12 +37,6 @@ pub mod integrity { } } -/// -pub mod checksum { - /// Returned by [`multi_index::File::verify_checksum()`][crate::multi_index::File::verify_checksum()]. - pub type Error = crate::verify::checksum::Error; -} - impl File where T: crate::FileData, @@ -55,7 +47,7 @@ where &self, progress: &mut dyn Progress, should_interrupt: &AtomicBool, - ) -> Result { + ) -> Result { crate::verify::checksum_on_disk_or_mmap( self.path(), &self.data, @@ -73,7 +65,7 @@ where &self, progress: &mut dyn DynNestedProgress, should_interrupt: &AtomicBool, - ) -> Result { + ) -> Result { self.verify_integrity_inner( progress, should_interrupt, @@ -91,7 +83,7 @@ where progress: &mut dyn DynNestedProgress, should_interrupt: &AtomicBool, options: index::verify::integrity::Options, - ) -> Result + ) -> Result where C: crate::cache::DecodeEntry, F: Fn() -> C + Send + Clone, @@ -105,7 +97,7 @@ where should_interrupt: &AtomicBool, deep_check: bool, options: index::verify::integrity::Options, - ) -> Result + ) -> Result where C: crate::cache::DecodeEntry, F: Fn() -> C + Send + Clone, diff --git a/gix-pack/src/multi_index/write.rs b/gix-pack/src/multi_index/write.rs index 569c42af037..b235bf3e520 100644 --- a/gix-pack/src/multi_index/write.rs +++ b/gix-pack/src/multi_index/write.rs @@ -2,9 +2,6 @@ use std::time::SystemTime; use crate::multi_index; -/// The error returned by [`crate::multi_index::write_from_index_paths()`]. -pub type Error = gix_error::Exn; - /// An entry suitable for sorting and writing pub(crate) struct Entry { pub(crate) id: gix_hash::ObjectId, @@ -68,7 +65,7 @@ pub(super) mod function { use crate::{MMap, multi_index}; - use super::{Entry, Error, Options, Outcome, ProgressId}; + use super::{Entry, Options, Outcome, ProgressId}; /// Create a new multi-index file for writing to `out` from the pack index files at `index_paths`. /// @@ -79,7 +76,7 @@ pub(super) mod function { progress: &mut dyn DynNestedProgress, should_interrupt: &AtomicBool, Options { object_hash }: Options, - ) -> Result { + ) -> Result { let out = gix_hash::io::Write::new(out, object_hash); let (index_paths_sorted, index_filenames_sorted) = { index_paths.sort(); diff --git a/gix-pack/src/testing.rs b/gix-pack/src/testing.rs index 2336974e814..feba5325d22 100644 --- a/gix-pack/src/testing.rs +++ b/gix-pack/src/testing.rs @@ -29,7 +29,7 @@ impl crate::Find for Memory { id: &gix_hash::oid, buffer: &'a mut Vec, _pack_cache: &mut dyn crate::cache::DecodeEntry, - ) -> Result, Option)>, gix_object::find::Error> { + ) -> Result, Option)>, gix_error::Exn> { Ok(self.objects.get(id).map(|(kind, data)| { buffer.clear(); buffer.extend_from_slice(data); diff --git a/gix-pack/src/verify.rs b/gix-pack/src/verify.rs index ec69ce1a766..3269805d702 100644 --- a/gix-pack/src/verify.rs +++ b/gix-pack/src/verify.rs @@ -3,12 +3,6 @@ use std::{path::Path, sync::atomic::AtomicBool}; use gix_error::{ErrorExt, ResultExt, RetryableError, message}; use gix_features::progress::Progress; -/// -pub mod checksum { - /// Returned by various methods to verify the checksum of a memory mapped file that might also exist on disk. - pub type Error = gix_error::Exn; -} - /// Returns the `index` at which the following `index + 1` value is not an increment over the value at `index`. pub fn fan(data: &[u32]) -> Option { data.windows(2) @@ -25,7 +19,7 @@ pub fn checksum_on_disk_or_mmap( object_hash: gix_hash::Kind, progress: &mut dyn Progress, should_interrupt: &AtomicBool, -) -> Result { +) -> Result { let data_len_without_trailer = data.len() - object_hash.len_in_bytes(); let actual = match gix_hash::bytes_of_file( data_path, diff --git a/gix-pack/tests/pack/data/input.rs b/gix-pack/tests/pack/data/input.rs index ede69fdd5b4..de1c7c12b9d 100644 --- a/gix-pack/tests/pack/data/input.rs +++ b/gix-pack/tests/pack/data/input.rs @@ -2,7 +2,7 @@ mod lookup_ref_delta_objects { use std::sync::atomic::{AtomicUsize, Ordering}; use gix_hash::{ObjectId, oid}; - use gix_object::{Data, find::Error}; + use gix_object::Data; use gix_pack::data::{entry::Header, input, input::LookupRefDeltaObjectsIter}; use crate::hex_to_id; @@ -60,7 +60,7 @@ mod lookup_ref_delta_objects { fn into_results_iter( entries: Vec, - ) -> impl ExactSizeIterator> { + ) -> impl ExactSizeIterator> { entries.into_iter().map(Ok) } @@ -79,7 +79,7 @@ mod lookup_ref_delta_objects { } impl gix_object::Find for FindData<'_> { - fn try_find<'a>(&self, id: &oid, buf: &'a mut Vec) -> Result>, Error> { + fn try_find<'a>(&self, id: &oid, buf: &'a mut Vec) -> Result>, gix_error::Exn> { self.calls.fetch_add(1, Ordering::Relaxed); if let Some(data) = self.data { buf.resize(data.len(), 0); @@ -264,7 +264,7 @@ mod lookup_ref_delta_objects { struct MaxSizeHint; impl Iterator for MaxSizeHint { - type Item = Result; + type Item = Result; fn next(&mut self) -> Option { None 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 6fae6e91868..264cdb1115f 100644 --- a/gix-pack/tests/pack/data/output/count_and_entries.rs +++ b/gix-pack/tests/pack/data/output/count_and_entries.rs @@ -5,10 +5,7 @@ use gix_features::{ progress, }; use gix_odb::{pack, pack::FindExt}; -use gix_pack::data::{ - output, - output::{count, entry}, -}; +use gix_pack::data::{output, output::count}; use crate::{ data::output::{DbKind, db}, @@ -527,7 +524,7 @@ fn entry_sizes_depend_on_compression_level() -> crate::Result { (tree_id, buf) }; - let entry_size = |compression| -> Result { + let entry_size = |compression| -> Result { Ok(output::Entry::from_data( &output::Count::from_data(tree_id, None), &gix_object::Data::new(&buf, gix_object::Kind::Tree, gix_hash::Kind::Sha1), @@ -586,7 +583,7 @@ fn write_and_verify( let (num_written_bytes, pack_hash) = { let num_entries = entries.len(); let mut pack_writer = output::bytes::FromEntriesIter::new( - std::iter::once(Ok::<_, entry::iter_from_counts::Error>(entries)), + std::iter::once(Ok::<_, gix_error::Exn>(entries)), &mut pack_file, num_entries as u32, pack::data::Version::V2, diff --git a/gix-packetline/src/async_io/encode.rs b/gix-packetline/src/async_io/encode.rs index da2c2fb413d..0e0293d8929 100644 --- a/gix-packetline/src/async_io/encode.rs +++ b/gix-packetline/src/async_io/encode.rs @@ -9,8 +9,7 @@ use futures_lite::AsyncWriteExt; use crate::{ BandRef, Channel, DELIMITER_LINE, ERR_PREFIX, ErrorRef, FLUSH_LINE, MAX_DATA_LEN, PacketLineRef, RESPONSE_END_LINE, - TextRef, - encode::{Error, u16_to_hex}, + TextRef, encode::u16_to_hex, }; pin_project_lite::pin_project! { @@ -59,11 +58,13 @@ impl AsyncWrite for LineWriter<'_, W> { State::Idle => { let data_len = this.prefix.len() + data.len() + this.suffix.len(); if data_len > MAX_DATA_LEN { - let err = Error::new(format!("Cannot encode more than {MAX_DATA_LEN} bytes, got {data_len}")); + let err = gix_error::ValidationError::new(format!( + "Cannot encode more than {MAX_DATA_LEN} bytes, got {data_len}" + )); return Poll::Ready(Err(io::Error::other(err))); } if data.is_empty() { - let err = Error::new("Empty lines are invalid"); + let err = gix_error::ValidationError::new("Empty lines are invalid"); return Poll::Ready(Err(io::Error::other(err))); } let data_len = data_len + 4; @@ -146,11 +147,12 @@ async fn prefixed_and_suffixed_data_to_write( ) -> io::Result { let data_len = prefix.len() + data.len() + suffix.len(); if data_len > MAX_DATA_LEN { - let err = Error::new(format!("Cannot encode more than {MAX_DATA_LEN} bytes, got {data_len}")); + let err = + gix_error::ValidationError::new(format!("Cannot encode more than {MAX_DATA_LEN} bytes, got {data_len}")); return Err(io::Error::other(err)); } if data.is_empty() { - let err = Error::new("Empty lines are invalid"); + let err = gix_error::ValidationError::new("Empty lines are invalid"); return Err(io::Error::other(err)); } diff --git a/gix-packetline/src/async_io/read.rs b/gix-packetline/src/async_io/read.rs index 5ff2a64b938..72493108964 100644 --- a/gix-packetline/src/async_io/read.rs +++ b/gix-packetline/src/async_io/read.rs @@ -38,7 +38,7 @@ where async fn read_line_inner<'a>( reader: &mut T, buf: &'a mut [u8], - ) -> io::Result, decode::Error>> { + ) -> io::Result, gix_error::ValidationError>> { if buf.len() < U16_HEX_BYTES { return Ok(Err(decode::not_enough_data(U16_HEX_BYTES - buf.len()))); } @@ -130,7 +130,7 @@ where /// * natural EOF /// * ERR packet line encountered if [`fail_on_err_lines()`](StreamingPeekableIterState::fail_on_err_lines()) is true. /// * A `delimiter` packet line encountered - pub async fn read_line(&mut self) -> Option, decode::Error>>> { + pub async fn read_line(&mut self) -> Option, gix_error::ValidationError>>> { let state = &mut self.state; if state.is_done { return None; @@ -162,7 +162,7 @@ where /// was encountered. /// /// Multiple calls to peek will return the same packet line, if there is one. - pub async fn peek_line(&mut self) -> Option, decode::Error>>> { + pub async fn peek_line(&mut self) -> Option, gix_error::ValidationError>>> { let state = &mut self.state; if state.is_done { return None; diff --git a/gix-packetline/src/async_io/sidebands.rs b/gix-packetline/src/async_io/sidebands.rs index a6648c02afb..7d0c60e92c6 100644 --- a/gix-packetline/src/async_io/sidebands.rs +++ b/gix-packetline/src/async_io/sidebands.rs @@ -7,9 +7,9 @@ use std::{ use futures_io::{AsyncBufRead, AsyncRead}; use super::read::StreamingPeekableIter; -use crate::{BandRef, PacketLineRef, TextRef, U16_HEX_BYTES, decode, read::ProgressAction}; +use crate::{BandRef, PacketLineRef, TextRef, U16_HEX_BYTES, read::ProgressAction}; -type ReadLineResult<'a> = Option, decode::Error>>>; +type ReadLineResult<'a> = Option, gix_error::ValidationError>>>; /// An implementor of [`AsyncBufRead`] yielding packet lines on each call to `read_line()`. /// It's also possible to hide the underlying packet lines using the [`Read`](AsyncRead) implementation which is useful /// if they represent binary data, like the one of a pack file. @@ -134,7 +134,7 @@ where /// # Warning /// /// This skips all sideband handling and may return an unprocessed line with sidebands still contained in it. - pub async fn peek_data_line(&mut self) -> Option>> { + pub async fn peek_data_line(&mut self) -> Option>> { match self.state { State::Idle { ref mut parent } => match parent .as_mut() @@ -161,7 +161,9 @@ where /// # Warning /// /// This skips all sideband handling and may return an unprocessed line with sidebands still contained in it. - pub async fn read_data_line(&mut self) -> Option, decode::Error>>> { + pub async fn read_data_line( + &mut self, + ) -> Option, gix_error::ValidationError>>> { match &mut self.state { State::Idle { parent: Some(parent) } => { assert_eq!( diff --git a/gix-packetline/src/blocking_io/encode.rs b/gix-packetline/src/blocking_io/encode.rs index 15380bfa53c..9f086123bea 100644 --- a/gix-packetline/src/blocking_io/encode.rs +++ b/gix-packetline/src/blocking_io/encode.rs @@ -2,8 +2,7 @@ use std::io; use crate::{ BandRef, Channel, DELIMITER_LINE, ERR_PREFIX, ErrorRef, FLUSH_LINE, MAX_DATA_LEN, PacketLineRef, RESPONSE_END_LINE, - TextRef, - encode::{Error, u16_to_hex}, + TextRef, encode::u16_to_hex, }; /// Write a response-end message to `out`. @@ -86,12 +85,14 @@ fn prefixed_and_suffixed_data_to_write( ) -> io::Result { let data_len = prefix.len() + data.len() + suffix.len(); if data_len > MAX_DATA_LEN { - return Err(io::Error::other(Error::new(format!( + return Err(io::Error::other(gix_error::ValidationError::new(format!( "Cannot encode more than {MAX_DATA_LEN} bytes, got {data_len}" )))); } if data.is_empty() { - return Err(io::Error::other(Error::new("Empty lines are invalid"))); + return Err(io::Error::other(gix_error::ValidationError::new( + "Empty lines are invalid", + ))); } let data_len = data_len + 4; diff --git a/gix-packetline/src/blocking_io/read.rs b/gix-packetline/src/blocking_io/read.rs index 0bc09fc8d2c..c96ed80e481 100644 --- a/gix-packetline/src/blocking_io/read.rs +++ b/gix-packetline/src/blocking_io/read.rs @@ -33,7 +33,10 @@ where } } - fn read_line_inner<'a>(reader: &mut T, buf: &'a mut [u8]) -> io::Result, decode::Error>> { + fn read_line_inner<'a>( + reader: &mut T, + buf: &'a mut [u8], + ) -> io::Result, gix_error::ValidationError>> { if buf.len() < U16_HEX_BYTES { return Ok(Err(decode::not_enough_data(U16_HEX_BYTES - buf.len()))); } @@ -127,7 +130,7 @@ where /// * natural EOF /// * ERR packet line encountered if [`fail_on_err_lines()`](StreamingPeekableIterState::fail_on_err_lines()) is true. /// * A `delimiter` packet line encountered - pub fn read_line(&mut self) -> Option, decode::Error>>> { + pub fn read_line(&mut self) -> Option, gix_error::ValidationError>>> { let state = &mut self.state; if state.is_done { return None; @@ -158,7 +161,7 @@ where /// was encountered. /// /// Multiple calls to peek will return the same packet line, if there is one. - pub fn peek_line(&mut self) -> Option, decode::Error>>> { + pub fn peek_line(&mut self) -> Option, gix_error::ValidationError>>> { let state = &mut self.state; if state.is_done { return None; diff --git a/gix-packetline/src/blocking_io/sidebands.rs b/gix-packetline/src/blocking_io/sidebands.rs index a96b56a2661..d53da970a61 100644 --- a/gix-packetline/src/blocking_io/sidebands.rs +++ b/gix-packetline/src/blocking_io/sidebands.rs @@ -89,7 +89,7 @@ where /// # Warning /// /// This skips all sideband handling and may return an unprocessed line with sidebands still contained in it. - pub fn peek_data_line(&mut self) -> Option>> { + pub fn peek_data_line(&mut self) -> Option>> { match self.parent.peek_line() { Some(Ok(Ok(PacketLineRef::Data(line)))) => Some(Ok(Ok(line))), Some(Ok(Err(err))) => Some(Ok(Err(err))), @@ -103,7 +103,7 @@ where /// # Warning /// /// This skips all sideband handling and may return an unprocessed line with sidebands still contained in it. - pub fn read_data_line(&mut self) -> Option, crate::decode::Error>>> { + pub fn read_data_line(&mut self) -> Option, gix_error::ValidationError>>> { assert_eq!( self.cap, 0, "we don't support partial buffers right now - read-line must be used consistently" diff --git a/gix-packetline/src/decode.rs b/gix-packetline/src/decode.rs index b216ed87928..c2e8f5da30a 100644 --- a/gix-packetline/src/decode.rs +++ b/gix-packetline/src/decode.rs @@ -1,26 +1,17 @@ use crate::{DELIMITER_LINE, FLUSH_LINE, MAX_DATA_LEN, MAX_LINE_LEN, PacketLineRef, RESPONSE_END_LINE, U16_HEX_BYTES}; -/// The error used in the [`decode`][mod@crate::decode] module -pub type Error = gix_error::ValidationError; - -pub(crate) fn data_length_limit_exceeded(length_in_bytes: usize) -> Error { - Error::new(format!( +pub(crate) fn data_length_limit_exceeded(length_in_bytes: usize) -> gix_error::ValidationError { + gix_error::ValidationError::new(format!( "The data received claims to be larger than the maximum allowed size: got {length_in_bytes}, exceeds {MAX_DATA_LEN}" )) } -pub(crate) fn not_enough_data(bytes_needed: usize) -> Error { - Error::new(format!( +pub(crate) fn not_enough_data(bytes_needed: usize) -> gix_error::ValidationError { + gix_error::ValidationError::new(format!( "Needing {bytes_needed} additional bytes to decode the line successfully" )) } -/// -pub mod band { - /// The error used in [`PacketLineRef::decode_band()`][super::PacketLineRef::decode_band()]. - pub type Error = gix_error::ValidationError; -} - /// A utility return type to support incremental parsing of packet lines. #[derive(Debug, Clone)] pub enum Stream<'a> { @@ -47,7 +38,7 @@ pub enum PacketLineOrWantedSize<'a> { } /// Decode the `four_bytes` packet line prefix provided in hexadecimal form and check it for validity. -pub fn hex_prefix(four_bytes: &[u8]) -> Result, Error> { +pub fn hex_prefix(four_bytes: &[u8]) -> Result, gix_error::ValidationError> { debug_assert_eq!(four_bytes.len(), 4, "need four hex bytes"); for (line_bytes, line_type) in &[ (FLUSH_LINE, PacketLineRef::Flush), @@ -61,17 +52,17 @@ pub fn hex_prefix(four_bytes: &[u8]) -> Result, Error let mut buf = [0u8; U16_HEX_BYTES / 2]; faster_hex::hex_decode(four_bytes, &mut buf).map_err(|err| { - Error::new(format!( + gix_error::ValidationError::new(format!( "Failed to decode the first four hex bytes indicating the line length: {err}" )) })?; let wanted_bytes = u16::from_be_bytes(buf); if wanted_bytes == 3 { - return Err(Error::new("Received an invalid line of length 3")); + return Err(gix_error::ValidationError::new("Received an invalid line of length 3")); } if wanted_bytes == 4 { - return Err(Error::new("Received an invalid empty line")); + return Err(gix_error::ValidationError::new("Received an invalid empty line")); } debug_assert!( wanted_bytes as usize > U16_HEX_BYTES, @@ -81,7 +72,7 @@ pub fn hex_prefix(four_bytes: &[u8]) -> Result, Error } /// Obtain a `PacketLine` from `data` after assuring `data` is small enough to fit. -pub fn to_data_line(data: &[u8]) -> Result, Error> { +pub fn to_data_line(data: &[u8]) -> Result, gix_error::ValidationError> { if data.len() > MAX_LINE_LEN { return Err(data_length_limit_exceeded(data.len())); } @@ -90,7 +81,7 @@ pub fn to_data_line(data: &[u8]) -> Result, Error> { } /// Decode `data` as packet line while reporting whether the data is complete or not using a [`Stream`]. -pub fn streaming(data: &[u8]) -> Result, Error> { +pub fn streaming(data: &[u8]) -> Result, gix_error::ValidationError> { let data_len = data.len(); if data_len < U16_HEX_BYTES { return Ok(Stream::Incomplete { @@ -125,7 +116,7 @@ pub fn streaming(data: &[u8]) -> Result, Error> { /// /// Note that failure also happens if there is not enough data to parse a complete packet line, as opposed to [`streaming()`] decoding /// succeeds in that case, stating how much more bytes are required. -pub fn all_at_once(data: &[u8]) -> Result, Error> { +pub fn all_at_once(data: &[u8]) -> Result, gix_error::ValidationError> { match streaming(data)? { Stream::Complete { line, .. } => Ok(line), Stream::Incomplete { bytes_needed } => Err(not_enough_data(bytes_needed)), diff --git a/gix-packetline/src/encode.rs b/gix-packetline/src/encode.rs index 01f5784c7e9..fe575daef0c 100644 --- a/gix-packetline/src/encode.rs +++ b/gix-packetline/src/encode.rs @@ -1,6 +1,3 @@ -/// The error returned by most functions in the [`encode`](crate::encode) module -pub type Error = gix_error::ValidationError; - pub(crate) fn u16_to_hex(value: u16) -> [u8; 4] { let mut buf = [0u8; 4]; faster_hex::hex_encode(&value.to_be_bytes(), &mut buf).expect("two bytes to 4 hex chars never fails"); diff --git a/gix-packetline/src/lib.rs b/gix-packetline/src/lib.rs index bc95c71f9c2..5f51b36a643 100644 --- a/gix-packetline/src/lib.rs +++ b/gix-packetline/src/lib.rs @@ -162,19 +162,19 @@ impl<'a> PacketLineRef<'a> { } /// Decode the band of this [`slice`](PacketLineRef::as_slice()) - pub fn decode_band(&self) -> Result, decode::band::Error> { - let d = self - .as_slice() - .ok_or_else(|| decode::band::Error::new("attempt to decode a non-data line into a side-channel band"))?; - let (&band_id, d) = d - .split_first() - .ok_or_else(|| decode::band::Error::new("attempt to decode a non-data line into a side-channel band"))?; + pub fn decode_band(&self) -> Result, gix_error::ValidationError> { + let d = self.as_slice().ok_or_else(|| { + gix_error::ValidationError::new("attempt to decode a non-data line into a side-channel band") + })?; + let (&band_id, d) = d.split_first().ok_or_else(|| { + gix_error::ValidationError::new("attempt to decode a non-data line into a side-channel band") + })?; Ok(match band_id { 1 => BandRef::Data(d), 2 => BandRef::Progress(d), 3 => BandRef::Error(d), band => { - return Err(decode::band::Error::new(format!( + return Err(gix_error::ValidationError::new(format!( "attempt to decode a non-side channel line or input was malformed: {band}" ))); } diff --git a/gix-packetline/src/read.rs b/gix-packetline/src/read.rs index e8ea4b1061e..ab305846f30 100644 --- a/gix-packetline/src/read.rs +++ b/gix-packetline/src/read.rs @@ -10,9 +10,9 @@ pub type ProgressAction = std::ops::ControlFlow<()>; #[cfg(any(feature = "blocking-io", feature = "async-io"))] pub(crate) type ExhaustiveOutcome<'a> = ( - bool, // is_done - Option>, // stopped_at - Option, crate::decode::Error>>>, // actual method result + bool, // is_done + Option>, // stopped_at + Option, gix_error::ValidationError>>>, // actual method result ); mod error { diff --git a/gix-packetline/tests/packetline/decode.rs b/gix-packetline/tests/packetline/decode.rs index 0b8ac5b929d..faa2d69d409 100644 --- a/gix-packetline/tests/packetline/decode.rs +++ b/gix-packetline/tests/packetline/decode.rs @@ -1,16 +1,16 @@ mod streaming { use gix_packetline::{ ErrorRef, PacketLineRef, - decode::{self, Stream, streaming}, + decode::{Stream, streaming}, }; use crate::assert_err_display; fn assert_complete( - res: Result, + res: Result, expected_consumed: usize, expected_value: PacketLineRef, - ) -> Result<(), decode::Error> { + ) -> Result<(), gix_error::ValidationError> { match res? { Stream::Complete { line, bytes_consumed } => { assert_eq!(bytes_consumed, expected_consumed); @@ -34,7 +34,7 @@ mod streaming { #[crate::bisync::bisync] #[cfg_attr(feature = "blocking-io", test)] #[cfg_attr(all(feature = "async-io", not(feature = "blocking-io")), async_std::test)] - async fn trailing_line_feeds_are_removed_explicitly() -> crate::Result { + async fn trailing_line_feeds_are_removed_explicitly() -> gix_error::TestResult { let line = decode::all_at_once(b"0006a\n")?; assert_eq!(line.as_text().expect("text").0.as_bstr(), b"a".as_bstr()); let mut out = Vec::new(); @@ -48,7 +48,7 @@ mod streaming { #[crate::bisync::bisync] #[cfg_attr(feature = "blocking-io", test)] #[cfg_attr(all(feature = "async-io", not(feature = "blocking-io")), async_std::test)] - async fn all_kinds_of_packetlines() -> crate::Result { + async fn all_kinds_of_packetlines() -> gix_error::TestResult { for (line, bytes) in &[ (PacketLineRef::ResponseEnd, 4), (PacketLineRef::Delimiter, 4), @@ -65,7 +65,7 @@ mod streaming { #[crate::bisync::bisync] #[cfg_attr(feature = "blocking-io", test)] #[cfg_attr(all(feature = "async-io", not(feature = "blocking-io")), async_std::test)] - async fn error_line() -> crate::Result { + async fn error_line() -> gix_error::TestResult { let mut out = Vec::new(); encode_io::write_error( &PacketLineRef::Data(b"the error").as_error().expect("data line"), @@ -80,7 +80,7 @@ mod streaming { #[crate::bisync::bisync] #[cfg_attr(feature = "blocking-io", test)] #[cfg_attr(all(feature = "async-io", not(feature = "blocking-io")), async_std::test)] - async fn side_bands() -> crate::Result { + async fn side_bands() -> gix_error::TestResult { for channel in &[Channel::Data, Channel::Error, Channel::Progress] { let mut out = Vec::new(); let band = PacketLineRef::Data(b"band data") @@ -107,19 +107,19 @@ mod streaming { } #[test] - fn flush() -> crate::Result { + fn flush() -> gix_error::TestResult { assert_complete(streaming(b"0000someotherstuff"), 4, PacketLineRef::Flush)?; Ok(()) } #[test] - fn trailing_line_feeds_are_not_removed_automatically() -> crate::Result { + fn trailing_line_feeds_are_not_removed_automatically() -> gix_error::TestResult { assert_complete(streaming(b"0006a\n"), 6, PacketLineRef::Data(b"a\n"))?; Ok(()) } #[test] - fn ignore_extra_bytes() -> crate::Result { + fn ignore_extra_bytes() -> gix_error::TestResult { assert_complete(streaming(b"0006a\nhello"), 6, PacketLineRef::Data(b"a\n"))?; Ok(()) } @@ -133,7 +133,7 @@ mod streaming { } #[test] - fn error_on_error_line() -> crate::Result { + fn error_on_error_line() -> gix_error::TestResult { let line = PacketLineRef::Data(b"ERR the error"); assert_complete( streaming(b"0011ERR the error-and just ignored because not part of the size"), @@ -161,9 +161,12 @@ mod streaming { } mod incomplete { - use gix_packetline::decode::{self, Stream, streaming}; + use gix_packetline::decode::{Stream, streaming}; - fn assert_incomplete(res: Result, expected_missing: usize) -> Result<(), decode::Error> { + fn assert_incomplete( + res: Result, + expected_missing: usize, + ) -> Result<(), gix_error::ValidationError> { match res? { Stream::Complete { .. } => { panic!("expected parsing to be partial, not complete"); @@ -176,14 +179,14 @@ mod streaming { } #[test] - fn missing_hex_bytes() -> crate::Result { + fn missing_hex_bytes() -> gix_error::TestResult { assert_incomplete(streaming(b"0"), 3)?; assert_incomplete(streaming(b"00"), 2)?; Ok(()) } #[test] - fn missing_data_bytes() -> crate::Result { + fn missing_data_bytes() -> gix_error::TestResult { assert_incomplete(streaming(b"0005"), 1)?; assert_incomplete(streaming(b"0006a"), 1)?; Ok(()) diff --git a/gix-packetline/tests/packetline/encode.rs b/gix-packetline/tests/packetline/encode.rs index 4b9d6ba460b..cb074ce95c1 100644 --- a/gix-packetline/tests/packetline/encode.rs +++ b/gix-packetline/tests/packetline/encode.rs @@ -15,7 +15,7 @@ mod data_to_write { #[crate::bisync::bisync] #[cfg_attr(feature = "blocking-io", test)] #[cfg_attr(all(feature = "async-io", not(feature = "blocking-io")), async_std::test)] - async fn binary_and_non_binary() -> crate::Result { + async fn binary_and_non_binary() -> gix_error::TestResult { let mut out = Vec::new(); let res = data_to_write(b"\0", &mut out).await?; assert_eq!(res, 5); @@ -59,7 +59,7 @@ mod text_to_write { #[crate::bisync::bisync] #[cfg_attr(feature = "blocking-io", test)] #[cfg_attr(all(feature = "async-io", not(feature = "blocking-io")), async_std::test)] - async fn always_appends_a_newline() -> crate::Result { + async fn always_appends_a_newline() -> gix_error::TestResult { let mut out = Vec::new(); let res = text_to_write(b"a", &mut out).await?; assert_eq!(res, 6); @@ -87,7 +87,7 @@ mod error { #[crate::bisync::bisync] #[cfg_attr(feature = "blocking-io", test)] #[cfg_attr(all(feature = "async-io", not(feature = "blocking-io")), async_std::test)] - async fn write_line() -> crate::Result { + async fn write_line() -> gix_error::TestResult { let mut out = Vec::new(); let res = error_to_write(b"hello error", &mut out).await?; assert_eq!(res, 19); @@ -106,7 +106,7 @@ mod flush_delim_response_end { #[crate::bisync::bisync] #[cfg_attr(feature = "blocking-io", test)] #[cfg_attr(all(feature = "async-io", not(feature = "blocking-io")), async_std::test)] - async fn success_flush_delim_response_end() -> crate::Result { + async fn success_flush_delim_response_end() -> gix_error::TestResult { let mut out = Vec::new(); let res = flush_to_write(&mut out).await?; assert_eq!(res, 4); diff --git a/gix-packetline/tests/packetline/main-async.rs b/gix-packetline/tests/packetline/main-async.rs index a299dee441b..9256e273ecf 100644 --- a/gix-packetline/tests/packetline/main-async.rs +++ b/gix-packetline/tests/packetline/main-async.rs @@ -2,8 +2,6 @@ pub mod bisync { pub use gix_macros::{discard as only_sync, keep as bisync, keep as only_async}; } -pub type Result = gix_error::TestResult; - pub fn assert_err_display( res: std::result::Result, expected: impl AsRef, diff --git a/gix-packetline/tests/packetline/main-blocking.rs b/gix-packetline/tests/packetline/main-blocking.rs index 6d54d0a4404..ed6f1d34b4e 100644 --- a/gix-packetline/tests/packetline/main-blocking.rs +++ b/gix-packetline/tests/packetline/main-blocking.rs @@ -2,8 +2,6 @@ pub mod bisync { pub use gix_macros::{discard as only_async, keep as only_sync, sync as bisync}; } -pub type Result = gix_error::TestResult; - pub fn assert_err_display( res: std::result::Result, expected: impl AsRef, diff --git a/gix-packetline/tests/packetline/read/mod.rs b/gix-packetline/tests/packetline/read/mod.rs index 4d701f2721c..d7a6af7ca60 100644 --- a/gix-packetline/tests/packetline/read/mod.rs +++ b/gix-packetline/tests/packetline/read/mod.rs @@ -25,7 +25,7 @@ pub mod streaming_peek_iter { #[crate::bisync::bisync] #[cfg_attr(feature = "blocking-io", test)] #[cfg_attr(all(feature = "async-io", not(feature = "blocking-io")), async_std::test)] - async fn peek_follows_read_line_delimiter_logic() -> crate::Result { + async fn peek_follows_read_line_delimiter_logic() -> gix_error::TestResult { let mut rd = StreamingPeekableIter::new(&b"0005a00000005b"[..], &[PacketLineRef::Flush], false); let res = rd.peek_line().await; assert_eq!(res.expect("line")??, PacketLineRef::Data(b"a")); @@ -53,7 +53,7 @@ pub mod streaming_peek_iter { #[crate::bisync::bisync] #[cfg_attr(feature = "blocking-io", test)] #[cfg_attr(all(feature = "async-io", not(feature = "blocking-io")), async_std::test)] - async fn peek_follows_read_line_err_logic() -> crate::Result { + async fn peek_follows_read_line_err_logic() -> gix_error::TestResult { let mut rd = StreamingPeekableIter::new(&b"0005a0009ERR e0000"[..], &[PacketLineRef::Flush], false); rd.fail_on_err_lines(true); let res = rd.peek_line().await; @@ -82,7 +82,7 @@ pub mod streaming_peek_iter { #[crate::bisync::bisync] #[cfg_attr(feature = "blocking-io", test)] #[cfg_attr(all(feature = "async-io", not(feature = "blocking-io")), async_std::test)] - async fn peek_eof_is_none() -> crate::Result { + async fn peek_eof_is_none() -> gix_error::TestResult { let mut rd = StreamingPeekableIter::new(&b"0005a0009ERR e0000"[..], &[PacketLineRef::Flush], false); rd.fail_on_err_lines(false); let res = rd.peek_line().await; @@ -105,7 +105,7 @@ pub mod streaming_peek_iter { #[crate::bisync::bisync] #[cfg_attr(feature = "blocking-io", test)] #[cfg_attr(all(feature = "async-io", not(feature = "blocking-io")), async_std::test)] - async fn peek_non_data() -> crate::Result { + async fn peek_non_data() -> gix_error::TestResult { let mut rd = StreamingPeekableIter::new(&b"000000010002"[..], &[PacketLineRef::ResponseEnd], false); let res = rd.read_line().await; assert_eq!(res.expect("line")??, PacketLineRef::Flush); @@ -133,7 +133,7 @@ pub mod streaming_peek_iter { #[crate::bisync::bisync] #[cfg_attr(feature = "blocking-io", test)] #[cfg_attr(all(feature = "async-io", not(feature = "blocking-io")), async_std::test)] - async fn fail_on_err_lines() -> crate::Result { + async fn fail_on_err_lines() -> gix_error::TestResult { let input = b"00010009ERR e0002"; let mut rd = StreamingPeekableIter::new(&input[..], &[], false); let res = rd.read_line().await; @@ -173,7 +173,7 @@ pub mod streaming_peek_iter { #[crate::bisync::bisync] #[cfg_attr(feature = "blocking-io", test)] #[cfg_attr(all(feature = "async-io", not(feature = "blocking-io")), async_std::test)] - async fn oversized_packet_lengths_are_reported_instead_of_panicking() -> crate::Result { + async fn oversized_packet_lengths_are_reported_instead_of_panicking() -> gix_error::TestResult { let mut rd = StreamingPeekableIter::new(&b"ffff\n"[..], &[], false); let err = rd .read_line() @@ -191,7 +191,7 @@ pub mod streaming_peek_iter { #[crate::bisync::bisync] #[cfg_attr(feature = "blocking-io", test)] #[cfg_attr(all(feature = "async-io", not(feature = "blocking-io")), async_std::test)] - async fn peek() -> crate::Result { + async fn peek() -> gix_error::TestResult { let bytes = fixture_bytes("v1/fetch/01-many-refs.response"); let mut rd = StreamingPeekableIter::new(&bytes[..], &[PacketLineRef::Flush], false); let res = rd.peek_line().await; @@ -229,7 +229,7 @@ pub mod streaming_peek_iter { #[crate::bisync::bisync] #[cfg_attr(feature = "blocking-io", test)] #[cfg_attr(all(feature = "async-io", not(feature = "blocking-io")), async_std::test)] - async fn read_from_file_and_reader_advancement() -> crate::Result { + async fn read_from_file_and_reader_advancement() -> gix_error::TestResult { let mut bytes = fixture_bytes("v1/fetch/01-many-refs.response"); bytes.extend(fixture_bytes("v1/fetch/01-many-refs.response")); let mut rd = StreamingPeekableIter::new(&bytes[..], &[PacketLineRef::Flush], false); diff --git a/gix-packetline/tests/packetline/read/sideband.rs b/gix-packetline/tests/packetline/read/sideband.rs index c472eb54bd0..ed3ae6d75b0 100644 --- a/gix-packetline/tests/packetline/read/sideband.rs +++ b/gix-packetline/tests/packetline/read/sideband.rs @@ -42,7 +42,7 @@ mod util { #[crate::bisync::bisync] #[cfg_attr(feature = "blocking-io", test)] #[cfg_attr(all(feature = "async-io", not(feature = "blocking-io")), async_std::test)] -async fn read_pack_with_progress_extraction() -> crate::Result { +async fn read_pack_with_progress_extraction() -> gix_error::TestResult { let buf = fixture_bytes("v1/01-clone.combined-output"); let mut rd = StreamingPeekableIter::new(&buf[..], &[PacketLineRef::Flush], false); @@ -105,7 +105,7 @@ async fn read_pack_with_progress_extraction() -> crate::Result { #[crate::bisync::bisync] #[cfg_attr(feature = "blocking-io", test)] #[cfg_attr(all(feature = "async-io", not(feature = "blocking-io")), async_std::test)] -async fn read_line_trait_method_reads_one_packet_line_at_a_time() -> crate::Result { +async fn read_line_trait_method_reads_one_packet_line_at_a_time() -> gix_error::TestResult { let buf = fixture_bytes("v1/01-clone.combined-output-no-binary"); let mut rd = StreamingPeekableIter::new(&buf[..], &[PacketLineRef::Flush], false); @@ -156,7 +156,7 @@ async fn read_line_trait_method_reads_one_packet_line_at_a_time() -> crate::Resu #[crate::bisync::bisync] #[cfg_attr(feature = "blocking-io", test)] #[cfg_attr(all(feature = "async-io", not(feature = "blocking-io")), async_std::test)] -async fn readline_reads_one_packet_line_at_a_time() -> crate::Result { +async fn readline_reads_one_packet_line_at_a_time() -> gix_error::TestResult { let buf = fixture_bytes("v1/01-clone.combined-output-no-binary"); let mut rd = StreamingPeekableIter::new(&buf[..], &[PacketLineRef::Flush], false); @@ -207,7 +207,7 @@ async fn readline_reads_one_packet_line_at_a_time() -> crate::Result { #[crate::bisync::bisync] #[cfg_attr(feature = "blocking-io", test)] #[cfg_attr(all(feature = "async-io", not(feature = "blocking-io")), async_std::test)] -async fn empty_progress_and_error_sidebands_are_forwarded_without_panic() -> crate::Result { +async fn empty_progress_and_error_sidebands_are_forwarded_without_panic() -> gix_error::TestResult { let input = b"0005\x020005\x030000"; let mut rd = StreamingPeekableIter::new(&input[..], &[PacketLineRef::Flush], false); let mut seen = Vec::new(); @@ -233,7 +233,7 @@ async fn empty_progress_and_error_sidebands_are_forwarded_without_panic() -> cra #[crate::bisync::bisync] #[cfg_attr(feature = "blocking-io", test)] #[cfg_attr(all(feature = "async-io", not(feature = "blocking-io")), async_std::test)] -async fn peek_past_an_actual_eof_is_an_error() -> crate::Result { +async fn peek_past_an_actual_eof_is_an_error() -> gix_error::TestResult { let input = b"0009ERR e"; let mut rd = StreamingPeekableIter::new(&input[..], &[], false); let mut reader = rd.as_read(); @@ -259,7 +259,7 @@ async fn peek_past_an_actual_eof_is_an_error() -> crate::Result { #[crate::bisync::bisync] #[cfg_attr(feature = "blocking-io", test)] #[cfg_attr(all(feature = "async-io", not(feature = "blocking-io")), async_std::test)] -async fn peek_past_a_delimiter_is_no_error() -> crate::Result { +async fn peek_past_a_delimiter_is_no_error() -> gix_error::TestResult { let input = b"0009hello0000"; let mut rd = StreamingPeekableIter::new(&input[..], &[PacketLineRef::Flush], false); let mut reader = rd.as_read(); diff --git a/gix-packetline/tests/packetline/write.rs b/gix-packetline/tests/packetline/write.rs index caae2c908ae..bfc7dbdca51 100644 --- a/gix-packetline/tests/packetline/write.rs +++ b/gix-packetline/tests/packetline/write.rs @@ -16,7 +16,7 @@ const MAX_LINE_LEN: usize = 4 + MAX_DATA_LEN; #[crate::bisync::bisync] #[cfg_attr(feature = "blocking-io", test)] #[cfg_attr(all(feature = "async-io", not(feature = "blocking-io")), async_std::test)] -async fn each_write_results_in_one_line() -> crate::Result { +async fn each_write_results_in_one_line() -> gix_error::TestResult { let mut w = Writer::new(Vec::new()); w.write_all(b"hello").await?; w.write(b"world!").await?; @@ -29,7 +29,7 @@ async fn each_write_results_in_one_line() -> crate::Result { #[crate::bisync::bisync] #[cfg_attr(feature = "blocking-io", test)] #[cfg_attr(all(feature = "async-io", not(feature = "blocking-io")), async_std::test)] -async fn write_text_and_write_binary() -> crate::Result { +async fn write_text_and_write_binary() -> gix_error::TestResult { let buf = { let mut w = Writer::new(Vec::new()); w.enable_text_mode(); @@ -46,7 +46,7 @@ async fn write_text_and_write_binary() -> crate::Result { #[crate::bisync::bisync] #[cfg_attr(feature = "blocking-io", test)] #[cfg_attr(all(feature = "async-io", not(feature = "blocking-io")), async_std::test)] -async fn huge_writes_are_split_into_lines() -> crate::Result { +async fn huge_writes_are_split_into_lines() -> gix_error::TestResult { let buf = { let data = vec![0u8; MAX_DATA_LEN * 2]; let mut w = Writer::new(Vec::new()); diff --git a/gix-path/src/convert.rs b/gix-path/src/convert.rs index ba2b9a37052..703168f474e 100644 --- a/gix-path/src/convert.rs +++ b/gix-path/src/convert.rs @@ -6,16 +6,13 @@ use std::{ use bstr::{BStr, BString}; -/// The error type returned by [`into_bstr()`] and others may suffer from failed conversions from or to bytes. -pub type Utf8Error = gix_error::ValidationError; - #[cfg(not(unix))] -fn utf8_error() -> Utf8Error { - Utf8Error::new("Could not convert to UTF8 or from UTF8 due to ill-formed input") +fn utf8_error() -> gix_error::ValidationError { + gix_error::ValidationError::new("Could not convert to UTF8 or from UTF8 due to ill-formed input") } /// 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> { +pub fn os_str_into_bstr(path: &OsStr) -> Result<&BStr, gix_error::ValidationError> { let path = try_into_bstr(Cow::Borrowed(path.as_ref()))?; match path { Cow::Borrowed(path) => Ok(path), @@ -24,7 +21,7 @@ pub fn os_str_into_bstr(path: &OsStr) -> Result<&BStr, Utf8Error> { } /// Like [`into_bstr()`], but takes `OsString` as input for a lossless, but fallible, conversion. -pub fn os_string_into_bstring(path: OsString) -> Result { +pub fn os_string_into_bstring(path: OsString) -> Result { let path = try_into_bstr(Cow::Owned(path.into()))?; match path { Cow::Borrowed(_path) => unreachable!("borrowed cows stay borrowed"), @@ -33,7 +30,7 @@ pub fn os_string_into_bstring(path: OsString) -> Result { } /// Like [`into_bstr()`], but takes `Cow` as input for a lossless, but fallible, conversion. -pub fn try_os_str_into_bstr(path: Cow<'_, OsStr>) -> Result, Utf8Error> { +pub fn try_os_str_into_bstr(path: Cow<'_, OsStr>) -> Result, gix_error::ValidationError> { match path { Cow::Borrowed(path) => os_str_into_bstr(path).map(Cow::Borrowed), Cow::Owned(path) => os_string_into_bstring(path).map(Cow::Owned), @@ -43,8 +40,8 @@ pub fn try_os_str_into_bstr(path: Cow<'_, OsStr>) -> Result, Utf8E /// Convert the given path either into its raw bytes on Unix or its UTF-8 encoded counterpart on non-Unix platforms. /// /// On non-Unix platforms, if the source `Path`` contains ill-formed, lone surrogates, the UTF-8 conversion will fail -/// causing `Utf8Error` to be returned. -pub fn try_into_bstr<'a>(path: impl Into>) -> Result, Utf8Error> { +/// causing `gix_error::ValidationError` to be returned. +pub fn try_into_bstr<'a>(path: impl Into>) -> Result, gix_error::ValidationError> { let path = path.into(); let path_str = match path { Cow::Owned(path) => Cow::Owned({ @@ -91,7 +88,7 @@ pub fn join_bstr_unix_pathsep<'a, 'b>(base: impl Into>, path: impl /// On non-Unix platforms, 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 installations which produce malformed UTF-16 /// if certain emojis are in the path. It's as rare as it sounds, but possible. -pub fn try_from_byte_slice(input: &[u8]) -> Result<&Path, Utf8Error> { +pub fn try_from_byte_slice(input: &[u8]) -> Result<&Path, gix_error::ValidationError> { #[cfg(unix)] let p = { use std::os::unix::ffi::OsStrExt; @@ -103,7 +100,7 @@ pub fn try_from_byte_slice(input: &[u8]) -> Result<&Path, Utf8Error> { } /// Similar to [`from_byte_slice()`], but takes either borrowed or owned `input`. -pub fn try_from_bstr<'a>(input: impl Into>) -> Result, Utf8Error> { +pub fn try_from_bstr<'a>(input: impl Into>) -> Result, gix_error::ValidationError> { let input = input.into(); match input { Cow::Borrowed(input) => try_from_byte_slice(input).map(Cow::Borrowed), @@ -117,7 +114,7 @@ pub fn from_bstr<'a>(input: impl Into>) -> Cow<'a, Path> { } /// Similar to [`try_from_bstr()`], but takes and produces owned data. -pub fn try_from_bstring(input: impl Into) -> Result { +pub fn try_from_bstring(input: impl Into) -> Result { let input = input.into(); #[cfg(unix)] let p = { diff --git a/gix-path/src/realpath.rs b/gix-path/src/realpath.rs index e1ac63c03e8..a252de8ae2e 100644 --- a/gix-path/src/realpath.rs +++ b/gix-path/src/realpath.rs @@ -1,6 +1,3 @@ -/// The error returned by [`realpath()`][super::realpath()]. -pub type Error = gix_error::Exn; - /// The default amount of symlinks we may follow when resolving a path in [`realpath()`][crate::realpath()]. pub const MAX_SYMLINKS: u8 = 32; @@ -11,7 +8,6 @@ pub(crate) mod function { Path, PathBuf, }; - use super::Error; use crate::realpath::MAX_SYMLINKS; /// Check each component of `path` and see if it is a symlink. If so, resolve it. @@ -20,7 +16,7 @@ pub(crate) mod function { /// If `path` is relative, the current working directory be used to make it absolute. /// Note that the returned path will be verbatim, and repositories with `core.precomposeUnicode` /// set will probably want to precompose the paths unicode. - pub fn realpath(path: impl AsRef) -> Result { + pub fn realpath(path: impl AsRef) -> Result { let path = path.as_ref(); let cwd = path .is_relative() @@ -32,7 +28,7 @@ pub(crate) mod function { /// The same as [`realpath()`], but allow to configure `max_symlinks` to configure how many symbolic links we are going to follow. /// This serves to avoid running into cycles or doing unreasonable amounts of work. - pub fn realpath_opts(path: &Path, cwd: &Path, max_symlinks: u8) -> Result { + pub fn realpath_opts(path: &Path, cwd: &Path, max_symlinks: u8) -> Result { if path.as_os_str().is_empty() { return Err(ValidationError::new("Empty is not a valid path").raise_erased()); } diff --git a/gix-path/src/relative_path.rs b/gix-path/src/relative_path.rs index c14f2e46f2f..c290ec97635 100644 --- a/gix-path/src/relative_path.rs +++ b/gix-path/src/relative_path.rs @@ -28,7 +28,7 @@ pub(super) mod types { use types::RelativePath; impl RelativePath { - fn new_unchecked(value: &BStr) -> Result<&RelativePath, Error> { + fn new_unchecked(value: &BStr) -> Result<&RelativePath, gix_error::Exn> { // SAFETY: `RelativePath` is transparent and equivalent to a `&BStr` if provided as reference. #[expect(unsafe_code)] unsafe { @@ -37,10 +37,10 @@ impl RelativePath { } } -/// The error used in [`RelativePath`]. -pub type Error = gix_error::Exn; - -fn relative_path_from_value_and_path<'a>(path_bstr: &'a BStr, path: &Path) -> Result<&'a RelativePath, Error> { +fn relative_path_from_value_and_path<'a>( + path_bstr: &'a BStr, + path: &Path, +) -> Result<&'a RelativePath, gix_error::Exn> { if path.is_absolute() { return Err(ValidationError::new("A RelativePath is not allowed to be absolute").raise()); } @@ -57,7 +57,7 @@ fn relative_path_from_value_and_path<'a>(path_bstr: &'a BStr, path: &Path) -> Re } impl<'a> TryFrom<&'a str> for &'a RelativePath { - type Error = Error; + type Error = gix_error::Exn; fn try_from(value: &'a str) -> Result { relative_path_from_value_and_path(value.into(), Path::new(value)) @@ -65,7 +65,7 @@ impl<'a> TryFrom<&'a str> for &'a RelativePath { } impl<'a> TryFrom<&'a BStr> for &'a RelativePath { - type Error = Error; + type Error = gix_error::Exn; fn try_from(value: &'a BStr) -> Result { let path = try_from_bstr(value)?; @@ -74,7 +74,7 @@ impl<'a> TryFrom<&'a BStr> for &'a RelativePath { } impl<'a> TryFrom<&'a [u8]> for &'a RelativePath { - type Error = Error; + type Error = gix_error::Exn; #[inline] fn try_from(value: &'a [u8]) -> Result { @@ -84,7 +84,7 @@ impl<'a> TryFrom<&'a [u8]> for &'a RelativePath { } impl<'a, const N: usize> TryFrom<&'a [u8; N]> for &'a RelativePath { - type Error = Error; + type Error = gix_error::Exn; #[inline] fn try_from(value: &'a [u8; N]) -> Result { @@ -94,7 +94,7 @@ impl<'a, const N: usize> TryFrom<&'a [u8; N]> for &'a RelativePath { } impl<'a> TryFrom<&'a BString> for &'a RelativePath { - type Error = Error; + type Error = gix_error::Exn; fn try_from(value: &'a BString) -> Result { let path = try_from_bstr(value.as_bstr())?; diff --git a/gix-path/tests/path/convert/normalize.rs b/gix-path/tests/path/convert/normalize.rs index 2ce82b0f08f..a5f2ff169ca 100644 --- a/gix-path/tests/path/convert/normalize.rs +++ b/gix-path/tests/path/convert/normalize.rs @@ -15,7 +15,7 @@ fn no_change_if_there_are_no_trailing_relative_components() { } #[test] -fn special_cases_around_cwd() -> crate::Result { +fn special_cases_around_cwd() -> gix_error::TestResult { let cwd = std::env::current_dir()?; assert_eq!( normalize(p("./../../.git/modules/src/llvm-project").into(), &cwd).unwrap(), diff --git a/gix-path/tests/path/main.rs b/gix-path/tests/path/main.rs index 956b5d5fe63..711c28b627b 100644 --- a/gix-path/tests/path/main.rs +++ b/gix-path/tests/path/main.rs @@ -1,5 +1,3 @@ -pub type Result = std::result::Result; - mod convert; mod realpath; mod relative_path; diff --git a/gix-path/tests/path/realpath.rs b/gix-path/tests/path/realpath.rs index fa68ac16c4b..6047d8e36f6 100644 --- a/gix-path/tests/path/realpath.rs +++ b/gix-path/tests/path/realpath.rs @@ -4,9 +4,9 @@ use std::{ }; use bstr::ByteVec; -use gix_path::{realpath::Error, realpath_opts}; +use gix_path::realpath_opts; -fn assert_validation(result: Result, expected_message: &str) { +fn assert_validation(result: Result, expected_message: &str) { let err = result.err().expect("input should be invalid"); let validation = err .downcast_any_ref::() @@ -15,7 +15,7 @@ fn assert_validation(result: Result, expected_message: &str) { } #[test] -fn fuzzed_timeout() -> crate::Result { +fn fuzzed_timeout() -> gix_error::TestResult { let path = PathBuf::from(std::fs::read("tests/fixtures/fuzzed/54k-path-components.path")?.into_string()?); assert_eq!(path.components().count(), 54862); let start = std::time::Instant::now(); @@ -33,7 +33,7 @@ fn fuzzed_timeout() -> crate::Result { } #[test] -fn assorted() -> crate::Result { +fn assorted() -> gix_error::TestResult { let cwd = tempfile::tempdir()?; let cwd = cwd.path(); let symlinks_disabled = 0; @@ -89,7 +89,7 @@ fn assorted() -> crate::Result { } #[test] -fn link_cycle_is_detected() -> crate::Result { +fn link_cycle_is_detected() -> gix_error::TestResult { let tmp_dir = canonicalized_tempdir()?; let dir = tmp_dir.path(); let link_name = "link"; @@ -106,7 +106,7 @@ fn link_cycle_is_detected() -> crate::Result { } #[test] -fn symlink_with_absolute_path_gets_expanded() -> crate::Result { +fn symlink_with_absolute_path_gets_expanded() -> gix_error::TestResult { let tmp_dir = canonicalized_tempdir()?; let dir = tmp_dir.path(); let link_from = dir.join("a").join("b").join("tmp_p_q_link"); @@ -122,7 +122,7 @@ fn symlink_with_absolute_path_gets_expanded() -> crate::Result { } #[test] -fn symlink_to_relative_path_gets_expanded_into_absolute_path() -> crate::Result { +fn symlink_to_relative_path_gets_expanded_into_absolute_path() -> gix_error::TestResult { let cwd = canonicalized_tempdir()?; let dir = cwd.path(); let link_name = "pq_link"; @@ -136,7 +136,7 @@ fn symlink_to_relative_path_gets_expanded_into_absolute_path() -> crate::Result } #[test] -fn symlink_processing_is_disabled_if_the_value_is_zero() -> crate::Result { +fn symlink_processing_is_disabled_if_the_value_is_zero() -> gix_error::TestResult { let cwd = canonicalized_tempdir()?; let link_name = "x_link"; create_symlink(cwd.path().join(link_name), Path::new("link destination does not exist"))?; diff --git a/gix-path/tests/path/relative_path.rs b/gix-path/tests/path/relative_path.rs index f32360494de..666c92caa2f 100644 --- a/gix-path/tests/path/relative_path.rs +++ b/gix-path/tests/path/relative_path.rs @@ -1,7 +1,11 @@ use bstr::{BStr, BString}; -use gix_path::{RelativePath, relative_path::Error}; +use gix_path::RelativePath; -fn assert_validation(result: Result, expected_message: &str, has_component_source: bool) { +fn assert_validation( + result: Result>, + expected_message: &str, + has_component_source: bool, +) { let err = result.err().expect("input should be invalid"); assert_eq!(err.message, expected_message); assert_eq!( diff --git a/gix-pathspec/src/defaults.rs b/gix-pathspec/src/defaults.rs index 8ce7b5c8939..75ce0f79bce 100644 --- a/gix-pathspec/src/defaults.rs +++ b/gix-pathspec/src/defaults.rs @@ -4,12 +4,6 @@ use gix_error::{ErrorExt, ValidationError}; use crate::{Defaults, MagicSignature, SearchMode}; -/// -pub mod from_environment { - /// The error returned by [Defaults::from_environment()](super::Defaults::from_environment()). - pub type Error = gix_error::Exn; -} - impl Defaults { /// Initialize this instance using information from the environment as /// [per the official documentation](https://git-scm.com/book/en/v2/Git-Internals-Environment-Variables) *(look for `PATHSPECS`)*, @@ -23,8 +17,10 @@ impl Defaults { /// /// Instead of failing if `GIT_LITERAL_PATHSPECS` is used with glob globals, we ignore these. Also our implementation allows global /// `icase` settings in combination with this setting. - pub fn from_environment(var: &mut dyn FnMut(&str) -> Option) -> Result { - let mut env_bool = |name: &str| -> Result, from_environment::Error> { + pub fn from_environment( + var: &mut dyn FnMut(&str) -> Option, + ) -> Result> { + let mut env_bool = |name: &str| -> Result, gix_error::Exn> { var(name) .map(|val| gix_config_value::Boolean::try_from(val).map(|b| b.0)) .transpose() diff --git a/gix-pathspec/src/lib.rs b/gix-pathspec/src/lib.rs index 5cac9d00442..6b99222e23a 100644 --- a/gix-pathspec/src/lib.rs +++ b/gix-pathspec/src/lib.rs @@ -46,12 +46,6 @@ use bstr::BString; /// `gix-glob` types are available through [`attributes::glob`]. pub use gix_attributes as attributes; -/// -pub mod normalize { - /// The error returned by [Pattern::normalize()](super::Pattern::normalize()). - pub type Error = gix_error::ValidationError; -} - mod pattern; /// @@ -169,6 +163,6 @@ pub enum SearchMode { /// setting the given `default` values in case these aren't specified in `input`. /// /// Note that empty [paths](Pattern::path) are allowed here, and generally some processing has to be performed. -pub fn parse(input: &[u8], default: Defaults) -> Result { +pub fn parse(input: &[u8], default: Defaults) -> Result { Pattern::from_bytes(input, default) } diff --git a/gix-pathspec/src/parse.rs b/gix-pathspec/src/parse.rs index ab81ddc88f7..4a2a4355f32 100644 --- a/gix-pathspec/src/parse.rs +++ b/gix-pathspec/src/parse.rs @@ -5,9 +5,6 @@ use gix_error::ValidationError; use crate::{Defaults, MagicSignature, Pattern, SearchMode}; -/// The error returned by [parse()][crate::parse()]. -pub type Error = gix_error::ValidationError; - impl Pattern { /// Try to parse a path-spec pattern from the given `input` bytes. pub fn from_bytes( @@ -17,7 +14,7 @@ impl Pattern { search_mode, literal, }: Defaults, - ) -> Result { + ) -> Result { if input.is_empty() { return Err(ValidationError::new_with_input( "An empty string is not a valid pathspec", @@ -74,7 +71,7 @@ impl Pattern { } } -fn parse_short_keywords(input: &[u8], cursor: &mut usize) -> Result { +fn parse_short_keywords(input: &[u8], cursor: &mut usize) -> Result { let unimplemented_chars = b"\"#%&'-',;<=>@_`~"; let mut signature = MagicSignature::empty(); @@ -97,7 +94,7 @@ fn parse_short_keywords(input: &[u8], cursor: &mut usize) -> Result Result<(), Error> { +fn parse_long_keywords(input: &[u8], p: &mut Pattern, cursor: &mut usize) -> Result<(), gix_error::ValidationError> { let end = input .find(")") .ok_or_else(|| ValidationError::new_with_input("Missing ')' at the end of pathspec signature", input))?; @@ -163,8 +160,8 @@ fn parse_long_keywords(input: &[u8], p: &mut Pattern, cursor: &mut usize) -> Res fn split_on_non_escaped_char( input: &[u8], split_char: u8, - mut f: impl FnMut(&[u8]) -> Result<(), Error>, -) -> Result<(), Error> { + mut f: impl FnMut(&[u8]) -> Result<(), gix_error::ValidationError>, +) -> Result<(), gix_error::ValidationError> { // Mirrors `strcspn_escaped()` in Git's `pathspec.c`: a backslash consumes the byte that // follows it, so `\,` is a literal comma while `\\,` is an escaped backslash followed by a // separator. Scanning byte-by-byte also lets a separator at index 0 be seen, which a @@ -185,7 +182,7 @@ fn split_on_non_escaped_char( f(&input[last..]) } -fn parse_attributes(input: &[u8]) -> Result, Error> { +fn parse_attributes(input: &[u8]) -> Result, gix_error::ValidationError> { if input.is_empty() { return Err(ValidationError::new("Attribute specification cannot be empty")); } @@ -221,7 +218,7 @@ fn parse_attributes(input: &[u8]) -> Result, Err .collect() } -fn unescape_and_check_attr_value(value: &BStr) -> Result { +fn unescape_and_check_attr_value(value: &BStr) -> Result { let mut out = BString::from(Vec::with_capacity(value.len())); let mut bytes = value.iter(); while let Some(mut b) = bytes.next().copied() { @@ -239,7 +236,7 @@ fn unescape_and_check_attr_value(value: &BStr) -> Result { Ok(out) } -fn check_attribute_value(input: &BStr) -> Result<(), Error> { +fn check_attribute_value(input: &BStr) -> Result<(), gix_error::ValidationError> { match input.iter().copied().find(|b| !is_valid_attr_value(*b)) { Some(b) => Err(ValidationError::new_with_input( "Invalid character in attribute value", @@ -253,7 +250,7 @@ fn is_valid_attr_value(byte: u8) -> bool { byte.is_ascii_alphanumeric() || b",-_".contains(&byte) } -fn validated_attr_value_byte(byte: u8) -> Result { +fn validated_attr_value_byte(byte: u8) -> Result { if is_valid_attr_value(byte) { Ok(byte) } else { diff --git a/gix-pathspec/src/pattern.rs b/gix-pathspec/src/pattern.rs index fa1423f5026..bdca7c5c439 100644 --- a/gix-pathspec/src/pattern.rs +++ b/gix-pathspec/src/pattern.rs @@ -3,7 +3,7 @@ use std::path::{Component, Path, PathBuf}; use bstr::{BStr, BString, ByteSlice, ByteVec}; use gix_error::ValidationError; -use crate::{MagicSignature, Pattern, SearchMode, normalize}; +use crate::{MagicSignature, Pattern, SearchMode}; /// Access impl Pattern { @@ -41,7 +41,7 @@ impl Pattern { /// `prefix` can be empty, we will still normalize this pathspec to resolve relative path components, and /// it is assumed not to contain any relative path components, e.g. '', 'a', 'a/b' are valid. /// `root` is the absolute path to the root of either the worktree or the repository's `git_dir`. - pub fn normalize(&mut self, prefix: &Path, root: &Path) -> Result<&mut Self, normalize::Error> { + pub fn normalize(&mut self, prefix: &Path, root: &Path) -> Result<&mut Self, gix_error::ValidationError> { fn prefix_components_to_subtract(path: &Path) -> usize { let parent_component_end_bound = path.components().enumerate().fold(None::, |acc, (idx, c)| { matches!(c, Component::ParentDir).then_some(idx + 1).or(acc) diff --git a/gix-pathspec/src/search/init.rs b/gix-pathspec/src/search/init.rs index 0c35088bbba..26d2d7a8592 100644 --- a/gix-pathspec/src/search/init.rs +++ b/gix-pathspec/src/search/init.rs @@ -8,7 +8,7 @@ fn mapping_from_pattern( prefix: &Path, root: &Path, sequence_number: usize, -) -> Result, crate::normalize::Error> { +) -> Result, gix_error::ValidationError> { pathspec.normalize(prefix, root)?; let mut match_all = pathspec.is_nil(); let glob = { @@ -98,12 +98,12 @@ impl Search { pathspecs: impl IntoIterator, prefix: Option<&std::path::Path>, root: &std::path::Path, - ) -> Result { + ) -> Result { fn inner( pathspecs: &mut dyn Iterator, prefix: Option<&std::path::Path>, root: &std::path::Path, - ) -> Result { + ) -> Result { let prefix = prefix.unwrap_or(std::path::Path::new("")); let mut patterns = pathspecs .enumerate() diff --git a/gix-pathspec/tests/normalize/mod.rs b/gix-pathspec/tests/normalize/mod.rs index 420402c77df..bf519e25b3e 100644 --- a/gix-pathspec/tests/normalize/mod.rs +++ b/gix-pathspec/tests/normalize/mod.rs @@ -128,11 +128,7 @@ fn absolute_path_escapes_worktree() { assert_eq!(err.input.as_ref().expect("offending path").to_str_lossy(), "/dev"); } -fn normalized_spec( - path: &str, - prefix: &str, - root: &str, -) -> Result { +fn normalized_spec(path: &str, prefix: &str, root: &str) -> Result { let mut spec = gix_pathspec::parse(path.as_bytes(), Default::default()).expect("valid"); spec.normalize(Path::new(prefix), Path::new(root))?; Ok(spec) diff --git a/gix-prompt/examples/credentials.rs b/gix-prompt/examples/credentials.rs index f90e36e3f23..debd474e982 100644 --- a/gix-prompt/examples/credentials.rs +++ b/gix-prompt/examples/credentials.rs @@ -1,4 +1,4 @@ -fn main() -> Result<(), gix_prompt::Error> { +fn main() -> Result<(), gix_error::Exn> { let user = gix_prompt::openly("Username: ")?; eprintln!("{user:?}"); let pass = gix_prompt::securely("Password: ")?; diff --git a/gix-prompt/src/lib.rs b/gix-prompt/src/lib.rs index 5e426c9cd10..c292056ccc4 100644 --- a/gix-prompt/src/lib.rs +++ b/gix-prompt/src/lib.rs @@ -8,7 +8,7 @@ #![forbid(unsafe_code)] mod types; -pub use types::{Error, Mode, Options}; +pub use types::{Mode, Options}; /// pub mod unix; @@ -17,16 +17,16 @@ use unix::imp; #[cfg(not(unix))] mod imp { - use crate::{Error, Options}; + use crate::Options; use gix_error::{ErrorExt, message}; - pub(crate) fn ask(_prompt: &str, _opts: &Options) -> Result { + pub(crate) fn ask(_prompt: &str, _opts: &Options) -> Result> { Err(message("The current platform has no implementation for prompting in the terminal").raise()) } } /// Ask the user given a `prompt`, returning the result. -pub fn ask(prompt: &str, opts: &Options) -> Result { +pub fn ask(prompt: &str, opts: &Options) -> Result> { if let Some(askpass) = opts.askpass.as_deref() { match gix_command::prepare(askpass).arg(prompt).spawn() { Ok(cmd) => { @@ -56,7 +56,7 @@ pub fn ask(prompt: &str, opts: &Options) -> Result { /// Ask for information typed by the user into the terminal after showing the prompt, like `"Username: `. /// /// Use [`ask()`] for more control. -pub fn openly(prompt: impl AsRef) -> Result { +pub fn openly(prompt: impl AsRef) -> Result> { imp::ask( prompt.as_ref(), &Options { @@ -69,7 +69,7 @@ pub fn openly(prompt: impl AsRef) -> Result { /// Ask for information _securely_ after showing the `prompt` (like `"password: "`) by not showing what's typed. /// /// Use [`ask()`] for more control. -pub fn securely(prompt: impl AsRef) -> Result { +pub fn securely(prompt: impl AsRef) -> Result> { imp::ask( prompt.as_ref(), &Options { diff --git a/gix-prompt/src/types.rs b/gix-prompt/src/types.rs index 5a919951d92..92f418fcfb2 100644 --- a/gix-prompt/src/types.rs +++ b/gix-prompt/src/types.rs @@ -1,8 +1,5 @@ use std::path::PathBuf; -/// The error returned by [ask()][crate::ask()]. -pub type Error = gix_error::Exn; - /// The way the user is prompted. #[derive(Default, Debug, Copy, Clone, Eq, PartialEq)] pub enum Mode { diff --git a/gix-prompt/src/unix.rs b/gix-prompt/src/unix.rs index cd72c6594de..9ea05932ae1 100644 --- a/gix-prompt/src/unix.rs +++ b/gix-prompt/src/unix.rs @@ -13,7 +13,7 @@ pub(crate) mod imp { use parking_lot::{Mutex, RawMutex, const_mutex, lock_api::MutexGuard}; use rustix::termios::{self, Termios}; - use crate::{Error, Mode, Options, unix::TTY_PATH}; + use crate::{Mode, Options, unix::TTY_PATH}; static TERM_STATE: Mutex> = const_mutex(None); @@ -26,7 +26,10 @@ pub(crate) mod imp { } /// Ask the user given a `prompt`, returning the result. - pub(crate) fn ask(prompt: &str, Options { mode, .. }: &Options) -> Result { + pub(crate) fn ask( + prompt: &str, + Options { mode, .. }: &Options, + ) -> Result> { match mode { Mode::Disable => Err(message("Terminal prompts are disabled").raise()), Mode::Hidden => { @@ -103,7 +106,7 @@ pub(crate) mod imp { } impl RestoreTerminalStateOnDrop<'_> { - fn restore_term_state(mut self) -> Result<(), Error> { + fn restore_term_state(mut self) -> Result<(), gix_error::Exn> { let state = self.state.take().expect("BUG: we exist only if something is saved"); termios::tcsetattr(&self.fd, termios::OptionalActions::Flush, &state).or_raise(terminal_configuration)?; Ok(()) @@ -121,7 +124,7 @@ pub(crate) mod imp { fn save_term_state_and_disable_echo( mut state: TermiosGuard<'_>, fd: File, - ) -> Result, Error> { + ) -> Result, gix_error::Exn> { assert!( state.is_none(), "BUG: recursive calls are not possible and we restore afterwards" diff --git a/gix-protocol/src/command.rs b/gix-protocol/src/command.rs index 909e8d6a3d0..9410e2fa213 100644 --- a/gix-protocol/src/command.rs +++ b/gix-protocol/src/command.rs @@ -175,7 +175,7 @@ mod with_io { server: &Capabilities, arguments: &[BString], features: &[Feature], - ) -> Result<(), validate_argument_prefixes::Error> { + ) -> Result<(), gix_error::ValidationError> { use gix_error::message; let allowed = self.all_argument_prefixes(); for arg in arguments { @@ -223,12 +223,4 @@ mod with_io { Ok(()) } } - - /// - pub mod validate_argument_prefixes { - /// The error returned by [Command::validate_argument_prefixes()](super::Command::validate_argument_prefixes()). - pub type Error = gix_error::ValidationError; - } } -#[cfg(any(test, feature = "async-client", feature = "blocking-client"))] -pub use with_io::validate_argument_prefixes; diff --git a/gix-protocol/src/fetch/error.rs b/gix-protocol/src/fetch/error.rs deleted file mode 100644 index 8032b7d4540..00000000000 --- a/gix-protocol/src/fetch/error.rs +++ /dev/null @@ -1,2 +0,0 @@ -/// The error returned by [`fetch()`](crate::fetch()). -pub type Error = gix_error::Exn; diff --git a/gix-protocol/src/fetch/function.rs b/gix-protocol/src/fetch/function.rs index cd80b4541cf..24395b0259d 100644 --- a/gix-protocol/src/fetch/function.rs +++ b/gix-protocol/src/fetch/function.rs @@ -7,7 +7,7 @@ use gix_error::{ErrorExt, ResultExt, RetryableError, ValidationError, message}; use gix_features::progress::DynNestedProgress; use crate::fetch::{ - Arguments, Context, Error, Negotiate, NegotiateOutcome, Options, Outcome, ProgressId, Shallow, Tags, negotiate, + Arguments, Context, Negotiate, NegotiateOutcome, Options, Outcome, ProgressId, Shallow, Tags, negotiate, }; #[crate::bisync::only_async] use crate::transport::client::async_io::{ExtendedBufRead, HandleProgress, Transport}; @@ -58,7 +58,7 @@ pub async fn fetch( tags, reject_shallow_remote, }: Options<'_>, -) -> Result, Error> +) -> Result, gix_error::Exn> where P: gix_features::progress::NestedProgress, P::SubProgress: 'static, @@ -197,7 +197,7 @@ fn consume_received_pack( consume: impl FnOnce(&mut dyn std::io::BufRead, &mut dyn DynNestedProgress, &AtomicBool) -> Result, progress: &mut dyn DynNestedProgress, should_interrupt: &AtomicBool, -) -> Result<(R, bool), Error> +) -> Result<(R, bool), gix_error::Exn> where R: crate::futures_io::AsyncBufRead + Unpin, { @@ -213,7 +213,7 @@ fn consume_received_pack( consume: impl FnOnce(&mut dyn std::io::BufRead, &mut dyn DynNestedProgress, &AtomicBool) -> Result, progress: &mut dyn DynNestedProgress, should_interrupt: &AtomicBool, -) -> Result<(R, bool), Error> +) -> Result<(R, bool), gix_error::Exn> where R: std::io::BufRead, { @@ -234,7 +234,7 @@ fn read_remaining(reader: &mut impl std::io::Read) -> std::io::Result<()> { std::io::copy(reader, &mut std::io::sink()).map(|_| ()) } -fn acquire_shallow_lock(shallow_file: &Path) -> Result { +fn acquire_shallow_lock(shallow_file: &Path) -> Result { gix_lock::File::acquire_to_update_resource(shallow_file, gix_lock::acquire::Fail::Immediately, None) .or_raise_erased(|| message("'shallow' file could not be locked in preparation for writing changes")) } @@ -243,7 +243,7 @@ fn add_shallow_args( args: &mut Arguments, shallow: &Shallow, shallow_file: &std::path::Path, -) -> Result<(Option>, Option), Error> { +) -> Result<(Option>, Option), gix_error::Exn> { let expect_change = *shallow != Shallow::NoChange; let shallow_lock = expect_change.then(|| acquire_shallow_lock(shallow_file)).transpose()?; diff --git a/gix-protocol/src/fetch/mod.rs b/gix-protocol/src/fetch/mod.rs index 1f8eb8445eb..f0af72722d8 100644 --- a/gix-protocol/src/fetch/mod.rs +++ b/gix-protocol/src/fetch/mod.rs @@ -21,12 +21,6 @@ mod arguments; pub use arguments::Arguments; -#[cfg(any(feature = "blocking-client", feature = "async-client"))] -#[cfg(feature = "fetch")] -mod error; -#[cfg(any(feature = "blocking-client", feature = "async-client"))] -#[cfg(feature = "fetch")] -pub use error::Error; /// pub mod response; diff --git a/gix-protocol/src/fetch/negotiate.rs b/gix-protocol/src/fetch/negotiate.rs index 7e16cb24976..6f1a4199d6a 100644 --- a/gix-protocol/src/fetch/negotiate.rs +++ b/gix-protocol/src/fetch/negotiate.rs @@ -16,9 +16,6 @@ use crate::fetch::{RefMap, Shallow, Tags, refmap}; type Queue = gix_revwalk::PriorityQueue; -/// The error returned during [`one_round()`] or [`mark_complete_and_common_ref()`]. -pub type Error = gix_error::Exn; - /// Determines what should be done after [preparing the commit-graph for negotiation](mark_complete_and_common_ref). #[must_use] #[derive(Debug, Clone)] @@ -111,7 +108,7 @@ pub fn mark_complete_and_common_ref( ref_map: &RefMap, shallow: &Shallow, mapping_is_ignored: impl Fn(&refmap::Mapping) -> bool, -) -> Result +) -> Result> where Out: Iterator, F: gix_object::Find, @@ -201,7 +198,7 @@ where Cow::Borrowed(&queue) }; - gix_trace::detail!("mark known_common").into_scope(|| -> Result<_, Error> { + gix_trace::detail!("mark known_common").into_scope(|| -> Result<_, gix_error::Exn> { // mark all complete advertised refs as common refs. for mapping in ref_map .mappings @@ -226,14 +223,16 @@ where // As negotiators currently may rely on getting `known_common` calls first and tips after, we adhere to that which is the only // reason we cached the set of tips. - gix_trace::detail!("mark tips", num_tips = tips.len()).into_scope(|| -> Result<_, Error> { - for tip in tips.iter_unordered() { - negotiator - .add_tip(*tip, graph) - .or_raise(|| message("Could not add negotiation tip"))?; - } - Ok(()) - })?; + gix_trace::detail!("mark tips", num_tips = tips.len()).into_scope( + || -> Result<_, gix_error::Exn> { + for tip in tips.iter_unordered() { + negotiator + .add_tip(*tip, graph) + .or_raise(|| message("Could not add negotiation tip"))?; + } + Ok(()) + }, + )?; Ok(Action::MustNegotiate { remote_ref_target_known, @@ -327,7 +326,7 @@ fn mark_recent_complete_commits( queue: &mut Queue, graph: &mut gix_negotiate::Graph<'_, '_>, cutoff: SecondsSinceUnixEpoch, -) -> Result<(), Error> { +) -> Result<(), gix_error::Exn> { let _span = gix_trace::detail!("mark_recent_complete", queue_len = queue.len()); while let Some(id) = queue .peek() @@ -358,7 +357,7 @@ fn mark_all_refs_in_repo( graph: &mut gix_negotiate::Graph<'_, '_>, queue: &mut Queue, mark: Flags, -) -> Result<(), Error> { +) -> Result<(), gix_error::Exn> { let _span = gix_trace::detail!("mark_all_refs"); for local_ref in store .iter() @@ -453,7 +452,7 @@ pub fn one_round( state: &mut one_round::State, arguments: &mut crate::fetch::Arguments, previous_response: Option<&crate::fetch::Response>, -) -> Result<(Round, bool), Error> { +) -> Result<(Round, bool), gix_error::Exn> { let mut seen_ack = false; if let Some(response) = previous_response { use crate::fetch::response::Acknowledgement; diff --git a/gix-protocol/src/fetch/refmap/init.rs b/gix-protocol/src/fetch/refmap/init.rs index 15d0eda839c..da1b712d3fc 100644 --- a/gix-protocol/src/fetch/refmap/init.rs +++ b/gix-protocol/src/fetch/refmap/init.rs @@ -10,9 +10,6 @@ use crate::{ handshake::Ref, }; -/// The error returned by [`crate::Handshake::prepare_lsrefs_or_extract_refmap()`]. -pub type Error = gix_error::Exn; - /// For use in [`RefMap::from_refs()`]. #[derive(Debug, Clone)] pub struct Context { @@ -36,7 +33,11 @@ impl Context { impl RefMap { /// Create a ref-map from already obtained `remote_refs`. Use `context` to pass in refspecs. /// `capabilities` are used to determine the object format. - pub fn from_refs(remote_refs: Vec, capabilities: &Capabilities, context: Context) -> Result { + pub fn from_refs( + remote_refs: Vec, + capabilities: &Capabilities, + context: Context, + ) -> Result { let all_refspecs = context.aggregate_refspecs(); let Context { fetch_refspecs, @@ -96,7 +97,7 @@ impl RefMap { /// When the capability is absent, the server is implicitly speaking Sha1 - older servers /// don't advertise it at all, and even newer ones may omit it for empty repositories. /// In builds whose `gix-hash` lacks the `sha1` feature, it's treated as unknown object format error. -fn extract_object_hash(capabilities: &Capabilities) -> Result { +fn extract_object_hash(capabilities: &Capabilities) -> Result { let object_format = match capabilities.capability("object-format").and_then(|c| c.value()) { Some(object_format) => object_format.to_str().or_raise_erased(|| { ValidationError::new_with_input("The object format used by the remote isn't valid UTF-8", object_format) diff --git a/gix-protocol/src/fetch/response/io.rs b/gix-protocol/src/fetch/response/io.rs index c9e83776ef3..c4e66bd1b62 100644 --- a/gix-protocol/src/fetch/response/io.rs +++ b/gix-protocol/src/fetch/response/io.rs @@ -8,7 +8,7 @@ use gix_error::{CorruptionError, ErrorExt, message}; use gix_transport::{Protocol, client, client::MessageKind}; use crate::fetch::{ - Response, response, + Response, response::{Acknowledgement, ShallowUpdate, WantedRef, shallow_update_from_line}, }; @@ -17,8 +17,8 @@ async fn parse_v2_section<'a, T>( line: &mut String, reader: &mut impl ExtendedBufRead<'a>, res: &mut Vec, - parse: impl Fn(&str) -> Result, -) -> Result { + parse: impl Fn(&str) -> Result, +) -> Result { line.clear(); while reader.readline_str(line).await.map_err(read_error)? != 0 { res.push(parse(line)?); @@ -53,7 +53,7 @@ impl Response { reader: &mut impl ExtendedBufRead<'a>, client_expects_pack: bool, wants_to_negotiate: bool, - ) -> Result { + ) -> Result { match version { Protocol::V0 | Protocol::V1 => { let mut line = String::new(); @@ -169,7 +169,7 @@ impl Response { } } -fn read_error(err: io::Error) -> response::Error { +fn read_error(err: io::Error) -> gix_error::Exn { let err = if err.kind() == io::ErrorKind::Other { match err.into_inner() { Some(err) => match err.downcast::() { @@ -184,7 +184,7 @@ fn read_error(err: io::Error) -> response::Error { transport_error(err.into()) } -fn transport_error(err: client::Error) -> response::Error { +fn transport_error(err: client::Error) -> gix_error::Exn { err.and_raise(message("Failed to read from line reader")).erased() } diff --git a/gix-protocol/src/fetch/response/mod.rs b/gix-protocol/src/fetch/response/mod.rs index 358a4e211ce..45d2c681916 100644 --- a/gix-protocol/src/fetch/response/mod.rs +++ b/gix-protocol/src/fetch/response/mod.rs @@ -4,10 +4,7 @@ use gix_transport::Protocol; use crate::{command::Feature, fetch::Response}; -/// The error returned in the [response module][crate::fetch::response]. -pub type Error = gix_error::Exn; - -fn unknown_line(line: &str) -> Error { +fn unknown_line(line: &str) -> gix_error::Exn { CorruptionError::new(format!("Encountered an unknown line prefix in {line:?}")).raise_erased() } @@ -36,7 +33,7 @@ pub struct WantedRef { } /// Parse a `ShallowUpdate` from a `line` as received to the server. -pub fn shallow_update_from_line(line: &str) -> Result { +pub fn shallow_update_from_line(line: &str) -> Result { match line.trim_end().split_once(' ') { Some((prefix, id)) => { let id = gix_hash::ObjectId::from_hex(id.as_bytes()) @@ -53,7 +50,7 @@ pub fn shallow_update_from_line(line: &str) -> Result { impl Acknowledgement { /// Parse an `Acknowledgement` from a `line` as received to the server. - pub fn from_line(line: &str) -> Result { + pub fn from_line(line: &str) -> Result { let mut tokens = line.trim_end().splitn(3, ' '); match (tokens.next(), tokens.next(), tokens.next()) { (Some(first), id, description) => Ok(match first { @@ -91,7 +88,7 @@ impl Acknowledgement { impl WantedRef { /// Parse a `WantedRef` from a `line` as received from the server. - pub fn from_line(line: &str) -> Result { + pub fn from_line(line: &str) -> Result { match line.trim_end().split_once(' ') { Some((id, path)) => { let id = gix_hash::ObjectId::from_hex(id.as_bytes()).or_raise_erased(|| { @@ -115,7 +112,7 @@ impl Response { /// /// Even though technically any set of features supported by the server could work, we only implement the ones that /// make it easy to maintain all versions with a single code base that aims to be and remain maintainable. - pub fn check_required_features(version: Protocol, features: &[Feature]) -> Result<(), Error> { + pub fn check_required_features(version: Protocol, features: &[Feature]) -> Result<(), gix_error::Exn> { match version { Protocol::V0 | Protocol::V1 => { let has = |name: &str| features.iter().any(|f| f.0 == name); diff --git a/gix-protocol/src/fetch/types.rs b/gix-protocol/src/fetch/types.rs index 705115362b4..e9cc5456b2f 100644 --- a/gix-protocol/src/fetch/types.rs +++ b/gix-protocol/src/fetch/types.rs @@ -69,7 +69,7 @@ mod with_fetch { /// Typical implementations use the utilities found in the [`negotiate`] module. pub trait Negotiate { /// Typically invokes [`negotiate::mark_complete_and_common_ref()`]. - fn mark_complete_and_common_ref(&mut self) -> Result; + fn mark_complete_and_common_ref(&mut self) -> Result>; /// Typically invokes [`negotiate::add_wants()`]. /// Returns `true` if wants were added, or `false` if the negotiation should be aborted. #[must_use] @@ -80,7 +80,7 @@ mod with_fetch { state: &mut negotiate::one_round::State, arguments: &mut fetch::Arguments, previous_response: Option<&fetch::Response>, - ) -> Result<(negotiate::Round, bool), negotiate::Error>; + ) -> Result<(negotiate::Round, bool), gix_error::Exn>; } /// The outcome of [`fetch()`](crate::fetch()). diff --git a/gix-protocol/src/handshake/function.rs b/gix-protocol/src/handshake/function.rs index 29a74315528..d3ed0a435c2 100644 --- a/gix-protocol/src/handshake/function.rs +++ b/gix-protocol/src/handshake/function.rs @@ -3,7 +3,6 @@ use gix_error::{ErrorExt, ResultExt, ValidationError, message}; use gix_features::{progress, progress::Progress}; use gix_transport::{Service, client}; -use super::Error; use crate::Handshake; #[crate::bisync::only_async] use crate::transport::client::async_io::{SetServiceResponse, Transport}; @@ -23,7 +22,7 @@ pub async fn handshake( mut authenticate: AuthFn, extra_parameters: Vec<(String, Option)>, progress: &mut impl Progress, -) -> Result +) -> Result where AuthFn: FnMut(credentials::helper::Action) -> credentials::protocol::Result, T: Transport, diff --git a/gix-protocol/src/handshake/mod.rs b/gix-protocol/src/handshake/mod.rs index 70517d14e0f..f48f94911db 100644 --- a/gix-protocol/src/handshake/mod.rs +++ b/gix-protocol/src/handshake/mod.rs @@ -92,7 +92,7 @@ pub(crate) mod hero { mut progress: impl Progress, transport: &mut impl $transport, trace_packetlines: bool, - ) -> Result { + ) -> Result { let (cmd, cx) = match self { ObtainRefMap::Existing(map) => return Ok(map), ObtainRefMap::LsRefsCommand(cmd, cx) => (cmd, cx), @@ -139,7 +139,7 @@ pub(crate) mod hero { user_agent: Feature, prefix_from_spec_as_filter_on_remote: bool, refmap_context: crate::fetch::refmap::init::Context, - ) -> Result, crate::fetch::refmap::init::Error> { + ) -> Result, gix_error::Exn> { if let Some(refs) = self.refs.take() { return Ok(ObtainRefMap::Existing(RefMap::from_refs( refs, @@ -161,15 +161,6 @@ pub(crate) mod hero { } } -#[cfg(feature = "handshake")] -mod error { - /// The error returned by [`handshake()`][crate::handshake()]. - pub type Error = gix_error::Exn; -} - -#[cfg(feature = "handshake")] -pub use error::Error; - #[cfg(any(feature = "blocking-client", feature = "async-client"))] #[cfg(feature = "handshake")] pub(crate) mod function; diff --git a/gix-protocol/src/handshake/refs/async_io.rs b/gix-protocol/src/handshake/refs/async_io.rs index bbb43e92623..d07f350cd05 100644 --- a/gix-protocol/src/handshake/refs/async_io.rs +++ b/gix-protocol/src/handshake/refs/async_io.rs @@ -1,11 +1,11 @@ #[cfg(not(feature = "blocking-client"))] use crate::fetch::response::ShallowUpdate; -use crate::handshake::{Ref, refs, refs::parse::Error}; +use crate::handshake::{Ref, refs}; use crate::transport::client::async_io::ReadlineBufRead; use gix_error::{ResultExt, message}; /// Parse refs from the given input line by line. Protocol V2 is required for this to succeed. -pub async fn from_v2_refs(in_refs: &mut dyn ReadlineBufRead) -> Result, Error> { +pub async fn from_v2_refs(in_refs: &mut dyn ReadlineBufRead) -> Result, gix_error::Exn> { let mut out_refs = Vec::new(); while let Some(line) = in_refs .readline() @@ -33,7 +33,7 @@ pub async fn from_v2_refs(in_refs: &mut dyn ReadlineBufRead) -> Result, pub async fn from_v1_refs_received_as_part_of_handshake_and_capabilities<'a>( in_refs: &mut dyn ReadlineBufRead, capabilities: impl Iterator>, -) -> Result<(Vec, Vec), refs::parse::Error> { +) -> Result<(Vec, Vec), gix_error::Exn> { let mut out_refs = refs::shared::from_capabilities(capabilities)?; let mut out_shallow = Vec::new(); let number_of_possible_symbolic_refs_for_lookup = out_refs.len(); diff --git a/gix-protocol/src/handshake/refs/blocking_io.rs b/gix-protocol/src/handshake/refs/blocking_io.rs index 0c431fcc76b..ff12ebf4fa8 100644 --- a/gix-protocol/src/handshake/refs/blocking_io.rs +++ b/gix-protocol/src/handshake/refs/blocking_io.rs @@ -1,12 +1,12 @@ use crate::transport::client::blocking_io::ReadlineBufRead; use crate::{ fetch::response::ShallowUpdate, - handshake::{Ref, refs, refs::parse::Error}, + handshake::{Ref, refs}, }; use gix_error::{ResultExt, message}; /// Parse refs from the given input line by line. Protocol V2 is required for this to succeed. -pub fn from_v2_refs(in_refs: &mut dyn ReadlineBufRead) -> Result, Error> { +pub fn from_v2_refs(in_refs: &mut dyn ReadlineBufRead) -> Result, gix_error::Exn> { let mut out_refs = Vec::new(); while let Some(line) = in_refs .readline() @@ -32,7 +32,7 @@ pub fn from_v2_refs(in_refs: &mut dyn ReadlineBufRead) -> Result, Error pub fn from_v1_refs_received_as_part_of_handshake_and_capabilities<'a>( in_refs: &mut dyn ReadlineBufRead, capabilities: impl Iterator>, -) -> Result<(Vec, Vec), Error> { +) -> Result<(Vec, Vec), gix_error::Exn> { let mut out_refs = refs::shared::from_capabilities(capabilities)?; let mut out_shallow = Vec::new(); let number_of_possible_symbolic_refs_for_lookup = out_refs.len(); diff --git a/gix-protocol/src/handshake/refs/mod.rs b/gix-protocol/src/handshake/refs/mod.rs index 92b3f43a5d8..9e50ae0b675 100644 --- a/gix-protocol/src/handshake/refs/mod.rs +++ b/gix-protocol/src/handshake/refs/mod.rs @@ -2,12 +2,6 @@ use bstr::BStr; use super::Ref; -/// -pub mod parse { - /// The error returned when parsing References/refs from the server response. - pub type Error = gix_error::Exn; -} - impl Ref { /// Provide shared fields referring to the ref itself, namely `(name, target, [peeled])`. /// In case of peeled refs, the tag object itself is returned as it is what the ref directly refers to, and target of the tag is returned diff --git a/gix-protocol/src/handshake/refs/shared.rs b/gix-protocol/src/handshake/refs/shared.rs index 84720de4916..7024cf6f3c4 100644 --- a/gix-protocol/src/handshake/refs/shared.rs +++ b/gix-protocol/src/handshake/refs/shared.rs @@ -1,10 +1,7 @@ use bstr::{BStr, BString, ByteSlice}; use gix_error::{CorruptionError, ErrorExt, ResultExt}; -use crate::{ - fetch::response::ShallowUpdate, - handshake::{Ref, refs::parse::Error}, -}; +use crate::{fetch::response::ShallowUpdate, handshake::Ref}; impl From for Ref { fn from(v: InternalRef) -> Self { @@ -95,7 +92,7 @@ impl InternalRef { pub(crate) fn from_capabilities<'a>( capabilities: impl Iterator>, -) -> Result, Error> { +) -> Result, gix_error::Exn> { let mut out_refs = Vec::new(); let symref_values = capabilities.filter_map(|c| { if c.name() == b"symref".as_bstr() { @@ -133,7 +130,7 @@ pub(in crate::handshake::refs) fn parse_v1( out_refs: &mut Vec, out_shallow: &mut Vec, line: &BStr, -) -> Result<(), Error> { +) -> Result<(), gix_error::Exn> { let trimmed = line.trim_end(); let (hex_hash, path) = trimmed.split_at(trimmed.find(b" ").ok_or_else(|| { CorruptionError::new(format!( @@ -209,7 +206,7 @@ pub(in crate::handshake::refs) fn parse_v1( Ok(()) } -pub(in crate::handshake::refs) fn parse_v2(line: &BStr) -> Result { +pub(in crate::handshake::refs) fn parse_v2(line: &BStr) -> Result { let trimmed = line.trim_end(); let mut tokens = trimmed.splitn(4, |b| *b == b' '); match (tokens.next(), tokens.next()) { diff --git a/gix-protocol/src/ls_refs.rs b/gix-protocol/src/ls_refs.rs index 01ee3320905..70bacb4611a 100644 --- a/gix-protocol/src/ls_refs.rs +++ b/gix-protocol/src/ls_refs.rs @@ -1,11 +1,3 @@ -#[cfg(any(feature = "blocking-client", feature = "async-client"))] -mod error { - /// The error returned by invoking a [`super::function::LsRefsCommand`]. - pub type Error = gix_error::Exn; -} -#[cfg(any(feature = "blocking-client", feature = "async-client"))] -pub use error::Error; - #[cfg(any(feature = "blocking-client", feature = "async-client"))] pub use self::function::RefPrefixes; @@ -18,7 +10,6 @@ pub(crate) mod function { use gix_features::progress::Progress; use gix_transport::client::Capabilities; - use super::Error; #[cfg(feature = "async-client")] use crate::transport::client::async_io::TransportV2Ext as _; #[cfg(feature = "blocking-client")] @@ -119,7 +110,7 @@ pub(crate) mod function { mut transport: impl $transport, progress: &mut impl Progress, trace: bool, - ) -> Result, Error> { + ) -> Result, gix_error::Exn> { let _span = gix_features::trace::detail!("gix_protocol::LsRefsCommand::invoke()", mode = $mode); Command::LsRefs .validate_argument_prefixes( diff --git a/gix-protocol/tests/protocol/fetch/_impl.rs b/gix-protocol/tests/protocol/fetch/_impl.rs index 08be6d25202..1d685f395ac 100644 --- a/gix-protocol/tests/protocol/fetch/_impl.rs +++ b/gix-protocol/tests/protocol/fetch/_impl.rs @@ -26,7 +26,6 @@ mod fetch_fn { use std::ops::ControlFlow; use super::{Action, Delegate, RefsAction}; - use crate::fetch::Error; /// A way to indicate how to treat the connection underlying the transport, potentially allowing to reuse it. #[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash)] @@ -77,7 +76,7 @@ mod fetch_fn { fetch_mode: FetchConnection, agent: impl Into, trace: bool, - ) -> Result<(), Error> + ) -> Result<(), gix_error::Exn> where F: FnMut(credentials::helper::Action) -> credentials::protocol::Result, D: Delegate, diff --git a/gix-protocol/tests/protocol/fetch/mod.rs b/gix-protocol/tests/protocol/fetch/mod.rs index 4cc2884e680..8ab2256b813 100644 --- a/gix-protocol/tests/protocol/fetch/mod.rs +++ b/gix-protocol/tests/protocol/fetch/mod.rs @@ -15,12 +15,6 @@ use _impl::{Action, DelegateBlocking, RefsAction}; mod ref_map; -mod error { - /// The error used in [`fetch()`][crate::fetch()]. - pub type Error = gix_error::Exn; -} -pub use error::Error; - mod arguments; #[cfg(feature = "blocking-client")] diff --git a/gix-protocol/tests/protocol/handshake.rs b/gix-protocol/tests/protocol/handshake.rs index f5b62cd3a46..979b097c141 100644 --- a/gix-protocol/tests/protocol/handshake.rs +++ b/gix-protocol/tests/protocol/handshake.rs @@ -211,7 +211,7 @@ impl std::io::BufRead for Fixture<'_> { impl gix_transport::client::blocking_io::ReadlineBufRead for Fixture<'_> { fn readline( &mut self, - ) -> Option, gix_packetline::decode::Error>>> { + ) -> Option, gix_error::ValidationError>>> { use bstr::{BStr, ByteSlice}; let bytes: &BStr = self.0.into(); let mut lines = bytes.lines(); @@ -275,7 +275,7 @@ impl futures_io::AsyncBufRead for Fixture<'_> { impl gix_transport::client::async_io::ReadlineBufRead for Fixture<'_> { async fn readline( &mut self, - ) -> Option, gix_packetline::decode::Error>>> { + ) -> Option, gix_error::ValidationError>>> { use bstr::{BStr, ByteSlice}; let bytes: &BStr = self.0.into(); let mut lines = bytes.lines(); diff --git a/gix-quote/src/ansi_c.rs b/gix-quote/src/ansi_c.rs index 8eeb9f565e3..78ed660e173 100644 --- a/gix-quote/src/ansi_c.rs +++ b/gix-quote/src/ansi_c.rs @@ -1,9 +1,3 @@ -/// -pub mod undo { - /// The error returned by [`ansi_c`](crate::ansi_c::undo()). - pub type Error = gix_error::Exn; -} - use std::{borrow::Cow, io::Read}; use bstr::{BStr, BString, ByteSlice}; @@ -58,7 +52,7 @@ pub fn quote(input: &BStr) -> Cow<'_, BStr> { /// See [the tests][tests] for quotation examples. /// /// [tests]: https://github.com/GitoxideLabs/gitoxide/blob/64872690e60efdd9267d517f4d9971eecd3b875c/gix-quote/tests/quote.rs#L57-L74 -pub fn undo(input: &BStr) -> Result<(Cow<'_, BStr>, usize), undo::Error> { +pub fn undo(input: &BStr) -> Result<(Cow<'_, BStr>, usize), gix_error::Exn> { if !input.starts_with(b"\"") { return Ok((input.into(), input.len())); } @@ -69,7 +63,7 @@ pub fn undo(input: &BStr) -> Result<(Cow<'_, BStr>, usize), undo::Error> { let mut input = &input[1..]; let mut consumed = 1; let mut out = BString::default(); - fn consume_one_past(input: &mut &BStr, position: usize) -> Result { + fn consume_one_past(input: &mut &BStr, position: usize) -> Result> { use gix_error::{OptionExt, ValidationError}; *input = input .get(position + 1..) diff --git a/gix-ref/src/peel.rs b/gix-ref/src/peel.rs index 8e3b82294c1..3010cbef504 100644 --- a/gix-ref/src/peel.rs +++ b/gix-ref/src/peel.rs @@ -45,8 +45,8 @@ pub mod to_id { } } - impl From for Error { - fn from(err: gix_object::find::Error) -> Self { + impl From for Error { + fn from(err: gix_error::Exn) -> Self { Error::Find(err.into_error()) } } diff --git a/gix-ref/src/store/file/overlay_iter.rs b/gix-ref/src/store/file/overlay_iter.rs index 22c1600132b..67c583c5c18 100644 --- a/gix-ref/src/store/file/overlay_iter.rs +++ b/gix-ref/src/store/file/overlay_iter.rs @@ -412,10 +412,13 @@ impl file::Store { } Some(namespace) => { let prefix = namespace.to_owned().into_namespaced_prefix(prefix); - let prefix = prefix - .as_bstr() - .try_into() - .map_err(|err: gix_path::relative_path::Error| std::io::Error::other(err.into_error()))?; + let prefix = + prefix + .as_bstr() + .try_into() + .map_err(|err: gix_error::Exn| { + std::io::Error::other(err.into_error()) + })?; let git_dir_info = IterInfo::from_prefix(self.git_dir(), prefix, self.precompose_unicode)?; let common_dir_info = self .common_dir() diff --git a/gix-ref/src/store/file/transaction/prepare.rs b/gix-ref/src/store/file/transaction/prepare.rs index 76c764afe66..b80bde815d8 100644 --- a/gix-ref/src/store/file/transaction/prepare.rs +++ b/gix-ref/src/store/file/transaction/prepare.rs @@ -51,7 +51,7 @@ impl Transaction<'_, '_> { /// (such as a path collision reported as `NotADirectory`) as [`Error::Io`] rather than /// burying them in [`Error::LockAcquire`], which is reserved for actual contention. // This happens for path collisions where `a` is a ref file, and `a/b` is the lock to be created. - fn lock_acquire_error(err: gix_lock::acquire::Error, full_name: &str) -> Error { + fn lock_acquire_error(err: gix_error::Exn, full_name: &str) -> Error { match ( err.downcast_any_ref::().is_some(), err.downcast_any_ref::().map(std::io::Error::kind), diff --git a/gix-ref/src/store/packed/transaction.rs b/gix-ref/src/store/packed/transaction.rs index 352c59b761c..4a34d9e1e36 100644 --- a/gix-ref/src/store/packed/transaction.rs +++ b/gix-ref/src/store/packed/transaction.rs @@ -266,7 +266,7 @@ pub(crate) fn buffer_into_transaction( lock_mode: gix_lock::acquire::Fail, precompose_unicode: bool, namespace: Option, -) -> Result { +) -> Result { let lock = gix_lock::File::acquire_to_update_resource(&buffer.path, lock_mode, None)?; Ok(packed::Transaction { buffer: Some(buffer), diff --git a/gix-ref/tests/refs/file/mod.rs b/gix-ref/tests/refs/file/mod.rs index 0ddaf38e4d6..f9d70984cd0 100644 --- a/gix-ref/tests/refs/file/mod.rs +++ b/gix-ref/tests/refs/file/mod.rs @@ -43,7 +43,7 @@ impl gix_object::Find for EmptyCommit { &self, id: &gix_hash::oid, _buffer: &'a mut Vec, - ) -> Result>, gix_object::find::Error> { + ) -> Result>, gix_error::Exn> { Ok(Some(gix_object::Data { kind: gix_object::Kind::Commit, object_hash: id.kind(), diff --git a/gix-refspec/src/parse.rs b/gix-refspec/src/parse.rs index 9d8e99e831a..59acc9a8a86 100644 --- a/gix-refspec/src/parse.rs +++ b/gix-refspec/src/parse.rs @@ -1,6 +1,3 @@ -/// The error returned by the [`parse()`][crate::parse()] function. -pub type Error = gix_error::Exn; - /// Define how the parsed refspec should be used. #[derive(PartialOrd, Ord, PartialEq, Eq, Copy, Clone, Hash, Debug)] pub enum Operation { @@ -11,16 +8,15 @@ pub enum Operation { } pub(crate) mod function { - use crate::{ - RefSpecRef, - parse::{Error, Operation}, - types::Mode, - }; + use crate::{RefSpecRef, parse::Operation, types::Mode}; use bstr::{BStr, ByteSlice}; use gix_error::{ErrorExt, ValidationError}; /// Parse `spec` for use in `operation` and return it if it is valid. - pub fn parse(mut spec: &BStr, operation: Operation) -> Result, Error> { + pub fn parse( + mut spec: &BStr, + operation: Operation, + ) -> Result, gix_error::Exn> { fn fetch_head_only(mode: Mode) -> RefSpecRef<'static> { RefSpecRef { mode, @@ -131,7 +127,7 @@ pub(crate) mod function { spec.len() >= gix_hash::Kind::shortest().len_in_hex() && spec.iter().all(u8::is_ascii_hexdigit) } - fn validate_partial_name_with_single_glob(spec: &BStr) -> Result<(), Error> { + fn validate_partial_name_with_single_glob(spec: &BStr) -> Result<(), gix_error::Exn> { let mut buf = smallvec::SmallVec::<[u8; 256]>::with_capacity(spec.len()); buf.extend_from_slice(spec); let glob_pos = buf.find_byte(b'*').expect("glob present"); @@ -146,7 +142,10 @@ pub(crate) mod function { /// Validate `spec`, and return it along with whether it holds a glob. /// /// `any_name` skips the check entirely, for the one side Git leaves unchecked. - fn validated(spec: Option<&BStr>, any_name: bool) -> Result<(Option<&BStr>, bool), Error> { + fn validated( + spec: Option<&BStr>, + any_name: bool, + ) -> Result<(Option<&BStr>, bool), gix_error::Exn> { match spec { Some(spec) => { let glob_count = spec.iter().filter(|b| **b == b'*').take(2).count(); diff --git a/gix-refspec/tests/refspec/parse/mod.rs b/gix-refspec/tests/refspec/parse/mod.rs index bc0e792fb54..22b808f3b0f 100644 --- a/gix-refspec/tests/refspec/parse/mod.rs +++ b/gix-refspec/tests/refspec/parse/mod.rs @@ -80,17 +80,17 @@ mod util { input.into() } - pub fn try_parse(spec: &str, op: Operation) -> Result, gix_refspec::parse::Error> { + pub fn try_parse(spec: &str, op: Operation) -> Result, gix_error::Exn> { gix_refspec::parse(spec.into(), op) } - pub fn assert_validation(spec: &str, op: Operation, message: &str) -> gix_refspec::parse::Error { + pub fn assert_validation(spec: &str, op: Operation, message: &str) -> gix_error::Exn { let err = try_parse(spec, op).expect_err("refspec is invalid"); assert_eq!(err.message, message); err } - pub fn assert_reference_error(spec: &str, op: Operation) -> gix_refspec::parse::Error { + pub fn assert_reference_error(spec: &str, op: Operation) -> gix_error::Exn { let err = try_parse(spec, op).expect_err("refspec contains an invalid reference name"); let source = err .downcast_any_ref::() diff --git a/gix-revision/src/describe.rs b/gix-revision/src/describe.rs index 37b1be8fbbe..2b58d2d518a 100644 --- a/gix-revision/src/describe.rs +++ b/gix-revision/src/describe.rs @@ -127,9 +127,6 @@ impl Default for Options<'_> { } } -/// The error returned by the [`describe()`](function::describe()) function. -pub type Error = gix_error::Message; - pub(crate) mod function { use std::{borrow::Cow, cmp::Ordering}; @@ -137,7 +134,7 @@ pub(crate) mod function { use gix_error::{Exn, ResultExt, message}; use gix_hash::oid; - use super::{Error, Outcome}; + use super::Outcome; use crate::{ Graph, PriorityQueue, describe::{CommitTime, Flags, MAX_CANDIDATES, Options}, @@ -157,7 +154,7 @@ pub(crate) mod function { fallback_to_oid, first_parent, }: Options<'name>, - ) -> Result>, Exn> { + ) -> Result>, Exn> { let _span = gix_trace::coarse!( "gix_revision::describe()", commit = %commit, @@ -299,7 +296,7 @@ pub(crate) mod function { commit: gix_hash::ObjectId, commit_flags: Flags, first_parent: bool, - ) -> Result<(), Exn> { + ) -> Result<(), Exn> { graph .insert_parents( &commit, @@ -319,7 +316,7 @@ pub(crate) mod function { graph: &mut Graph<'_, '_, Flags>, best_candidate: &mut Candidate<'_>, first_parent: bool, - ) -> Result> { + ) -> Result> { let mut commits_seen = 0; while let Some(commit) = queue.pop_value() { commits_seen += 1; diff --git a/gix-revision/src/spec/parse/function.rs b/gix-revision/src/spec/parse/function.rs index 1549952afd6..03d4cc7421c 100644 --- a/gix-revision/src/spec/parse/function.rs +++ b/gix-revision/src/spec/parse/function.rs @@ -2,7 +2,7 @@ use std::str::FromStr; use crate::{ spec, - spec::parse::{Delegate, Error, delegate, delegate::SiblingBranch}, + spec::parse::{Delegate, delegate, delegate::SiblingBranch}, }; use bstr::{BStr, BString, ByteSlice, ByteVec}; use gix_error::{ErrorExt, Exn, ResultExt}; @@ -13,7 +13,7 @@ use gix_error::{ErrorExt, Exn, ResultExt}; /// Note that the `delegate` is expected to maintain enough state to lookup revisions properly. /// Returns `Ok(())` if all of `input` was consumed, or the error if either the `revspec` syntax was incorrect or /// the `delegate` failed to perform the request. -pub fn parse(mut input: &BStr, delegate: &mut impl Delegate) -> Result<(), Exn> { +pub fn parse(mut input: &BStr, delegate: &mut impl Delegate) -> Result<(), Exn> { use delegate::{Kind, Revision}; let mut delegate = InterceptRev::new(delegate); let mut prev_kind = None; @@ -22,7 +22,7 @@ pub fn parse(mut input: &BStr, delegate: &mut impl Delegate) -> Result<(), Exn Result<(), Exn Result<(), Exn Result<(), Exn Option<&BStr> { } type InsideParensRestConsumed<'a> = (std::borrow::Cow<'a, BStr>, &'a BStr, usize); -fn parens(input: &[u8]) -> Result>, Error> { +fn parens(input: &[u8]) -> Result>, gix_error::ValidationError> { if input.first() != Some(&b'{') { return Ok(None); } @@ -343,17 +343,17 @@ fn parens(input: &[u8]) -> Result>, Error> { return Ok(Some((inner, input[idx + 1..].as_bstr(), idx + 1))); } } - Err(Error::new_with_input("unclosed brace pair", input)) + Err(gix_error::ValidationError::new_with_input("unclosed brace pair", input)) } -fn try_parse(input: &BStr) -> Result, Error> { +fn try_parse(input: &BStr) -> Result, gix_error::ValidationError> { input .to_str() .ok() .and_then(|n| { n.parse().ok().map(|n| { if n == T::default() && input[0] == b'-' { - return Err(Error::new_with_input( + return Err(gix_error::ValidationError::new_with_input( "negative zero is invalid - remove the minus sign", input, )); @@ -364,25 +364,35 @@ fn try_parse(input: &BStr) -> Result .transpose() } -fn revision<'a, T>(mut input: &'a BStr, delegate: &mut InterceptRev<'_, T>) -> Result<&'a BStr, Exn> +fn revision<'a, T>( + mut input: &'a BStr, + delegate: &mut InterceptRev<'_, T>, +) -> Result<&'a BStr, Exn> where T: Delegate, { use delegate::{Navigate, Revision}; - fn consume_all(res: Result<(), Exn>, err: impl FnOnce() -> String) -> Result<&'static BStr, Exn> { - res.map(|_| "".into()).or_raise(|| Error::new(err())) + fn consume_all( + res: Result<(), Exn>, + err: impl FnOnce() -> String, + ) -> Result<&'static BStr, Exn> { + res.map(|_| "".into()) + .or_raise(|| gix_error::ValidationError::new(err())) } match input.as_bytes() { [b':'] => { - return Err( - Error::new("':' must be followed by either slash and regex or path to lookup in HEAD tree").raise(), - ); + return Err(gix_error::ValidationError::new( + "':' must be followed by either slash and regex or path to lookup in HEAD tree", + ) + .raise()); + } + [b':', b'/'] => { + return Err(gix_error::ValidationError::new("':/' must be followed by a regular expression").raise()); } - [b':', b'/'] => return Err(Error::new("':/' must be followed by a regular expression").raise()), [b':', b'/', regex @ ..] => { let (regex, negated) = parse_regex_prefix(regex.as_bstr())?; if regex.is_empty() { - return Err(Error::new_with_input("unconsumed input", input).raise()); + return Err(gix_error::ValidationError::new_with_input("unconsumed input", input).raise()); } return consume_all(delegate.find(regex, negated), || { format!("Delegate couldn't find '{regex}' (negated: {negated})") @@ -461,7 +471,7 @@ where if name.is_empty() && sep == Some(b'@') && sep_pos.and_then(|pos| input.get(pos + 1)) != Some(&b'{') { delegate .find_ref("HEAD".into()) - .or_raise(|| Error::new("delegate did not find the HEAD reference"))?; + .or_raise(|| gix_error::ValidationError::new("delegate did not find the HEAD reference"))?; sep_pos = sep_pos.map(|pos| pos + 1); sep = match sep_pos.and_then(|pos| input.get(pos).copied()) { None => return Ok("".into()), @@ -493,14 +503,16 @@ where } }) }) - .ok_or_else(|| Error::new_with_input("couldn't parse revision", input).raise_all(errors))?; + .ok_or_else(|| { + gix_error::ValidationError::new_with_input("couldn't parse revision", input).raise_all(errors) + })?; } input = { if let Some(b'@') = sep { let past_sep = input[sep_pos.map_or(input.len(), |pos| pos + 1)..].as_bstr(); let (nav, rest, _consumed) = parens(past_sep)?.ok_or_else(|| { - Error::new_with_input( + gix_error::ValidationError::new_with_input( "@ character must be standalone or followed by {}", &input[sep_pos.unwrap_or(input.len())..], ) @@ -510,13 +522,13 @@ where if n < 0 { if name.is_empty() { delegate.nth_checked_out_branch(n.unsigned_abs()).or_raise(|| { - Error::new_with_input( + gix_error::ValidationError::new_with_input( format!("delegate.nth_checked_out_branch({n:?}) didn't find a branch"), nav, ) })?; } else { - return Err(Error::new_with_input( + return Err(gix_error::ValidationError::new_with_input( "reference name must be followed by positive numbers in @{n}", nav, ) @@ -526,28 +538,42 @@ where let lookup = if n >= 100000000 { let time = nav .to_str() - .or_raise(|| Error::new_with_input("could not parse time for reflog lookup", nav)) + .or_raise(|| { + gix_error::ValidationError::new_with_input( + "could not parse time for reflog lookup", + nav, + ) + }) .and_then(|date| { - gix_date::parse(date, None) - .or_raise(|| Error::new_with_input("could not parse time for reflog lookup", nav)) + gix_date::parse(date, None).or_raise(|| { + gix_error::ValidationError::new_with_input( + "could not parse time for reflog lookup", + nav, + ) + }) })?; delegate::ReflogLookup::Date(time) } else { delegate::ReflogLookup::Entry(n.try_into().expect("non-negative isize fits usize")) }; - delegate - .reflog(lookup) - .or_raise(|| Error::new_with_input(format!("delegate.reflog({lookup:?}) failed"), nav))?; + delegate.reflog(lookup).or_raise(|| { + gix_error::ValidationError::new_with_input(format!("delegate.reflog({lookup:?}) failed"), nav) + })?; } else { - return Err(Error::new_with_input("reflog entries require a ref name", *name).raise()); + return Err( + gix_error::ValidationError::new_with_input("reflog entries require a ref name", *name).raise(), + ); } } else if let Some(kind) = SiblingBranch::parse(nav) { if has_ref_or_implied_name { - delegate - .sibling_branch(kind) - .or_raise(|| Error::new_with_input(format!("delegate.sibling_branch({kind:?}) failed"), nav)) + delegate.sibling_branch(kind).or_raise(|| { + gix_error::ValidationError::new_with_input( + format!("delegate.sibling_branch({kind:?}) failed"), + nav, + ) + }) } else { - Err(Error::new_with_input( + Err(gix_error::ValidationError::new_with_input( "sibling branches like 'upstream' or 'push' require a branch name with remote configuration", *name, ) @@ -556,22 +582,27 @@ where } else if has_ref_or_implied_name { let time = nav .to_str() - .map_err(|_| Error::new_with_input("could not parse time for reflog lookup", nav)) + .map_err(|_| { + gix_error::ValidationError::new_with_input("could not parse time for reflog lookup", nav) + }) .and_then(|date| { - gix_date::parse(date, Some(gix_date::Zoned::now())) - .map_err(|_| Error::new_with_input("could not parse time for reflog lookup", nav)) + gix_date::parse(date, Some(gix_date::Zoned::now())).map_err(|_| { + gix_error::ValidationError::new_with_input("could not parse time for reflog lookup", nav) + }) })?; let lookup = delegate::ReflogLookup::Date(time); - delegate - .reflog(lookup) - .or_raise(|| Error::new_with_input(format!("delegate.reflog({lookup:?}) failed"), nav))?; + delegate.reflog(lookup).or_raise(|| { + gix_error::ValidationError::new_with_input(format!("delegate.reflog({lookup:?}) failed"), nav) + })?; } else { - return Err(Error::new_with_input("reflog entries require a ref name", *name).raise()); + return Err( + gix_error::ValidationError::new_with_input("reflog entries require a ref name", *name).raise(), + ); } rest } else { if sep_pos == Some(0) && sep == Some(b'~') { - return Err(Error::new("tilde needs to follow an anchor, like @~").raise()); + return Err(gix_error::ValidationError::new("tilde needs to follow an anchor, like @~").raise()); } input[sep_pos.unwrap_or(input.len())..].as_bstr() } @@ -580,7 +611,10 @@ where navigate(input, delegate) } -fn navigate<'a, T>(input: &'a BStr, delegate: &mut InterceptRev<'_, T>) -> Result<&'a BStr, Exn> +fn navigate<'a, T>( + input: &'a BStr, + delegate: &mut InterceptRev<'_, T>, +) -> Result<&'a BStr, Exn> where T: Delegate, { @@ -597,9 +631,12 @@ where .transpose()? .unwrap_or((1, 0)); let traversal = delegate::Traversal::NthAncestor(number); - delegate - .traverse(traversal) - .or_raise(|| Error::new_with_input(format!("delegate.traverse({traversal:?}) failed"), input))?; + delegate.traverse(traversal).or_raise(|| { + gix_error::ValidationError::new_with_input( + format!("delegate.traverse({traversal:?}) failed"), + input, + ) + })?; cursor += consumed; } b'^' => { @@ -613,20 +650,23 @@ where number .checked_mul(-1) .ok_or_else(|| { - Error::new_with_input("could not parse number", past_sep.expect("present")) + gix_error::ValidationError::new_with_input( + "could not parse number", + past_sep.expect("present"), + ) })? .try_into() .expect("non-negative"), ); delegate.traverse(traversal).or_raise(|| { - Error::new_with_input( + gix_error::ValidationError::new_with_input( "delegate.traverse({traversal:?}) failed", past_sep.unwrap_or_default(), ) })?; let kind = spec::Kind::RangeBetween; delegate.kind(kind).or_raise(|| { - Error::new_with_input( + gix_error::ValidationError::new_with_input( format!("delegate.kind({kind:?}) failed"), past_sep.unwrap_or_default(), ) @@ -637,24 +677,30 @@ where None => delegate.disambiguate_prefix(prefix, None), } .or_raise(|| { - Error::new_with_input( + gix_error::ValidationError::new_with_input( format!("delegate.disambiguate_prefix({hint:?}) failed"), past_sep.unwrap_or_default(), ) })?; } else if let Some(name) = delegate.last_ref.take() { delegate.find_ref(name.as_bstr()).or_raise(|| { - Error::new_with_input( + gix_error::ValidationError::new_with_input( format!("delegate.find_ref({name}) failed"), past_sep.unwrap_or_default(), ) })?; } else { - return Err(Error::new_with_input("unconsumed input", &input[cursor..]).raise()); + return Err(gix_error::ValidationError::new_with_input( + "unconsumed input", + &input[cursor..], + ) + .raise()); } cursor += consumed; let rest = input[cursor..].as_bstr(); - delegate.done().or_raise(|| Error::new_with_input(done_msg, rest))?; + delegate + .done() + .or_raise(|| gix_error::ValidationError::new_with_input(done_msg, rest))?; return Ok(rest); } else if number == 0 { delegate.peel_until(delegate::PeelTo::ObjectKind(gix_object::Kind::Commit)) @@ -663,7 +709,9 @@ where number.try_into().expect("positive number"), )) } - .or_raise(|| Error::new_with_input("unknown navigation", past_sep.unwrap_or_default()))?; + .or_raise(|| { + gix_error::ValidationError::new_with_input("unknown navigation", past_sep.unwrap_or_default()) + })?; cursor += consumed; } else if let Some((kind, _rest, consumed)) = past_sep.and_then(|past_sep| parens(past_sep).transpose()).transpose()? @@ -679,14 +727,22 @@ where regex if regex.starts_with(b"/") => { let (regex, negated) = parse_regex_prefix(regex[1..].as_bstr())?; delegate.find(regex, negated).or_raise(|| { - Error::new(format!("Delegate couldn't find '{regex}' (negated: {negated})")) + gix_error::ValidationError::new(format!( + "Delegate couldn't find '{regex}' (negated: {negated})" + )) })?; continue; } - invalid => return Err(Error::new_with_input("cannot peel to unknown target", invalid).raise()), + invalid => { + return Err(gix_error::ValidationError::new_with_input( + "cannot peel to unknown target", + invalid, + ) + .raise()); + } }; delegate.peel_until(target).or_raise(|| { - Error::new_with_input( + gix_error::ValidationError::new_with_input( format!("delegate.peel_until({target:?}) failed"), past_sep.unwrap_or_default(), ) @@ -694,23 +750,27 @@ where } else if past_sep.and_then(<[_]>::first) == Some(&b'!') { let rest = input[cursor + 1..].as_bstr(); let kind = spec::Kind::ExcludeReachableFromParents; + delegate.kind(kind).or_raise(|| { + gix_error::ValidationError::new_with_input(format!("delegate.kind({kind:?}) failed"), rest) + })?; delegate - .kind(kind) - .or_raise(|| Error::new_with_input(format!("delegate.kind({kind:?}) failed"), rest))?; - delegate.done().or_raise(|| Error::new_with_input(done_msg, rest))?; + .done() + .or_raise(|| gix_error::ValidationError::new_with_input(done_msg, rest))?; return Ok(rest); } else if past_sep.and_then(<[_]>::first) == Some(&b'@') { let rest = input[cursor + 1..].as_bstr(); let kind = spec::Kind::IncludeReachableFromParents; + delegate.kind(kind).or_raise(|| { + gix_error::ValidationError::new_with_input(format!("delegate.kind({kind:?}) failed"), rest) + })?; delegate - .kind(kind) - .or_raise(|| Error::new_with_input(format!("delegate.kind({kind:?}) failed"), rest))?; - delegate.done().or_raise(|| Error::new_with_input(done_msg, rest))?; + .done() + .or_raise(|| gix_error::ValidationError::new_with_input(done_msg, rest))?; return Ok(rest); } else { let parent = delegate::Traversal::NthParent(1); delegate.traverse(parent).or_raise(|| { - Error::new_with_input( + gix_error::ValidationError::new_with_input( format!("delegate.parent({parent:?}) failed"), past_sep.unwrap_or_default(), ) @@ -721,7 +781,7 @@ where let to = delegate::PeelTo::Path(input[cursor..].as_bstr()); delegate .peel_until(to) - .or_raise(|| Error::new(format!("delegate.peel_until({to:?}) failed")))?; + .or_raise(|| gix_error::ValidationError::new(format!("delegate.peel_until({to:?}) failed")))?; return Ok("".into()); } _ => return Ok(input[cursor - 1..].as_bstr()), @@ -730,19 +790,24 @@ where Ok("".into()) } -fn parse_regex_prefix(regex: &BStr) -> Result<(&BStr, bool), Error> { +fn parse_regex_prefix(regex: &BStr) -> Result<(&BStr, bool), gix_error::ValidationError> { Ok(match regex.strip_prefix(b"!") { Some(regex) if regex.first() == Some(&b'!') => (regex.as_bstr(), false), Some(regex) if regex.first() == Some(&b'-') => (regex[1..].as_bstr(), true), - Some(_regex) => return Err(Error::new_with_input("need one character after /!, typically -", regex)), + Some(_regex) => { + return Err(gix_error::ValidationError::new_with_input( + "need one character after /!, typically -", + regex, + )); + } None => (regex, false), }) } -fn try_parse_usize(input: &BStr) -> Result, Error> { +fn try_parse_usize(input: &BStr) -> Result, gix_error::ValidationError> { let mut bytes = input.iter().peekable(); if bytes.peek().filter(|&&&b| b == b'-' || b == b'+').is_some() { - return Err(Error::new_with_input( + return Err(gix_error::ValidationError::new_with_input( "negative or explicitly positive numbers are invalid here", input, )); @@ -752,14 +817,15 @@ fn try_parse_usize(input: &BStr) -> Result, Error> { return Ok(None); } let input = &input[..num_digits]; - let number = try_parse(input)?.ok_or_else(|| Error::new_with_input("could not parse number", input))?; + let number = + try_parse(input)?.ok_or_else(|| gix_error::ValidationError::new_with_input("could not parse number", input))?; Ok(Some((number, num_digits))) } -fn try_parse_isize(input: &BStr) -> Result, Error> { +fn try_parse_isize(input: &BStr) -> Result, gix_error::ValidationError> { let mut bytes = input.iter().peekable(); if bytes.peek().filter(|&&&b| b == b'+').is_some() { - return Err(Error::new_with_input( + return Err(gix_error::ValidationError::new_with_input( "explicitly positive numbers are invalid here", input, )); @@ -772,7 +838,8 @@ fn try_parse_isize(input: &BStr) -> Result, Error> return Ok(Some((-1, negative, num_digits))); } let input = &input[..num_digits]; - let number = try_parse(input)?.ok_or_else(|| Error::new_with_input("could not parse number", input))?; + let number = + try_parse(input)?.ok_or_else(|| gix_error::ValidationError::new_with_input("could not parse number", input))?; Ok(Some((number, negative, num_digits))) } diff --git a/gix-revision/src/spec/parse/mod.rs b/gix-revision/src/spec/parse/mod.rs index d5107dfa00c..a8478033969 100644 --- a/gix-revision/src/spec/parse/mod.rs +++ b/gix-revision/src/spec/parse/mod.rs @@ -1,6 +1,4 @@ use gix_error::Exn; -/// The error returned by [`spec::parse()`](crate::spec::parse()). -pub use gix_error::ValidationError as Error; /// pub mod delegate; diff --git a/gix-revision/tests/revision/describe/mod.rs b/gix-revision/tests/revision/describe/mod.rs index a39587f1370..2b6bab5ceda 100644 --- a/gix-revision/tests/revision/describe/mod.rs +++ b/gix-revision/tests/revision/describe/mod.rs @@ -1,9 +1,6 @@ use gix_error::Exn; use gix_object::bstr::ByteSlice; -use gix_revision::{ - describe, - describe::{Error, Outcome}, -}; +use gix_revision::{describe, describe::Outcome}; use std::{borrow::Cow, path::PathBuf}; use crate::{hex_to_id, odb_at}; @@ -14,7 +11,7 @@ fn run_test( transform_odb: impl FnOnce(gix_odb::Handle) -> gix_odb::Handle, options: impl Fn(gix_hash::ObjectId) -> gix_revision::describe::Options<'static>, run_assertions: impl Fn( - Result>, Exn>, + Result>, Exn>, gix_hash::ObjectId, ) -> Result<(), gix_error::Error>, ) -> Result<(), gix_error::Error> { diff --git a/gix-revision/tests/revision/spec/parse/mod.rs b/gix-revision/tests/revision/spec/parse/mod.rs index df0abc3e96e..495b2bd3f71 100644 --- a/gix-revision/tests/revision/spec/parse/mod.rs +++ b/gix-revision/tests/revision/spec/parse/mod.rs @@ -210,11 +210,11 @@ fn parse(spec: &str) -> Recorder { try_parse_opts(spec, Options::default()).unwrap() } -fn try_parse(spec: &str) -> Result> { +fn try_parse(spec: &str) -> Result> { try_parse_opts(spec, Default::default()) } -fn try_parse_opts(spec: &str, options: Options) -> Result> { +fn try_parse_opts(spec: &str, options: Options) -> Result> { let mut rec = Recorder::with(options); spec::parse(spec.into(), &mut rec)?; Ok(rec) diff --git a/gix-revwalk/src/graph/commit.rs b/gix-revwalk/src/graph/commit.rs index 71c488bf2e8..6d97aa9d781 100644 --- a/gix-revwalk/src/graph/commit.rs +++ b/gix-revwalk/src/graph/commit.rs @@ -19,7 +19,7 @@ impl<'graph, 'cache> LazyCommit<'graph, 'cache> { /// /// This is the single-most important date for determining recency of commits. /// Note that this can only fail if the commit is backed by the object database *and* parsing fails. - pub fn committer_timestamp(&self) -> Result { + pub fn committer_timestamp(&self) -> Result { Ok(match &self.backing { Either::Left(buf) => gix_object::CommitRefIter::from_bytes(buf, self.object_hash) .committer()? @@ -39,7 +39,7 @@ impl<'graph, 'cache> LazyCommit<'graph, 'cache> { /// Returns the generation of the commit and its commit-time, either from cache if available, or parsed from the object buffer. pub fn generation_and_timestamp( &self, - ) -> Result<(Option, SecondsSinceUnixEpoch), gix_object::decode::Error> { + ) -> Result<(Option, SecondsSinceUnixEpoch), gix_error::ValidationError> { Ok(match &self.backing { Either::Left(buf) => ( None, @@ -60,7 +60,10 @@ impl<'graph, 'cache> LazyCommit<'graph, 'cache> { /// Convert ourselves into an owned version, which effectively detaches us from the underlying graph. /// Use `new_data()` to provide the `data` field for the owned `Commit`. - pub fn to_owned(&self, new_data: impl FnOnce() -> T) -> Result, to_owned::Error> { + pub fn to_owned( + &self, + new_data: impl FnOnce() -> T, + ) -> Result, gix_error::Exn> { let data = new_data(); Ok(match &self.backing { Either::Left(buf) => { @@ -128,7 +131,7 @@ pub struct Parents<'graph, 'cache> { } impl Iterator for Parents<'_, '_> { - type Item = Result; + type Item = Result>; fn next(&mut self) -> Option { match &mut self.backing { @@ -154,15 +157,3 @@ impl Iterator for Parents<'_, '_> { } } } - -/// -pub mod iter_parents { - /// The error returned by the [`Parents`][super::Parents] iterator. - pub type Error = gix_error::Exn; -} - -/// -pub mod to_owned { - /// The error returned by [`to_owned()`][crate::graph::LazyCommit::to_owned()]. - pub type Error = gix_error::Exn; -} diff --git a/gix-revwalk/src/graph/mod.rs b/gix-revwalk/src/graph/mod.rs index 18d0467fda8..43c99f3f549 100644 --- a/gix-revwalk/src/graph/mod.rs +++ b/gix-revwalk/src/graph/mod.rs @@ -12,20 +12,6 @@ pub type IdMap = gix_hashtable::HashMap; /// pub mod commit; -mod errors { - /// - pub mod insert_parents { - /// The error returned by [`insert_parents()`](crate::Graph::insert_parents()). - pub type Error = gix_error::Exn; - } - - /// - pub mod get_or_insert_default { - /// The error returned by [`try_lookup_or_insert_default()`](crate::Graph::try_lookup_or_insert_default()). - pub type Error = gix_error::Exn; - } -} -pub use errors::{get_or_insert_default, insert_parents}; use gix_date::SecondsSinceUnixEpoch; /// The generation away from the HEAD of graph, useful to limit algorithms by topological depth as well. @@ -49,7 +35,7 @@ impl<'cache, T: Default> Graph<'_, 'cache, T> { &mut self, id: gix_hash::ObjectId, update_data: impl FnOnce(&mut T), - ) -> Result>, get_or_insert_default::Error> { + ) -> Result>, gix_error::Exn> { self.try_lookup_or_insert_default(id, T::default, update_data) } } @@ -95,7 +81,7 @@ impl<'cache, T> Graph<'_, 'cache, T> { mut make_data: impl FnMut(LazyCommit<'_, 'cache>) -> Result, ) -> Result, E> where - E: From, + E: From, { let value = make_data(self.lookup(&id).map_err(E::from)?)?; Ok(self.map.insert(id, value)) @@ -116,7 +102,7 @@ impl<'cache, T> Graph<'_, 'cache, T> { new_parent_data: &mut dyn FnMut(gix_hash::ObjectId, SecondsSinceUnixEpoch) -> T, update_existing: &mut dyn FnMut(gix_hash::ObjectId, &mut T), first_parent: bool, - ) -> Result<(), insert_parents::Error> { + ) -> Result<(), gix_error::Exn> { let commit = self.lookup(id)?; let parents: SmallVec<[_; 2]> = commit.iter_parents().collect(); for parent_id in parents { @@ -154,7 +140,7 @@ impl<'cache, T> Graph<'_, 'cache, T> { parent_data: &mut dyn FnMut(gix_hash::ObjectId, LazyCommit<'_, 'cache>, Option<&mut T>) -> Result, ) -> Result<(), E> where - E: From + From, + E: From + From>, { let commit = self.lookup(id).map_err(E::from)?; let parents: SmallVec<[_; 2]> = commit.iter_parents().collect(); @@ -216,7 +202,7 @@ impl Graph<'_, '_, Commit> { id: gix_hash::ObjectId, new_data: impl FnOnce() -> T, update_data: impl FnOnce(&mut T), - ) -> Result>, get_or_insert_default::Error> { + ) -> Result>, gix_error::Exn> { match self.map.entry(id) { gix_hashtable::hash_map::Entry::Vacant(entry) => { let res = try_lookup(&id, &*self.find, self.cache, &mut self.buf)?; @@ -255,7 +241,7 @@ impl Graph<'_, '_, Commit> { &mut self, id: gix_hash::ObjectId, update_data: impl FnOnce(&mut T), - ) -> Result>, get_or_insert_default::Error> { + ) -> Result>, gix_error::Exn> { self.get_or_insert_commit_default(id, T::default, update_data) } @@ -267,7 +253,7 @@ impl Graph<'_, '_, Commit> { &mut self, id: gix_hash::ObjectId, update_commit: impl FnOnce(&mut Commit), - ) -> Result>, get_or_insert_default::Error> { + ) -> Result>, gix_error::Exn> { match self.map.entry(id) { gix_hashtable::hash_map::Entry::Vacant(entry) => { let res = try_lookup(&id, &*self.find, self.cache, &mut self.buf)?; @@ -304,7 +290,7 @@ impl<'cache, T> Graph<'_, 'cache, T> { id: gix_hash::ObjectId, default: impl FnOnce() -> T, update_data: impl FnOnce(&mut T), - ) -> Result>, get_or_insert_default::Error> { + ) -> Result>, gix_error::Exn> { let res = try_lookup(&id, &*self.find, self.cache, &mut self.buf)?; Ok(res.inspect(|_commit| match self.map.entry(id) { gix_hashtable::hash_map::Entry::Vacant(entry) => { @@ -322,18 +308,12 @@ impl<'cache, T> Graph<'_, 'cache, T> { /// or isn't a commit. /// /// It's possible that commits don't exist if the repository is shallow. - pub fn try_lookup( - &mut self, - id: &gix_hash::oid, - ) -> Result>, gix_object::find::existing_iter::Error> { + pub fn try_lookup(&mut self, id: &gix_hash::oid) -> Result>, gix_error::Exn> { try_lookup(id, &*self.find, self.cache, &mut self.buf) } /// Lookup `id` and return a handle to it, or fail if it doesn't exist or is no commit. - pub fn lookup( - &mut self, - id: &gix_hash::oid, - ) -> Result, gix_object::find::existing_iter::Error> { + pub fn lookup(&mut self, id: &gix_hash::oid) -> Result, gix_error::Exn> { use gix_error::NotFoundError; self.try_lookup(id)? @@ -346,7 +326,7 @@ fn try_lookup<'graph, 'cache>( objects: &dyn gix_object::Find, cache: Option<&'cache gix_commitgraph::Graph>, buf: &'graph mut Vec, -) -> Result>, gix_object::find::existing_iter::Error> { +) -> Result>, gix_error::Exn> { if let Some(cache) = cache && let Some(pos) = cache.lookup(id) { diff --git a/gix-shallow/src/lib.rs b/gix-shallow/src/lib.rs index 689c5a026f3..638115efdc0 100644 --- a/gix-shallow/src/lib.rs +++ b/gix-shallow/src/lib.rs @@ -48,7 +48,7 @@ pub enum Update { /// The list of shallow commits represents the shallow boundary, beyond which we are lacking all (parent) commits. /// Note that the list is never empty, as `Ok(None)` is returned in that case indicating the repository /// isn't a shallow clone. -pub fn read(shallow_file: &std::path::Path) -> Result>, read::Error> { +pub fn read(shallow_file: &std::path::Path) -> Result>, gix_error::Exn> { use bstr::ByteSlice; use gix_error::{CorruptionError, ErrorExt, ResultExt, message}; let buf = match std::fs::read(shallow_file) { @@ -80,7 +80,6 @@ pub mod write { use gix_error::{ErrorExt, ResultExt, message}; - use super::Error; use crate::Update; /// Write the [previously obtained](crate::read()) (possibly non-existing) `shallow_commits` to the shallow `file` @@ -95,7 +94,7 @@ pub mod write { mut file: gix_lock::File, shallow_commits: Option>, updates: &[Update], - ) -> Result<(), Error> { + ) -> Result<(), gix_error::Exn> { let mut shallow_commits = shallow_commits.map(Vec::from).unwrap_or_default(); for update in updates { match update { @@ -130,14 +129,5 @@ pub mod write { Ok(()) } } - - /// The error returned by [`write()`](crate::write()). - pub type Error = gix_error::Exn; } pub use write::function::write; - -/// -pub mod read { - /// The error returned by [`read`](crate::read()). - pub type Error = gix_error::Exn; -} diff --git a/gix-status/src/index_as_worktree/function.rs b/gix-status/src/index_as_worktree/function.rs index 40643539430..42d9d18c231 100644 --- a/gix-status/src/index_as_worktree/function.rs +++ b/gix-status/src/index_as_worktree/function.rs @@ -19,7 +19,7 @@ use crate::{ index_as_worktree::{ Change, Conflict, Context, EntryStatus, Outcome, VisitEntry, traits, traits::{CompareBlobs, SubmoduleStatus, read_data::Stream}, - types::{Error, Options}, + types::Options, }, is_dir_to_mode, }; @@ -67,7 +67,7 @@ pub fn index_as_worktree<'index, T, U, Find>( should_interrupt, }: Context<'_>, options: Options, -) -> Result +) -> Result where T: Send, U: Send, @@ -255,7 +255,8 @@ struct State<'a, 'b> { odb_reads: &'a AtomicUsize, } -type StatusResult<'index, T, U> = Result<(&'index gix_index::Entry, usize, &'index BStr, EntryStatus), Error>; +type StatusResult<'index, T, U> = + Result<(&'index gix_index::Entry, usize, &'index BStr, EntryStatus), gix_error::Exn>; impl<'index> State<'_, 'index> { #[expect(clippy::too_many_arguments)] @@ -375,7 +376,7 @@ impl<'index> State<'_, 'index> { diff: &mut impl CompareBlobs, submodule: &mut impl SubmoduleStatus, objects: &Find, - ) -> Result>, Error> + ) -> Result>, gix_error::Exn> where Find: gix_object::Find, { @@ -551,7 +552,7 @@ impl<'index, T, U, C: VisitEntry<'index, ContentChange = T, SubmoduleStatus = U> type Output = (); - type Error = Error; + type Error = gix_error::Exn; fn feed(&mut self, items: Self::Input) -> Result { for item in items { @@ -591,7 +592,7 @@ impl<'a, Find> traits::ReadData<'a> for ReadDataImpl<'a, Find> where Find: gix_object::Find, { - fn read_blob(self) -> Result<&'a [u8], Error> { + fn read_blob(self) -> Result<&'a [u8], gix_error::Exn> { self.objects .find_blob(self.id, self.buf) .or_raise_erased(|| message("Failed to obtain blob from object database")) @@ -602,7 +603,7 @@ where }) } - fn stream_worktree_file(self) -> Result, Error> { + fn stream_worktree_file(self) -> Result, gix_error::Exn> { self.buf.clear(); // symlinks are only stored as actual symlinks if the FS supports it otherwise they are just // normal files with their content equal to the linked path (so can be read normally) @@ -732,7 +733,7 @@ impl Conflict { } } -fn live_metadata(worktree_path: &Path) -> Result, Error> { +fn live_metadata(worktree_path: &Path) -> Result, gix_error::Exn> { match gix_index::fs::Metadata::from_path_no_follow(worktree_path) { Ok(md) => Ok(Some(md)), Err(err) if gix_fs::io_err::is_not_found(err.kind(), err.raw_os_error()) => Ok(None), diff --git a/gix-status/src/index_as_worktree/mod.rs b/gix-status/src/index_as_worktree/mod.rs index 8c55c913ab4..f1471d69e7f 100644 --- a/gix-status/src/index_as_worktree/mod.rs +++ b/gix-status/src/index_as_worktree/mod.rs @@ -1,7 +1,7 @@ //! Changes between an index and a worktree. /// mod types; -pub use types::{Change, Conflict, ConflictIndexEntry, Context, EntryStatus, Error, Options, Outcome, VisitEntry}; +pub use types::{Change, Conflict, ConflictIndexEntry, Context, EntryStatus, Options, Outcome, VisitEntry}; mod recorder; pub use recorder::{Record, Recorder}; diff --git a/gix-status/src/index_as_worktree/traits.rs b/gix-status/src/index_as_worktree/traits.rs index ca5678a55bd..4f8bfc66d1b 100644 --- a/gix-status/src/index_as_worktree/traits.rs +++ b/gix-status/src/index_as_worktree/traits.rs @@ -5,8 +5,6 @@ use gix_hash::ObjectId; use gix_index as index; use index::Entry; -use crate::index_as_worktree::Error; - /// Compares the content of two blobs in some way. pub trait CompareBlobs { /// Output data produced by [`compare_blobs()`][CompareBlobs::compare_blobs()]. @@ -23,7 +21,7 @@ pub trait CompareBlobs { worktree_blob_size: u64, data: impl ReadData<'a>, buf: &mut Vec, - ) -> Result, Error>; + ) -> Result, gix_error::Exn>; } /// Determine the status of a submodule, which always indicates that it changed if present. @@ -41,10 +39,10 @@ pub trait ReadData<'a> { /// /// This potentially performs IO and other expensive operations /// and should only be called when necessary. - fn read_blob(self) -> Result<&'a [u8], Error>; + fn read_blob(self) -> Result<&'a [u8], gix_error::Exn>; /// Stream a worktree file in such a manner that its content matches what would be put into git. - fn stream_worktree_file(self) -> Result, Error>; + fn stream_worktree_file(self) -> Result, gix_error::Exn>; } /// @@ -107,7 +105,7 @@ impl CompareBlobs for FastEq { worktree_file_size: u64, data: impl ReadData<'a>, buf: &mut Vec, - ) -> Result, Error> { + ) -> Result, gix_error::Exn> { // make sure to account for racily smudged entries here so that they don't always keep // showing up as modified even after their contents have changed again, to a potentially // unmodified state. That means that we want to ignore stat.size == 0 for non_empty_blobs. @@ -136,7 +134,7 @@ impl CompareBlobs for HashEq { _worktree_blob_size: u64, data: impl ReadData<'a>, buf: &mut Vec, - ) -> Result, Error> { + ) -> Result, gix_error::Exn> { let mut stream = data.stream_worktree_file()?; match stream.as_bytes() { Some(buffer) => { diff --git a/gix-status/src/index_as_worktree/types.rs b/gix-status/src/index_as_worktree/types.rs index 07177307e0f..8b48f7b629c 100644 --- a/gix-status/src/index_as_worktree/types.rs +++ b/gix-status/src/index_as_worktree/types.rs @@ -3,9 +3,6 @@ use std::sync::atomic::AtomicBool; use bstr::BStr; use gix_index::entry; -/// The error returned by [index_as_worktree()`](crate::index_as_worktree()). -pub type Error = gix_error::Exn; - /// Options that control how the index status with a worktree is computed. #[derive(Clone, Default, Debug, PartialEq, Eq, Hash)] pub struct Options { diff --git a/gix-status/src/index_as_worktree_with_renames/mod.rs b/gix-status/src/index_as_worktree_with_renames/mod.rs index 9563d15a387..f4daffd9311 100644 --- a/gix-status/src/index_as_worktree_with_renames/mod.rs +++ b/gix-status/src/index_as_worktree_with_renames/mod.rs @@ -1,6 +1,6 @@ //! Changes between the index and the worktree along with optional rename tracking. mod types; -pub use types::{Context, DirwalkContext, Entry, Error, Options, Outcome, RewriteSource, Sorting, Summary, VisitEntry}; +pub use types::{Context, DirwalkContext, Entry, Options, Outcome, RewriteSource, Sorting, Summary, VisitEntry}; mod recorder; pub use recorder::Recorder; @@ -15,8 +15,7 @@ pub(super) mod function { use crate::{ index_as_worktree::traits::{CompareBlobs, SubmoduleStatus}, index_as_worktree_with_renames::{ - Context, Entry, Error, Options, Outcome, RewriteSource, VisitEntry, - function::rewrite::ModificationOrDirwalkEntry, + Context, Entry, Options, Outcome, RewriteSource, VisitEntry, function::rewrite::ModificationOrDirwalkEntry, }, is_dir_to_mode, }; @@ -56,7 +55,7 @@ pub(super) mod function { progress: &mut dyn gix_features::progress::Progress, mut ctx: Context<'_>, options: Options<'_>, - ) -> Result + ) -> Result where T: Send + Clone, U: Send + Clone, @@ -64,7 +63,7 @@ pub(super) mod function { { let mut tracked_file_modifications = options.tracked_file_modifications; tracked_file_modifications.fscache = options.fscache; - gix_features::parallel::threads(|scope| -> Result { + gix_features::parallel::threads(|scope| -> Result { let (tx, rx) = std::sync::mpsc::channel(); let walk_outcome = options .dirwalk @@ -91,7 +90,7 @@ pub(super) mod function { .any(|p| !p.attributes.is_empty()) .then(|| ctx.resource_cache.attr_stack.clone()); let mut pathspec = ctx.pathspec.clone(); - move || -> Result<_, Error> { + move || -> Result<_, gix_error::Exn> { gix_dir::walk( worktree, gix_dir::walk::Context { @@ -140,7 +139,7 @@ pub(super) mod function { let objects = objects.clone(); let stack = ctx.resource_cache.attr_stack.clone(); let filter = ctx.resource_cache.filter.worktree_filter.clone(); - move || -> Result<_, Error> { + move || -> Result<_, gix_error::Exn> { crate::index_as_worktree( index, worktree, @@ -419,7 +418,7 @@ pub(super) mod function { use crate::{ index_as_worktree::{Change, EntryStatus}, - index_as_worktree_with_renames::{Entry, Error}, + index_as_worktree_with_renames::Entry, }; use bstr::BStr; use gix_diff::{rewrites::tracker::ChangeKind, tree::visit::Relation}; @@ -516,7 +515,7 @@ pub(super) mod function { objects: &dyn gix_object::Find, buf: &mut Vec, should_interrupt: &std::sync::atomic::AtomicBool, - ) -> Result { + ) -> Result { let Some(kind) = disk_kind else { return Ok(object_hash.null()); }; 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 1bc6d984472..6bfc3d60590 100644 --- a/gix-status/src/index_as_worktree_with_renames/types.rs +++ b/gix-status/src/index_as_worktree_with_renames/types.rs @@ -4,9 +4,6 @@ use bstr::{BStr, ByteSlice}; use crate::index_as_worktree::{Change, EntryStatus}; -/// The error returned by [index_as_worktree_with_renames()`](crate::index_as_worktree_with_renames()). -pub type Error = gix_error::Exn; - /// The way all output should be sorted. #[derive(Clone, Copy, Default, Debug, Eq, PartialEq, PartialOrd, Ord, Hash)] pub enum Sorting { diff --git a/gix-status/tests/status/index_as_worktree.rs b/gix-status/tests/status/index_as_worktree.rs index 2e8dcf2b083..655b903a9f8 100644 --- a/gix-status/tests/status/index_as_worktree.rs +++ b/gix-status/tests/status/index_as_worktree.rs @@ -309,13 +309,13 @@ fn hash_errors_preserve_io_kinds() { #[test] fn hash_errors_without_io_causes_preserve_hashing_failure() { - let err = gix_hash::io::from_hasher(gix_hash::hasher::Error::new("hash collision")); + let err = gix_hash::io::from_hasher(gix_error::CorruptionError::new("hash collision")); assert!( err.downcast_any_ref::().is_none(), "a hashing failure without an I/O cause has no native kind" ); assert!( - err.downcast_any_ref::() + err.downcast_any_ref::() .is_some_and(|source| source.to_string().contains("hash collision")), "the original hashing failure remains available for diagnostics" ); @@ -1223,7 +1223,7 @@ fn racy_git() { worktree_file_size: u64, data: impl ReadData<'a>, buf: &mut Vec, - ) -> Result, gix_status::index_as_worktree::Error> { + ) -> Result, gix_error::Exn> { self.0.fetch_add(1, Ordering::Relaxed); self.1.compare_blobs(entry, worktree_file_size, data, buf) } diff --git a/gix-submodule/src/access.rs b/gix-submodule/src/access.rs index 5264c3aa252..02f47a63ca9 100644 --- a/gix-submodule/src/access.rs +++ b/gix-submodule/src/access.rs @@ -4,7 +4,7 @@ use bstr::{BStr, BString, ByteSlice}; use gix_error::{ErrorExt, ResultExt, ValidationError}; use crate::{ - File, IsActivePlatform, config, + File, IsActivePlatform, config::{Branch, FetchRecurse, Ignore, Update}, }; @@ -58,8 +58,8 @@ impl File { + 'a ), ) -> Result< - impl Iterator)> + 'a, - crate::is_active_platform::Error, + impl Iterator>)> + 'a, + gix_error::ValidationError, > { let mut platform = self.is_active_platform(config, defaults)?; let iter = self @@ -78,10 +78,10 @@ impl File { &self, config: &gix_config::File, defaults: gix_pathspec::Defaults, - ) -> Result { + ) -> Result { let search = config .strings("submodule.active") - .map(|patterns| -> Result<_, crate::is_active_platform::Error> { + .map(|patterns| -> Result<_, gix_error::ValidationError> { let patterns = patterns .into_iter() .map(|pattern| gix_pathspec::parse(&pattern, defaults)) @@ -114,7 +114,7 @@ impl File { /// /// Git currently allows absolute paths to be used when adding submodules, but fails later as it can't find the submodule by /// relative path anymore. Let's play it safe here. - pub fn path(&self, name: &BStr) -> Result { + pub fn path(&self, name: &BStr) -> Result { let path_bstr = self.config.string(&format!("submodule.{name}.path")).ok_or_else(|| { ValidationError::new(format!( "The submodule '{name}' was missing its 'path' field or it was empty" @@ -142,7 +142,7 @@ impl File { } /// Retrieve the `url` field of the submodule named `name`. It's an error if it doesn't exist or is empty. - pub fn url(&self, name: &BStr) -> Result { + pub fn url(&self, name: &BStr) -> Result> { let url = self.config.string(&format!("submodule.{name}.url")).ok_or_else(|| { ValidationError::new(format!( "The submodule '{name}' was missing its 'url' field or it was empty" @@ -162,7 +162,7 @@ impl File { } /// Retrieve the `update` field of the submodule named `name`, if present. - pub fn update(&self, name: &BStr) -> Result, config::update::Error> { + pub fn update(&self, name: &BStr) -> Result, gix_error::ValidationError> { let mut value_is_from_modules_file = None; let our_meta = self.config.meta(); let value: Update = match self.config.string_filter(&format!("submodule.{name}.update"), |meta| { @@ -189,7 +189,7 @@ impl File { /// Retrieve the `branch` field of the submodule named `name`, or `None` if unset. /// /// Note that `Default` is implemented for [`Branch`]. - pub fn branch(&self, name: &BStr) -> Result, config::branch::Error> { + pub fn branch(&self, name: &BStr) -> Result, gix_error::Exn> { let branch = match self.config.string(&format!("submodule.{name}.branch")) { Some(v) => v, None => return Ok(None), @@ -206,7 +206,7 @@ impl File { /// Retrieve the `fetchRecurseSubmodules` field of the submodule named `name`, or `None` if unset. /// /// Note that if it's unset, it should be retrieved from `fetch.recurseSubmodules` in the configuration. - pub fn fetch_recurse(&self, name: &BStr) -> Result, config::Error> { + pub fn fetch_recurse(&self, name: &BStr) -> Result, gix_error::ValidationError> { FetchRecurse::new(self.config.boolean(&format!("submodule.{name}.fetchRecurseSubmodules"))).map_err(|value| { ValidationError::new_with_input( format!("The 'fetchRecurseSubmodules' field of submodule '{name}' was invalid"), @@ -216,7 +216,7 @@ impl File { } /// Retrieve the `ignore` field of the submodule named `name`, or `None` if unset. - pub fn ignore(&self, name: &BStr) -> Result, config::Error> { + pub fn ignore(&self, name: &BStr) -> Result, gix_error::ValidationError> { self.config .string(&format!("submodule.{name}.ignore")) .map(|value| { @@ -233,7 +233,7 @@ impl File { /// Retrieve the `shallow` field of the submodule named `name`, or `None` if unset. /// /// If `true`, the submodule will be checked out with `depth = 1`. If unset, `false` is assumed. - pub fn shallow(&self, name: &BStr) -> Result, gix_config::value::Error> { + pub fn shallow(&self, name: &BStr) -> Result, gix_error::Exn> { self.config.boolean(&format!("submodule.{name}.shallow")) } } diff --git a/gix-submodule/src/config.rs b/gix-submodule/src/config.rs index 48f67959e09..15df41b2ea0 100644 --- a/gix-submodule/src/config.rs +++ b/gix-submodule/src/config.rs @@ -55,7 +55,9 @@ impl FetchRecurse { /// Check if `boolean` is set and translate it the respective variant, or check the underlying string /// value for non-boolean options. /// On error, it returns the obtained string value which would be the invalid value. - pub fn new(boolean: Result, gix_config::value::Error>) -> Result, BString> { + pub fn new( + boolean: Result, gix_error::Exn>, + ) -> Result, BString> { Ok(match boolean { Ok(Some(value)) => Some(if value { FetchRecurse::Always @@ -93,7 +95,7 @@ impl Default for Branch { } impl TryFrom<&BStr> for Branch { - type Error = gix_refspec::parse::Error; + type Error = gix_error::Exn; fn try_from(value: &BStr) -> Result { if value == "." { @@ -140,30 +142,3 @@ impl TryFrom<&BStr> for Update { }) } } - -/// The error returned by [File::fetch_recurse()](crate::File::fetch_recurse) and [File::ignore()](crate::File::ignore). -pub type Error = gix_error::ValidationError; - -/// -pub mod branch { - /// The error returned by [File::branch()](crate::File::branch). - pub type Error = gix_error::Exn; -} - -/// -pub mod update { - /// The error returned by [File::update()](crate::File::update). - pub type Error = gix_error::ValidationError; -} - -/// -pub mod url { - /// The error returned by [File::url()](crate::File::url). - pub type Error = gix_error::Exn; -} - -/// -pub mod path { - /// The error returned by [File::path()](crate::File::path). - pub type Error = gix_error::ValidationError; -} diff --git a/gix-submodule/src/is_active_platform.rs b/gix-submodule/src/is_active_platform.rs index 62bcabc3cd6..a65e69def8f 100644 --- a/gix-submodule/src/is_active_platform.rs +++ b/gix-submodule/src/is_active_platform.rs @@ -2,9 +2,6 @@ use bstr::BStr; use crate::IsActivePlatform; -/// The error returned by [File::names_and_active_state](crate::File::names_and_active_state()). -pub type Error = gix_error::ValidationError; - impl IsActivePlatform { /// Returns `true` if the submodule named `name` is active or `false` otherwise. /// `config` is the configuration that was passed to the originating [modules file](crate::File). @@ -26,7 +23,7 @@ impl IsActivePlatform { bool, &mut gix_pathspec::attributes::search::Outcome, ) -> bool, - ) -> Result { + ) -> Result> { if let Some(val) = config.boolean(&format!("submodule.{name}.active"))? { return Ok(val); } diff --git a/gix-submodule/src/lib.rs b/gix-submodule/src/lib.rs index 45292e1e098..268356e4fdb 100644 --- a/gix-submodule/src/lib.rs +++ b/gix-submodule/src/lib.rs @@ -43,7 +43,7 @@ impl File { pub fn append_submodule_overrides( &mut self, config: &gix_config::File, - ) -> Result<&mut Self, gix_config::file::section::value::Error> { + ) -> Result<&mut Self, gix_error::ValidationError> { let mut values = BTreeMap::<_, Vec<_>>::new(); for (module_name, section) in config .sections_by_name("submodule") @@ -112,10 +112,6 @@ pub mod init { /// A marker we use when listing names to not pick them up from overridden sections. pub(crate) const META_MARKER: gix_config::Source = gix_config::Source::Api; - /// Lifecycle - /// The error returned when parsing a submodule configuration file. - pub type Error = gix_error::Exn; - impl File { /// Parse `bytes` as git configuration, typically from `.gitmodules`, without doing any further validation. /// `path` can be provided to keep track of where the file was read from in the underlying [`config`](Self::config()) @@ -133,7 +129,7 @@ pub mod init { bytes: &[u8], path: impl Into>, config: &gix_config::File, - ) -> Result { + ) -> Result> { let metadata = { let mut meta = gix_config::file::Metadata::from(META_MARKER); meta.path = path.into(); diff --git a/gix-submodule/tests/file/mod.rs b/gix-submodule/tests/file/mod.rs index 20b152c07d7..1e35b7d5499 100644 --- a/gix-submodule/tests/file/mod.rs +++ b/gix-submodule/tests/file/mod.rs @@ -163,11 +163,10 @@ mod is_active_platform { } mod path { - use gix_submodule::config::path::Error; use crate::file::submodule; - fn submodule_path(value: &str) -> Error { + fn submodule_path(value: &str) -> gix_error::ValidationError { let module = submodule(&format!("[submodule.a]\npath = {value}")); module.path("a".into()).unwrap_err() } @@ -215,11 +214,10 @@ mod path { } mod url { - use gix_submodule::config::url::Error; use crate::file::submodule; - fn submodule_url(value: &str) -> Error { + fn submodule_url(value: &str) -> gix_error::Exn { let module = submodule(&format!("[submodule.a]\nurl = {value}")); module.url("a".into()).unwrap_err() } @@ -265,11 +263,11 @@ mod url { mod update { use std::str::FromStr; - use gix_submodule::config::{Update, update::Error}; + use gix_submodule::config::Update; use crate::file::submodule; - fn submodule_update(value: &str) -> Error { + fn submodule_update(value: &str) -> gix_error::ValidationError { let module = submodule(&format!("[submodule.a]\nupdate = {value}")); module.update("a".into()).unwrap_err() } diff --git a/gix-tix/src/history.rs b/gix-tix/src/history.rs index b0606af6079..2692c97d534 100644 --- a/gix-tix/src/history.rs +++ b/gix-tix/src/history.rs @@ -1658,7 +1658,7 @@ fn decode_commit( } fn decode_metadata<'a>( - tokens: impl Iterator, gix::objs::decode::Error>>, + tokens: impl Iterator, gix::error::ValidationError>>, authors: &mut Authors, attributions: &mut Vec, ) -> Result> { diff --git a/gix-tix/src/test_repository.rs b/gix-tix/src/test_repository.rs index 9509ba5a559..f4ba24119e6 100644 --- a/gix-tix/src/test_repository.rs +++ b/gix-tix/src/test_repository.rs @@ -9,11 +9,11 @@ const DEFAULT_OVERRIDES: &[&str] = &[ "core.editor=:", ]; -pub(crate) fn open(path: impl AsRef) -> Result { +pub(crate) fn open(path: impl AsRef) -> Result { open_with(path, std::iter::empty::()) } -pub(crate) fn open_with(path: impl AsRef, overrides: I) -> Result +pub(crate) fn open_with(path: impl AsRef, overrides: I) -> Result where I: IntoIterator, S: Into, diff --git a/gix-transport/src/client/async_io/bufread_ext.rs b/gix-transport/src/client/async_io/bufread_ext.rs index f9b25886679..5e392eb2334 100644 --- a/gix-transport/src/client/async_io/bufread_ext.rs +++ b/gix-transport/src/client/async_io/bufread_ext.rs @@ -33,7 +33,7 @@ pub trait ReadlineBufRead: AsyncBufRead { /// * A `delimiter` packet line encountered async fn readline( &mut self, - ) -> Option, gix_packetline::decode::Error>>>; + ) -> Option, gix_error::ValidationError>>>; /// Read a line similar to `BufRead::read_line()`, but assure it doesn't try to find newlines /// which might concatenate multiple distinct packet lines. @@ -61,7 +61,7 @@ pub trait ExtendedBufRead<'a>: ReadlineBufRead { #[async_trait(?Send)] impl ReadlineBufRead for Box { - async fn readline(&mut self) -> Option, gix_packetline::decode::Error>>> { + async fn readline(&mut self) -> Option, gix_error::ValidationError>>> { self.deref_mut().readline().await } async fn readline_str(&mut self, line: &mut String) -> io::Result { @@ -90,7 +90,7 @@ impl<'a, T: ExtendedBufRead<'a> + ?Sized + 'a + Unpin> ExtendedBufRead<'a> for B #[async_trait(?Send)] impl ReadlineBufRead for WithSidebands<'_, T, for<'b> fn(bool, &'b [u8]) -> ProgressAction> { - async fn readline(&mut self) -> Option, gix_packetline::decode::Error>>> { + async fn readline(&mut self) -> Option, gix_error::ValidationError>>> { self.read_data_line().await } async fn readline_str(&mut self, line: &mut String) -> io::Result { @@ -100,7 +100,7 @@ impl ReadlineBufRead for WithSidebands<'_, T, for<'b> fn(b #[async_trait(?Send)] impl<'a, T: AsyncRead + Unpin> ReadlineBufRead for WithSidebands<'a, T, HandleProgress<'a>> { - async fn readline(&mut self) -> Option, gix_packetline::decode::Error>>> { + async fn readline(&mut self) -> Option, gix_error::ValidationError>>> { self.read_data_line().await } async fn readline_str(&mut self, line: &mut String) -> io::Result { diff --git a/gix-transport/src/client/async_io/connect.rs b/gix-transport/src/client/async_io/connect.rs index ca3e371b5c4..55e6c3131aa 100644 --- a/gix-transport/src/client/async_io/connect.rs +++ b/gix-transport/src/client/async_io/connect.rs @@ -1,8 +1,8 @@ -pub use crate::client::non_io_types::connect::{Error, Options}; +pub use crate::client::non_io_types::connect::Options; #[cfg(feature = "async-std")] pub(crate) mod function { - use crate::client::{async_io::Transport, git::async_io::Connection, non_io_types::connect::Error}; + use crate::client::{async_io::Transport, git::async_io::Connection}; use gix_error::{ErrorExt, ResultExt, message}; /// A general purpose connector connecting to a repository identified by the given `url`. @@ -11,7 +11,10 @@ pub(crate) mod function { /// [git daemons][crate::client::git::connect()] only at the moment. /// /// Use `options` to further control specifics of the transport resulting from the connection. - pub async fn connect(url: Url, options: super::Options) -> Result, Error> + pub async fn connect( + url: Url, + options: super::Options, + ) -> Result, gix_error::Exn> where Url: TryInto, E: std::error::Error + Send + Sync + 'static, diff --git a/gix-transport/src/client/blocking_io/bufread_ext.rs b/gix-transport/src/client/blocking_io/bufread_ext.rs index ac6177d29b4..1f4fae182a0 100644 --- a/gix-transport/src/client/blocking_io/bufread_ext.rs +++ b/gix-transport/src/client/blocking_io/bufread_ext.rs @@ -25,9 +25,8 @@ pub trait ReadlineBufRead: io::BufRead { /// * natural EOF /// * ERR packet line encountered /// * A `delimiter` packet line encountered - fn readline( - &mut self, - ) -> Option, gix_packetline::decode::Error>>>; + fn readline(&mut self) + -> Option, gix_error::ValidationError>>>; /// Read a line similar to `BufRead::read_line()`, but assure it doesn't try to find newlines /// which might concatenate multiple distinct packet lines. @@ -53,7 +52,7 @@ pub trait ExtendedBufRead<'a>: ReadlineBufRead { } impl ReadlineBufRead for Box { - fn readline(&mut self) -> Option, gix_packetline::decode::Error>>> { + fn readline(&mut self) -> Option, gix_error::ValidationError>>> { ReadlineBufRead::readline(self.deref_mut()) } fn readline_str(&mut self, line: &mut String) -> io::Result { @@ -80,7 +79,7 @@ impl<'a, T: ExtendedBufRead<'a> + ?Sized + 'a> ExtendedBufRead<'a> for Box { } impl ReadlineBufRead for WithSidebands<'_, T, fn(bool, &[u8]) -> ProgressAction> { - fn readline(&mut self) -> Option, gix_packetline::decode::Error>>> { + fn readline(&mut self) -> Option, gix_error::ValidationError>>> { self.read_data_line() } @@ -90,7 +89,7 @@ impl ReadlineBufRead for WithSidebands<'_, T, fn(bool, &[u8]) -> Pr } impl<'a, T: io::Read> ReadlineBufRead for WithSidebands<'a, T, HandleProgress<'a>> { - fn readline(&mut self) -> Option, gix_packetline::decode::Error>>> { + fn readline(&mut self) -> Option, gix_error::ValidationError>>> { self.read_data_line() } diff --git a/gix-transport/src/client/blocking_io/connect.rs b/gix-transport/src/client/blocking_io/connect.rs index eadf0f70ff2..7631c53680c 100644 --- a/gix-transport/src/client/blocking_io/connect.rs +++ b/gix-transport/src/client/blocking_io/connect.rs @@ -1,11 +1,11 @@ -pub use crate::client::non_io_types::connect::{Error, Options}; +pub use crate::client::non_io_types::connect::Options; pub(crate) mod function { + use crate::client::blocking_io::Transport; #[cfg(feature = "http-client-curl")] use crate::client::blocking_io::http::curl::Curl; #[cfg(all(feature = "http-client-reqwest", not(feature = "http-client-curl")))] use crate::client::blocking_io::http::reqwest::Remote as Reqwest; - use crate::client::{blocking_io::Transport, non_io_types::connect::Error}; use gix_error::{ErrorExt, ResultExt, message}; /// A general purpose connector connecting to a repository identified by the given `url`. @@ -17,7 +17,10 @@ pub(crate) mod function { /// and if compiled in connections to [git repositories over https](crate::client::blocking_io::http::connect()). /// /// Use `options` to further control specifics of the transport resulting from the connection. - pub fn connect(url: Url, options: super::Options) -> Result, Error> + pub fn connect( + url: Url, + options: super::Options, + ) -> Result, gix_error::Exn> where Url: TryInto, E: std::error::Error + Send + Sync + 'static, diff --git a/gix-transport/src/client/blocking_io/http/curl/mod.rs b/gix-transport/src/client/blocking_io/http/curl/mod.rs index e870cae893b..df41792ba33 100644 --- a/gix-transport/src/client/blocking_io/http/curl/mod.rs +++ b/gix-transport/src/client/blocking_io/http/curl/mod.rs @@ -23,11 +23,6 @@ pub struct Options { pub schannel_check_revoke: Option, } -/// The error returned by the 'remote' helper, a purely internal construct to perform http requests. -/// -/// It can be used for downcasting errors, which are boxed to hide the actual implementation. -pub type Error = gix_error::Exn; - pub(crate) fn curl_is_retryable(err: &curl::Error) -> bool { err.is_couldnt_connect() || err.is_couldnt_resolve_proxy() @@ -45,13 +40,13 @@ pub(crate) fn curl_is_retryable(err: &curl::Error) -> bool { pub struct Curl { req: SyncSender, res: Receiver, - handle: Option>>, + handle: Option>>>, config: http::Options, redirected_base_url: Arc>>, } impl Curl { - fn restore_thread_after_failure(&mut self) -> http::Error { + fn restore_thread_after_failure(&mut self) -> gix_error::Exn { let err_that_brought_thread_down = self .handle .take() @@ -73,7 +68,10 @@ impl Curl { base_url: &str, headers: impl IntoIterator>, upload_body_kind: Option, - ) -> Result, http::Error> { + ) -> Result< + http::PostResponse, + gix_error::Exn, + > { let mut list = curl::easy::List::new(); for header in headers { list.append(header.as_ref()) @@ -138,7 +136,7 @@ impl http::Http for Curl { url: &str, base_url: &str, headers: impl IntoIterator>, - ) -> Result, http::Error> { + ) -> Result, gix_error::Exn> { self.make_request(url, base_url, headers, None).map(Into::into) } @@ -148,7 +146,8 @@ impl http::Http for Curl { base_url: &str, headers: impl IntoIterator>, body: PostBodyDataKind, - ) -> Result, http::Error> { + ) -> Result, gix_error::Exn> + { self.make_request(url, base_url, headers, Some(body)) } diff --git a/gix-transport/src/client/blocking_io/http/curl/remote.rs b/gix-transport/src/client/blocking_io/http/curl/remote.rs index a68fb5bf61a..f7b475c8e27 100644 --- a/gix-transport/src/client/blocking_io/http/curl/remote.rs +++ b/gix-transport/src/client/blocking_io/http/curl/remote.rs @@ -17,7 +17,6 @@ use parking_lot::Mutex; use crate::client::blocking_io::http::{ self, - curl::Error, curl::curl_is_retryable, options::{FollowRedirects, HttpVersion, ProxyAuthMethod, SslVersion}, redirect::{self, Action as RedirectAction}, @@ -373,7 +372,7 @@ pub struct Response { } type Worker = ( - thread::JoinHandle>, + thread::JoinHandle>>, SyncSender, Receiver, SharedRedirectedBaseUrl, @@ -384,7 +383,7 @@ pub fn new() -> Worker { let redirected_base_url_shared_out = redirected_base_url_shared.clone(); let (req_send, req_recv) = sync_channel(0); let (res_send, res_recv) = sync_channel(0); - let handle = std::thread::spawn(move || -> Result<(), Error> { + let handle = std::thread::spawn(move || -> Result<(), gix_error::Exn> { let mut handle = Easy2::new(Handler::default()); // We don't wait for the possibility for pipelining to become clear, and curl tries to reuse connections by default anyway. curl!(handle.pipewait(false)); diff --git a/gix-transport/src/client/blocking_io/http/mod.rs b/gix-transport/src/client/blocking_io/http/mod.rs index f9586b46ebc..f2d9f553983 100644 --- a/gix-transport/src/client/blocking_io/http/mod.rs +++ b/gix-transport/src/client/blocking_io/http/mod.rs @@ -9,7 +9,7 @@ use std::{ use base64::Engine; use bstr::BStr; use gix_error::{ErrorExt, message}; -pub use traits::{Error, GetResponse, Http, PostBodyDataKind, PostResponse}; +pub use traits::{GetResponse, Http, PostBodyDataKind, PostResponse}; use crate::{ Protocol, Service, @@ -537,7 +537,7 @@ impl BufRead for HeadersThenBody { } impl ReadlineBufRead for HeadersThenBody { - fn readline(&mut self) -> Option, gix_packetline::decode::Error>>> { + fn readline(&mut self) -> Option, gix_error::ValidationError>>> { if let Err(err) = self.handle_headers() { return Some(Err(err)); } diff --git a/gix-transport/src/client/blocking_io/http/redirect.rs b/gix-transport/src/client/blocking_io/http/redirect.rs index 754894a9283..48d0f5f03d5 100644 --- a/gix-transport/src/client/blocking_io/http/redirect.rs +++ b/gix-transport/src/client/blocking_io/http/redirect.rs @@ -1,8 +1,5 @@ use gix_error::{ErrorExt, message}; -/// The error provided when redirection went beyond what we deem acceptable. -pub type Error = gix_error::Exn; - #[derive(Default, Clone, Copy, Debug, Eq, PartialEq)] pub(crate) enum Action { Follow, @@ -65,7 +62,11 @@ pub(crate) fn can_reuse_identity(redirect_url: &str, original_url: &str) -> bool false } -pub(crate) fn base_url(redirect_url: &str, base_url: &str, url: String) -> Result { +pub(crate) fn base_url( + redirect_url: &str, + base_url: &str, + url: String, +) -> Result> { let tail = url .strip_prefix(base_url) .expect("BUG: caller assures `base_url` is subset of `url`"); diff --git a/gix-transport/src/client/blocking_io/http/reqwest/mod.rs b/gix-transport/src/client/blocking_io/http/reqwest/mod.rs index 1398faeea4a..90553566e6d 100644 --- a/gix-transport/src/client/blocking_io/http/reqwest/mod.rs +++ b/gix-transport/src/client/blocking_io/http/reqwest/mod.rs @@ -1,7 +1,7 @@ /// An implementation for HTTP requests via `reqwest`. pub struct Remote { /// A worker thread which performs the actual request. - handle: Option>>, + handle: Option>>>, /// A channel to send requests (work) to the worker thread. request: std::sync::mpsc::SyncSender, /// A channel to receive the result of the prior request. diff --git a/gix-transport/src/client/blocking_io/http/reqwest/remote.rs b/gix-transport/src/client/blocking_io/http/reqwest/remote.rs index 706a6c21e2c..781731254d9 100644 --- a/gix-transport/src/client/blocking_io/http/reqwest/remote.rs +++ b/gix-transport/src/client/blocking_io/http/reqwest/remote.rs @@ -17,9 +17,6 @@ use crate::client::blocking_io::http::{ traits::PostBodyDataKind, }; -/// The error returned by the 'remote' helper, a purely internal construct to perform http requests. -pub type Error = gix_error::Exn; - fn classify_reqwest(err: reqwest::Error) -> gix_error::Error { if err.is_timeout() || err.is_connect() || err.status().is_some_and(|status| status.is_server_error()) { gix_error::Error::from_error(gix_error::RetryableError::new(err)) @@ -40,7 +37,7 @@ impl Default for Remote { let (res_send, res_recv) = std::sync::mpsc::sync_channel(0); let redirected_base_url_shared = Arc::new(Mutex::new(None)); let redirected_base_url_shared_for_field = redirected_base_url_shared.clone(); - let handle = std::thread::spawn(move || -> Result<(), Error> { + let handle = std::thread::spawn(move || -> Result<(), gix_error::Exn> { let mut follow = None; let redirect_action = Arc::new(Mutex::new(RedirectAction::Stop)); let redirect_tail = Arc::new(Mutex::new(String::new())); @@ -256,7 +253,7 @@ impl Default for Remote { /// utilities impl Remote { - fn restore_thread_after_failure(&mut self) -> http::Error { + fn restore_thread_after_failure(&mut self) -> gix_error::Exn { let err_that_brought_thread_down = self .handle .take() @@ -274,7 +271,7 @@ impl Remote { base_url: &str, headers: impl IntoIterator>, upload_body_kind: Option, - ) -> Result, http::Error> { + ) -> Result, gix_error::Exn> { let mut header_map = reqwest::header::HeaderMap::new(); for header_line in headers { insert_header(&mut header_map, header_line.as_ref()); @@ -344,7 +341,7 @@ impl http::Http for Remote { url: &str, base_url: &str, headers: impl IntoIterator>, - ) -> Result, http::Error> { + ) -> Result, gix_error::Exn> { self.make_request(url, base_url, headers, None).map(Into::into) } @@ -354,7 +351,8 @@ impl http::Http for Remote { base_url: &str, headers: impl IntoIterator>, post_body_kind: PostBodyDataKind, - ) -> Result, http::Error> { + ) -> Result, gix_error::Exn> + { self.make_request(url, base_url, headers, Some(post_body_kind)) } diff --git a/gix-transport/src/client/blocking_io/http/traits.rs b/gix-transport/src/client/blocking_io/http/traits.rs index 4075dc28532..06129aabf78 100644 --- a/gix-transport/src/client/blocking_io/http/traits.rs +++ b/gix-transport/src/client/blocking_io/http/traits.rs @@ -1,8 +1,5 @@ use crate::client::WriteMode; -/// The error used by the [Http] trait. -pub type Error = gix_error::Exn; - /// The return value of [`Http::get()`]. pub struct GetResponse { /// The response headers. @@ -75,7 +72,7 @@ pub trait Http { url: &str, base_url: &str, headers: impl IntoIterator>, - ) -> Result, Error>; + ) -> Result, gix_error::Exn>; /// Initiate a `POST` request to `url` providing with the given `headers`, where `base_url` is so that `base_url + tail == url`. /// @@ -91,7 +88,7 @@ pub trait Http { base_url: &str, headers: impl IntoIterator>, body: PostBodyDataKind, - ) -> Result, Error>; + ) -> Result, gix_error::Exn>; /// Pass `config` which can deserialize in the implementation's configuration, as documented separately. /// diff --git a/gix-transport/src/client/capabilities.rs b/gix-transport/src/client/capabilities.rs index 06dbf2f981b..dd98e40eb44 100644 --- a/gix-transport/src/client/capabilities.rs +++ b/gix-transport/src/client/capabilities.rs @@ -4,9 +4,6 @@ use gix_error::{ErrorExt, message}; #[cfg(any(feature = "blocking-client", feature = "async-client"))] use crate::{Protocol, client}; -/// The error used in [`Capabilities::from_bytes()`] and [`Capabilities::from_lines()`]. -pub type Error = gix_error::Exn; - /// A structure to represent multiple [capabilities](Capability) or features supported by the server. /// /// ### Deviation @@ -67,7 +64,7 @@ impl Capabilities { /// Parse capabilities from the given `bytes`. /// /// Useful in case they are encoded within a `ref` behind a null byte. - pub fn from_bytes(bytes: &[u8]) -> Result<(Capabilities, usize), Error> { + pub fn from_bytes(bytes: &[u8]) -> Result<(Capabilities, usize), gix_error::Exn> { let delimiter_pos = bytes .find_byte(0) .ok_or_else(|| message("Capabilities were missing entirely as there was no 0 byte").raise())?; @@ -90,7 +87,7 @@ impl Capabilities { /// Useful for parsing capabilities from a data sent from a server, and to avoid having to deal with /// blocking and async traits for as long as possible. There is no value in parsing a few bytes /// in a non-blocking fashion. - pub fn from_lines(lines_buf: BString) -> Result { + pub fn from_lines(lines_buf: BString) -> Result> { let mut lines = <_ as bstr::ByteSlice>::lines(lines_buf.as_slice().trim()); let version_line = lines .next() diff --git a/gix-transport/src/client/git/blocking_io.rs b/gix-transport/src/client/git/blocking_io.rs index 5de2db7aa42..78fb0934ea8 100644 --- a/gix-transport/src/client/git/blocking_io.rs +++ b/gix-transport/src/client/git/blocking_io.rs @@ -179,10 +179,7 @@ pub mod connect { use super::Connection; use crate::client::git; - /// The error used in [`connect()`]. - pub type Error = gix_error::Exn; - - fn parse_host(input: String) -> Result<(String, Option), Error> { + fn parse_host(input: String) -> Result<(String, Option), gix_error::Exn> { let mut tokens = input.splitn(2, ':'); Ok(match (tokens.next(), tokens.next()) { (Some(host), None) => (host.to_owned(), None), @@ -206,7 +203,7 @@ pub mod connect { desired_version: crate::Protocol, port: Option, trace: bool, - ) -> Result, Error> { + ) -> Result, gix_error::Exn> { let read = TcpStream::connect_timeout( &(host, port.unwrap_or(9418)) .to_socket_addrs() diff --git a/gix-transport/src/client/non_io_types.rs b/gix-transport/src/client/non_io_types.rs index e86c91475f7..ba149f82f85 100644 --- a/gix-transport/src/client/non_io_types.rs +++ b/gix-transport/src/client/non_io_types.rs @@ -44,11 +44,6 @@ pub(crate) mod connect { /// If `true`, all packetlines received or sent will be passed to the facilities of the `gix-trace` crate. pub trace: bool, } - - /// The error used in `connect()`. - /// - /// (Both blocking and async I/O use the same error type.) - pub type Error = gix_error::Exn; } mod error { @@ -58,14 +53,9 @@ mod error { #[cfg(feature = "blocking-client")] use crate::client::blocking_io::ssh; - use crate::client::capabilities; - #[cfg(feature = "http-client")] - type HttpError = gix_error::Error; #[cfg(feature = "blocking-client")] type SshInvocationError = ssh::invocation::Error; - #[cfg(not(feature = "http-client"))] - type HttpError = std::convert::Infallible; #[cfg(not(feature = "blocking-client"))] type SshInvocationError = std::convert::Infallible; @@ -97,17 +87,29 @@ mod error { pub enum Error { MissingHandshake, Io(std::io::Error), - Capabilities { err: gix_error::Error }, - LineDecode { err: gix_packetline::decode::Error }, + Capabilities { + err: gix_error::Error, + }, + LineDecode { + err: gix_error::ValidationError, + }, ExpectedLine(&'static str), ExpectedDataLine, AuthenticationUnsupported, AuthenticationRefused(&'static str), UnsupportedProtocolVersion(BString), - InvokeProgram { source: std::io::Error, command: OsString }, - Http(HttpError), + InvokeProgram { + source: std::io::Error, + command: OsString, + }, + #[cfg(feature = "http-client")] + Http(gix_error::Error), + #[cfg(not(feature = "http-client"))] + Http(std::convert::Infallible), SshInvocation(SshInvocationError), - AmbiguousPath { path: BString }, + AmbiguousPath { + path: BString, + }, } impl std::fmt::Display for Error { @@ -163,14 +165,14 @@ mod error { } } - impl From for Error { - fn from(err: capabilities::Error) -> Self { + impl From> for Error { + fn from(err: gix_error::Exn) -> Self { Error::Capabilities { err: err.into_error() } } } - impl From for Error { - fn from(err: gix_packetline::decode::Error) -> Self { + impl From for Error { + fn from(err: gix_error::ValidationError) -> Self { Error::LineDecode { err } } } diff --git a/gix-traverse/src/commit/mod.rs b/gix-traverse/src/commit/mod.rs index 7ffd65fe48f..c24d5148389 100644 --- a/gix-traverse/src/commit/mod.rs +++ b/gix-traverse/src/commit/mod.rs @@ -81,7 +81,7 @@ pub enum Either<'buf, 'cache> { impl Either<'_, '_> { /// Get a commit’s `tree_id` by either getting it from a [`gix_commitgraph::Graph`], if /// present, or a [`gix_object::CommitRefIter`] otherwise. - pub fn tree_id(self) -> Result { + pub fn tree_id(self) -> Result { match self { Self::CommitRefIter(mut commit_ref_iter) => commit_ref_iter.tree_id(), Self::CachedCommit(commit) => Ok(commit.root_tree_id().into()), @@ -90,7 +90,7 @@ impl Either<'_, '_> { /// Get a committer timestamp by either getting it from a [`gix_commitgraph::Graph`], if /// present, or a [`gix_object::CommitRefIter`] otherwise. - pub fn commit_time(self) -> Result { + pub fn commit_time(self) -> Result { match self { Self::CommitRefIter(commit_ref_iter) => commit_ref_iter.committer().map(|c| c.seconds()), Self::CachedCommit(commit) => Ok(commit.committer_timestamp() as gix_date::SecondsSinceUnixEpoch), @@ -105,7 +105,7 @@ pub fn find<'cache, 'buf, Find>( objects: Find, id: &gix_hash::oid, buf: &'buf mut Vec, -) -> Result, gix_object::find::existing_iter::Error> +) -> Result, gix_error::Exn> where Find: gix_object::Find, { diff --git a/gix-traverse/src/commit/simple.rs b/gix-traverse/src/commit/simple.rs index 6be98d24c7e..9943cb41593 100644 --- a/gix-traverse/src/commit/simple.rs +++ b/gix-traverse/src/commit/simple.rs @@ -68,9 +68,6 @@ pub enum Sorting { }, } -/// The error is part of the item returned by the [Ancestors](super::Simple) iterator. -pub type Error = gix_error::Exn; - use Result as Either; type QueueKey = Either>; @@ -171,7 +168,7 @@ fn compute_hidden_frontier( hidden_tips: &[ObjectId], objects: &impl gix_object::Find, cache: Option<&gix_commitgraph::Graph>, -) -> Result, Error> { +) -> Result, gix_error::Exn> { let mut graph = gix_revwalk::Graph::>::new(objects, cache); let mut queue = gix_revwalk::PriorityQueue::::new(); @@ -228,7 +225,7 @@ fn compute_hidden_frontier( /// mod init { use super::{ - CommitDateQueue, CommitTimeOrder, Error, Sorting, State, collect_parents, compute_hidden_frontier, to_queue_key, + CommitDateQueue, CommitTimeOrder, Sorting, State, collect_parents, compute_hidden_frontier, to_queue_key, }; use crate::commit::{Either, Info, ParentIds, Parents, Simple}; use gix_date::SecondsSinceUnixEpoch; @@ -292,7 +289,7 @@ mod init { Find: gix_object::Find, { /// Set the `sorting` method. - pub fn sorting(mut self, sorting: Sorting) -> Result { + pub fn sorting(mut self, sorting: Sorting) -> Result { self.sorting = sorting; match self.sorting { Sorting::BreadthFirst => self.queue_to_vecdeque(), @@ -324,7 +321,7 @@ mod init { /// Hide the given `tips`, along with all commits reachable by them so that they will not be returned /// by the traversal. - pub fn hide(mut self, tips: impl IntoIterator) -> Result { + pub fn hide(mut self, tips: impl IntoIterator) -> Result { self.state.hidden_tips = tips.into_iter().collect(); Ok(self) } @@ -360,7 +357,7 @@ mod init { out } - fn compute_hidden_frontier(&mut self, hidden_tips: Vec) -> Result<(), Error> { + fn compute_hidden_frontier(&mut self, hidden_tips: Vec) -> Result<(), gix_error::Exn> { self.state.hidden.clear(); if hidden_tips.is_empty() { return Ok(()); @@ -387,7 +384,7 @@ mod init { queue: &mut CommitDateQueue, objects: &impl gix_object::Find, buf: &mut Vec, - ) -> Result<(), Error> { + ) -> Result<(), gix_error::Exn> { let commit_iter = objects.find_commit_iter(&commit_id, buf)?; let time = commit_iter .committer() @@ -480,7 +477,7 @@ mod init { Find: gix_object::Find, Predicate: FnMut(&oid) -> bool, { - type Item = Result; + type Item = Result; fn next(&mut self) -> Option { if !self.state.hidden_tips.is_empty() { @@ -513,7 +510,7 @@ mod init { &mut self, order: CommitTimeOrder, cutoff: Option, - ) -> Option> { + ) -> Option> { let state = &mut self.state; let next = &mut state.queue; @@ -599,7 +596,7 @@ mod init { } } - fn next_by_topology(&mut self) -> Option> { + fn next_by_topology(&mut self) -> Option> { let state = &mut self.state; let next = &mut state.next; diff --git a/gix-traverse/src/commit/topo/init.rs b/gix-traverse/src/commit/topo/init.rs index b98331aa24d..3816d804939 100644 --- a/gix-traverse/src/commit/topo/init.rs +++ b/gix-traverse/src/commit/topo/init.rs @@ -6,7 +6,7 @@ use gix_revwalk::{PriorityQueue, graph::IdMap}; use crate::commit::{ Info, Parents, Topo, find, - topo::{Error, Sorting, WalkFlags, iter::gen_and_commit_time}, + topo::{Sorting, WalkFlags, iter::gen_and_commit_time}, }; /// Builder for [`Topo`]. @@ -119,7 +119,7 @@ where /// Build a new [`Topo`] instance. /// /// Note that merely building an instance is currently expensive. - pub fn build(self) -> Result, Error> { + pub fn build(self) -> Result, gix_error::Exn> { let mut w = Topo { commit_graph: self.commit_graph, find: self.find, diff --git a/gix-traverse/src/commit/topo/iter.rs b/gix-traverse/src/commit/topo/iter.rs index 951e8c1628d..ee8c590143b 100644 --- a/gix-traverse/src/commit/topo/iter.rs +++ b/gix-traverse/src/commit/topo/iter.rs @@ -5,16 +5,16 @@ use smallvec::SmallVec; use crate::commit::{ Either, Info, Parents, Topo, find, - topo::{Error, Sorting, WalkFlags}, + topo::{Sorting, WalkFlags}, }; pub(in crate::commit) type GenAndCommitTime = (u32, i64); -fn missing_indegree() -> Error { +fn missing_indegree() -> gix_error::Exn { CorruptionError::new("Indegree information is missing").raise_erased() } -fn missing_state() -> Error { +fn missing_state() -> gix_error::Exn { CorruptionError::new("Internal state (bitflags) not found").raise_erased() } @@ -61,7 +61,7 @@ impl Topo where Find: gix_object::Find, { - pub(super) fn compute_indegrees_to_depth(&mut self, gen_cutoff: u32) -> Result<(), Error> { + pub(super) fn compute_indegrees_to_depth(&mut self, gen_cutoff: u32) -> Result<(), gix_error::Exn> { while let Some(((generation, _), _)) = self.indegree_queue.peek() { if *generation >= gen_cutoff { self.indegree_walk_step()?; @@ -73,7 +73,7 @@ where Ok(()) } - fn indegree_walk_step(&mut self) -> Result<(), Error> { + fn indegree_walk_step(&mut self) -> Result<(), gix_error::Exn> { if let Some(((generation, _), id)) = self.indegree_queue.pop() { self.explore_to_depth(generation)?; @@ -91,7 +91,7 @@ where Ok(()) } - fn explore_to_depth(&mut self, gen_cutoff: u32) -> Result<(), Error> { + fn explore_to_depth(&mut self, gen_cutoff: u32) -> Result<(), gix_error::Exn> { while let Some(((generation, _), _)) = self.explore_queue.peek() { if *generation >= gen_cutoff { self.explore_walk_step()?; @@ -102,7 +102,7 @@ where Ok(()) } - fn explore_walk_step(&mut self) -> Result<(), Error> { + fn explore_walk_step(&mut self) -> Result<(), gix_error::Exn> { if let Some((_, id)) = self.explore_queue.pop() { let parents = self.collect_parents(&id)?; self.process_parents(&id, &parents)?; @@ -119,7 +119,7 @@ where Ok(()) } - fn expand_topo_walk(&mut self, id: &oid) -> Result<(), Error> { + fn expand_topo_walk(&mut self, id: &oid) -> Result<(), gix_error::Exn> { let parents = self.collect_parents(id)?; self.process_parents(id, &parents)?; @@ -155,7 +155,7 @@ where Ok(()) } - fn process_parents(&mut self, id: &oid, parents: &[(ObjectId, GenAndCommitTime)]) -> Result<(), Error> { + fn process_parents(&mut self, id: &oid, parents: &[(ObjectId, GenAndCommitTime)]) -> Result<(), gix_error::Exn> { let state = self.states.get_mut(id).ok_or_else(missing_state)?; if state.contains(WalkFlags::Added) { return Ok(()); @@ -191,7 +191,7 @@ where Ok(()) } - fn collect_parents(&mut self, id: &oid) -> Result, Error> { + fn collect_parents(&mut self, id: &oid) -> Result, gix_error::Exn> { collect_parents( &mut self.commit_graph, &self.find, @@ -205,11 +205,11 @@ where pub(super) fn collect_all_parents( &mut self, id: &oid, - ) -> Result, Error> { + ) -> Result, gix_error::Exn> { collect_parents(&mut self.commit_graph, &self.find, id, false, &mut self.buf) } - fn pop_commit(&mut self) -> Option> { + fn pop_commit(&mut self) -> Option> { let commit = self.topo_queue.pop()?; let i = match self.indegrees.get_mut(&commit.id) { Some(i) => i, @@ -232,7 +232,7 @@ where Find: gix_object::Find, Predicate: FnMut(&oid) -> bool, { - type Item = Result; + type Item = Result; fn next(&mut self) -> Option { loop { @@ -254,7 +254,7 @@ fn collect_parents( id: &oid, first_only: bool, buf: &mut Vec, -) -> Result, Error> +) -> Result, gix_error::Exn> where Find: gix_object::Find, { @@ -310,7 +310,7 @@ where Ok(parents) } -pub(super) fn gen_and_commit_time(c: Either<'_, '_>) -> Result { +pub(super) fn gen_and_commit_time(c: Either<'_, '_>) -> Result { match c { Either::CommitRefIter(c) => { let mut commit_time = 0; diff --git a/gix-traverse/src/commit/topo/mod.rs b/gix-traverse/src/commit/topo/mod.rs index 7b8ec8adb10..6e05ba52637 100644 --- a/gix-traverse/src/commit/topo/mod.rs +++ b/gix-traverse/src/commit/topo/mod.rs @@ -2,9 +2,6 @@ use bitflags::bitflags; -/// The errors that can occur during creation and iteration. -pub type Error = gix_error::Exn; - bitflags! { /// Set of flags to describe the state of a particular commit while iterating. // NOTE: The names correspond to the names of the flags in revision.h diff --git a/gix-traverse/src/tree/breadthfirst.rs b/gix-traverse/src/tree/breadthfirst.rs index cb7f30c65b8..d2ed273e924 100644 --- a/gix-traverse/src/tree/breadthfirst.rs +++ b/gix-traverse/src/tree/breadthfirst.rs @@ -2,10 +2,6 @@ use std::collections::VecDeque; use gix_hash::ObjectId; -/// The error is part of the item returned by the [`breadthfirst()`](crate::tree::breadthfirst()) and -///[`depthfirst()`](crate::tree::depthfirst()) functions. -pub type Error = gix_error::Exn; - /// The state used and potentially shared by multiple tree traversals. #[derive(Default, Clone)] pub struct State { @@ -26,7 +22,7 @@ pub(super) mod function { use gix_error::{CorruptionError, ErrorExt, ResultExt, message}; use gix_object::{FindExt, TreeRefIter}; - use super::{Error, State}; + use super::State; use crate::tree::Visit; /// Start a breadth-first iteration over the `root` trees entries. @@ -48,7 +44,7 @@ pub(super) mod function { mut state: StateMut, objects: Find, delegate: &mut V, - ) -> Result<(), Error> + ) -> Result<(), gix_error::Exn> where Find: gix_object::Find, StateMut: BorrowMut, diff --git a/gix-traverse/src/tree/depthfirst.rs b/gix-traverse/src/tree/depthfirst.rs index d111e1a5044..e059e1b20d4 100644 --- a/gix-traverse/src/tree/depthfirst.rs +++ b/gix-traverse/src/tree/depthfirst.rs @@ -1,5 +1,3 @@ -pub use super::breadthfirst::Error; - /// The state used and potentially shared by multiple tree traversals, reusing memory. #[derive(Default, Clone)] pub struct State { @@ -31,7 +29,7 @@ pub(super) mod function { use gix_hash::ObjectId; use gix_object::{FindExt, TreeRefIter}; - use super::{Error, State}; + use super::State; use crate::tree::Visit; /// A depth-first traversal of the `root` tree, that preserves the natural order of a tree while immediately descending @@ -43,7 +41,7 @@ pub(super) mod function { mut state: StateMut, objects: Find, delegate: &mut V, - ) -> Result<(), Error> + ) -> Result<(), gix_error::Exn> where Find: gix_object::Find, StateMut: BorrowMut, diff --git a/gix-traverse/tests/traverse/commit/simple/hide.rs b/gix-traverse/tests/traverse/commit/simple/hide.rs index 1e426ab8709..84d6f5c6050 100644 --- a/gix-traverse/tests/traverse/commit/simple/hide.rs +++ b/gix-traverse/tests/traverse/commit/simple/hide.rs @@ -321,7 +321,7 @@ impl gix_object::Find for CountingFind<'_> { &self, id: &gix_hash::oid, buffer: &'a mut Vec, - ) -> Result>, gix_object::find::Error> { + ) -> Result>, gix_error::Exn> { self.lookups.set(self.lookups.get() + 1); gix_object::Find::try_find(self.inner, id, buffer) } diff --git a/gix-url/fuzz/Cargo.toml b/gix-url/fuzz/Cargo.toml index c43facd03af..27287213c8d 100644 --- a/gix-url/fuzz/Cargo.toml +++ b/gix-url/fuzz/Cargo.toml @@ -9,6 +9,7 @@ edition = "2024" cargo-fuzz = true [dependencies] +gix-error = { path = "../../gix-error" } libfuzzer-sys = "0.4" [dependencies.gix-url] diff --git a/gix-url/fuzz/fuzz_targets/parse.rs b/gix-url/fuzz/fuzz_targets/parse.rs index a65bc5f523c..2de3bab2bfa 100644 --- a/gix-url/fuzz/fuzz_targets/parse.rs +++ b/gix-url/fuzz/fuzz_targets/parse.rs @@ -3,7 +3,7 @@ use libfuzzer_sys::fuzz_target; use std::hint::black_box; use std::path::Path; -fn fuzz(data: &[u8]) -> Result<(), gix_url::parse::Error> { +fn fuzz(data: &[u8]) -> Result<(), gix_error::Exn> { let url = gix_url::parse(data)?; _ = black_box(url.user()); _ = black_box(url.password()); diff --git a/gix-url/src/expand_path.rs b/gix-url/src/expand_path.rs index eac2549e8a2..902e4427495 100644 --- a/gix-url/src/expand_path.rs +++ b/gix-url/src/expand_path.rs @@ -23,9 +23,6 @@ impl From for Option { } } -/// The error used by [`parse()`], [`with()`] and [`expand_path()`](crate::expand_path()). -pub type Error = gix_error::Exn; - fn path_segments(path: &BStr) -> Option> { if path.starts_with(b"/") { Some(path[1..].split(|c| *c == b'/')) @@ -41,7 +38,7 @@ fn path_segments(path: &BStr) -> Option> { /// * `/~user/repopath` - the named user's home, returning `/repopath`. /// /// Paths without a leading slash or home marker are returned unchanged without user information. -pub fn parse(path: &BStr) -> Result<(Option, BString), Error> { +pub fn parse(path: &BStr) -> Result<(Option, BString), gix_error::Exn> { Ok(path_segments(path) .and_then(|mut iter| { iter.next().map(|segment| { @@ -100,7 +97,7 @@ pub fn with( user: Option<&ForUser>, path: &BStr, home_for_user: impl FnOnce(&ForUser) -> Option, -) -> Result { +) -> Result { fn make_relative(path: &Path) -> PathBuf { path.components().skip(1).collect() } diff --git a/gix-url/src/lib.rs b/gix-url/src/lib.rs index 6e6f81aa1a6..fdb80c3a1a6 100644 --- a/gix-url/src/lib.rs +++ b/gix-url/src/lib.rs @@ -73,7 +73,7 @@ mod simple_url; /// /// Also unlike Git, an empty remote-helper name as in `::address` is not accepted, as the `git-remote-` program it /// would name cannot meaningfully exist. -pub fn parse(input: impl AsBStr) -> Result { +pub fn parse(input: impl AsBStr) -> Result> { use parse::InputScheme; let input = input.as_bstr(); match parse::find_scheme(input) { @@ -89,7 +89,7 @@ pub fn parse(input: impl AsBStr) -> Result { /// directory automatically. /// /// If more precise control of the resolution mechanism is needed, then use the [expand_path::with()] function. -pub fn expand_path(user: Option<&expand_path::ForUser>, path: &BStr) -> Result { +pub fn expand_path(user: Option<&expand_path::ForUser>, path: &BStr) -> Result { expand_path::with(user, path, |user| match user { expand_path::ForUser::Current => gix_path::env::home_dir(), expand_path::ForUser::Name(user) => { @@ -162,7 +162,7 @@ pub struct PathComponents<'a> { /// whereas [`Url::to_bstring()`] includes all URL parts. /// **Beware that some URLs still print secrets if they use them outside of the designated password fields.** /// -/// Also note that URLs that fail to parse are typically stored in [the resulting error](parse::Error) type +/// Also note that URLs that fail to parse are typically stored in [the resulting error](gix_error::Exn) type /// and printed in full using its display implementation. #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] #[cfg_attr(feature = "serde", derive(serde::Serialize))] @@ -324,7 +324,7 @@ impl Url { port: Option, path: BString, serialize_alternative_form: bool, - ) -> Result { + ) -> Result> { if let Scheme::Helper(name) = &scheme && !parse::is_valid_remote_helper_name(name.as_bytes()) { @@ -393,7 +393,7 @@ impl Url { /// Resolve the path of a file location against `current_dir` and normalize it in place. /// /// Other schemes are unchanged. - pub fn canonicalize(&mut self, current_dir: &std::path::Path) -> Result<(), gix_path::realpath::Error> { + pub fn canonicalize(&mut self, current_dir: &std::path::Path) -> Result<(), gix_error::Exn> { if self.scheme == Scheme::File { let path = gix_path::from_bstr(Cow::Borrowed(self.path.as_ref())); let abs_path = gix_path::realpath_opts(path.as_ref(), current_dir, gix_path::realpath::MAX_SYMLINKS)?; @@ -512,7 +512,7 @@ impl Url { /// /// url.path = "/my repo".into(); /// assert_eq!(url.original_path(), "/my%20repo", "restoring the path reuses the original spelling"); - /// # Ok::<(), gix_url::parse::Error>(()) + /// # Ok::<(), gix_error::Exn>(()) /// ``` pub fn original_path(&self) -> &BStr { self.path_with_percent_escapes().unwrap_or(self.path.as_ref()) @@ -545,7 +545,7 @@ impl Url { /// assert_eq!(query[0].1.as_ref(), "a&b"); /// assert_eq!(parts.fragment, Some("fragment#one".into())); /// assert_eq!(url.path, "/repo#one?x=a&b#fragment#one"); - /// # Ok::<(), gix_url::parse::Error>(()) + /// # Ok::<(), gix_error::Exn>(()) /// ``` pub fn path_query_fragment(&self) -> PathComponents<'_> { fn decoded_len(original: &[u8], is_encoded: bool) -> usize { @@ -668,7 +668,7 @@ impl Url { /// Return a clone whose file path is resolved against `current_dir` and normalized. /// /// Other schemes are returned unchanged. - pub fn canonicalized(&self, current_dir: &std::path::Path) -> Result { + pub fn canonicalized(&self, current_dir: &std::path::Path) -> Result { let mut res = self.clone(); res.canonicalize(current_dir)?; Ok(res) @@ -899,7 +899,7 @@ impl Url { /// Deserialization impl Url { /// Parse a URL from `bytes`. - pub fn from_bytes(bytes: &BStr) -> Result { + pub fn from_bytes(bytes: &BStr) -> Result> { parse(bytes) } } diff --git a/gix-url/src/parse.rs b/gix-url/src/parse.rs index 9c379298b1a..506c0626008 100644 --- a/gix-url/src/parse.rs +++ b/gix-url/src/parse.rs @@ -3,9 +3,6 @@ use gix_error::{ErrorExt, ResultExt, ValidationError}; use crate::Scheme; -/// The error returned by [parse()](crate::parse()). -pub type Error = gix_error::Exn; - /// The syntax used to interpret an input location. #[derive(Debug, Clone, Copy)] pub enum UrlKind { @@ -116,7 +113,7 @@ pub(crate) fn remote_helper(input: &BStr, helper_end: usize) -> crate::Url { } } -pub(crate) fn url(input: &BStr, protocol_end: usize) -> Result { +pub(crate) fn url(input: &BStr, protocol_end: usize) -> Result> { const MAX_LEN: usize = 1024; let input_after_protocol = &input[protocol_end + "://".len()..]; let scheme = &input[..protocol_end]; @@ -222,7 +219,7 @@ pub(crate) fn url(input: &BStr, protocol_end: usize) -> Result Result { +pub(crate) fn scp(input: &BStr, colon: usize) -> Result> { let input = input_to_utf8(input, UrlKind::Scp)?; // TODO: this incorrectly splits at IPv6 addresses, check for `[]` before splitting @@ -283,7 +280,10 @@ pub(crate) fn scp(input: &BStr, colon: usize) -> Result { }) } -pub(crate) fn file_url(input: &BStr, protocol_colon: usize) -> Result { +pub(crate) fn file_url( + input: &BStr, + protocol_colon: usize, +) -> Result> { let input = input_to_utf8(input, UrlKind::Url)?; let input_after_protocol = &input[protocol_colon + "://".len()..]; @@ -342,7 +342,7 @@ pub(crate) fn file_url(input: &BStr, protocol_colon: usize) -> Result Result { +pub(crate) fn local(input: &BStr) -> Result> { if input.is_empty() { return Err(ValidationError::new_with_input( format!( @@ -366,12 +366,15 @@ pub(crate) fn local(input: &BStr) -> Result { }) } -fn input_to_utf8(input: &BStr, kind: UrlKind) -> Result<&str, Error> { +fn input_to_utf8(input: &BStr, kind: UrlKind) -> Result<&str, gix_error::Exn> { let kind = kind.as_str(); std::str::from_utf8(input).or_raise(|| ValidationError::new_with_input(format!("{kind} is not valid UTF-8"), input)) } -fn input_to_utf8_and_url(input: &BStr, kind: UrlKind) -> Result<(&str, crate::simple_url::ParsedUrl), Error> { +fn input_to_utf8_and_url( + input: &BStr, + kind: UrlKind, +) -> Result<(&str, crate::simple_url::ParsedUrl), gix_error::Exn> { let input = input_to_utf8(input, kind)?; crate::simple_url::ParsedUrl::parse(input) .map(|url| (input, url)) diff --git a/gix-url/tests/url/access.rs b/gix-url/tests/url/access.rs index c330dd0e9e9..2939ab4b3c3 100644 --- a/gix-url/tests/url/access.rs +++ b/gix-url/tests/url/access.rs @@ -55,7 +55,7 @@ mod path_query_fragment { } #[test] - fn http_delimiters_are_recognized_before_decoding() -> crate::Result { + fn http_delimiters_are_recognized_before_decoding() -> gix_error::TestResult { for scheme in ["http", "https"] { for (suffix, expected) in [ ("/repo%23one?query=value#fragment", "/repo#one"), @@ -102,7 +102,7 @@ mod path_query_fragment { } #[test] - fn other_schemes_keep_their_stored_paths() -> crate::Result { + fn other_schemes_keep_their_stored_paths() -> gix_error::TestResult { for (input, expected) in [ ("git@host:repo#one?two%23three", "repo#one?two%23three"), ("ssh://git@host/repo%23one?two", "/repo#one?two"), @@ -127,7 +127,7 @@ mod path_query_fragment { } #[test] - fn constructed_and_mutated_http_paths_are_already_decoded() -> crate::Result { + fn constructed_and_mutated_http_paths_are_already_decoded() -> gix_error::TestResult { for scheme in [Scheme::Http, Scheme::Https] { for (path, expected) in [ ("/repo%23one?query=value#fragment", "/repo%23one"), @@ -168,7 +168,7 @@ mod path_query_fragment { } #[test] - fn mutation_keeps_paths_byte_oriented_and_uses_the_current_scheme() -> crate::Result { + fn mutation_keeps_paths_byte_oriented_and_uses_the_current_scheme() -> gix_error::TestResult { let mut url = gix_url::parse("https://host/repo%23one?query=value#fragment")?; url.scheme = Scheme::Ssh; assert_eq!( @@ -210,7 +210,7 @@ mod path_query_fragment { } #[test] - fn query_pairs_and_fragments_are_split_before_decoding() -> crate::Result { + fn query_pairs_and_fragments_are_split_before_decoding() -> gix_error::TestResult { for scheme in ["http", "https"] { for (suffix, path, query, fragment) in [ ( @@ -296,7 +296,7 @@ mod path_query_fragment { } #[test] - fn empty_and_absent_components_remain_distinct() -> crate::Result { + fn empty_and_absent_components_remain_distinct() -> gix_error::TestResult { for (suffix, has_query, fragment) in [ ("", false, None), ("/repo", false, None), @@ -323,7 +323,7 @@ mod path_query_fragment { } #[test] - fn constructed_and_mutated_components_keep_literal_percent_escapes() -> crate::Result { + fn constructed_and_mutated_components_keep_literal_percent_escapes() -> gix_error::TestResult { let path = "/repo%23one?x=a%26b&space=+%2B#frag%23+"; let constructed = Url::from_parts(Scheme::Https, None, None, Some("host".into()), None, path.into(), false)?; let mut mutated = gix_url::parse("https://host/old%23repo?x=%26#old%23fragment")?; @@ -354,7 +354,7 @@ mod path_query_fragment { #[cfg(feature = "serde")] #[test] - fn serde_preserves_component_boundaries() -> crate::Result { + fn serde_preserves_component_boundaries() -> gix_error::TestResult { let mut mutated = gix_url::parse("https://host/old%23repo?x=%26#old%23fragment")?; mutated.path = "/repo%23one?x=a%26b&space=+%2B#frag%23+".into(); for url in [ @@ -373,7 +373,7 @@ mod path_query_fragment { } #[test] -fn user() -> crate::Result { +fn user() -> gix_error::TestResult { let mut url = gix_url::parse("https://user:password@host/path")?; assert_eq!(url.user(), Some("user")); @@ -384,7 +384,7 @@ fn user() -> crate::Result { } #[test] -fn password() -> crate::Result { +fn password() -> gix_error::TestResult { let mut url = gix_url::parse("https://user:password@host/path")?; assert_eq!(url.password(), Some("password")); @@ -395,7 +395,7 @@ fn password() -> crate::Result { } #[test] -fn mutation_roundtrip() -> crate::Result { +fn mutation_roundtrip() -> gix_error::TestResult { let mut url = gix_url::parse("https://user@host/path")?; url.set_user(Some("newuser".into())); url.set_password(Some("secret".into())); @@ -411,7 +411,7 @@ fn mutation_roundtrip() -> crate::Result { } #[test] -fn from_bytes_roundtrip() -> crate::Result { +fn from_bytes_roundtrip() -> gix_error::TestResult { let original = "https://user:password@example.com:8080/path/to/repo"; let url = gix_url::parse(original)?; @@ -425,7 +425,7 @@ fn from_bytes_roundtrip() -> crate::Result { } #[test] -fn from_bytes_with_non_utf8_path() -> crate::Result { +fn from_bytes_with_non_utf8_path() -> gix_error::TestResult { let url = gix_url::parse(b"/path/to\xff/repo".as_slice())?; let bytes = url.to_bstring(); let from_bytes = gix_url::Url::from_bytes(bytes.as_ref())?; @@ -437,7 +437,7 @@ fn from_bytes_with_non_utf8_path() -> crate::Result { } #[test] -fn user_argument_safety() -> crate::Result { +fn user_argument_safety() -> gix_error::TestResult { let url = gix_url::parse("ssh://-Fconfigfile@foo/bar")?; assert_eq!(url.user(), Some("-Fconfigfile")); @@ -455,7 +455,7 @@ fn user_argument_safety() -> crate::Result { } #[test] -fn host_argument_safety() -> crate::Result { +fn host_argument_safety() -> gix_error::TestResult { let url = gix_url::parse("ssh://-oProxyCommand=open$IFS-aCalculator/foo")?; assert_eq!(url.user(), None); @@ -480,7 +480,7 @@ fn host_argument_safety() -> crate::Result { } #[test] -fn path_argument_safety() -> crate::Result { +fn path_argument_safety() -> gix_error::TestResult { let url = gix_url::parse("ssh://foo/-oProxyCommand=open$IFS-aCalculator")?; assert_eq!(url.user(), None); @@ -511,7 +511,7 @@ fn path_argument_safety() -> crate::Result { } #[test] -fn all_argument_safety_safe() -> crate::Result { +fn all_argument_safety_safe() -> gix_error::TestResult { let url = gix_url::parse("ssh://user.name@example.com/path/to/file")?; assert_eq!(url.user(), Some("user.name")); @@ -529,7 +529,7 @@ fn all_argument_safety_safe() -> crate::Result { } #[test] -fn all_argument_safety_not_safe() -> crate::Result { +fn all_argument_safety_not_safe() -> gix_error::TestResult { let all_bad = "ssh://-Fconfigfile@-oProxyCommand=open$IFS-aCalculator/-oProxyCommand=open$IFS-aCalculator"; let url = gix_url::parse(all_bad)?; diff --git a/gix-url/tests/url/expand_path.rs b/gix-url/tests/url/expand_path.rs index 3611f526490..11a409fc32d 100644 --- a/gix-url/tests/url/expand_path.rs +++ b/gix-url/tests/url/expand_path.rs @@ -25,7 +25,7 @@ fn user_home(name: &str) -> std::path::PathBuf { } #[test] -fn without_username() -> crate::Result { +fn without_username() -> gix_error::TestResult { let (user, resolved_path) = expand_path::parse(b"/~/hello/git".as_bstr())?; let resolved_path = expand_path::with(user.as_ref(), resolved_path.as_ref(), |user: &ForUser| match user { ForUser::Current => Some(user_home("byron")), @@ -36,7 +36,7 @@ fn without_username() -> crate::Result { } #[test] -fn with_username() -> crate::Result { +fn with_username() -> gix_error::TestResult { let (user, resolved_path) = expand_path::parse(b"/~byron/hello/git".as_bstr())?; let resolved_path = expand_path::with(user.as_ref(), resolved_path.as_ref(), |user: &ForUser| match user { ForUser::Current => unreachable!("we have a name"), diff --git a/gix-url/tests/url/main.rs b/gix-url/tests/url/main.rs index d883999915a..41aa3a821f1 100644 --- a/gix-url/tests/url/main.rs +++ b/gix-url/tests/url/main.rs @@ -1,5 +1,3 @@ -pub type Result = gix_error::TestResult; - mod access; mod baseline; mod expand_path; diff --git a/gix-url/tests/url/parse/file.rs b/gix-url/tests/url/parse/file.rs index 62d7e70523d..dc2036106fd 100644 --- a/gix-url/tests/url/parse/file.rs +++ b/gix-url/tests/url/parse/file.rs @@ -4,7 +4,7 @@ use gix_url::Scheme; use crate::parse::{assert_url_roundtrip, url, url_alternate}; #[test] -fn file_path_with_protocol() -> crate::Result { +fn file_path_with_protocol() -> gix_error::TestResult { Ok(assert_url_roundtrip( "file:///path/to/git", url(Scheme::File, None, None, None, b"/path/to/git"), @@ -12,7 +12,7 @@ fn file_path_with_protocol() -> crate::Result { } #[test] -fn file_to_root() -> crate::Result { +fn file_to_root() -> gix_error::TestResult { Ok(assert_url_roundtrip( "file:///", url(Scheme::File, None, None, None, b"/"), @@ -20,7 +20,7 @@ fn file_to_root() -> crate::Result { } #[test] -fn file_path_without_protocol() -> crate::Result { +fn file_path_without_protocol() -> gix_error::TestResult { Ok(assert_url_roundtrip( "/path/to/git", url_alternate(Scheme::File, None, None, None, b"/path/to/git"), @@ -28,7 +28,7 @@ fn file_path_without_protocol() -> crate::Result { } #[test] -fn file_path_with_whitespace() -> crate::Result { +fn file_path_with_whitespace() -> gix_error::TestResult { Ok(assert_url_roundtrip( "/path/to/git with spaces ", url_alternate(Scheme::File, None, None, None, b"/path/to/git with spaces "), @@ -36,7 +36,7 @@ fn file_path_with_whitespace() -> crate::Result { } #[test] -fn no_username_expansion_for_file_paths_without_protocol() -> crate::Result { +fn no_username_expansion_for_file_paths_without_protocol() -> gix_error::TestResult { Ok(assert_url_roundtrip( "~/path/to/git", url_alternate(Scheme::File, None, None, None, b"~/path/to/git"), @@ -44,7 +44,7 @@ fn no_username_expansion_for_file_paths_without_protocol() -> crate::Result { } #[test] -fn no_username_expansion_for_file_paths_with_protocol() -> crate::Result { +fn no_username_expansion_for_file_paths_with_protocol() -> gix_error::TestResult { assert_url_roundtrip( "file:///~username/path/to/git", url(Scheme::File, None, None, None, b"/~username/path/to/git"), @@ -56,7 +56,7 @@ fn no_username_expansion_for_file_paths_with_protocol() -> crate::Result { } #[test] -fn non_utf8_file_path_without_protocol() -> crate::Result { +fn non_utf8_file_path_without_protocol() -> gix_error::TestResult { let url = gix_url::parse(b"/path/to\xff/git".as_bstr())?; assert_eq!(url, url_alternate(Scheme::File, None, None, None, b"/path/to\xff/git")); let url_lossless = url.to_bstring(); @@ -70,7 +70,7 @@ fn non_utf8_file_path_without_protocol() -> crate::Result { } #[test] -fn relative_file_path_without_protocol() -> crate::Result { +fn relative_file_path_without_protocol() -> gix_error::TestResult { assert_url_roundtrip( "../../path/to/git", url_alternate(Scheme::File, None, None, None, b"../../path/to/git"), @@ -82,7 +82,7 @@ fn relative_file_path_without_protocol() -> crate::Result { } #[test] -fn shortest_possible_absolute_path() -> crate::Result { +fn shortest_possible_absolute_path() -> gix_error::TestResult { assert_url_roundtrip("/", url_alternate(Scheme::File, None, None, None, b"/"))?; Ok(assert_url_roundtrip( "file:///", @@ -91,7 +91,7 @@ fn shortest_possible_absolute_path() -> crate::Result { } #[test] -fn shortest_possible_relative_path() -> crate::Result { +fn shortest_possible_relative_path() -> gix_error::TestResult { assert_url_roundtrip("a", url_alternate(Scheme::File, None, None, None, b"a"))?; assert_url_roundtrip("../", url_alternate(Scheme::File, None, None, None, b"../"))?; assert_url_roundtrip(r"..\", url_alternate(Scheme::File, None, None, None, br"..\"))?; @@ -102,7 +102,7 @@ fn shortest_possible_relative_path() -> crate::Result { } #[test] -fn no_relative_paths_if_protocol() -> crate::Result { +fn no_relative_paths_if_protocol() -> gix_error::TestResult { assert_url_roundtrip("file://../", url(Scheme::File, None, "..", None, b"/"))?; assert_url_roundtrip("file://./", url(Scheme::File, None, ".", None, b"/"))?; assert_url_roundtrip("file://a/", url(Scheme::File, None, "a", None, b"/"))?; @@ -125,7 +125,7 @@ fn no_relative_paths_if_protocol() -> crate::Result { } #[test] -fn interior_relative_file_path_without_protocol() -> crate::Result { +fn interior_relative_file_path_without_protocol() -> gix_error::TestResult { Ok(assert_url_roundtrip( "/abs/path/../../path/to/git", url_alternate(Scheme::File, None, None, None, b"/abs/path/../../path/to/git"), @@ -133,7 +133,7 @@ fn interior_relative_file_path_without_protocol() -> crate::Result { } #[test] -fn url_from_relative_path_with_colon_in_name() -> crate::Result { +fn url_from_relative_path_with_colon_in_name() -> gix_error::TestResult { Ok(assert_url_roundtrip( "./weird/directory/na:me", url_alternate(Scheme::File, None, None, None, b"./weird/directory/na:me"), @@ -147,7 +147,7 @@ mod windows { use crate::parse::{assert_url, assert_url_roundtrip, url, url_alternate}; #[test] - fn reproduce_1063() -> crate::Result { + fn reproduce_1063() -> gix_error::TestResult { let input = r"C:\Users\RUNNER~1\AppData\Local\Temp\tmp.vIa4tyjv17"; let url_input = r"file://C:\Users\RUNNER~1\AppData\Local\Temp\tmp.vIa4tyjv17"; assert_url(url_input, url(Scheme::File, None, None, None, input.as_bytes()))?; @@ -156,7 +156,7 @@ mod windows { } #[test] - fn url_from_absolute_path() -> crate::Result { + fn url_from_absolute_path() -> gix_error::TestResult { // Test with a Windows path directly instead of using url::Url::from_directory_path assert_url( r"C:\users\1\", @@ -169,7 +169,7 @@ mod windows { } #[test] - fn file_path_without_protocol() -> crate::Result { + fn file_path_without_protocol() -> gix_error::TestResult { Ok(assert_url_roundtrip( "x:/path/to/git", url_alternate(Scheme::File, None, None, None, b"x:/path/to/git"), @@ -177,7 +177,7 @@ mod windows { } #[test] - fn file_path_with_backslashes_without_protocol() -> crate::Result { + fn file_path_with_backslashes_without_protocol() -> gix_error::TestResult { Ok(assert_url_roundtrip( r"x:\path\to\git", url_alternate(Scheme::File, None, None, None, br"x:\path\to\git"), @@ -185,7 +185,7 @@ mod windows { } #[test] - fn file_path_with_protocol() -> crate::Result { + fn file_path_with_protocol() -> gix_error::TestResult { Ok(assert_url_roundtrip( "file://x:/path/to/git", url(Scheme::File, None, None, None, b"x:/path/to/git"), @@ -200,7 +200,7 @@ mod unix { use crate::parse::{assert_url_roundtrip, url, url_alternate}; #[test] - fn url_from_absolute_path() -> crate::Result { + fn url_from_absolute_path() -> gix_error::TestResult { // Test with a simple file path instead of using url::Url::from_directory_path Ok(assert_url_roundtrip( "/users/foo/", @@ -209,7 +209,7 @@ mod unix { } #[test] - fn file_path_without_protocol() -> crate::Result { + fn file_path_without_protocol() -> gix_error::TestResult { Ok(assert_url_roundtrip( "x:/path/to/git", url_alternate(Scheme::Ssh, None, "x", None, b"/path/to/git"), @@ -217,7 +217,7 @@ mod unix { } #[test] - fn file_path_with_backslashes_without_protocol() -> crate::Result { + fn file_path_with_backslashes_without_protocol() -> gix_error::TestResult { Ok(assert_url_roundtrip( r"x:\path\to\git", url_alternate(Scheme::Ssh, None, "x", None, br"\path\to\git"), @@ -225,7 +225,7 @@ mod unix { } #[test] - fn file_path_with_protocol() -> crate::Result { + fn file_path_with_protocol() -> gix_error::TestResult { Ok(assert_url_roundtrip( "file://x:/path/to/git", url(Scheme::File, None, "x:", None, b"/path/to/git"), @@ -233,7 +233,7 @@ mod unix { } #[test] - fn file_url_with_ipv6_and_user() -> crate::Result { + fn file_url_with_ipv6_and_user() -> gix_error::TestResult { Ok(assert_url_roundtrip( "file://User@[::1]/repo", gix_url::Url::from_parts( @@ -249,7 +249,7 @@ mod unix { } #[test] - fn file_url_with_ipv6() -> crate::Result { + fn file_url_with_ipv6() -> gix_error::TestResult { Ok(assert_url_roundtrip( "file://[::1]/repo", url(Scheme::File, None, "[::1]", None, b"/repo"), diff --git a/gix-url/tests/url/parse/http.rs b/gix-url/tests/url/parse/http.rs index 80b30310a14..94e1e1cdcbc 100644 --- a/gix-url/tests/url/parse/http.rs +++ b/gix-url/tests/url/parse/http.rs @@ -12,7 +12,7 @@ fn test_password(with_dot: bool) -> String { } #[test] -fn username_expansion_is_unsupported() -> crate::Result { +fn username_expansion_is_unsupported() -> gix_error::TestResult { Ok(assert_url_roundtrip( "http://example.com/~byron/hello", url(Scheme::Http, None, "example.com", None, b"/~byron/hello"), @@ -20,7 +20,7 @@ fn username_expansion_is_unsupported() -> crate::Result { } #[test] -fn empty_user_cannot_roundtrip() -> crate::Result { +fn empty_user_cannot_roundtrip() -> gix_error::TestResult { let actual = gix_url::parse("http://@example.com/~byron/hello")?; let expected = url(Scheme::Http, None, "example.com", None, b"/~byron/hello"); assert_eq!(actual, expected); @@ -33,7 +33,7 @@ fn empty_user_cannot_roundtrip() -> crate::Result { } #[test] -fn username_and_password() -> crate::Result { +fn username_and_password() -> gix_error::TestResult { let password = test_password(false); Ok(assert_url_roundtrip( &format!("http://user:{password}@example.com/~byron/hello"), @@ -42,7 +42,7 @@ fn username_and_password() -> crate::Result { } #[test] -fn colon_in_username_roundtrips() -> crate::Result { +fn colon_in_username_roundtrips() -> gix_error::TestResult { Ok(assert_url_roundtrip( "http://a%3Ab@example.com/", url(Scheme::Http, "a:b", "example.com", None, b"/"), @@ -50,7 +50,7 @@ fn colon_in_username_roundtrips() -> crate::Result { } #[test] -fn colon_in_password_roundtrips() -> crate::Result { +fn colon_in_password_roundtrips() -> gix_error::TestResult { let password = format!("a:{}", std::process::id()); Ok(assert_url_roundtrip( &format!("http://user:{password}@example.com/"), @@ -59,7 +59,7 @@ fn colon_in_password_roundtrips() -> crate::Result { } #[test] -fn username_and_password_and_port() -> crate::Result { +fn username_and_password_and_port() -> gix_error::TestResult { let password = test_password(false); Ok(assert_url_roundtrip( &format!("http://user:{password}@example.com:8080/~byron/hello"), @@ -68,7 +68,7 @@ fn username_and_password_and_port() -> crate::Result { } #[test] -fn username_and_password_with_spaces_and_port() -> crate::Result { +fn username_and_password_with_spaces_and_port() -> gix_error::TestResult { let expected = gix_url::Url::from_parts( Scheme::Http, Some("user name".into()), @@ -88,7 +88,7 @@ fn username_and_password_with_spaces_and_port() -> crate::Result { } #[test] -fn only_password() -> crate::Result { +fn only_password() -> gix_error::TestResult { let password = test_password(false); Ok(assert_url_roundtrip( &format!("http://:{password}@example.com/~byron/hello"), @@ -97,7 +97,7 @@ fn only_password() -> crate::Result { } #[test] -fn username_and_empty_password() -> crate::Result { +fn username_and_empty_password() -> gix_error::TestResult { let actual = gix_url::parse("http://user:@example.com/~byron/hello")?; let expected = url(Scheme::Http, "user", "example.com", None, b"/~byron/hello"); assert_eq!(actual, expected); @@ -110,7 +110,7 @@ fn username_and_empty_password() -> crate::Result { } #[test] -fn secure() -> crate::Result { +fn secure() -> gix_error::TestResult { Ok(assert_url_roundtrip( "https://github.com/byron/gitoxide", url(Scheme::Https, None, "github.com", None, b"/byron/gitoxide"), @@ -118,14 +118,14 @@ fn secure() -> crate::Result { } #[test] -fn http_missing_path() -> crate::Result { +fn http_missing_path() -> gix_error::TestResult { assert_url_roundtrip("http://host.xz/", url(Scheme::Http, None, "host.xz", None, b"/"))?; assert_url("http://host.xz", url(Scheme::Http, None, "host.xz", None, b"/"))?; Ok(()) } #[test] -fn username_with_dot_is_not_percent_encoded() -> crate::Result { +fn username_with_dot_is_not_percent_encoded() -> gix_error::TestResult { Ok(assert_url_roundtrip( "http://user.name@example.com/repo", url(Scheme::Http, "user.name", "example.com", None, b"/repo"), @@ -133,7 +133,7 @@ fn username_with_dot_is_not_percent_encoded() -> crate::Result { } #[test] -fn password_with_dot_is_not_percent_encoded() -> crate::Result { +fn password_with_dot_is_not_percent_encoded() -> gix_error::TestResult { let password = test_password(true); Ok(assert_url_roundtrip( &format!("http://user:{password}@example.com/repo"), @@ -142,7 +142,7 @@ fn password_with_dot_is_not_percent_encoded() -> crate::Result { } #[test] -fn username_and_password_with_dots_are_not_percent_encoded() -> crate::Result { +fn username_and_password_with_dots_are_not_percent_encoded() -> gix_error::TestResult { let password = test_password(true); Ok(assert_url_roundtrip( &format!("http://user.name:{password}@example.com/repo"), @@ -151,7 +151,7 @@ fn username_and_password_with_dots_are_not_percent_encoded() -> crate::Result { } #[test] -fn http_with_ipv6() -> crate::Result { +fn http_with_ipv6() -> gix_error::TestResult { Ok(assert_url_roundtrip( "http://[::1]/repo", url(Scheme::Http, None, "[::1]", None, b"/repo"), @@ -159,7 +159,7 @@ fn http_with_ipv6() -> crate::Result { } #[test] -fn http_with_ipv6_and_port() -> crate::Result { +fn http_with_ipv6_and_port() -> gix_error::TestResult { Ok(assert_url_roundtrip( "http://[::1]:8080/repo", url(Scheme::Http, None, "[::1]", 8080, b"/repo"), @@ -167,7 +167,7 @@ fn http_with_ipv6_and_port() -> crate::Result { } #[test] -fn https_with_ipv6_user_and_port() -> crate::Result { +fn https_with_ipv6_user_and_port() -> gix_error::TestResult { Ok(assert_url_roundtrip( "https://user@[2001:db8::1]:8443/repo", url(Scheme::Https, "user", "[2001:db8::1]", 8443, b"/repo"), @@ -175,7 +175,7 @@ fn https_with_ipv6_user_and_port() -> crate::Result { } #[test] -fn percent_encoded_path() -> crate::Result { +fn percent_encoded_path() -> gix_error::TestResult { let url = gix_url::parse("https://example.com/path/with%20spaces/file")?; assert_eq!(url.path, "/path/with spaces/file", "paths are now decoded"); assert_eq!( @@ -187,14 +187,14 @@ fn percent_encoded_path() -> crate::Result { } #[test] -fn percent_encoded_international_path() -> crate::Result { +fn percent_encoded_international_path() -> gix_error::TestResult { let url = gix_url::parse("https://example.com/caf%C3%A9")?; assert_eq!(url.path, "/café", "international characters are decoded in path"); Ok(()) } #[test] -fn original_path_preserves_exact_spelling() -> crate::Result { +fn original_path_preserves_exact_spelling() -> gix_error::TestResult { for (input, decoded_path, original_path, message) in [ ( "https://example.com/plain", @@ -230,7 +230,7 @@ fn original_path_preserves_exact_spelling() -> crate::Result { } #[test] -fn reserved_percent_encoded_path_octets_remain_lossless() -> crate::Result { +fn reserved_percent_encoded_path_octets_remain_lossless() -> gix_error::TestResult { for (input, expected_path, message) in [ ( "https://example.com/a%2Fb", @@ -261,7 +261,7 @@ fn reserved_percent_encoded_path_octets_remain_lossless() -> crate::Result { } #[test] -fn literal_percent_escape_text_from_parts_is_encoded() -> crate::Result { +fn literal_percent_escape_text_from_parts_is_encoded() -> gix_error::TestResult { let url = gix_url::Url::from_parts( Scheme::Https, None, @@ -305,7 +305,7 @@ fn literal_percent_escape_text_from_parts_is_encoded() -> crate::Result { } #[test] -fn percent_encoded_path_roundtrips_in_lossless_serialization() -> crate::Result { +fn percent_encoded_path_roundtrips_in_lossless_serialization() -> gix_error::TestResult { for (input, message, expected_host, expected_path) in [ ( "https://%20@%40.example.org/%20%25", @@ -331,7 +331,7 @@ fn percent_encoded_path_roundtrips_in_lossless_serialization() -> crate::Result } #[test] -fn query_and_fragment_delimiters_in_path_roundtrip() -> crate::Result { +fn query_and_fragment_delimiters_in_path_roundtrip() -> gix_error::TestResult { assert_url_roundtrip( "https://host/repo.git?token=abc", url(Scheme::Https, None, "host", None, b"/repo.git?token=abc"), @@ -344,7 +344,7 @@ fn query_and_fragment_delimiters_in_path_roundtrip() -> crate::Result { } #[test] -fn query_and_fragment_delimiters_end_the_authority() -> crate::Result { +fn query_and_fragment_delimiters_end_the_authority() -> gix_error::TestResult { for input in ["https://host?@redirected/repo", "https://host#@redirected/repo"] { let url = gix_url::parse(input)?; assert_eq!(url.host(), Some("host"), "the authority ends at the delimiter"); @@ -366,7 +366,7 @@ fn query_and_fragment_delimiters_end_the_authority() -> crate::Result { } #[test] -fn authority_length_limit_excludes_the_scheme_separator() -> crate::Result { +fn authority_length_limit_excludes_the_scheme_separator() -> gix_error::TestResult { let at_limit = format!("https://{}", "a".repeat(1024)); assert_eq!( gix_url::parse(&at_limit)?.host().map(str::len), diff --git a/gix-url/tests/url/parse/mod.rs b/gix-url/tests/url/parse/mod.rs index 45d8492b2f8..03ae26f813e 100644 --- a/gix-url/tests/url/parse/mod.rs +++ b/gix-url/tests/url/parse/mod.rs @@ -1,7 +1,7 @@ use bstr::ByteSlice; use gix_url::{Scheme, parse, testing::TestUrlExtension}; -fn assert_url(url: &str, expected: gix_url::Url) -> Result { +fn assert_url(url: &str, expected: gix_url::Url) -> Result> { let actual = gix_url::parse(url)?; assert_eq!(actual, expected); // Note that this must not match on the name, as `Scheme::Helper("http")` is a remote helper. @@ -18,7 +18,7 @@ fn assert_url(url: &str, expected: gix_url::Url) -> Result Result<(), gix_url::parse::Error> { +fn assert_url_roundtrip(url: &str, expected: gix_url::Url) -> Result<(), gix_error::Exn> { assert_eq!(assert_url(url, expected)?.to_bstring(), url); Ok(()) } @@ -89,7 +89,7 @@ mod radicle { use crate::parse::{assert_url_roundtrip, url}; #[test] - fn basic() -> crate::Result { + fn basic() -> gix_error::TestResult { Ok(assert_url_roundtrip( "rad://hynkuwzskprmswzeo4qdtku7grdrs4ffj3g9tjdxomgmjzhtzpqf81@hwd1yregyf1dudqwkx85x5ps3qsrqw3ihxpx3ieopq6ukuuq597p6m8161c.git", url( @@ -110,7 +110,7 @@ mod ports { use gix_url::Scheme; #[test] - fn max_valid_port() -> crate::Result { + fn max_valid_port() -> gix_error::TestResult { Ok(assert_url_roundtrip( "ssh://host.xz:65535/repo", url(Scheme::Ssh, None, "host.xz", 65535, b"/repo"), @@ -118,7 +118,7 @@ mod ports { } #[test] - fn port_one() -> crate::Result { + fn port_one() -> gix_error::TestResult { Ok(assert_url_roundtrip( "ssh://host.xz:1/repo", url(Scheme::Ssh, None, "host.xz", 1, b"/repo"), @@ -132,7 +132,7 @@ mod git { use crate::parse::{assert_url_roundtrip, url}; #[test] - fn username_expansion_with_username() -> crate::Result { + fn username_expansion_with_username() -> gix_error::TestResult { Ok(assert_url_roundtrip( "git://example.com/~byron/hello", url(Scheme::Git, None, "example.com", None, b"~byron/hello"), @@ -140,14 +140,14 @@ mod git { } #[test] - fn default_port_is_9418() -> crate::Result { + fn default_port_is_9418() -> gix_error::TestResult { let url = url(Scheme::Git, None, "example.com", None, b"/repo"); assert_eq!(url.port_or_default(), Some(9418)); Ok(()) } #[test] - fn git_with_explicit_port() -> crate::Result { + fn git_with_explicit_port() -> gix_error::TestResult { Ok(assert_url_roundtrip( "git://example.com:1234/repo", url(Scheme::Git, None, "example.com", 1234, b"/repo"), @@ -161,7 +161,7 @@ mod unknown { use crate::parse::{assert_url_roundtrip, url}; #[test] - fn any_protocol_is_supported_via_a_remote_helper_url() -> crate::Result { + fn any_protocol_is_supported_via_a_remote_helper_url() -> gix_error::TestResult { Ok(assert_url_roundtrip( "abc://example.com/~byron/hello", url( diff --git a/gix-url/tests/url/parse/remote_helper.rs b/gix-url/tests/url/parse/remote_helper.rs index 90d96b9eda4..72dbb0b63e1 100644 --- a/gix-url/tests/url/parse/remote_helper.rs +++ b/gix-url/tests/url/parse/remote_helper.rs @@ -3,7 +3,7 @@ use gix_url::Scheme; use crate::parse::{assert_url, assert_url_roundtrip, url_alternate}; #[test] -fn address_may_contain_a_url() -> crate::Result { +fn address_may_contain_a_url() -> gix_error::TestResult { Ok(assert_url_roundtrip( "codecommit::eu-central-1://myaccount@my-repo", helper("codecommit", b"eu-central-1://myaccount@my-repo"), @@ -11,7 +11,7 @@ fn address_may_contain_a_url() -> crate::Result { } #[test] -fn address_is_not_interpreted() -> crate::Result { +fn address_is_not_interpreted() -> gix_error::TestResult { for (input, name, address) in [ ("transport::address", "transport", &b"address"[..]), ("myhelper::/abs/path", "myhelper", b"/abs/path"), @@ -29,7 +29,7 @@ fn address_is_not_interpreted() -> crate::Result { } #[test] -fn ext_commands_are_normalized_and_distinguishable_from_other_helpers() -> crate::Result { +fn ext_commands_are_normalized_and_distinguishable_from_other_helpers() -> gix_error::TestResult { assert_eq!( Scheme::from("ext"), Scheme::Ext, @@ -62,14 +62,14 @@ fn ext_commands_are_normalized_and_distinguishable_from_other_helpers() -> crate } #[test] -fn address_may_be_empty_or_contain_more_separators() -> crate::Result { +fn address_may_be_empty_or_contain_more_separators() -> gix_error::TestResult { assert_url_roundtrip("foo::", helper("foo", b""))?; assert_url_roundtrip("foo::a::b", helper("foo", b"a::b"))?; Ok(assert_url_roundtrip("a:::b", helper("a", b":b"))?) } #[test] -fn address_may_be_an_arbitrary_command_line() -> crate::Result { +fn address_may_be_an_arbitrary_command_line() -> gix_error::TestResult { // Helpers can be as flexible as `git-remote-bash` running `eval "$2"`, so nothing about the // address may be assumed. Ok(assert_url_roundtrip( @@ -82,7 +82,7 @@ fn address_may_be_an_arbitrary_command_line() -> crate::Result { } #[test] -fn a_single_letter_name_is_not_a_dos_drive_letter() -> crate::Result { +fn a_single_letter_name_is_not_a_dos_drive_letter() -> gix_error::TestResult { // A DOS drive letter is followed by a single `:`, so these remain remote helpers on all platforms, // just like in Git. assert_url_roundtrip("c::foo", helper("c", b"foo"))?; @@ -90,7 +90,7 @@ fn a_single_letter_name_is_not_a_dos_drive_letter() -> crate::Result { } #[test] -fn helper_names_shadow_built_in_schemes() -> crate::Result { +fn helper_names_shadow_built_in_schemes() -> gix_error::TestResult { for name in ["ssh", "file", "git", "http", "https"] { assert_url_roundtrip(&format!("{name}::address"), helper(name, b"address"))?; } @@ -98,7 +98,7 @@ fn helper_names_shadow_built_in_schemes() -> crate::Result { } #[test] -fn helper_names_are_case_sensitive() -> crate::Result { +fn helper_names_are_case_sensitive() -> gix_error::TestResult { Ok(assert_url_roundtrip( "CodeCommit::address", helper("CodeCommit", b"address"), @@ -106,7 +106,7 @@ fn helper_names_are_case_sensitive() -> crate::Result { } #[test] -fn helper_names_may_contain_special_characters_and_start_with_a_digit() -> crate::Result { +fn helper_names_may_contain_special_characters_and_start_with_a_digit() -> gix_error::TestResult { for name in ["a1.2+3-4", "9foo", "foo.bar", "foo-bar", "foo+bar"] { assert_url_roundtrip(&format!("{name}::address"), helper(name, b"address"))?; } @@ -114,7 +114,7 @@ fn helper_names_may_contain_special_characters_and_start_with_a_digit() -> crate } #[test] -fn addresses_may_contain_arbitrary_bytes() -> crate::Result { +fn addresses_may_contain_arbitrary_bytes() -> gix_error::TestResult { let url = gix_url::parse(bstr::BStr::new(b"foo::\xff\xfe"))?; assert_eq!(url.scheme, Scheme::Helper("foo".into()), "the name is always ASCII"); assert_eq!( @@ -131,7 +131,7 @@ fn addresses_may_contain_arbitrary_bytes() -> crate::Result { } #[test] -fn transport_form_and_url_form_are_distinguishable() -> crate::Result { +fn transport_form_and_url_form_are_distinguishable() -> gix_error::TestResult { let helper_form = assert_url("codecommit::my-repo", helper("codecommit", b"my-repo"))?; let url_form = assert_url( "codecommit://my-repo", @@ -162,7 +162,7 @@ fn from_parts_rejects_invalid_helper_names() { } #[test] -fn helper_form_is_always_retained() -> crate::Result { +fn helper_form_is_always_retained() -> gix_error::TestResult { for alt in [false, true] { for input in ["9foo::address", "ssh::address", "foo::address", "ext::address"] { let url = gix_url::parse(input)?.with_request_alternate_form(alt); @@ -183,7 +183,7 @@ mod not_a_remote_helper { use crate::parse::{assert_url, url_alternate}; #[test] - fn names_with_characters_git_does_not_accept() -> crate::Result { + fn names_with_characters_git_does_not_accept() -> gix_error::TestResult { // `_` and `%` are not part of `[A-Za-z0-9][A-Za-z0-9+.-]*`, so Git falls back to SCP-like syntax. for (input, host, path) in [("foo_bar::baz", "foo_bar", &b":baz"[..]), ("f%o::bar", "f%o", b":bar")] { assert_url(input, url_alternate(Scheme::Ssh, None, host, None, path))?; @@ -192,7 +192,7 @@ mod not_a_remote_helper { } #[test] - fn names_that_do_not_start_with_an_alphanumeric_character() -> crate::Result { + fn names_that_do_not_start_with_an_alphanumeric_character() -> gix_error::TestResult { assert_url(".foo::bar", url_alternate(Scheme::Ssh, None, ".foo", None, b":bar"))?; assert!( gix_url::parse("::bar").is_err(), @@ -202,7 +202,7 @@ mod not_a_remote_helper { } #[test] - fn a_single_colon_does_not_start_an_address() -> crate::Result { + fn a_single_colon_does_not_start_an_address() -> gix_error::TestResult { // Note that the name is longer than one character on purpose, as a single one would be a DOS // drive letter on Windows, which is decided after this and is unrelated to remote helpers. assert_url("ab:b::c", url_alternate(Scheme::Ssh, None, "ab", None, b"b::c"))?; diff --git a/gix-url/tests/url/parse/ssh.rs b/gix-url/tests/url/parse/ssh.rs index 179327a6075..bc035342dc9 100644 --- a/gix-url/tests/url/parse/ssh.rs +++ b/gix-url/tests/url/parse/ssh.rs @@ -3,7 +3,7 @@ use gix_url::Scheme; use crate::parse::{assert_url, assert_url_roundtrip, url, url_alternate, url_with_pass}; #[test] -fn without_user_and_without_port() -> crate::Result { +fn without_user_and_without_port() -> gix_error::TestResult { Ok(assert_url_roundtrip( "ssh://host.xz/path/to/repo.git/", url(Scheme::Ssh, None, "host.xz", None, b"/path/to/repo.git/"), @@ -11,7 +11,7 @@ fn without_user_and_without_port() -> crate::Result { } #[test] -fn without_user_and_with_port() -> crate::Result { +fn without_user_and_with_port() -> gix_error::TestResult { Ok(assert_url_roundtrip( "ssh://host.xz:21/", url(Scheme::Ssh, None, "host.xz", 21, b"/"), @@ -19,7 +19,7 @@ fn without_user_and_with_port() -> crate::Result { } #[test] -fn host_is_ipv4() -> crate::Result { +fn host_is_ipv4() -> gix_error::TestResult { Ok(assert_url_roundtrip( "ssh://127.69.0.1/hello", url(Scheme::Ssh, None, "127.69.0.1", None, b"/hello"), @@ -27,7 +27,7 @@ fn host_is_ipv4() -> crate::Result { } #[test] -fn username_expansion_with_username() -> crate::Result { +fn username_expansion_with_username() -> gix_error::TestResult { Ok(assert_url_roundtrip( "ssh://example.com/~byron/hello/git", url(Scheme::Ssh, None, "example.com", None, b"~byron/hello/git"), @@ -35,7 +35,7 @@ fn username_expansion_with_username() -> crate::Result { } #[test] -fn username_expansion_without_username() -> crate::Result { +fn username_expansion_without_username() -> gix_error::TestResult { Ok(assert_url_roundtrip( "ssh://example.com/~/hello/git", url(Scheme::Ssh, None, "example.com", None, b"~/hello/git"), @@ -43,7 +43,7 @@ fn username_expansion_without_username() -> crate::Result { } #[test] -fn scp_like_with_ssh_host_alias() -> crate::Result { +fn scp_like_with_ssh_host_alias() -> gix_error::TestResult { Ok(assert_url_roundtrip( "user@alias:username/repo.git", url_alternate(Scheme::Ssh, "user", "alias", None, b"username/repo.git"), @@ -51,7 +51,7 @@ fn scp_like_with_ssh_host_alias() -> crate::Result { } #[test] -fn with_user_and_without_port() -> crate::Result { +fn with_user_and_without_port() -> gix_error::TestResult { Ok(assert_url_roundtrip( "ssh://user@host.xz/.git", url(Scheme::Ssh, "user", "host.xz", None, b"/.git"), @@ -59,7 +59,7 @@ fn with_user_and_without_port() -> crate::Result { } #[test] -fn username_with_dot_is_not_percent_encoded() -> crate::Result { +fn username_with_dot_is_not_percent_encoded() -> gix_error::TestResult { Ok(assert_url_roundtrip( "ssh://user.name@host.xz/.git", url(Scheme::Ssh, "user.name", "host.xz", None, b"/.git"), @@ -67,7 +67,7 @@ fn username_with_dot_is_not_percent_encoded() -> crate::Result { } #[test] -fn with_user_and_port_and_absolute_path() -> crate::Result { +fn with_user_and_port_and_absolute_path() -> gix_error::TestResult { Ok(assert_url_roundtrip( "ssh://user@host.xz:42/.git", url(Scheme::Ssh, "user", "host.xz", 42, b"/.git"), @@ -75,7 +75,7 @@ fn with_user_and_port_and_absolute_path() -> crate::Result { } #[test] -fn ssh_alias_without_username() -> crate::Result { +fn ssh_alias_without_username() -> gix_error::TestResult { let url = assert_url( "host:/path/to/git", url_alternate(Scheme::Ssh, None, "host", None, b"/path/to/git"), @@ -86,14 +86,14 @@ fn ssh_alias_without_username() -> crate::Result { } #[test] -fn default_port_is_22() -> crate::Result { +fn default_port_is_22() -> gix_error::TestResult { let url = url_alternate(Scheme::Ssh, None, "host.xz", None, b"path/to/git"); assert_eq!(url.port_or_default(), Some(22)); Ok(()) } #[test] -fn scp_like_without_user() -> crate::Result { +fn scp_like_without_user() -> gix_error::TestResult { let url = assert_url( "host.xz:path/to/git", url_alternate(Scheme::Ssh, None, "host.xz", None, b"path/to/git"), @@ -104,7 +104,7 @@ fn scp_like_without_user() -> crate::Result { } #[test] -fn scp_like_with_absolute_path() -> crate::Result { +fn scp_like_with_absolute_path() -> gix_error::TestResult { let url = assert_url( "host.xz:/path/to/git", url_alternate(Scheme::Ssh, None, "host.xz", None, b"/path/to/git"), @@ -115,7 +115,7 @@ fn scp_like_with_absolute_path() -> crate::Result { } #[test] -fn scp_like_with_absolute_path_with_whitespace() -> crate::Result { +fn scp_like_with_absolute_path_with_whitespace() -> gix_error::TestResult { let url = assert_url( "host.xz:/path/to/git with space", url_alternate(Scheme::Ssh, None, "host.xz", None, b"/path/to/git with space"), @@ -126,7 +126,7 @@ fn scp_like_with_absolute_path_with_whitespace() -> crate::Result { } #[test] -fn scp_like_without_user_and_username_expansion_without_username() -> crate::Result { +fn scp_like_without_user_and_username_expansion_without_username() -> gix_error::TestResult { let url = assert_url( "host.xz:~/to/git", url_alternate(Scheme::Ssh, None, "host.xz", None, b"~/to/git"), @@ -137,7 +137,7 @@ fn scp_like_without_user_and_username_expansion_without_username() -> crate::Res } #[test] -fn scp_like_without_user_and_username_expansion_with_username() -> crate::Result { +fn scp_like_without_user_and_username_expansion_with_username() -> gix_error::TestResult { let url = assert_url( "host.xz:~byron/to/git", url_alternate(Scheme::Ssh, None, "host.xz", None, b"~byron/to/git"), @@ -148,7 +148,7 @@ fn scp_like_without_user_and_username_expansion_with_username() -> crate::Result } #[test] -fn scp_like_with_user_and_relative_path_keep_relative_path() -> crate::Result { +fn scp_like_with_user_and_relative_path_keep_relative_path() -> gix_error::TestResult { let url = assert_url( "user@host.xz:relative", url_alternate(Scheme::Ssh, "user", "host.xz", None, b"relative"), @@ -187,7 +187,7 @@ fn scp_like_with_user_and_relative_path_keep_relative_path() -> crate::Result { } #[test] -fn canonical_form_is_used_only_if_it_preserves_scp_path_semantics() -> crate::Result { +fn canonical_form_is_used_only_if_it_preserves_scp_path_semantics() -> gix_error::TestResult { assert_eq!( gix_url::parse("user@host.xz:relative")? .with_request_alternate_form(false) @@ -222,7 +222,7 @@ fn canonical_form_is_used_only_if_it_preserves_scp_path_semantics() -> crate::Re } #[test] -fn scp_like_with_windows_path() -> crate::Result { +fn scp_like_with_windows_path() -> gix_error::TestResult { let url = assert_url( "user@host.xz:C:/strange/absolute/path", url_alternate(Scheme::Ssh, "user", "host.xz", None, b"C:/strange/absolute/path"), @@ -233,7 +233,7 @@ fn scp_like_with_windows_path() -> crate::Result { } #[test] -fn scp_like_with_windows_path_and_port_thinks_port_is_part_of_path() -> crate::Result { +fn scp_like_with_windows_path_and_port_thinks_port_is_part_of_path() -> gix_error::TestResult { let url = gix_url::parse("user@host.xz:42:C:/strange/absolute/path")?; assert_eq!( url.to_bstring(), @@ -249,7 +249,7 @@ fn scp_like_with_windows_path_and_port_thinks_port_is_part_of_path() -> crate::R } #[test] -fn scp_like_with_non_alphanumeric_username() -> crate::Result { +fn scp_like_with_non_alphanumeric_username() -> gix_error::TestResult { let url = assert_url( "_user.name@host.xz:C:/path", url_alternate(Scheme::Ssh, "_user.name", "host.xz", None, b"C:/path"), @@ -262,7 +262,7 @@ fn scp_like_with_non_alphanumeric_username() -> crate::Result { // Git passes the non-path part "user@name@host.xz" to OpenSSH, and the ssh // command interprets it as user = "user@name", host = "host.xz". #[test] -fn scp_like_with_username_including_at() -> crate::Result { +fn scp_like_with_username_including_at() -> gix_error::TestResult { let url = assert_url( "user@name@host.xz:path", url_alternate(Scheme::Ssh, "user@name", "host.xz", None, b"path"), @@ -273,7 +273,7 @@ fn scp_like_with_username_including_at() -> crate::Result { } #[test] -fn scp_like_username_with_colon_is_not_mistaken_for_a_password() -> crate::Result { +fn scp_like_username_with_colon_is_not_mistaken_for_a_password() -> gix_error::TestResult { let input = "[:[@]:\x1a"; let url = gix_url::parse(input)?; assert_eq!(url.user(), Some("[:["), "OpenSSH splits user and host at the last @"); @@ -287,14 +287,14 @@ fn scp_like_username_with_colon_is_not_mistaken_for_a_password() -> crate::Resul // I btw tested this, yes you can really clone a repository from there, just `git init` // in the directory above your home directory on the remote machine. #[test] -fn strange_scp_like_with_host_named_file() -> crate::Result { +fn strange_scp_like_with_host_named_file() -> gix_error::TestResult { let url = assert_url("file:..", url_alternate(Scheme::Ssh, None, "file", None, b".."))?; assert_eq!(url.to_bstring(), "file:.."); Ok(()) } #[test] -fn bad_alternative_form_with_password() -> crate::Result { +fn bad_alternative_form_with_password() -> gix_error::TestResult { let password = std::process::id().to_string(); let url = url_with_pass(Scheme::Ssh, "user", password.as_str(), "host.xz", None, b"/") .with_request_alternate_form(true) @@ -304,21 +304,21 @@ fn bad_alternative_form_with_password() -> crate::Result { } #[test] -fn bad_alternative_form_with_port() -> crate::Result { +fn bad_alternative_form_with_port() -> gix_error::TestResult { let url = url_alternate(Scheme::Ssh, None, "host.xz", 21, b"/").to_bstring(); assert_eq!(url, "ssh://host.xz:21/"); Ok(()) } #[test] -fn ipv6_address_without_port() -> crate::Result { +fn ipv6_address_without_port() -> gix_error::TestResult { let url = assert_url("ssh://[::1]/repo", url(Scheme::Ssh, None, "::1", None, b"/repo"))?; assert_eq!(url.host(), Some("::1"), "brackets are stripped for SSH"); Ok(()) } #[test] -fn ipv6_address_with_port() -> crate::Result { +fn ipv6_address_with_port() -> gix_error::TestResult { let url = assert_url("ssh://[::1]:22/repo", url(Scheme::Ssh, None, "::1", 22, b"/repo"))?; assert_eq!(url.host(), Some("::1")); assert_eq!(url.port, Some(22)); @@ -326,7 +326,7 @@ fn ipv6_address_with_port() -> crate::Result { } #[test] -fn ipv6_address_with_user() -> crate::Result { +fn ipv6_address_with_user() -> gix_error::TestResult { let url = assert_url("ssh://user@[::1]/repo", url(Scheme::Ssh, "user", "::1", None, b"/repo"))?; assert_eq!(url.host(), Some("::1")); assert_eq!(url.user(), Some("user")); @@ -334,7 +334,7 @@ fn ipv6_address_with_user() -> crate::Result { } #[test] -fn ipv6_address_with_user_and_port() -> crate::Result { +fn ipv6_address_with_user_and_port() -> gix_error::TestResult { let url = assert_url( "ssh://user@[::1]:22/repo", url(Scheme::Ssh, "user", "::1", 22, b"/repo"), @@ -346,7 +346,7 @@ fn ipv6_address_with_user_and_port() -> crate::Result { } #[test] -fn ipv6_full_address() -> crate::Result { +fn ipv6_full_address() -> gix_error::TestResult { let url = assert_url( "ssh://[2001:db8::1]/repo", url(Scheme::Ssh, None, "2001:db8::1", None, b"/repo"), @@ -356,7 +356,7 @@ fn ipv6_full_address() -> crate::Result { } #[test] -fn scoped_ipv6_address() -> crate::Result { +fn scoped_ipv6_address() -> gix_error::TestResult { let input = "ssh://[fe80::1%25Eth0]/repo"; let url = gix_url::parse(input)?; assert_eq!( @@ -370,7 +370,7 @@ fn scoped_ipv6_address() -> crate::Result { } #[test] -fn scp_like_scoped_ipv6_address_uses_a_raw_zone_separator() -> crate::Result { +fn scp_like_scoped_ipv6_address_uses_a_raw_zone_separator() -> gix_error::TestResult { let input = "[fe80::1%Eth0]:repo"; let url = gix_url::parse(input)?; assert_eq!(url.host(), Some("fe80::1%Eth0"), "the raw percent sign is host data"); @@ -384,7 +384,7 @@ fn scp_like_scoped_ipv6_address_uses_a_raw_zone_separator() -> crate::Result { } #[test] -fn scoped_ipv6_address_with_empty_port() -> crate::Result { +fn scoped_ipv6_address_with_empty_port() -> gix_error::TestResult { let url = gix_url::parse("ssh://[fe80::1%25Eth0]:/repo")?; assert_eq!( url.host(), @@ -401,7 +401,7 @@ fn scoped_ipv6_address_with_empty_port() -> crate::Result { } #[test] -fn bracketed_host_is_percent_decoded_once() -> crate::Result { +fn bracketed_host_is_percent_decoded_once() -> gix_error::TestResult { let url = gix_url::parse("ssh://[::81ssssssssssssssssssssssssssssssssssssss%2585]:/00%2585]://")?; assert_eq!( url.host(), @@ -413,7 +413,7 @@ fn bracketed_host_is_percent_decoded_once() -> crate::Result { } #[test] -fn escaped_authority_delimiters_remain_host_data() -> crate::Result { +fn escaped_authority_delimiters_remain_host_data() -> gix_error::TestResult { for (input, expected_host) in [ ("ssh://host%3A123/repo", "host:123"), ("ssh://host%2Fname/repo", "host/name"), @@ -439,7 +439,7 @@ fn escaped_authority_delimiters_remain_host_data() -> crate::Result { } #[test] -fn percent_encoded_paths_are_decoded_and_the_original_is_retained() -> crate::Result { +fn percent_encoded_paths_are_decoded_and_the_original_is_retained() -> gix_error::TestResult { for (input, decoded_path, original_path, message) in [ ( "ssh://example.com/a%2Fb", @@ -463,14 +463,14 @@ fn percent_encoded_paths_are_decoded_and_the_original_is_retained() -> crate::Re } #[test] -fn ipv6_address_scp_like() -> crate::Result { +fn ipv6_address_scp_like() -> gix_error::TestResult { let url = assert_url("[::1]:repo", url_alternate(Scheme::Ssh, None, "::1", None, b"repo"))?; assert_eq!(url.host(), Some("::1"), "SCP-like format with IPv6"); Ok(()) } #[test] -fn ipv6_address_scp_like_with_user() -> crate::Result { +fn ipv6_address_scp_like_with_user() -> gix_error::TestResult { let result = gix_url::parse("user@[::1]:repo"); assert!( result.is_err(), diff --git a/gix-worktree-state/src/checkout/chunk.rs b/gix-worktree-state/src/checkout/chunk.rs index 08e959249e2..05fd2b9c6f9 100644 --- a/gix-worktree-state/src/checkout/chunk.rs +++ b/gix-worktree-state/src/checkout/chunk.rs @@ -10,17 +10,16 @@ use gix_worktree::Stack; use crate::{checkout, checkout::entry}; mod reduce { - use crate::checkout; pub struct Reduce<'entry> { pub aggregate: super::Outcome<'entry>, } impl<'entry> gix_features::parallel::Reduce for Reduce<'entry> { - type Input = Result, checkout::Error>; + type Input = Result, gix_error::Exn>; type FeedProduce = (); type Output = super::Outcome<'entry>; - type Error = checkout::Error; + type Error = gix_error::Exn; fn feed(&mut self, item: Self::Input) -> Result { let item = item?; @@ -106,7 +105,7 @@ pub fn process<'entry, Find>( bytes: &AtomicUsize, delayed_filter_results: &mut Vec>, ctx: &mut Context, -) -> Result, checkout::Error> +) -> Result, gix_error::Exn> where Find: gix_object::Find + Clone, { @@ -161,7 +160,7 @@ pub fn process_delayed_filter_results( bytes: &AtomicUsize, out: &mut Outcome<'_>, ctx: &mut Context, -) -> Result<(), checkout::Error> +) -> Result<(), gix_error::Exn> where Find: gix_object::Find + Clone, { @@ -305,7 +304,7 @@ pub fn checkout_entry_handle_result<'entry, Find>( buf, options, }: &mut Context, -) -> Result, checkout::Error> +) -> Result, gix_error::Exn> where Find: gix_object::Find + Clone, { @@ -343,12 +342,12 @@ where } fn handle_error( - err: checkout::Error, + err: gix_error::Exn, entry_path: &BStr, files: &AtomicUsize, errors: &mut Vec, keep_going: bool, -) -> Result<(), checkout::Error> { +) -> Result<(), gix_error::Exn> { if keep_going { errors.push(checkout::ErrorRecord { path: entry_path.into(), diff --git a/gix-worktree-state/src/checkout/entry.rs b/gix-worktree-state/src/checkout/entry.rs index 815c2cf0009..308e16c5a48 100644 --- a/gix-worktree-state/src/checkout/entry.rs +++ b/gix-worktree-state/src/checkout/entry.rs @@ -77,7 +77,7 @@ pub fn checkout<'entry, Find>( filter_process_delay, .. }: crate::checkout::chunk::Options, -) -> Result, crate::checkout::Error> +) -> Result, gix_error::Exn> where Find: gix_object::Find, { @@ -300,7 +300,7 @@ pub(crate) fn finalize_entry( file: std::fs::File, desired_bytes: u64, #[cfg_attr(windows, allow(unused_variables))] executable_bit_change: ExecutableBitChange, -) -> Result<(), crate::checkout::Error> { +) -> Result<(), gix_error::Exn> { // For possibly existing, overwritten files, we must change the file mode explicitly to match the index. #[cfg(unix)] match executable_bit_change { diff --git a/gix-worktree-state/src/checkout/function.rs b/gix-worktree-state/src/checkout/function.rs index 3c24be2d841..23a3d5fe395 100644 --- a/gix-worktree-state/src/checkout/function.rs +++ b/gix-worktree-state/src/checkout/function.rs @@ -24,7 +24,7 @@ pub fn checkout( bytes: &dyn gix_features::progress::Count, should_interrupt: &AtomicBool, options: crate::checkout::Options, -) -> Result +) -> Result where Find: gix_object::Find + Send + Clone, { @@ -44,7 +44,7 @@ fn checkout_inner( bytes: &dyn gix_features::progress::Count, should_interrupt: &AtomicBool, mut options: crate::checkout::Options, -) -> Result +) -> Result where Find: gix_object::Find + Send + Clone, { diff --git a/gix-worktree-state/src/checkout/mod.rs b/gix-worktree-state/src/checkout/mod.rs index ede16160a0d..7949fe2f89e 100644 --- a/gix-worktree-state/src/checkout/mod.rs +++ b/gix-worktree-state/src/checkout/mod.rs @@ -74,9 +74,6 @@ pub struct Options { pub filter_process_delay: gix_filter::driver::apply::Delay, } -/// The error returned by the [checkout()][crate::checkout()] function. -pub type Error = gix_error::Exn; - mod chunk; mod entry; pub(crate) mod function; diff --git a/gix-worktree-state/tests/worktree-state/checkout.rs b/gix-worktree-state/tests/worktree-state/checkout.rs index ce22118c901..3a6e36eb6e4 100644 --- a/gix-worktree-state/tests/worktree-state/checkout.rs +++ b/gix-worktree-state/tests/worktree-state/checkout.rs @@ -972,11 +972,7 @@ fn checkout_index_in_tmp_dir_opts( Allow: FnMut(&gix_hash::oid) -> bool + Send + Clone, Find: gix_object::Find + Send + Clone, { - fn try_find<'a>( - &self, - id: &gix_hash::oid, - buf: &'a mut Vec, - ) -> Result>, gix_object::find::Error> { + fn try_find<'a>(&self, id: &gix_hash::oid, buf: &'a mut Vec) -> Result>, gix_error::Exn> { if (self.allow.borrow_mut())(id) { self.objects.try_find(id, buf) } else { diff --git a/gix-worktree-stream/src/entry.rs b/gix-worktree-stream/src/entry.rs index f24456bceb9..2e30f2ed70c 100644 --- a/gix-worktree-stream/src/entry.rs +++ b/gix-worktree-stream/src/entry.rs @@ -8,12 +8,9 @@ use gix_object::bstr::BStr; use crate::{Entry, Stream, protocol}; -/// The error returned by [`next_entry()`][Stream::next_entry()]. -pub type Error = gix_error::Exn; - impl Stream { /// Access the next entry of the stream or `None` if there is nothing more to read. - pub fn next_entry(&mut self) -> Result>, Error> { + pub fn next_entry(&mut self) -> Result>, gix_error::Exn> { assert!( self.path_buf.is_some(), "BUG: must consume and drop entry before getting the next one" diff --git a/gix-worktree-stream/src/from_tree/mod.rs b/gix-worktree-stream/src/from_tree/mod.rs index 05df4de5c4f..d039439eb92 100644 --- a/gix-worktree-stream/src/from_tree/mod.rs +++ b/gix-worktree-stream/src/from_tree/mod.rs @@ -4,7 +4,7 @@ use gix_object::{FindExt, bstr::BStr}; use gix_error::{ResultExt, message}; -use crate::{AdditionalEntry, SharedErrorSlot, Stream, entry, entry::Error, protocol}; +use crate::{AdditionalEntry, SharedErrorSlot, Stream, entry, protocol}; /// Use `objects` to traverse `tree` and fetch the contained blobs to return as [`Stream`], which makes them queryable /// on demand with support for streaming each entry. @@ -84,7 +84,7 @@ fn run( out: &mut gix_features::io::pipe::Writer, err: SharedErrorSlot, additional_entries: std::sync::mpsc::Receiver, -) -> Result<(), Error> +) -> Result<(), gix_error::Exn> where Find: gix_object::Find + Clone, E: std::error::Error + Send + Sync + 'static, diff --git a/gix-worktree-stream/src/from_tree/traverse.rs b/gix-worktree-stream/src/from_tree/traverse.rs index 4318465321f..6b60d002a0b 100644 --- a/gix-worktree-stream/src/from_tree/traverse.rs +++ b/gix-worktree-stream/src/from_tree/traverse.rs @@ -13,7 +13,7 @@ use gix_traverse::tree::{Visit, visit::Action}; use gix_error::{ResultExt, message}; -use crate::{SharedErrorSlot, entry::Error, protocol}; +use crate::{SharedErrorSlot, protocol}; pub struct Delegate<'a, AttributesFn, Find> where @@ -33,8 +33,12 @@ where impl Delegate<'_, AttributesFn, Find> where Find: gix_object::Find, - AttributesFn: - FnMut(&BStr, gix_object::tree::EntryMode, &mut gix_attributes::search::Outcome) -> Result<(), Error> + 'static, + AttributesFn: FnMut( + &BStr, + gix_object::tree::EntryMode, + &mut gix_attributes::search::Outcome, + ) -> Result<(), gix_error::Exn> + + 'static, { fn pop_element(&mut self) { if let Some(pos) = self.path.rfind_byte(b'/') { @@ -63,7 +67,7 @@ where .state } - fn handle_entry(&mut self, entry: &tree::EntryRef<'_>) -> Result { + fn handle_entry(&mut self, entry: &tree::EntryRef<'_>) -> Result> { if !entry.mode.is_blob_or_symlink() { return Ok(std::ops::ControlFlow::Continue(true)); } @@ -121,8 +125,12 @@ where impl Visit for Delegate<'_, AttributesFn, Find> where Find: gix_object::Find, - AttributesFn: - FnMut(&BStr, gix_object::tree::EntryMode, &mut gix_attributes::search::Outcome) -> Result<(), Error> + 'static, + AttributesFn: FnMut( + &BStr, + gix_object::tree::EntryMode, + &mut gix_attributes::search::Outcome, + ) -> Result<(), gix_error::Exn> + + 'static, { fn pop_back_tracked_path_and_set_current(&mut self) { self.path = self.path_deque.pop_back().unwrap_or_default(); diff --git a/gix-worktree-stream/src/lib.rs b/gix-worktree-stream/src/lib.rs index 9485fce71bd..b67a2b7b6db 100644 --- a/gix-worktree-stream/src/lib.rs +++ b/gix-worktree-stream/src/lib.rs @@ -32,7 +32,7 @@ pub(crate) mod protocol; mod from_tree; pub use from_tree::from_tree; -pub(crate) type SharedErrorSlot = Arc>>; +pub(crate) type SharedErrorSlot = Arc>>>; /// An entry in a stream. Note that they must be consumed fully, by reading from them till exhaustion. /// diff --git a/gix-worktree-stream/tests/stream.rs b/gix-worktree-stream/tests/stream.rs index 273747b5825..87031c0d13e 100644 --- a/gix-worktree-stream/tests/stream.rs +++ b/gix-worktree-stream/tests/stream.rs @@ -63,11 +63,7 @@ mod from_tree { struct FailObjectRetrieval; impl gix_object::Find for FailObjectRetrieval { - fn try_find<'a>( - &self, - _id: &oid, - _buffer: &'a mut Vec, - ) -> Result>, gix_object::find::Error> { + fn try_find<'a>(&self, _id: &oid, _buffer: &'a mut Vec) -> Result>, gix_error::Exn> { Err(Error::other("object retrieval failed").raise_erased()) } } diff --git a/gix-worktree/Cargo.toml b/gix-worktree/Cargo.toml index c35d59ab801..4879cceef63 100644 --- a/gix-worktree/Cargo.toml +++ b/gix-worktree/Cargo.toml @@ -45,6 +45,7 @@ bstr = { version = "1.12.0", default-features = false } document-features = { version = "0.2.0", optional = true } [dev-dependencies] +gix-error = { version = "^0.3.2", path = "../gix-error" } gix-hash = { path = "../gix-hash", features = ["sha1", "sha256"] } gix-testtools = { path = "../tests/tools", default-features = false, features = ["sha1", "sha256"] } gix-odb = { path = "../gix-odb", features = ["sha1", "sha256"] } diff --git a/gix-worktree/tests/worktree/stack/ignore.rs b/gix-worktree/tests/worktree/stack/ignore.rs index 8dc7204c11b..2261a5e5a56 100644 --- a/gix-worktree/tests/worktree/stack/ignore.rs +++ b/gix-worktree/tests/worktree/stack/ignore.rs @@ -60,7 +60,7 @@ fn exclude_by_dir_is_handled_just_like_git() { &self, id: &gix_hash::oid, buffer: &'a mut Vec, - ) -> Result>, gix_object::find::Error> { + ) -> Result>, gix_error::Exn> { gix_object::FindExt::find(&gix_object::find::Never, id, buffer).map(Some) } } diff --git a/gix-zlib/src/decompress.rs b/gix-zlib/src/decompress.rs index e0b6bc80662..1224b4ffbd8 100644 --- a/gix-zlib/src/decompress.rs +++ b/gix-zlib/src/decompress.rs @@ -4,9 +4,6 @@ use gix_error::{CorruptionError, ErrorExt, ResourceExhaustionError, ResourceExha use zlib_rs::InflateError; use crate::{Decompress, FlushDecompress, Status}; -/// -/// The error produced by [`Decompress::decompress()`]. -pub type DecompressError = gix_error::Exn; impl Default for Decompress { fn default() -> Self { @@ -49,7 +46,7 @@ impl Decompress { input: &[u8], output: &mut [u8], flush: FlushDecompress, - ) -> Result { + ) -> Result { let inflate_flush = match flush { FlushDecompress::None => zlib_rs::InflateFlush::NoFlush, FlushDecompress::Sync => zlib_rs::InflateFlush::SyncFlush, diff --git a/gix-zlib/src/inflate.rs b/gix-zlib/src/inflate.rs index d42c0040e69..97cca4b18b8 100644 --- a/gix-zlib/src/inflate.rs +++ b/gix-zlib/src/inflate.rs @@ -2,12 +2,9 @@ use gix_error::{Message, ResultExt, message}; use crate::{FlushDecompress, Inflate, Status}; -/// The error returned by various [Inflate methods][super::Inflate] -pub type Error = gix_error::Exn; - impl Inflate { /// Run the decompressor exactly once. Cannot be run multiple times - pub fn once(&mut self, input: &[u8], out: &mut [u8]) -> Result<(Status, usize, usize), Error> { + pub fn once(&mut self, input: &[u8], out: &mut [u8]) -> Result<(Status, usize, usize), gix_error::Exn> { let before_in = self.state.total_in(); let before_out = self.state.total_out(); let status = self diff --git a/gix-zlib/src/lib.rs b/gix-zlib/src/lib.rs index b3dddec0d26..d49cba46ecd 100644 --- a/gix-zlib/src/lib.rs +++ b/gix-zlib/src/lib.rs @@ -96,4 +96,3 @@ pub mod stream; pub mod inflate; mod decompress; -pub use decompress::DecompressError; diff --git a/gix-zlib/src/stream/deflate.rs b/gix-zlib/src/stream/deflate.rs index bd92d20cddf..365817ac8d0 100644 --- a/gix-zlib/src/stream/deflate.rs +++ b/gix-zlib/src/stream/deflate.rs @@ -58,7 +58,12 @@ impl Compress { } /// Compress `input` and write compressed bytes to `output`, with `flush` controlling additional characteristics. - pub fn compress(&mut self, input: &[u8], output: &mut [u8], flush: FlushCompress) -> Result { + pub fn compress( + &mut self, + input: &[u8], + output: &mut [u8], + flush: FlushCompress, + ) -> Result { let flush = match flush { FlushCompress::None => zlib_rs::DeflateFlush::NoFlush, FlushCompress::Partial => zlib_rs::DeflateFlush::PartialFlush, @@ -82,9 +87,6 @@ impl Compress { } } -/// The error produced by [`Compress::compress()`]. -pub type CompressError = gix_error::Exn; - /// Values which indicate the form of flushing to be used when compressing /// in-memory data. #[derive(Copy, Clone, PartialEq, Eq, Debug)] diff --git a/gix-zlib/tests/zlib/stream/deflate.rs b/gix-zlib/tests/zlib/stream/deflate.rs index a88ac861324..251f733fba6 100644 --- a/gix-zlib/tests/zlib/stream/deflate.rs +++ b/gix-zlib/tests/zlib/stream/deflate.rs @@ -8,8 +8,6 @@ use bstr::ByteSlice; use gix_zlib::stream::deflate::{self, Compress, FlushCompress}; use gix_zlib::{Compression, Decompress}; -type TestResult = gix_error::TestResult; - pub(crate) fn compressed(data: &[u8]) -> Vec { let mut writer = deflate::Write::new(Vec::new(), Compression::BEST_SPEED); writer.write_all(data).expect("in-memory writes never fail"); @@ -46,7 +44,7 @@ where } #[test] -fn small_file_decompress() -> TestResult { +fn small_file_decompress() -> gix_error::TestResult { fn fixture_path(path: &str) -> std::path::PathBuf { std::path::Path::new(env!("CARGO_MANIFEST_DIR")) .join("../gix-odb/tests/fixtures") @@ -64,7 +62,7 @@ fn small_file_decompress() -> TestResult { } #[test] -fn all_at_once() -> TestResult { +fn all_at_once() -> gix_error::TestResult { let mut w = deflate::Write::new(Vec::new(), Compression::BEST_SPEED); assert_eq!(w.write(b"hello")?, 5); w.flush()?; @@ -84,7 +82,7 @@ fn assert_deflate_buffer(out: Vec, expected: &[u8]) -> io::Result<()> { } #[test] -fn higher_levels_compress_better() -> TestResult { +fn higher_levels_compress_better() -> gix_error::TestResult { let data: Vec = (0..128 * 1024).map(|i| (i % 100) as u8).collect(); let mut sizes = Vec::new(); for level in [Compression::NONE, Compression::BEST_SPEED, Compression::DEFAULT] { @@ -103,7 +101,7 @@ fn higher_levels_compress_better() -> TestResult { } #[test] -fn big_file_small_writes() -> TestResult { +fn big_file_small_writes() -> gix_error::TestResult { let mut w = deflate::Write::new(Vec::new(), Compression::BEST_SPEED); let bytes = include_bytes!( "../../../../gix-odb/tests/fixtures/objects/pack/pack-11fdfa9e156ab73caae3b6da867192221f2089c2.pack" @@ -118,7 +116,7 @@ fn big_file_small_writes() -> TestResult { } #[test] -fn big_file_a_few_big_writes() -> TestResult { +fn big_file_a_few_big_writes() -> gix_error::TestResult { let mut w = deflate::Write::new(Vec::new(), Compression::BEST_SPEED); let bytes = include_bytes!( "../../../../gix-odb/tests/fixtures/objects/pack/pack-11fdfa9e156ab73caae3b6da867192221f2089c2.pack" @@ -133,7 +131,7 @@ fn big_file_a_few_big_writes() -> TestResult { } #[test] -fn compressor_lifecycle_counters_and_flush_modes() -> TestResult { +fn compressor_lifecycle_counters_and_flush_modes() -> gix_error::TestResult { for flush in [ FlushCompress::None, FlushCompress::Partial, @@ -169,7 +167,7 @@ fn compressor_lifecycle_counters_and_flush_modes() -> TestResult { } #[test] -fn writer_clone_and_reset() -> TestResult { +fn writer_clone_and_reset() -> gix_error::TestResult { let original = deflate::Write::new(Vec::new(), Compression::BEST_SPEED); let mut cloned = original.clone(); cloned.write_all(b"clone owns a fresh compressor")?; diff --git a/gix/src/clone/access.rs b/gix/src/clone/access.rs index e81078d76f4..7ddb47e365f 100644 --- a/gix/src/clone/access.rs +++ b/gix/src/clone/access.rs @@ -28,7 +28,7 @@ impl PrepareFetch { /// [`configure_remote()`](Self::configure_remote()). /// /// If not set here, it defaults to `origin` or the value of `clone.defaultRemoteName`. - pub fn with_remote_name(mut self, name: impl Into) -> Result { + pub fn with_remote_name(mut self, name: impl Into) -> Result { self.remote_name = Some(crate::remote::name::validated(name)?); Ok(self) } diff --git a/gix/src/clone/checkout.rs b/gix/src/clone/checkout.rs index 09ecf97f492..f3e281d68ba 100644 --- a/gix/src/clone/checkout.rs +++ b/gix/src/clone/checkout.rs @@ -8,9 +8,6 @@ pub mod main_worktree { use crate::{Progress, Repository, clone::PrepareCheckout}; - /// The error returned by [`PrepareCheckout::main_worktree()`]. - pub type Error = gix_error::Error; - /// The progress ids used in [`PrepareCheckout::main_worktree()`]. /// /// Use this information to selectively extract the progress of interest in case the parent application has custom visualization. @@ -48,7 +45,7 @@ pub mod main_worktree { &mut self, mut progress: P, should_interrupt: &AtomicBool, - ) -> Result<(Repository, gix_worktree_state::checkout::Outcome), Error> + ) -> Result<(Repository, gix_worktree_state::checkout::Outcome), crate::Error> where P: gix_features::progress::NestedProgress, P::SubProgress: gix_features::progress::NestedProgress + 'static, @@ -60,7 +57,7 @@ pub mod main_worktree { &mut self, progress: &mut dyn gix_features::progress::DynNestedProgress, should_interrupt: &AtomicBool, - ) -> Result<(Repository, gix_worktree_state::checkout::Outcome), Error> { + ) -> Result<(Repository, gix_worktree_state::checkout::Outcome), crate::Error> { let _span = gix_trace::coarse!("gix::clone::PrepareCheckout::main_worktree()"); let repo = self .repo diff --git a/gix/src/clone/fetch/mod.rs b/gix/src/clone/fetch/mod.rs index 5f0f09ba2bf..d285d55e429 100644 --- a/gix/src/clone/fetch/mod.rs +++ b/gix/src/clone/fetch/mod.rs @@ -8,9 +8,6 @@ fn mapping_validation(err: &gix_error::Error) -> Option<&gix_refspec::match_grou err.downcast_any_ref() } -/// The error returned by [`PrepareFetch::fetch_only()`]. -pub type Error = gix_error::Error; - /// Modification impl PrepareFetch { /// Fetch a pack and update local branches according to refspecs, providing `progress` and checking `should_interrupt` to stop @@ -32,7 +29,7 @@ impl PrepareFetch { &mut self, mut progress: P, should_interrupt: &std::sync::atomic::AtomicBool, - ) -> Result<(crate::Repository, crate::remote::fetch::Outcome), Error> + ) -> Result<(crate::Repository, crate::remote::fetch::Outcome), crate::Error> where P: crate::NestedProgress, P::SubProgress: 'static, @@ -377,7 +374,7 @@ impl PrepareFetch { &mut self, progress: P, should_interrupt: &std::sync::atomic::AtomicBool, - ) -> Result<(crate::clone::PrepareCheckout, crate::remote::fetch::Outcome), Error> + ) -> Result<(crate::clone::PrepareCheckout, crate::remote::fetch::Outcome), crate::Error> where P: crate::NestedProgress, P::SubProgress: 'static, diff --git a/gix/src/clone/fetch/util.rs b/gix/src/clone/fetch/util.rs index 8d09355bc0c..77b499e9f1e 100644 --- a/gix/src/clone/fetch/util.rs +++ b/gix/src/clone/fetch/util.rs @@ -6,7 +6,6 @@ use gix_ref::{ transaction::{LogChange, RefLog}, }; -use super::Error; use crate::{ Repository, bstr::{BStr, BString, ByteSlice}, @@ -19,7 +18,7 @@ enum WriteMode { pub fn append_remote_to_local_config_file( remote: &mut crate::Remote<'_>, remote_name: BString, -) -> Result { +) -> Result { let mut config = gix_config::File::new(local_config_meta(remote.repo)); remote .save_as_to(remote_name, &mut config) @@ -50,7 +49,7 @@ pub fn append_remote_to_local_config_file( pub(super) fn reinitialize_with_object_hash( repo: &crate::Repository, object_hash: gix_hash::Kind, -) -> Result { +) -> Result { let git_dir = repo.git_dir(); let config_path = git_dir.join("config"); @@ -122,7 +121,7 @@ fn write_to_local_config(config: &gix_config::File, mode: WriteMode) -> std::io: pub fn append_config_to_repo_config( repo: &mut Repository, config: gix_config::File, -) -> Result<(), gix_config::parse::span::Error> { +) -> Result<(), gix_error::ValidationError> { let repo_config = gix_features::threading::OwnShared::make_mut(&mut repo.config.resolved); repo_config.append(config)?; Ok(()) @@ -138,10 +137,10 @@ pub fn update_head( remote_name: &BStr, ref_name: Option<&PartialName>, revision: Option<&gix_refspec::RefSpec>, -) -> Result<(), Error> { +) -> Result<(), crate::Error> { use gix_ref::transaction::{PreviousValue, RefEdit}; let revision_head_id = revision - .map(|revision| -> Result { + .map(|revision| -> Result { let mapping = find_revision(ref_map, revision)?; let id = mapping.remote.peeled_id().ok_or_else(|| revision_missing(revision))?; Ok(repo @@ -265,7 +264,7 @@ pub fn update_head( pub(super) fn find_revision<'a>( ref_map: &'a crate::remote::fetch::RefMap, revision: &gix_refspec::RefSpec, -) -> Result<&'a gix_protocol::fetch::refmap::Mapping, Error> { +) -> Result<&'a gix_protocol::fetch::refmap::Mapping, crate::Error> { ref_map .mappings .iter() @@ -278,7 +277,7 @@ pub(super) fn find_revision<'a>( .ok_or_else(|| revision_missing(revision)) } -fn revision_missing(revision: &gix_refspec::RefSpec) -> Error { +fn revision_missing(revision: &gix_refspec::RefSpec) -> crate::Error { gix_error::Error::from_error(gix_error::NotFoundError::new(format!( "The remote didn't have the requested revision {:?}", revision.to_ref().source().expect("validated revision") @@ -288,11 +287,11 @@ fn revision_missing(revision: &gix_refspec::RefSpec) -> Error { /// Resolve `ref_name` to its object ID and full name among the mapped remote references. /// /// Full names match directly. Partial names prefer branches over tags, then use normal refspec matching. -/// Returns [`Error::RefNameMissing`] or [`Error::RefNameAmbiguous`] when there is no unique match. +/// Returns an error when there is no unique match. pub(super) fn find_custom_refname<'a>( ref_map: &'a crate::remote::fetch::RefMap, ref_name: &PartialName, -) -> Result<(&'a gix_hash::oid, &'a BStr), Error> { +) -> Result<(&'a gix_hash::oid, &'a BStr), crate::Error> { let group = gix_refspec::MatchGroup::from_fetch_specs(Some( gix_refspec::parse(ref_name.as_ref().as_bstr(), gix_refspec::parse::Operation::Fetch) .expect("partial names are valid refs"), @@ -372,7 +371,7 @@ fn setup_branch_config( branch: &FullNameRef, branch_id: Option<&gix_hash::oid>, remote_name: &BStr, -) -> Result<(), Error> { +) -> Result<(), crate::Error> { let short_name = match branch.category_and_short_name() { Some((gix_ref::Category::LocalBranch, shortened)) => match shortened.to_str() { Ok(s) => s, diff --git a/gix/src/clone/mod.rs b/gix/src/clone/mod.rs index 4ce09d6682b..904b1772e5f 100644 --- a/gix/src/clone/mod.rs +++ b/gix/src/clone/mod.rs @@ -78,16 +78,13 @@ pub mod with_revision { } } - impl From for Error { - fn from(err: gix_refspec::parse::Error) -> Self { + impl From> for Error { + fn from(err: gix_error::Exn) -> Self { Error::Parse(err.into_error()) } } } -/// The error returned by [`PrepareFetch::new()`]. -pub type Error = gix_error::Error; - /// Instantiation impl PrepareFetch { /// Create a new repository at `path` with `create_opts` which is ready to clone from `url`, possibly after making additional adjustments to @@ -106,7 +103,7 @@ impl PrepareFetch { kind: crate::create::Kind, create_opts: crate::create::Options, open_opts: crate::open::Options, - ) -> Result + ) -> Result where Url: TryInto, E: std::error::Error + Send + Sync + 'static, @@ -119,7 +116,7 @@ impl PrepareFetch { kind: crate::create::Kind, mut create_opts: crate::create::Options, mut open_opts: crate::open::Options, - ) -> Result { + ) -> Result { if create_opts.destination_must_be_empty.is_none() { create_opts.destination_must_be_empty = Some(true); } diff --git a/gix/src/commit/mod.rs b/gix/src/commit/mod.rs index 5b457fad4e8..740970df2d3 100644 --- a/gix/src/commit/mod.rs +++ b/gix/src/commit/mod.rs @@ -42,9 +42,6 @@ fn signature_program( /// An empty array of a type usable with the `gix::easy` API to help declaring no parents should be used pub const NO_PARENT_IDS: [gix_hash::ObjectId; 0] = []; -/// The error returned by [`commit(…)`](crate::Repository::commit()). -pub type Error = gix_error::Error; - /// #[cfg(feature = "revision")] pub mod describe { @@ -65,7 +62,7 @@ pub mod describe { impl Resolution<'_> { /// Turn this instance into something displayable. - pub fn format(self) -> Result, Error> { + pub fn format(self) -> Result, crate::Error> { let prefix = self .id .shorten() @@ -83,7 +80,7 @@ pub mod describe { pub fn format_with_dirty_suffix( self, dirty_suffix: impl Into>, - ) -> Result, Error> { + ) -> Result, crate::Error> { let prefix = self .id .shorten() @@ -98,9 +95,6 @@ pub mod describe { } } - /// The error returned by [`try_format()`][Platform::try_format()]. - pub type Error = gix_error::Error; - /// A selector to choose what kind of references should contribute to names. #[derive(Default, Debug, Clone, Copy, PartialOrd, PartialEq, Ord, Eq, Hash)] pub enum SelectRef { @@ -114,7 +108,7 @@ pub mod describe { } impl SelectRef { - fn names(&self, repo: &Repository) -> Result>, Error> { + fn names(&self, repo: &Repository) -> Result>, crate::Error> { let platform = repo.references().or_erased()?; Ok(match self { @@ -222,7 +216,7 @@ pub mod describe { /// Try to find a name for the configured commit id using all prior configuration, returning `Some(describe::Format)` /// if one was found, or `None` if that wasn't the case. - pub fn try_format(&self) -> Result>, Error> { + pub fn try_format(&self) -> Result>, crate::Error> { self.try_resolve()?.map(Resolution::format).transpose() } @@ -238,7 +232,7 @@ pub mod describe { pub fn try_resolve_with_cache( &self, cache: Option<&'_ gix_commitgraph::Graph>, - ) -> Result>, Error> { + ) -> Result>, crate::Error> { let mut graph = self.repo.revision_graph(cache); let outcome = gix_revision::describe( &self.id, @@ -262,13 +256,13 @@ pub mod describe { /// # Performance /// /// Prefer to use the [`Self::try_resolve_with_cache()`] method when processing more than one commit at a time. - pub fn try_resolve(&self) -> Result>, Error> { + pub fn try_resolve(&self) -> Result>, crate::Error> { let cache = self.repo.commit_graph_if_enabled()?; self.try_resolve_with_cache(cache.as_ref()) } /// Like [`try_format()`](Self::try_format()), but turns `id_as_fallback()` on to always produce a format. - pub fn format(&mut self) -> Result, Error> { + pub fn format(&mut self) -> Result, crate::Error> { self.id_as_fallback = true; Ok(self.try_format()?.expect("BUG: fallback must always produce a format")) } diff --git a/gix/src/commit/sign.rs b/gix/src/commit/sign.rs index 1a9e5243f37..e28eb6699a3 100644 --- a/gix/src/commit/sign.rs +++ b/gix/src/commit/sign.rs @@ -9,16 +9,7 @@ use gix_error::ResultExt; use gix_object::signature::sign::is_literal_ssh_key; pub use gix_object::signature::{Format, sign::Options}; -/// Errors encountered when applying resolved signing options to a commit. -pub type Error = gix_error::Error; - -/// Errors encountered when resolving commit-signing options. -pub mod options { - /// The error returned by [`crate::Repository::commit_signing_options()`]. - pub type Error = gix_error::Error; -} - -pub(crate) fn sign<'repo>(commit: &crate::Commit<'repo>) -> Result, Error> { +pub(crate) fn sign<'repo>(commit: &crate::Commit<'repo>) -> Result, crate::Error> { let options = commit.repo.commit_signing_options()?; let signed = commit .decode() @@ -29,15 +20,14 @@ pub(crate) fn sign<'repo>(commit: &crate::Commit<'repo>) -> Result Result { - use options::Error; - +pub(crate) fn signing_options(repo: &crate::Repository) -> Result { let config = repo.config_snapshot(); let format = config .string(Gpg::FORMAT) .unwrap_or_else(|| Gpg::FORMAT.default_value_or_panic().into()); - let format = parse_format(format.trim()) - .ok_or_else(|| Error::from_error(gix_error::message!("Unsupported value for gpg.format: {format:?}")))?; + let format = parse_format(format.trim()).ok_or_else(|| { + gix_error::Error::from_error(gix_error::message!("Unsupported value for gpg.format: {format:?}")) + })?; let program = super::signature_program(&config, format) .or_raise(|| gix_error::message("Could not interpolate the configured signing program path"))?; let signing_key = match config @@ -50,7 +40,7 @@ pub(crate) fn signing_options(repo: &crate::Repository) -> Result gix_path::from_bstring(key).into_os_string(), _ if format == Format::Ssh => default_ssh_key(&config)?.ok_or_else(|| { - Error::from_error(gix_error::message( + gix_error::Error::from_error(gix_error::message( "user.signingKey or gpg.ssh.defaultKeyCommand must provide an SSH signing key", )) })?, @@ -58,7 +48,7 @@ pub(crate) fn signing_options(repo: &crate::Repository) -> Result Result Result, options::Error> { +pub(crate) fn signing_options_if_enabled(repo: &crate::Repository) -> Result, crate::Error> { let enabled = repo .config .may_sign_commits() @@ -100,9 +90,7 @@ fn parse_format(value: &[u8]) -> Option { } } -fn default_ssh_key(config: &crate::config::Snapshot<'_>) -> Result, options::Error> { - use options::Error; - +fn default_ssh_key(config: &crate::config::Snapshot<'_>) -> Result, crate::Error> { let Some(program) = config.trusted_program(gpg::Ssh::DEFAULT_KEY_COMMAND) else { return Ok(None); }; @@ -116,14 +104,14 @@ fn default_ssh_key(config: &crate::config::Snapshot<'_>) -> Result) -> Result, Error> { +pub(crate) fn verify(commit: &crate::Commit<'_>) -> Result, crate::Error> { let Some((signature, signed_data)) = commit .signature() .or_raise(|| gix_error::message("Could not decode the commit signature"))? @@ -25,8 +22,9 @@ pub(crate) fn verify(commit: &crate::Commit<'_>) -> Result, Erro .transpose() .or_raise(|| gix_error::message("The configured minimum signature trust level is invalid"))? .unwrap_or_default(); - let format = Format::from_signature(&signature) - .ok_or_else(|| Error::from_error(gix_error::CorruptionError::new("The signature format is unsupported")))?; + let format = Format::from_signature(&signature).ok_or_else(|| { + gix_error::Error::from_error(gix_error::CorruptionError::new("The signature format is unsupported")) + })?; let program = super::signature_program(&config, format) .or_raise(|| gix_error::message("Could not interpolate the configured signature-verification program path"))?; let options = match format { @@ -47,7 +45,7 @@ pub(crate) fn verify(commit: &crate::Commit<'_>) -> Result, Erro .trusted_path(gpg::Ssh::ALLOWED_SIGNERS_FILE) .or_raise(|| gix_error::message("Could not interpolate a configured signature-verification path"))? .ok_or_else(|| { - Error::from_error(gix_error::message( + gix_error::Error::from_error(gix_error::message( "gpg.ssh.allowedSignersFile must be configured for SSH signature verification", )) })?; diff --git a/gix/src/config/cache/access.rs b/gix/src/config/cache/access.rs index 7ecb24dc93c..90978926861 100644 --- a/gix/src/config/cache/access.rs +++ b/gix/src/config/cache/access.rs @@ -42,7 +42,7 @@ impl Cache { } #[cfg(feature = "blob-diff")] - pub(crate) fn diff_drivers(&self) -> Result, config::diff::drivers::Error> { + pub(crate) fn diff_drivers(&self) -> Result, crate::Error> { use crate::config::cache::util::ApplyLeniencyDefault; let mut out = Vec::::new(); for section in self @@ -107,7 +107,7 @@ impl Cache { } #[cfg(feature = "merge")] - pub(crate) fn merge_drivers(&self) -> Result, config::merge::drivers::Error> { + pub(crate) fn merge_drivers(&self) -> Result, crate::Error> { let mut out = Vec::::new(); for section in self .resolved @@ -143,18 +143,14 @@ impl Cache { } #[cfg(feature = "merge")] - pub(crate) fn merge_pipeline_options( - &self, - ) -> Result { + pub(crate) fn merge_pipeline_options(&self) -> Result { Ok(gix_merge::blob::pipeline::Options { large_file_threshold_bytes: self.big_file_threshold().map_err(gix_error::Error::from)?, }) } #[cfg(feature = "blob-diff")] - pub(crate) fn diff_pipeline_options( - &self, - ) -> Result { + pub(crate) fn diff_pipeline_options(&self) -> Result { Ok(gix_diff::blob::pipeline::Options { large_file_threshold_bytes: self.big_file_threshold().map_err(gix_error::Error::from)?, fs: self.fs_capabilities().map_err(gix_error::Error::from)?, @@ -162,7 +158,7 @@ impl Cache { } #[cfg(feature = "blob-diff")] - pub(crate) fn diff_renames(&self) -> Result<(Option, bool), crate::diff::new_rewrites::Error> { + pub(crate) fn diff_renames(&self) -> Result<(Option, bool), crate::Error> { self.diff_renames .get_or_try_init(|| crate::diff::new_rewrites(&self.resolved, self.lenient_config)) .copied() @@ -212,7 +208,7 @@ impl Cache { } #[cfg(any(feature = "blocking-network-client", feature = "async-network-client"))] - pub(crate) fn url_scheme(&self) -> Result<&remote::url::SchemePermission, remote::url::scheme_permission::Error> { + pub(crate) fn url_scheme(&self) -> Result<&remote::url::SchemePermission, crate::Error> { self.url_scheme .get_or_try_init(|| remote::url::SchemePermission::from_config(&self.resolved, self.filter_config_section)) } @@ -264,16 +260,13 @@ impl Cache { /// The path to the user-level excludes file to ignore certain files in the worktree. #[cfg(feature = "excludes")] - pub(crate) fn excludes_file(&self) -> Result, gix_config::path::interpolate::Error> { + pub(crate) fn excludes_file(&self) -> Result, gix_error::Exn> { self.trusted_file_path(Core::EXCLUDES_FILE) } /// A helper to obtain a file from trusted configuration at `section_name`, `subsection_name`, and `key`, which is interpolated /// if present. - pub(crate) fn trusted_file_path( - &self, - key: impl gix_config::AsKey, - ) -> Result, gix_config::path::interpolate::Error> { + pub(crate) fn trusted_file_path(&self, key: impl gix_config::AsKey) -> Result, gix_error::Exn> { trusted_file_path( &self.resolved, key, @@ -297,7 +290,7 @@ impl Cache { } #[cfg(feature = "index")] - pub(crate) fn stat_options(&self) -> Result { + pub(crate) fn stat_options(&self) -> Result { use crate::config::tree::gitoxide; Ok(gix_index::entry::stat::Options { trust_ctime: boolean(self, "core.trustCTime", &Core::TRUST_C_TIME, true).map_err(gix_error::Error::from)?, @@ -345,7 +338,7 @@ impl Cache { &self, repo: &crate::Repository, attributes_source: gix_worktree::stack::state::attributes::Source, - ) -> Result { + ) -> Result { use crate::config::tree::gitoxide; let git_dir = repo.git_dir(); let thread_limit = self @@ -414,7 +407,7 @@ impl Cache { overrides: Option, source: gix_worktree::stack::state::ignore::Source, buf: &mut Vec, - ) -> Result { + ) -> Result { let excludes_file = match self.excludes_file().map_err(|err| { gix_error::Error::from(err.raise(gix_error::message( "The value for `core.excludesFile` could not be read from configuration", @@ -440,7 +433,7 @@ impl Cache { git_dir: &std::path::Path, source: gix_worktree::stack::state::attributes::Source, attributes: crate::open::permissions::Attributes, - ) -> Result<(gix_worktree::stack::state::Attributes, Vec), config::attribute_stack::Error> { + ) -> Result<(gix_worktree::stack::state::Attributes, Vec), crate::Error> { use gix_attributes::Source; let configured_or_user_attributes = match self.trusted_file_path(Core::ATTRIBUTES_FILE).or_raise(|| { gix_error::message("Failed to interpolate the attribute file configured at `core.attributesFile`") @@ -479,7 +472,7 @@ impl Cache { #[cfg(feature = "attributes")] pub(crate) fn pathspec_defaults( &self, - ) -> Result { + ) -> Result> { use crate::config::tree::gitoxide; let res = gix_pathspec::Defaults::from_environment(&mut |name| { let key = [ @@ -557,7 +550,7 @@ fn compression( mut filter_config_section: fn(&gix_config::file::Metadata) -> bool, key: &'static config::tree::keys::Compression, default: gix_zlib::Compression, -) -> Result { +) -> Result { let level = match key .try_into_compression(config.integer_filter(key, &mut filter_config_section)) .with_leniency(lenient)? @@ -574,7 +567,7 @@ pub(crate) fn loose_compression( config: &gix_config::File, lenient: bool, filter_config_section: fn(&gix_config::file::Metadata) -> bool, -) -> Result { +) -> Result { compression( config, lenient, @@ -588,7 +581,7 @@ pub(crate) fn pack_compression( config: &gix_config::File, lenient: bool, filter_config_section: fn(&gix_config::file::Metadata) -> bool, -) -> Result { +) -> Result { compression( config, lenient, @@ -604,7 +597,7 @@ pub(crate) fn trusted_file_path( filter: impl FnMut(&Metadata) -> bool, lenient_config: bool, environment: crate::open::permissions::Environment, -) -> Result, gix_config::path::interpolate::Error> { +) -> Result, gix_error::Exn> { let Some(path) = config.path_filter(key, filter) else { return Ok(None); }; diff --git a/gix/src/config/cache/incubate.rs b/gix/src/config/cache/incubate.rs index af54d22dc97..d87b4b98c2e 100644 --- a/gix/src/config/cache/incubate.rs +++ b/gix/src/config/cache/incubate.rs @@ -1,6 +1,6 @@ #![allow(clippy::result_large_err)] -use super::{Error, util}; +use super::util; use crate::config::{ cache::util::{ApplyLeniency, ApplyLeniencyDefaultValue}, tree::{Core, Extensions, gitoxide}, @@ -29,7 +29,7 @@ impl StageOne { git_dir_trust: gix_sec::Trust, lossy: bool, lenient: bool, - ) -> Result { + ) -> Result { let mut buf = Vec::with_capacity(512); let mut config = load_config( common_dir.join("config"), @@ -109,7 +109,7 @@ impl StageOne { /// Git interprets a missing objectFormat as the original Sha1 layout, so we return /// gix_hash::Kind::Sha1 whenever this build can handle it. /// In Sha256-only builds we cannot open such a repository, so return an error instead. -fn legacy_object_hash() -> Result { +fn legacy_object_hash() -> Result { #[cfg(feature = "sha1")] { Ok(gix_hash::Kind::Sha1) @@ -129,7 +129,7 @@ fn load_config( git_dir_trust: gix_sec::Trust, lossy: bool, lenient: bool, -) -> Result { +) -> Result { let metadata = gix_config::file::Metadata::from(source) .at(&config_path) .with(git_dir_trust); diff --git a/gix/src/config/cache/init.rs b/gix/src/config/cache/init.rs index 642d442ec47..2cf5b615422 100644 --- a/gix/src/config/cache/init.rs +++ b/gix/src/config/cache/init.rs @@ -4,7 +4,7 @@ use std::ffi::OsString; use gix_error::{ErrorExt, ResultExt}; use gix_sec::Permission; -use super::{Error, StageOne, interpolate_context, util}; +use super::{StageOne, interpolate_context, util}; use crate::{ bstr::BString, config, @@ -45,7 +45,7 @@ impl Cache { api_config_overrides: &[BString], cli_config_overrides: &[BString], use_repository_local_environment: bool, - ) -> Result { + ) -> Result { let config = load( Some(git_dir_config), &mut buf, @@ -120,7 +120,10 @@ impl Cache { /// However, those that are lazily read won't be re-evaluated right away and might thus pass now but fail later. /// /// Note that we unconditionally re-read all values. - pub fn reread_values_and_clear_caches_replacing_config(&mut self, config: crate::Config) -> Result<(), Error> { + pub fn reread_values_and_clear_caches_replacing_config( + &mut self, + config: crate::Config, + ) -> Result<(), crate::Error> { let prev = std::mem::replace(&mut self.resolved, config); match self.reread_values_and_clear_caches() { Err(err) => { @@ -133,7 +136,7 @@ impl Cache { /// Similar to `reread_values_and_clear_caches_replacing_config()`, but works on the existing configuration instead of a passed /// in one that it them makes the default. - pub fn reread_values_and_clear_caches(&mut self) -> Result<(), Error> { + pub fn reread_values_and_clear_caches(&mut self) -> Result<(), crate::Error> { let config = &self.resolved; let hex_len = util::parse_core_abbrev(config, self.object_hash).with_leniency(self.lenient_config)?; @@ -276,7 +279,7 @@ pub(crate) fn load( api_config_overrides: &[BString], cli_config_overrides: &[BString], use_repository_local_environment: bool, -) -> Result { +) -> Result { let options = gix_config::file::init::Options { includes: if use_includes { gix_config::file::includes::Options::follow( @@ -379,7 +382,7 @@ impl crate::Repository { pub(crate) fn reread_values_and_clear_caches_replacing_config( &mut self, config: crate::Config, - ) -> Result<(), Error> { + ) -> Result<(), crate::Error> { let ( previous_static_pack_cache_limit_bytes, previous_pack_cache_bytes, @@ -421,7 +424,7 @@ fn apply_environment_overrides( identity: Permission, objects: Permission, use_repository_local_environment: bool, -) -> Result<(), Error> { +) -> Result<(), crate::Error> { fn env(key: &'static dyn config::tree::Key) -> &'static str { key.the_environment_override() } diff --git a/gix/src/config/cache/mod.rs b/gix/src/config/cache/mod.rs index ef618963d4d..f3539e0d027 100644 --- a/gix/src/config/cache/mod.rs +++ b/gix/src/config/cache/mod.rs @@ -1,4 +1,4 @@ -use super::{Cache, Error}; +use super::Cache; mod incubate; pub(crate) use incubate::StageOne; diff --git a/gix/src/config/cache/util.rs b/gix/src/config/cache/util.rs index 83125cd36a2..ed1beb121a4 100644 --- a/gix/src/config/cache/util.rs +++ b/gix/src/config/cache/util.rs @@ -1,5 +1,5 @@ #![allow(clippy::result_large_err)] -use super::Error; + use crate::{ config, config::tree::{Core, gitoxide}, @@ -31,7 +31,7 @@ pub(crate) fn config_bool( key_str: &str, default: bool, lenient: bool, -) -> Result { +) -> Result { use config::tree::Key; debug_assert_eq!( key_str, @@ -40,7 +40,7 @@ pub(crate) fn config_bool( ); Ok(key .enrich_error(config.boolean(key_str)) - .map_err(Error::from) + .map_err(gix_error::Error::from) .with_lenient_default(lenient)? .unwrap_or(default)) } @@ -50,7 +50,7 @@ pub(crate) fn config_bool_opt( key: &'static config::tree::keys::Boolean, key_str: &str, lenient: bool, -) -> Result, Error> { +) -> Result, crate::Error> { use config::tree::Key; debug_assert_eq!( key_str, @@ -58,14 +58,14 @@ pub(crate) fn config_bool_opt( "BUG: key name and hardcoded name must match" ); key.enrich_error(config.boolean(key_str)) - .map_err(Error::from) + .map_err(gix_error::Error::from) .with_leniency(lenient) } pub(crate) fn query_refupdates( config: &gix_config::File, lenient_config: bool, -) -> Result, Error> { +) -> Result, crate::Error> { let key = "core.logAllRefUpdates"; Core::LOG_ALL_REF_UPDATES .try_into_ref_updates(config.boolean(key)) @@ -103,7 +103,7 @@ pub(crate) fn parse_object_caches( config: &gix_config::File, lenient: bool, mut filter_config_section: fn(&gix_config::file::Metadata) -> bool, -) -> Result { +) -> Result { let static_pack_cache_limit = gitoxide::Core::DEFAULT_PACK_CACHE_MEMORY_LIMIT .try_into_usize(config.integer_filter("gitoxide.core.deltaBaseCacheLimit", &mut filter_config_section)) .with_leniency(lenient)?; @@ -128,7 +128,7 @@ pub(crate) fn parse_object_caches( pub(crate) fn parse_core_abbrev( config: &gix_config::File, object_hash: gix_hash::Kind, -) -> Result, Error> { +) -> Result, crate::Error> { Ok(config .string("core.abbrev") .map(|abbrev| Core::ABBREV.try_into_abbreviation(abbrev, object_hash)) diff --git a/gix/src/config/file_mut.rs b/gix/src/config/file_mut.rs index 21993b34210..f6862ad9378 100644 --- a/gix/src/config/file_mut.rs +++ b/gix/src/config/file_mut.rs @@ -9,9 +9,6 @@ use gix_error::{ErrorExt, ResultExt, message}; use super::FileTransaction; -/// The error produced by [`crate::config_path()`] or when opening or committing a [`FileTransaction`]. -pub type Error = gix_error::Error; - impl FileTransaction { pub(crate) fn open( path: std::path::PathBuf, @@ -19,7 +16,7 @@ impl FileTransaction { trust: gix_sec::Trust, lock_mode: gix_lock::acquire::Fail, shared_repository_permissions: i32, - ) -> Result { + ) -> Result { let adjust_permissions = |permissions| gix_fs::adjust_shared_repository_permissions(permissions, shared_repository_permissions); let adjust_permissions: Option<&dyn Fn(std::fs::Permissions) -> std::fs::Permissions> = @@ -64,7 +61,7 @@ impl FileTransaction { } /// Write this physical file atomically without changing any repository instance. - pub fn commit(mut self) -> Result<(), Error> { + pub fn commit(mut self) -> Result<(), crate::Error> { let path = self.lock.resource_path(); self.config .write_to(&mut self.lock) @@ -99,7 +96,7 @@ impl FileTransaction { pub(crate) fn shared_repository_permissions( config: &gix_config::File, filter: fn(&gix_config::file::Metadata) -> bool, -) -> Result { +) -> Result { let value = config.sections_by_name_and_filter("core", filter).and_then(|sections| { sections .filter(|section| section.header().subsection_name().is_none()) diff --git a/gix/src/config/mod.rs b/gix/src/config/mod.rs index 85a80897c8a..7d5e9013078 100644 --- a/gix/src/config/mod.rs +++ b/gix/src/config/mod.rs @@ -63,32 +63,6 @@ pub mod section { } } -/// -pub mod set_value { - /// The error produced when calling [`SnapshotMut::set(_subsection)?_value()`][crate::config::SnapshotMut::set_value()] - pub type Error = gix_error::Error; -} - -/// The error returned when failing to initialize the repository configuration. -/// -/// This configuration is on the critical path when opening a repository. -pub type Error = gix_error::Error; - -/// -pub mod merge { - /// - pub mod pipeline_options { - /// The error produced when obtaining options needed to fill in [gix_merge::blob::pipeline::Options]. - pub type Error = gix_error::Error; - } - - /// - pub mod drivers { - /// The error produced when obtaining a list of [Drivers](gix_merge::blob::Driver). - pub type Error = gix_error::Error; - } -} - /// pub mod diff { /// @@ -114,66 +88,6 @@ pub mod diff { impl std::error::Error for Error {} } - - /// - pub mod pipeline_options { - /// The error produced when obtaining options needed to fill in [gix_diff::blob::pipeline::Options]. - pub type Error = gix_error::Error; - } - - /// - pub mod drivers { - /// The error produced when obtaining a list of [Drivers](gix_diff::blob::Driver). - pub type Error = gix_error::Error; - } -} - -/// -pub mod stat_options { - /// The error produced when collecting stat information, and returned by [Repository::stat_options()](crate::Repository::stat_options()). - pub type Error = gix_error::Error; -} - -/// -#[cfg(feature = "attributes")] -pub mod checkout_options { - /// The error produced when collecting all information needed for checking out files into a worktree. - pub type Error = gix_error::Error; -} - -/// -#[cfg(feature = "command")] -pub mod command_context { - /// The error produced when collecting all information relevant to spawned commands, - /// obtained via [Repository::command_context()](crate::Repository::command_context()). - pub type Error = gix_error::Error; -} - -/// -pub mod exclude_stack { - /// The error produced when setting up a stack to query `gitignore` information. - pub type Error = gix_error::Error; -} - -/// -pub mod attribute_stack { - /// The error produced when setting up the attribute stack to query `gitattributes`. - pub type Error = gix_error::Error; -} - -/// -pub mod protocol { - /// - pub mod allow { - /// The error returned when obtaining the permission for a particular scheme. - pub type Error = gix_error::Error; - } -} - -/// -pub mod ssh_connect_options { - /// The error produced when obtaining ssh connection configuration. - pub type Error = gix_error::Error; } /// @@ -314,12 +228,6 @@ pub mod key { pub type GenericErrorWithValue = Error; } -/// -pub mod encoding { - /// The error produced when failing to parse the `core.checkRoundTripEncoding` key. - pub type Error = gix_error::Error; -} - /// pub mod checkout { /// @@ -331,31 +239,19 @@ pub mod checkout { } } -/// -pub mod abbrev { - /// The error describing an incorrect `core.abbrev` value. - pub type Error = gix_error::Error; -} - /// pub mod remote { /// pub mod symbolic_name { /// The error produced when failing to produce a symbolic remote name from configuration. - pub type Error = super::super::key::Error; + pub type Error = super::super::key::Error; } } /// pub mod time { /// The error produced when failing to parse time from configuration. - pub type Error = super::key::Error; -} - -/// -pub mod commit_signature { - /// The error produced when obtaining or installing a fallback commit signature. - pub type Error = gix_error::Error; + pub type Error = super::key::Error; } /// @@ -412,18 +308,6 @@ pub mod ssl_version { pub type Error = super::key::Error; } -/// -pub mod transport { - /// The error produced when configuring a transport for a particular protocol. - pub type Error = gix_error::Error; - - /// - pub mod http { - /// The error produced when configuring a HTTP transport. - pub type Error = gix_error::Error; - } -} - /// Utility type to keep pre-obtained configuration values, only for those required during initial setup /// and other basic operations that are common enough to warrant a permanent cache. /// diff --git a/gix/src/config/overrides.rs b/gix/src/config/overrides.rs index 5bb286c33fb..fe160340c30 100644 --- a/gix/src/config/overrides.rs +++ b/gix/src/config/overrides.rs @@ -2,15 +2,12 @@ use gix_error::ResultExt; use crate::bstr::{BStr, BString, ByteSlice}; -/// The error returned by [`SnapshotMut::apply_cli_overrides()`][crate::config::SnapshotMut::append_config()]. -pub type Error = gix_error::Error; - pub(crate) fn append( config: &mut gix_config::File, values: impl IntoIterator, source: gix_config::Source, mut make_comment: impl FnMut(&BStr) -> Option, -) -> Result<(), Error> { +) -> Result<(), crate::Error> { let mut file = gix_config::File::new(gix_config::file::Metadata::from(source)); for key_value in values { let key_value = key_value.as_bstr(); diff --git a/gix/src/config/snapshot/access.rs b/gix/src/config/snapshot/access.rs index c5eadcc7295..865932063a3 100644 --- a/gix/src/config/snapshot/access.rs +++ b/gix/src/config/snapshot/access.rs @@ -97,7 +97,7 @@ impl<'repo> SnapshotMut<'repo> { &mut self, values: impl IntoIterator, source: gix_config::Source, - ) -> Result<&mut Self, crate::config::overrides::Error> { + ) -> Result<&mut Self, crate::Error> { crate::config::overrides::append(&mut self.config, values, source, |v| Some(format!("-c {v}").into()))?; Ok(self) } @@ -105,7 +105,7 @@ impl<'repo> SnapshotMut<'repo> { /// /// Note that this would also happen once this instance is dropped, but using this method may be more intuitive and won't squelch errors /// in case the new configuration is partially invalid. - pub fn commit(mut self) -> Result<&'repo mut crate::Repository, crate::config::Error> { + pub fn commit(mut self) -> Result<&'repo mut crate::Repository, crate::Error> { let repo = self.repo.take().expect("always present here"); self.commit_inner(repo) } @@ -116,7 +116,7 @@ impl<'repo> SnapshotMut<'repo> { &mut self, key: &'static dyn crate::config::tree::Key, new_value: impl gix_utils::AsBStr, - ) -> Result, crate::config::set_value::Error> { + ) -> Result, crate::Error> { if let Some(crate::config::tree::SubSectionRequirement::Parameter(_)) = key.subsection_requirement() { return Err(gix_error::Error::from_error(gix_error::ValidationError::new( "The key needs a subsection parameter to be valid.", @@ -145,7 +145,7 @@ impl<'repo> SnapshotMut<'repo> { key: &'static dyn crate::config::tree::Key, subsection: impl gix_utils::AsBStr, new_value: impl gix_utils::AsBStr, - ) -> Result, crate::config::set_value::Error> { + ) -> Result, crate::Error> { if let Some(crate::config::tree::SubSectionRequirement::Never) = key.subsection_requirement() { return Err(gix_error::Error::from_error(gix_error::ValidationError::new( "The key must not be used with a subsection", @@ -169,13 +169,13 @@ impl<'repo> SnapshotMut<'repo> { pub(crate) fn commit_inner( &mut self, repo: &'repo mut crate::Repository, - ) -> Result<&'repo mut crate::Repository, crate::config::Error> { + ) -> Result<&'repo mut crate::Repository, crate::Error> { repo.reread_values_and_clear_caches_replacing_config(std::mem::take(&mut self.config).into())?; Ok(repo) } /// Create a structure the temporarily commits the changes, but rolls them back when dropped. - pub fn commit_auto_rollback(mut self) -> Result, crate::config::Error> { + pub fn commit_auto_rollback(mut self) -> Result, crate::Error> { let repo = self.repo.take().expect("this only runs once on consumption"); let prev_config = OwnShared::clone(&repo.config.resolved); @@ -195,7 +195,7 @@ impl<'repo> SnapshotMut<'repo> { /// Utilities impl<'repo> CommitAutoRollback<'repo> { /// Rollback the changes previously applied and all values before the change. - pub fn rollback(mut self) -> Result<&'repo mut crate::Repository, crate::config::Error> { + pub fn rollback(mut self) -> Result<&'repo mut crate::Repository, crate::Error> { let repo = self.repo.take().expect("still present, consumed only once"); self.rollback_inner(repo) } @@ -203,7 +203,7 @@ impl<'repo> CommitAutoRollback<'repo> { pub(crate) fn rollback_inner( &mut self, repo: &'repo mut crate::Repository, - ) -> Result<&'repo mut crate::Repository, crate::config::Error> { + ) -> Result<&'repo mut crate::Repository, crate::Error> { repo.reread_values_and_clear_caches_replacing_config(OwnShared::clone(&self.prev_config))?; Ok(repo) } diff --git a/gix/src/config/snapshot/credential_helpers.rs b/gix/src/config/snapshot/credential_helpers.rs index b74d8604964..8f6f42dd1f9 100644 --- a/gix/src/config/snapshot/credential_helpers.rs +++ b/gix/src/config/snapshot/credential_helpers.rs @@ -1,8 +1,5 @@ use crate::config::Snapshot; -/// The error returned by [`Snapshot::credential_helpers()`][Snapshot::credential_helpers()]. -pub type Error = gix_error::Error; - impl Snapshot<'_> { /// Returns the configuration for all git-credential helpers from trusted configuration that apply /// to the given `url` along with an action preconfigured to invoke the cascade with. @@ -16,7 +13,7 @@ impl Snapshot<'_> { gix_credentials::helper::Action, gix_prompt::Options, ), - Error, + crate::Error, > { let repo = self.repo; function::credential_helpers( @@ -37,7 +34,6 @@ pub(super) mod function { bstr::{ByteSlice, ByteVec}, config::{ cache::util::ApplyLeniency, - credential_helpers::Error, tree::{Core, Credential, credential, gitoxide::Credentials}, }, }; @@ -85,7 +81,7 @@ pub(super) mod function { gix_credentials::helper::Action, gix_prompt::Options, ), - Error, + crate::Error, > { let mut programs = Vec::new(); let mut context_options = gix_credentials::protocol::ContextOptions::default(); diff --git a/gix/src/config/tree/keys.rs b/gix/src/config/tree/keys.rs index 2585bd5c1f8..013d50ba0bc 100644 --- a/gix/src/config/tree/keys.rs +++ b/gix/src/config/tree/keys.rs @@ -139,7 +139,7 @@ impl Key for Any { self.name } - fn validate(&self, value: &BStr) -> Result<(), config::tree::key::validate::Error> { + fn validate(&self, value: &BStr) -> Result<(), crate::Error> { self.validate.validate(value).map_err(|err| { err.raise(gix_error::ValidationError::new_with_input( format!("Invalid value for configuration key '{}'", self.logical_name()), @@ -261,7 +261,7 @@ mod duration { /// Return a valid duration as parsed from an integer that is interpreted as milliseconds. pub fn try_into_duration( &'static self, - value: Result, gix_config::value::Error>, + value: Result, gix_error::Exn>, ) -> Result, config::duration::Error> { let Some(value) = value.map_err(|err| config::duration::Error::from(self).with_source(err.into_error()))? else { @@ -294,7 +294,7 @@ mod lock_timeout { /// Return information on how long to wait for locked files. pub fn try_into_lock_timeout( &'static self, - value: Result, gix_config::value::Error>, + value: Result, gix_error::Exn>, ) -> Result, config::lock_timeout::Error> { let Some(value) = value.map_err(|err| config::lock_timeout::Error::from(self).with_source(err.into_error()))? @@ -328,7 +328,7 @@ mod compression { /// zlib default, just like `git` does. pub fn try_into_compression( &'static self, - value: Result, gix_config::value::Error>, + value: Result, gix_error::Exn>, ) -> Result, config::key::GenericError> { let Some(value) = value.map_err(|err| config::key::GenericError::from(self).with_source(err.into_error()))? @@ -433,7 +433,7 @@ mod workers { /// Convert `value` into a `usize` or wrap it into a specialized error. pub fn try_into_usize( &'static self, - value: Result, gix_config::value::Error>, + value: Result, gix_error::Exn>, ) -> Result, crate::config::unsigned_integer::Error> { let value = value .map_err(|err| crate::config::unsigned_integer::Error::from(self).with_source(err.into_error()))?; @@ -449,7 +449,7 @@ mod workers { /// Convert `value` into a `u64` or wrap it into a specialized error. pub fn try_into_u64( &'static self, - value: Result, gix_config::value::Error>, + value: Result, gix_error::Exn>, ) -> Result, crate::config::unsigned_integer::Error> { let value = value .map_err(|err| crate::config::unsigned_integer::Error::from(self).with_source(err.into_error()))?; @@ -465,7 +465,7 @@ mod workers { /// Convert `value` into a `u32` or wrap it into a specialized error. pub fn try_into_u32( &'static self, - value: Result, gix_config::value::Error>, + value: Result, gix_error::Exn>, ) -> Result, crate::config::unsigned_integer::Error> { let value = value .map_err(|err| crate::config::unsigned_integer::Error::from(self).with_source(err.into_error()))?; @@ -501,13 +501,13 @@ mod time { &self, value: impl gix_utils::AsBStr, now: Option, - ) -> Result> { + ) -> Result> { let value = value.as_bstr(); gix_date::parse( value .as_bstr() .to_str() - .map_err(|_| gix_date::Error::new_with_input("UTF8 conversion failed", value))?, + .map_err(|_| gix_error::ValidationError::new_with_input("UTF8 conversion failed", value))?, now, ) } @@ -534,7 +534,7 @@ mod boolean { /// `value` is expected to be provided by [`gix_config::File::boolean()`]. pub fn enrich_error( &'static self, - value: Result, gix_config::value::Error>, + value: Result, gix_error::Exn>, ) -> Result, config::boolean::Error> { value.map_err(|err| config::boolean::Error::from(self).with_source(err.into_error())) } diff --git a/gix/src/config/tree/mod.rs b/gix/src/config/tree/mod.rs index 66c65a44746..642812a5e6c 100644 --- a/gix/src/config/tree/mod.rs +++ b/gix/src/config/tree/mod.rs @@ -127,19 +127,5 @@ pub use sections::{Status, status}; /// Generic value implementations for static instantiation. pub mod keys; -/// -pub mod key { - /// - pub mod validate { - /// The error returned by [`Key::validate()`][crate::config::tree::Key::validate()]. - pub type Error = gix_error::Error; - } - /// - pub mod validate_assignment { - /// The error returned by [`Key::validated_assignment`*()][crate::config::tree::Key::validated_assignment_fmt()]. - pub type Error = gix_error::Error; - } -} - mod traits; pub use traits::{Key, Link, Note, Section, SubSectionRequirement}; diff --git a/gix/src/config/tree/sections/checkout.rs b/gix/src/config/tree/sections/checkout.rs index 4d83e79f030..b46131fcbfd 100644 --- a/gix/src/config/tree/sections/checkout.rs +++ b/gix/src/config/tree/sections/checkout.rs @@ -30,7 +30,7 @@ mod workers { /// or `None` if the value isn't set which is typically interpreted as "as many threads as available" pub fn try_from_workers( &'static self, - value: Result, gix_config::value::Error>, + value: Result, gix_error::Exn>, ) -> Result, crate::config::checkout::workers::Error> { match value { Ok(Some(v)) if v < 0 => Ok(Some(0)), diff --git a/gix/src/config/tree/sections/core.rs b/gix/src/config/tree/sections/core.rs index 4a960a8c582..b102ec95eca 100644 --- a/gix/src/config/tree/sections/core.rs +++ b/gix/src/config/tree/sections/core.rs @@ -203,7 +203,6 @@ mod filter { mod check_round_trip_encoding { use crate::{ bstr::ByteSlice, - config, config::tree::{Key, core::CheckRoundTripEncoding}, }; @@ -213,7 +212,7 @@ mod filter { pub fn try_into_encodings( &'static self, value: Option, - ) -> Result, config::encoding::Error> { + ) -> Result, crate::Error> { Ok(match value { None => vec![gix_filter::encoding::SHIFT_JIS], Some(value) => { @@ -397,7 +396,7 @@ mod log_all_ref_updates { /// the interpretation of booleans in special in `git-config`, i.e. we can't just treat it as string. pub fn try_into_ref_updates( &'static self, - value: Result, gix_config::value::Error>, + value: Result, gix_error::Exn>, ) -> Result, config::key::GenericErrorWithValue> { match value { Ok(Some(bool)) => Ok(Some(if bool { @@ -441,9 +440,8 @@ mod check_stat { } mod abbrev { - use config::abbrev::Error; - use crate::{bstr::ByteSlice, config, config::tree::core::Abbrev}; + use crate::{bstr::ByteSlice, config::tree::core::Abbrev}; impl Abbrev { /// Convert the given `hex_len_str` into the amount of characters that a short hash should have. @@ -452,7 +450,7 @@ mod abbrev { &'static self, hex_len_str: impl gix_utils::AsBStr, object_hash: gix_hash::Kind, - ) -> Result, Error> { + ) -> Result, crate::Error> { let hex_len_str = hex_len_str.as_bstr(); let max = object_hash.len_in_hex() as u8; let invalid = || { diff --git a/gix/src/config/tree/sections/diff.rs b/gix/src/config/tree/sections/diff.rs index 77b05c470d6..893f9c001c8 100644 --- a/gix/src/config/tree/sections/diff.rs +++ b/gix/src/config/tree/sections/diff.rs @@ -177,7 +177,7 @@ mod renames { /// the boolean as string pub fn try_into_renames( &'static self, - value: Result, gix_config::value::Error>, + value: Result, gix_error::Exn>, ) -> Result, GenericError> { Ok(match value { Ok(Some(true)) => Some(Tracking::Renames), diff --git a/gix/src/config/tree/sections/fetch.rs b/gix/src/config/tree/sections/fetch.rs index ec5b64e5e11..ac48def8832 100644 --- a/gix/src/config/tree/sections/fetch.rs +++ b/gix/src/config/tree/sections/fetch.rs @@ -64,7 +64,7 @@ mod algorithm { /// Obtain the way submodules should be updated. pub fn try_into_recurse_submodules( &'static self, - value: Result, gix_config::value::Error>, + value: Result, gix_error::Exn>, ) -> Result, crate::config::key::GenericErrorWithValue> { gix_submodule::config::FetchRecurse::new(value) .map_err(|err| crate::config::key::GenericErrorWithValue::from_value(self, err)) diff --git a/gix/src/config/tree/sections/http.rs b/gix/src/config/tree/sections/http.rs index e3eecb3c5e2..1efc3ec7449 100644 --- a/gix/src/config/tree/sections/http.rs +++ b/gix/src/config/tree/sections/http.rs @@ -124,7 +124,7 @@ mod key_impls { pub fn try_into_follow_redirects( &'static self, value: impl gix_utils::AsBStr, - boolean: impl FnOnce() -> Result, gix_config::value::Error>, + boolean: impl FnOnce() -> Result, gix_error::Exn>, ) -> Result< crate::protocol::transport::client::blocking_io::http::options::FollowRedirects, crate::config::key::GenericErrorWithValue, diff --git a/gix/src/config/tree/sections/pack.rs b/gix/src/config/tree/sections/pack.rs index fe6029caef4..6c6502a03dc 100644 --- a/gix/src/config/tree/sections/pack.rs +++ b/gix/src/config/tree/sections/pack.rs @@ -27,7 +27,7 @@ mod index_version { /// Try to interpret an integer value as index version. pub fn try_into_index_version( &'static self, - value: Result, gix_config::value::Error>, + value: Result, gix_error::Exn>, ) -> Result, config::key::GenericError> { let Some(value) = value.map_err(|err| config::key::GenericError::from(self).with_source(err.into_error()))? diff --git a/gix/src/config/tree/sections/protocol.rs b/gix/src/config/tree/sections/protocol.rs index 1c29cebfb2b..acd252f726a 100644 --- a/gix/src/config/tree/sections/protocol.rs +++ b/gix/src/config/tree/sections/protocol.rs @@ -21,7 +21,7 @@ pub type Version = keys::Any; #[cfg(any(feature = "blocking-network-client", feature = "async-network-client"))] mod allow { - use crate::{bstr::ByteSlice, config, config::tree::protocol::Allow, remote::url::scheme_permission}; + use crate::{bstr::ByteSlice, config::tree::protocol::Allow, remote::url::scheme_permission}; impl Allow { /// Convert `value` into its respective `Allow` variant, possibly informing about the `scheme` we are looking at in the error. @@ -29,7 +29,7 @@ mod allow { &'static self, value: impl gix_utils::AsBStr, scheme: Option<&str>, - ) -> Result { + ) -> Result { let value = value.as_bstr(); scheme_permission::Allow::try_from(value.as_bstr()).map_err(|value| { gix_error::Error::from_error(gix_error::ValidationError::new(format!( @@ -83,7 +83,7 @@ mod key_impls { #[cfg(any(feature = "blocking-network-client", feature = "async-network-client"))] pub fn try_into_protocol_version( &'static self, - value: Result, gix_config::value::Error>, + value: Result, gix_error::Exn>, ) -> Result { let value = match value { Ok(None) => return Ok(gix_protocol::transport::Protocol::V2), diff --git a/gix/src/config/tree/traits.rs b/gix/src/config/tree/traits.rs index d01d783e881..2e536bf9ea2 100644 --- a/gix/src/config/tree/traits.rs +++ b/gix/src/config/tree/traits.rs @@ -1,7 +1,4 @@ -use crate::{ - bstr::{BStr, BString, ByteVec}, - config::tree::key::validate_assignment, -}; +use crate::bstr::{BStr, BString, ByteVec}; /// Provide information about a configuration section. pub trait Section { @@ -53,7 +50,7 @@ pub trait Key: std::fmt::Debug { /// The key's name, like `url` in `remote.origin.url`. fn name(&self) -> &str; /// See if `value` is allowed as value of this key, or return a descriptive error if it is not. - fn validate(&self, value: &BStr) -> Result<(), crate::config::tree::key::validate::Error>; + fn validate(&self, value: &BStr) -> Result<(), crate::Error>; /// The section containing this key. Git configuration has no free-standing keys, they are always underneath a section. fn section(&self) -> &dyn Section; /// The return value encodes three possible states to indicate subsection requirements @@ -175,7 +172,7 @@ pub trait Key: std::fmt::Debug { /// Return an assignment with the keys full name to `value`, suitable for [configuration overrides][crate::open::Options::config_overrides()]. /// Note that this will fail if the key requires a subsection name. - fn validated_assignment(&self, value: &BStr) -> Result { + fn validated_assignment(&self, value: &BStr) -> Result { self.validate(value)?; let mut key = self .full_name(None) @@ -187,21 +184,14 @@ pub trait Key: std::fmt::Debug { /// Return an assignment with the keys full name to `value`, suitable for [configuration overrides][crate::open::Options::config_overrides()]. /// Note that this will fail if the key requires a subsection name. - fn validated_assignment_fmt( - &self, - value: &dyn std::fmt::Display, - ) -> Result { + fn validated_assignment_fmt(&self, value: &dyn std::fmt::Display) -> Result { let value = value.to_string(); self.validated_assignment(value.as_str().into()) } /// Return an assignment to `value` with the keys full name within `subsection`, suitable for [configuration overrides][crate::open::Options::config_overrides()]. /// Note that this is only valid if this key supports parameterized sub-sections, or else an error is returned. - fn validated_assignment_with_subsection( - &self, - value: &BStr, - subsection: &BStr, - ) -> Result { + fn validated_assignment_with_subsection(&self, value: &BStr, subsection: &BStr) -> Result { self.validate(value)?; let mut key = self .full_name(Some(subsection)) diff --git a/gix/src/create.rs b/gix/src/create.rs index 358038d1c03..6197333f76f 100644 --- a/gix/src/create.rs +++ b/gix/src/create.rs @@ -7,10 +7,7 @@ use std::{ use gix_discover::DOT_GIT_DIR; use gix_error::{ErrorExt, ResultExt}; -/// The error used in [`into()`]. -pub type Error = gix_error::Error; - -fn io_error(source: std::io::Error, action: &str, path: &Path) -> Error { +fn io_error(source: std::io::Error, action: &str, path: &Path) -> crate::Error { source .and_raise(gix_error::message!("{action} at '{}'", path.display())) .into() @@ -52,7 +49,7 @@ impl PathCursor<'_> { } impl NewDir<'_> { - fn at(self, component: &str) -> Result { + fn at(self, component: &str) -> Result { self.0.push(component); create_dir(self.0)?; Ok(self) @@ -74,7 +71,7 @@ impl Drop for PathCursor<'_> { } } -fn write_file(data: &[u8], path: &Path) -> Result<(), Error> { +fn write_file(data: &[u8], path: &Path) -> Result<(), crate::Error> { let mut file = OpenOptions::new() .write(true) .create(true) @@ -86,7 +83,7 @@ fn write_file(data: &[u8], path: &Path) -> Result<(), Error> { .map_err(|err| io_error(err, "Could not write data", path)) } -fn create_dir(p: &Path) -> Result<(), Error> { +fn create_dir(p: &Path) -> Result<(), crate::Error> { fs::create_dir_all(p).map_err(|err| io_error(err, "Could not create directory", p)) } @@ -151,7 +148,7 @@ pub fn into( directory: impl Into, kind: Kind, options: Options, -) -> Result { +) -> Result { into_with_capabilities(directory, kind, options).map(|(path, _)| path) } @@ -163,7 +160,7 @@ pub(crate) fn into_with_capabilities( destination_must_be_empty, object_hash, }: Options, -) -> Result<(gix_discover::repository::Path, gix_fs::Capabilities), Error> { +) -> Result<(gix_discover::repository::Path, gix_fs::Capabilities), crate::Error> { let mut dot_git = directory.into(); let bare = matches!(kind, Kind::Bare); diff --git a/gix/src/diff.rs b/gix/src/diff.rs index 355eb237b88..622514ccefe 100644 --- a/gix/src/diff.rs +++ b/gix/src/diff.rs @@ -1,14 +1,8 @@ use gix_diff::tree::recorder::Location; pub use gix_diff::*; -/// -pub mod options { - /// - pub mod init { - /// The error returned when instantiating [diff options](crate::diff::Options). - pub type Error = gix_error::Error; - } -} +#[cfg(feature = "blob-diff")] +pub use utils::{new_rewrites, resource_cache}; /// General diff-related options for configuring rename-tracking and blob diffs. #[derive(Debug, Copy, Clone)] @@ -42,7 +36,7 @@ impl From for gix_diff::tree_with_rewrites::Options { /// Lifecycle impl Options { #[cfg(feature = "blob-diff")] - pub(crate) fn from_configuration(config: &crate::config::Cache) -> Result { + pub(crate) fn from_configuration(config: &crate::config::Cache) -> Result { Ok(Options { location: Some(Location::Path), rewrites: { @@ -133,26 +127,11 @@ pub(crate) mod utils { diff::rename::Tracking, }; - /// - pub mod new_rewrites { - /// The error returned by [`new_rewrites()`](super::new_rewrites()). - pub type Error = gix_error::Error; - } - - /// - pub mod resource_cache { - /// The error returned by [`resource_cache()`](super::resource_cache()). - pub type Error = gix_error::Error; - } - /// Create an instance by reading all relevant information from the `config`uration, while being `lenient` or not. /// Returns `Ok((None, false))` if nothing is configured, or `Ok((None, true))` if it's configured and disabled. /// /// Note that missing values will be defaulted similar to what git does. - pub fn new_rewrites( - config: &gix_config::File, - lenient: bool, - ) -> Result<(Option, bool), new_rewrites::Error> { + pub fn new_rewrites(config: &gix_config::File, lenient: bool) -> Result<(Option, bool), crate::Error> { new_rewrites_inner(config, lenient, &Diff::RENAMES, &Diff::RENAME_LIMIT) } @@ -161,7 +140,7 @@ pub(crate) mod utils { lenient: bool, renames: &'static crate::config::tree::diff::Renames, rename_limit: &'static crate::config::tree::keys::UnsignedInteger, - ) -> Result<(Option, bool), new_rewrites::Error> { + ) -> Result<(Option, bool), crate::Error> { let copies = match renames .try_into_renames(config.boolean(renames)) .with_leniency(lenient) @@ -205,7 +184,7 @@ pub(crate) mod utils { mode: gix_diff::blob::pipeline::Mode, attr_stack: gix_worktree::Stack, roots: gix_diff::blob::pipeline::WorktreeRoots, - ) -> Result { + ) -> Result { let diff_algo = repo.config.diff_algorithm().or_erased()?; let diff_cache = gix_diff::blob::Platform::new( gix_diff::blob::platform::Options { @@ -224,5 +203,3 @@ pub(crate) mod utils { Ok(diff_cache) } } -#[cfg(feature = "blob-diff")] -pub use utils::{new_rewrites, resource_cache}; diff --git a/gix/src/dirwalk/iter.rs b/gix/src/dirwalk/iter.rs index 1d09bbedebf..ad8b7122f44 100644 --- a/gix/src/dirwalk/iter.rs +++ b/gix/src/dirwalk/iter.rs @@ -43,9 +43,6 @@ pub struct Outcome { pub dirwalk: gix_dir::walk::Outcome, } -/// The error returned by [Repository::dirwalk_iter()]. -pub type Error = gix_error::Error; - /// Lifecycle impl Iter { pub(crate) fn new( @@ -54,7 +51,7 @@ impl Iter { patterns: Vec, should_interrupt: OwnedOrStaticAtomicBool, options: dirwalk::Options, - ) -> Result { + ) -> Result { #[cfg(feature = "parallel")] { let repo = repo.clone().into_sync(); @@ -63,7 +60,7 @@ impl Iter { .name("gix::dirwalk::iter::producer".into()) .spawn({ let should_interrupt = should_interrupt.clone(); - move || -> Result { + move || -> Result { let repo: Repository = repo.into(); let mut collect = Collect { tx }; let out = repo.dirwalk(&index, patterns, &should_interrupt, options, &mut collect)?; @@ -119,7 +116,7 @@ impl Iter { } impl Iterator for Iter { - type Item = Result; + type Item = Result; fn next(&mut self) -> Option { #[cfg(feature = "parallel")] diff --git a/gix/src/dirwalk/mod.rs b/gix/src/dirwalk/mod.rs index 33c6cc99f70..90db7eb83ab 100644 --- a/gix/src/dirwalk/mod.rs +++ b/gix/src/dirwalk/mod.rs @@ -27,7 +27,7 @@ pub struct Iter { #[expect(clippy::type_complexity)] rx_and_join: Option<( std::sync::mpsc::Receiver, - std::thread::JoinHandle>, + std::thread::JoinHandle>, )>, #[cfg(feature = "parallel")] should_interrupt: crate::util::OwnedOrStaticAtomicBool, @@ -38,9 +38,6 @@ pub struct Iter { out: Option, } -/// The error returned by [dirwalk()](crate::Repository::dirwalk()). -pub type Error = gix_error::Error; - /// The outcome of the [dirwalk()](crate::Repository::dirwalk). pub struct Outcome<'repo> { /// The excludes stack used for the dirwalk, for access of `.gitignore` information. diff --git a/gix/src/discover.rs b/gix/src/discover.rs index d7f53643671..387099601d5 100644 --- a/gix/src/discover.rs +++ b/gix/src/discover.rs @@ -7,13 +7,10 @@ pub use gix_discover::*; use crate::{ThreadSafeRepository, bstr::BString}; -/// The error returned by [`crate::discover()`]. -pub type Error = gix_error::Error; - impl ThreadSafeRepository { /// Try to open a git repository in `directory` and search upwards through its parents until one is found, /// using default trust options which matters in case the found repository isn't owned by the current user. - pub fn discover(directory: impl AsRef) -> Result { + pub fn discover(directory: impl AsRef) -> Result { Self::discover_opts(directory, Default::default(), Default::default()) } @@ -29,7 +26,7 @@ impl ThreadSafeRepository { directory: impl AsRef, options: upwards::Options<'_>, trust_map: gix_sec::trust::Mapping, - ) -> Result { + ) -> Result { let _span = gix_trace::coarse!("ThreadSafeRepository::discover()"); let (path, trust) = upwards_opts(directory.as_ref(), options).map_err(gix_error::Exn::into_error)?; let (git_dir, worktree_dir) = path.into_repository_and_work_tree_directories(); @@ -48,7 +45,7 @@ impl ThreadSafeRepository { /// while applying discovery options from the environment. /// /// For more, see [`ThreadSafeRepository::discover_with_environment_overrides_opts()`]. - pub fn discover_with_environment_overrides(directory: impl AsRef) -> Result { + pub fn discover_with_environment_overrides(directory: impl AsRef) -> Result { Self::discover_with_environment_overrides_opts(directory, Default::default(), Default::default()) } @@ -83,7 +80,7 @@ impl ThreadSafeRepository { directory: impl AsRef, mut options: upwards::Options<'_>, trust_map: gix_sec::trust::Mapping, - ) -> Result { + ) -> Result { fn apply_additional_environment(mut opts: upwards::Options<'_>) -> upwards::Options<'_> { use crate::bstr::ByteVec; diff --git a/gix/src/env.rs b/gix/src/env.rs index 62358707d39..4d5522297e9 100644 --- a/gix/src/env.rs +++ b/gix/src/env.rs @@ -36,22 +36,3 @@ pub fn args_os_opt(precompose_unicode: bool) -> impl Iterator { pub fn os_str_to_bstring(input: &OsStr) -> Option { Vec::from_os_string(input.into()).map(Into::into).ok() } - -/// Utilities to collate errors of common operations into one error type. -/// -/// This is useful as this type can present an API to answer common questions, like whether a network request seems to have failed -/// spuriously or if the underlying repository seems to be corrupted. -/// Error collation supports all operations, including opening the repository. -/// -/// ### Usage -/// -/// The caller may define a function that specifies the result type as `Result` to collect -/// errors into a well-known error type which provides an API for simple queries. -pub mod collate { - - /// - pub mod fetch { - /// An error which combines all possible errors when opening a repository, finding remotes and using them to fetch. - pub type Error = gix_error::Error; - } -} diff --git a/gix/src/filter.rs b/gix/src/filter.rs index 0cb0e26deae..61d2bac061e 100644 --- a/gix/src/filter.rs +++ b/gix/src/filter.rs @@ -13,33 +13,6 @@ use crate::{ prelude::ObjectIdExt, }; -/// -pub mod pipeline { - /// - pub mod options { - /// The error returned by [Pipeline::options()](crate::filter::Pipeline::options()). - pub type Error = gix_error::Error; - } - - /// - pub mod convert_to_git { - /// The error returned by [Pipeline::convert_to_git()](crate::filter::Pipeline::convert_to_git()). - pub type Error = gix_error::Error; - } - - /// - pub mod convert_to_worktree { - /// The error returned by [Pipeline::convert_to_worktree()](crate::filter::Pipeline::convert_to_worktree()). - pub type Error = gix_error::Error; - } - - /// - pub mod worktree_file_to_object { - /// The error returned by [Pipeline::worktree_file_to_object()](crate::filter::Pipeline::worktree_file_to_object()). - pub type Error = gix_error::Error; - } -} - /// A git pipeline for transforming data *to-git* and *to-worktree*, based /// [on git configuration and attributes](https://git-scm.com/docs/gitattributes). #[derive(Clone)] @@ -53,7 +26,7 @@ pub struct Pipeline<'repo> { /// Lifecycle impl<'repo> Pipeline<'repo> { /// Extract options from `repo` that are needed to properly drive a standard git filter pipeline. - pub fn options(repo: &'repo Repository) -> Result { + pub fn options(repo: &'repo Repository) -> Result { let config = &repo.config.resolved; let encodings = Core::CHECK_ROUND_TRIP_ENCODING .try_into_encodings(config.string("core.checkRoundtripEncoding")) @@ -93,7 +66,7 @@ impl<'repo> Pipeline<'repo> { /// Create a new instance by extracting all necessary information and configuration from a `repo` along with `cache` for accessing /// attributes. The `index` is used for some filters which may access it under very specific circumstances. - pub fn new(repo: &'repo Repository, cache: gix_worktree::Stack) -> Result { + pub fn new(repo: &'repo Repository, cache: gix_worktree::Stack) -> Result { let pipeline = gix_filter::Pipeline::new(repo.command_context()?, Self::options(repo)?); Ok(Pipeline { inner: pipeline, @@ -120,7 +93,7 @@ impl Pipeline<'_> { src: R, rela_path: &std::path::Path, index: &gix_index::State, - ) -> Result, pipeline::convert_to_git::Error> + ) -> Result, crate::Error> where R: std::io::Read, { @@ -135,7 +108,7 @@ impl Pipeline<'_> { &mut |_, attrs| { entry.matching_attributes(attrs); }, - &mut |buf| -> Result<_, gix_object::find::Error> { + &mut |buf| -> Result<_, gix_error::Exn> { let entry = match index .entry_by_path(gix_path::to_unix_separators_on_windows(gix_path::into_bstr(rela_path)).as_ref()) { @@ -162,8 +135,7 @@ impl Pipeline<'_> { src: &'input [u8], rela_path: &BStr, options: gix_filter::pipeline::convert::to_worktree::Options, - ) -> Result, pipeline::convert_to_worktree::Error> - { + ) -> Result, crate::Error> { let entry = self.cache.at_entry(rela_path, None, &self.repo.objects).or_erased()?; Ok(self.inner.convert_to_worktree( src, @@ -188,10 +160,7 @@ impl Pipeline<'_> { &mut self, rela_path: &BStr, index: &gix_index::State, - ) -> Result< - Option<(gix_hash::ObjectId, gix_object::tree::EntryKind, std::fs::Metadata)>, - pipeline::worktree_file_to_object::Error, - > { + ) -> Result, crate::Error> { let rela_path_as_path = gix_path::from_bstr(rela_path); let repo = self.repo; let worktree_dir = repo.workdir().ok_or_else(|| { @@ -268,7 +237,7 @@ impl Pipeline<'_> { } /// Obtain a list of all configured driver, but ignore those in sections that we don't trust enough. -fn extract_drivers(repo: &Repository) -> Result, pipeline::options::Error> { +fn extract_drivers(repo: &Repository) -> Result, crate::Error> { let mut drivers = Vec::::new(); for section in repo .config diff --git a/gix/src/head/mod.rs b/gix/src/head/mod.rs index da8027d361b..74582d19a09 100644 --- a/gix/src/head/mod.rs +++ b/gix/src/head/mod.rs @@ -139,10 +139,7 @@ mod remote { /// handle detached heads, i.e. obtain a remote even in case of detached heads, /// or call [`Repository::find_fetch_remote(…)`](crate::Repository::find_fetch_remote()) for the highest-level way of finding /// the right remote, just like `git fetch` does. - pub fn into_remote( - self, - direction: remote::Direction, - ) -> Option, remote::find::existing::Error>> { + pub fn into_remote(self, direction: remote::Direction) -> Option, crate::Error>> { let repo = self.repo; self.try_into_referent()? .remote(direction) diff --git a/gix/src/head/peel.rs b/gix/src/head/peel.rs index fb4a3115a11..11730af29e4 100644 --- a/gix/src/head/peel.rs +++ b/gix/src/head/peel.rs @@ -6,38 +6,12 @@ use crate::{ head::Kind, }; -mod error { - /// The error returned by [`Head::peel_to_id()`](super::Head::try_peel_to_id()) and - /// [`Head::into_fully_peeled_id()`](super::Head::try_into_peeled_id()). - pub type Error = gix_error::Error; -} - -pub use error::Error; - -/// -pub mod into_id { - /// The error returned by [`Head::into_peeled_id()`](super::Head::into_peeled_id()). - pub type Error = gix_error::Error; -} - -/// -pub mod to_commit { - /// The error returned by [`Head::peel_to_commit()`](super::Head::peel_to_commit()). - pub type Error = gix_error::Error; -} - -/// -pub mod to_object { - /// The error returned by [`Head::peel_to_object()`](super::Head::peel_to_object()). - pub type Error = gix_error::Error; -} - impl<'repo> Head<'repo> { /// Peel this instance and consume it to make obtaining its final target id possible, while returning an error on unborn heads. /// /// The final target is obtained by following symbolic references and peeling tags to their final destination, which /// typically is a commit, but can be any object. - pub fn into_peeled_id(mut self) -> Result, into_id::Error> { + pub fn into_peeled_id(mut self) -> Result, crate::Error> { self.try_peel_to_id()?; self.id().ok_or_else(|| match self.kind { Kind::Symbolic(gix_ref::Reference { name, .. }) | Kind::Unborn(name) => gix_error::Error::from_error( @@ -51,7 +25,7 @@ impl<'repo> Head<'repo> { /// /// The final target is obtained by following symbolic references and peeling tags to their final destination, which /// typically is a commit, but can be any object as well. - pub fn into_peeled_object(mut self) -> Result, to_object::Error> { + pub fn into_peeled_object(mut self) -> Result, crate::Error> { self.peel_to_object() } @@ -60,7 +34,7 @@ impl<'repo> Head<'repo> { /// /// The final target is obtained by following symbolic references and peeling tags to their final destination, which /// typically is a commit, but can be any object. - pub fn try_into_peeled_id(mut self) -> Result>, Error> { + pub fn try_into_peeled_id(mut self) -> Result>, crate::Error> { self.try_peel_to_id() } @@ -72,7 +46,7 @@ impl<'repo> Head<'repo> { /// The final target is obtained by following symbolic references and peeling tags to their final destination, which /// typically is a commit, but can be any object. #[deprecated = "Use `try_peel_to_id()` instead"] - pub fn try_peel_to_id_in_place(&mut self) -> Result>, Error> { + pub fn try_peel_to_id_in_place(&mut self) -> Result>, crate::Error> { self.try_peel_to_id() } @@ -85,7 +59,7 @@ impl<'repo> Head<'repo> { /// typically is a commit, but can be any object. /// /// Note that this method mutates `self` in place. - pub fn try_peel_to_id(&mut self) -> Result>, Error> { + pub fn try_peel_to_id(&mut self) -> Result>, crate::Error> { Ok(Some(match &mut self.kind { Kind::Unborn(_name) => return Ok(None), Kind::Detached { @@ -120,7 +94,7 @@ impl<'repo> Head<'repo> { /// /// Returns an error if the head is unborn or if it doesn't point to a commit. #[deprecated = "Use `peel_to_object()` instead"] - pub fn peel_to_object_in_place(&mut self) -> Result, to_object::Error> { + pub fn peel_to_object_in_place(&mut self) -> Result, crate::Error> { self.peel_to_object() } @@ -130,7 +104,7 @@ impl<'repo> Head<'repo> { /// Returns an error if the head is unborn or if it doesn't point to a commit. /// /// Note that this method mutates `self` in place. - pub fn peel_to_object(&mut self) -> Result, to_object::Error> { + pub fn peel_to_object(&mut self) -> Result, crate::Error> { let id = self.try_peel_to_id()?.ok_or_else(|| { gix_error::Error::from_error(gix_error::NotFoundError::new(format!( "Branch '{}' does not have any commits", @@ -145,7 +119,7 @@ impl<'repo> Head<'repo> { /// /// Returns an error if the head is unborn or if it doesn't point to a commit. #[deprecated = "Use `peel_to_commit()` instead"] - pub fn peel_to_commit_in_place(&mut self) -> Result, to_commit::Error> { + pub fn peel_to_commit_in_place(&mut self) -> Result, crate::Error> { self.peel_to_commit() } @@ -155,7 +129,7 @@ impl<'repo> Head<'repo> { /// Returns an error if the head is unborn or if it doesn't point to a commit. /// /// Note that this method mutates `self` in place. - pub fn peel_to_commit(&mut self) -> Result, to_commit::Error> { + pub fn peel_to_commit(&mut self) -> Result, crate::Error> { Ok(self.peel_to_object()?.try_into_commit().or_erased()?) } } diff --git a/gix/src/id.rs b/gix/src/id.rs index 646ddd1a3d0..6ae41c38a7b 100644 --- a/gix/src/id.rs +++ b/gix/src/id.rs @@ -6,7 +6,7 @@ use std::ops::Deref; use gix_hash::{ObjectId, oid}; -use crate::{Id, Object, object::find}; +use crate::{Id, Object}; /// An [object id][ObjectId] infused with a [`Repository`][crate::Repository]. impl<'repo> Id<'repo> { @@ -15,14 +15,14 @@ impl<'repo> Id<'repo> { /// # Note /// /// There can only be one `ObjectRef` per `Easy`. To increase that limit, clone the `Easy`. - pub fn object(&self) -> Result, find::existing::Error> { + pub fn object(&self) -> Result, crate::Error> { self.repo.find_object(self.inner) } /// Find the [`header`][gix_odb::find::Header] associated with this object id, or an error if it doesn't exist. /// /// Use this method if there is no interest in the contents of the object, which generally is much faster to obtain. - pub fn header(&self) -> Result { + pub fn header(&self) -> Result { self.repo.find_header(self.inner) } @@ -31,19 +31,19 @@ impl<'repo> Id<'repo> { /// # Note /// /// There can only be one `ObjectRef` per `Easy`. To increase that limit, clone the `Easy`. - pub fn try_object(&self) -> Result>, find::Error> { + pub fn try_object(&self) -> Result>, crate::Error> { self.repo.try_find_object(self.inner) } /// Find the [`header`][gix_odb::find::Header] associated with this object id, or return `None` if it doesn't exist. /// /// Use this method if there is no interest in the contents of the object, which generally is much faster to obtain. - pub fn try_header(&self) -> Result, find::Error> { + pub fn try_header(&self) -> Result, crate::Error> { self.repo.try_find_header(self.inner) } /// Turn this object id into a shortened id with a length in hex as configured by `core.abbrev`. - pub fn shorten(&self) -> Result { + pub fn shorten(&self) -> Result { let hex_len = self .repo .config @@ -81,12 +81,6 @@ fn calculate_auto_hex_len(num_packed_objects: u64) -> usize { len.max(7) as usize } -/// -pub mod shorten { - /// Returned by [`Id::prefix()`][super::Id::shorten()]. - pub type Error = gix_error::Error; -} - impl Deref for Id<'_> { type Target = oid; diff --git a/gix/src/init.rs b/gix/src/init.rs index 2b18988dcfd..9188aceafa7 100644 --- a/gix/src/init.rs +++ b/gix/src/init.rs @@ -18,9 +18,6 @@ use gix_error::ResultExt; /// We use `main` instead of `master`. pub const DEFAULT_BRANCH_NAME: &str = "main"; -/// The error returned by [`crate::init()`]. -pub type Error = gix_error::Error; - impl ThreadSafeRepository { /// Create a repository with work-tree within `directory`, creating intermediate directories as needed. /// @@ -32,7 +29,7 @@ impl ThreadSafeRepository { directory: impl AsRef, kind: crate::create::Kind, options: crate::create::Options, - ) -> Result { + ) -> Result { use gix_sec::trust::DefaultForLevel; let open_options = crate::open::Options::default_for_level(gix_sec::Trust::Full); Self::init_opts(directory, kind, options, open_options) @@ -49,7 +46,7 @@ impl ThreadSafeRepository { kind: crate::create::Kind, create_options: crate::create::Options, mut open_options: crate::open::Options, - ) -> Result { + ) -> Result { let (path, capabilities) = crate::create::into_with_capabilities(directory.as_ref(), kind, create_options)?; if !capabilities.symlink { open_options.api_config_overrides.push("core.symlinks=false".into()); diff --git a/gix/src/lib.rs b/gix/src/lib.rs index 163beaa23fb..c3f8868b1d5 100644 --- a/gix/src/lib.rs +++ b/gix/src/lib.rs @@ -255,7 +255,7 @@ pub mod merge; /// assert!(repo.workdir_path("this").expect("non-bare").is_file()); /// # Ok(()) } /// ``` -pub fn discover(directory: impl AsRef) -> Result { +pub fn discover(directory: impl AsRef) -> Result { ThreadSafeRepository::discover(directory).map(Into::into) } @@ -266,7 +266,7 @@ pub fn discover_opts( directory: impl AsRef, options: discover::upwards::Options<'_>, open_options: open::Options, -) -> Result { +) -> Result { ThreadSafeRepository::discover_opts( directory, options, @@ -281,16 +281,14 @@ pub fn discover_opts( /// Try to discover a git repository directly from the environment. /// /// For details, see [`ThreadSafeRepository::discover_with_environment_overrides_opts()`]. -pub fn discover_with_environment_overrides( - directory: impl AsRef, -) -> Result { +pub fn discover_with_environment_overrides(directory: impl AsRef) -> Result { ThreadSafeRepository::discover_with_environment_overrides(directory).map(Into::into) } /// Try to open a git repository directly from the environment. /// /// See [`ThreadSafeRepository::open_with_environment_overrides()`]. -pub fn open_with_environment_overrides(directory: impl Into) -> Result { +pub fn open_with_environment_overrides(directory: impl Into) -> Result { ThreadSafeRepository::open_with_environment_overrides(directory, Default::default()).map(Into::into) } @@ -309,12 +307,12 @@ pub fn open_with_environment_overrides(directory: impl Into) /// assert!(repo.head()?.is_unborn()); /// # Ok(()) } /// ``` -pub fn init(directory: impl AsRef) -> Result { +pub fn init(directory: impl AsRef) -> Result { ThreadSafeRepository::init(directory, create::Kind::WithWorktree, create::Options::default()).map(Into::into) } /// See [`ThreadSafeRepository::init()`], but returns a [`Repository`] instead. -pub fn init_bare(directory: impl AsRef) -> Result { +pub fn init_bare(directory: impl AsRef) -> Result { ThreadSafeRepository::init(directory, create::Kind::Bare, create::Options::default()).map(Into::into) } @@ -325,7 +323,7 @@ pub fn init_bare(directory: impl AsRef) -> Result( url: Url, path: impl AsRef, -) -> Result +) -> Result where Url: std::convert::TryInto, E: std::error::Error + Send + Sync + 'static, @@ -343,7 +341,7 @@ where /// (but amended with using configuration from the git installation to ensure all authentication options are honored). /// /// See [`clone::PrepareFetch::new()`] for a function to take full control over all options. -pub fn prepare_clone(url: Url, path: impl AsRef) -> Result +pub fn prepare_clone(url: Url, path: impl AsRef) -> Result where Url: std::convert::TryInto, E: std::error::Error + Send + Sync + 'static, @@ -379,13 +377,13 @@ fn open_opts_with_git_binary_config() -> open::Options { /// # Ok(()) } /// ``` #[doc(alias = "git2")] -pub fn open(directory: impl Into) -> Result { +pub fn open(directory: impl Into) -> Result { ThreadSafeRepository::open(directory).map(Into::into) } /// See [`ThreadSafeRepository::open_opts()`], but returns a [`Repository`] instead. #[doc(alias = "open_ext", alias = "git2")] -pub fn open_opts(directory: impl Into, options: open::Options) -> Result { +pub fn open_opts(directory: impl Into, options: open::Options) -> Result { ThreadSafeRepository::open_opts(directory, options).map(Into::into) } diff --git a/gix/src/mailmap.rs b/gix/src/mailmap.rs index 51db8fc1df3..5a8acac1a20 100644 --- a/gix/src/mailmap.rs +++ b/gix/src/mailmap.rs @@ -1,7 +1 @@ pub use gix_mailmap::*; - -/// -pub mod load { - /// The error returned by [`crate::Repository::open_mailmap_into()`]. - pub type Error = gix_error::Error; -} diff --git a/gix/src/object/blob.rs b/gix/src/object/blob.rs index a277baea1e2..b2b7e816917 100644 --- a/gix/src/object/blob.rs +++ b/gix/src/object/blob.rs @@ -15,19 +15,10 @@ pub mod diff { pub resource_cache: &'a mut gix_diff::blob::Platform, } - /// - pub mod init { - /// The error returned by [`object::tree::diff::Change::diff`](crate::object::tree::diff::Change::diff()). - pub type Error = gix_error::Error; - } - /// pub mod lines { use crate::bstr::BStr; - /// The error returned by [Platform::lines()](super::Platform::lines()). - pub type Error = gix_error::Error; - /// A change to a hunk of lines. pub enum Change<'a, 'data> { /// Lines were added. @@ -59,7 +50,7 @@ pub mod diff { pub fn lines( &mut self, mut process_hunk: FnH, - ) -> Result, lines::Error> + ) -> Result, crate::Error> where FnH: FnMut(lines::Change<'_, '_>) -> Result<(), E>, E: std::error::Error + Send + Sync + 'static, @@ -121,9 +112,7 @@ pub mod diff { /// Count the amount of removed and inserted lines efficiently. /// Note that nothing will happen if one of the inputs is binary, and `None` will be returned. - pub fn line_counts( - &mut self, - ) -> Result, gix_diff::blob::platform::prepare_diff::Error> { + pub fn line_counts(&mut self) -> Result, gix_error::ValidationError> { self.resource_cache.options.skip_internal_diff_if_external_is_configured = false; let prep = self.resource_cache.prepare_diff()?; diff --git a/gix/src/object/commit.rs b/gix/src/object/commit.rs index 5900ea0494c..bc92addbd3d 100644 --- a/gix/src/object/commit.rs +++ b/gix/src/object/commit.rs @@ -1,13 +1,6 @@ use crate::{Commit, ObjectDetached, Tree, bstr, bstr::BStr}; use gix_error::ResultExt; -mod error { - /// The error returned by commit accessors. - pub type Error = gix_error::Error; -} - -pub use error::Error; - /// Remove Lifetime impl Commit<'_> { /// Create an owned instance of this object, copying our data in the process. @@ -49,13 +42,13 @@ impl<'repo> Commit<'repo> { /// assert_eq!(short_id.to_string(), "3189cd3"); /// # Ok(()) } /// ``` - pub fn short_id(&self) -> Result { + pub fn short_id(&self) -> Result { use crate::ext::ObjectIdExt; self.id.attach(self.repo).shorten() } /// Parse the commits message into a [`MessageRef`][gix_object::commit::MessageRef] - pub fn message(&self) -> Result, gix_object::decode::Error> { + pub fn message(&self) -> Result, gix_error::ValidationError> { Ok(gix_object::commit::MessageRef::from_bytes(self.message_raw()?)) } /// Decode the commit object until the message and return it. @@ -71,7 +64,7 @@ impl<'repo> Commit<'repo> { /// assert_eq!(commit.message_raw()?, "c2\n"); /// # Ok(()) } /// ``` - pub fn message_raw(&self) -> Result<&'_ BStr, gix_object::decode::Error> { + pub fn message_raw(&self) -> Result<&'_ BStr, gix_error::ValidationError> { gix_object::CommitRefIter::from_bytes(&self.data, self.id.kind()).message() } /// Obtain the message by using intricate knowledge about the encoding, which is fastest and @@ -88,7 +81,7 @@ impl<'repo> Commit<'repo> { /// Decode the commit and obtain the time at which the commit was created. /// /// For the time at which it was authored, refer to `.author()?.time()`. - pub fn time(&self) -> Result { + pub fn time(&self) -> Result { self.committer() .or_raise(|| gix_error::message("The commit could not be decoded fully or partially"))? .time() @@ -103,7 +96,7 @@ impl<'repo> Commit<'repo> { /// Note that the returned commit object does make lookup easy and should be /// used for successive calls to string-ish information to avoid decoding the object /// more than once. - pub fn decode(&self) -> Result, gix_object::decode::Error> { + pub fn decode(&self) -> Result, gix_error::ValidationError> { gix_object::CommitRef::from_bytes(&self.data, self.id.kind()) } @@ -113,14 +106,14 @@ impl<'repo> Commit<'repo> { } /// Return the commits author, with surrounding whitespace trimmed. - pub fn author(&self) -> Result, gix_object::decode::Error> { + pub fn author(&self) -> Result, gix_error::ValidationError> { gix_object::CommitRefIter::from_bytes(&self.data, self.id.kind()) .author() .map(|s| s.trim()) } /// Return the commits committer. with surrounding whitespace trimmed. - pub fn committer(&self) -> Result, gix_object::decode::Error> { + pub fn committer(&self) -> Result, gix_error::ValidationError> { gix_object::CommitRefIter::from_bytes(&self.data, self.id.kind()) .committer() .map(|s| s.trim()) @@ -163,12 +156,12 @@ impl<'repo> Commit<'repo> { /// assert_eq!(tree.id, repo.head_tree_id()?); /// # Ok(()) } /// ``` - pub fn tree(&self) -> Result, Error> { + pub fn tree(&self) -> Result, crate::Error> { Ok(self.tree_id().or_erased()?.object()?.try_into_tree().or_erased()?) } /// Parse the commit and return the tree id it points to. - pub fn tree_id(&self) -> Result, gix_object::decode::Error> { + pub fn tree_id(&self) -> Result, gix_error::ValidationError> { gix_object::CommitRefIter::from_bytes(&self.data, self.id.kind()) .tree_id() .map(|id| crate::Id::from_id(id, self.repo)) @@ -203,7 +196,7 @@ impl<'repo> Commit<'repo> { // TODO: make it possible to verify the signature, probably by wrapping `SignedData`. It's quite some work to do it properly. pub fn signature( &self, - ) -> Result, gix_object::signature::SignedData<'_>)>, gix_object::decode::Error> + ) -> Result, gix_object::signature::SignedData<'_>)>, gix_error::ValidationError> { gix_object::CommitRefIter::signature(&self.data, self.id.kind()) } @@ -215,7 +208,7 @@ impl<'repo> Commit<'repo> { /// signer identity, and verifier output. A successful call does not necessarily mean that the signature is valid; /// use [`Outcome::is_valid()`](crate::commit::verify::Outcome::is_valid) to determine whether Git would accept it. #[cfg(feature = "command")] - pub fn verify_signature(&self) -> Result, crate::commit::verify::Error> { + pub fn verify_signature(&self) -> Result, crate::Error> { crate::commit::verify::verify(self) } @@ -224,7 +217,7 @@ impl<'repo> Commit<'repo> { /// /// An existing signature for the repository's object format is replaced. #[cfg(feature = "command")] - pub fn signed(&self) -> Result, crate::commit::sign::Error> { + pub fn signed(&self) -> Result, crate::Error> { crate::commit::sign::sign(self) } } diff --git a/gix/src/object/errors.rs b/gix/src/object/errors.rs index 51ec3ad3f93..e3753121b53 100644 --- a/gix/src/object/errors.rs +++ b/gix/src/object/errors.rs @@ -1,33 +1,3 @@ -pub(crate) fn existing_error(err: gix_object::find::existing::Error) -> gix_error::Error { +pub(crate) fn existing_error(err: gix_error::Exn) -> gix_error::Error { err.into_error() } - -/// -pub mod conversion { - - /// The error returned by [`crate::object::try_to_()`][crate::Object::try_to_commit_ref()]. - pub type Error = gix_error::Error; -} - -/// -pub mod find { - /// Indicate that an error occurred when trying to find an object. - pub type Error = gix_error::Error; - - /// - pub mod existing { - /// An object could not be found in the database, or an error occurred when trying to obtain it. - pub type Error = gix_error::Error; - /// - pub mod with_conversion { - /// The error returned by [Repository::find_commit()](crate::Repository::find_commit). - pub type Error = gix_error::Error; - } - } -} - -/// -pub mod write { - /// An error to indicate writing to the loose object store failed. - pub type Error = gix_error::Error; -} diff --git a/gix/src/object/mod.rs b/gix/src/object/mod.rs index 438d9f8f065..9e08d4750b4 100644 --- a/gix/src/object/mod.rs +++ b/gix/src/object/mod.rs @@ -12,7 +12,7 @@ pub(crate) mod cache { pub use gix_pack::cache::object::MemoryCappedHashmap; } pub(crate) use errors::existing_error; -pub use errors::{conversion, find, write}; + /// pub mod blob; /// @@ -23,12 +23,6 @@ mod tag; /// pub mod tree; -/// -pub mod try_into { - /// The error returned when converting an object to an unexpected kind. - pub type Error = gix_error::ValidationError; -} - impl ObjectDetached { /// Infuse this owned object with `repo` access. pub fn attach(self, repo: &crate::Repository) -> Object<'_> { @@ -103,7 +97,7 @@ impl<'repo> Object<'repo> { } /// Transform this object into a commit, or return it as part of the `Err` if it is no commit. - pub fn try_into_commit(self) -> Result, try_into::Error> { + pub fn try_into_commit(self) -> Result, gix_error::ValidationError> { self.try_into().map_err(|this: Self| { gix_error::ValidationError::new(format!( "Object named {} was supposed to be of kind {}, but was kind {}.", @@ -115,7 +109,7 @@ impl<'repo> Object<'repo> { } /// Transform this object into a tag, or return it as part of the `Err` if it is no commit. - pub fn try_into_tag(self) -> Result, try_into::Error> { + pub fn try_into_tag(self) -> Result, gix_error::ValidationError> { self.try_into().map_err(|this: Self| { gix_error::ValidationError::new(format!( "Object named {} was supposed to be of kind {}, but was kind {}.", @@ -127,7 +121,7 @@ impl<'repo> Object<'repo> { } /// Transform this object into a tree, or return it as part of the `Err` if it is no tree. - pub fn try_into_tree(self) -> Result, try_into::Error> { + pub fn try_into_tree(self) -> Result, gix_error::ValidationError> { self.try_into().map_err(|this: Self| { gix_error::ValidationError::new(format!( "Object named {} was supposed to be of kind {}, but was kind {}.", @@ -139,7 +133,7 @@ impl<'repo> Object<'repo> { } /// Transform this object into a blob, or return it as part of the `Err` if it is no blob. - pub fn try_into_blob(self) -> Result, try_into::Error> { + pub fn try_into_blob(self) -> Result, gix_error::ValidationError> { self.try_into().map_err(|this: Self| { gix_error::ValidationError::new(format!( "Object named {} was supposed to be of kind {}, but was kind {}.", @@ -180,7 +174,7 @@ impl<'repo> Object<'repo> { } /// Obtain a fully parsed commit whose fields reference our data buffer. - pub fn try_to_commit_ref(&self) -> Result, conversion::Error> { + pub fn try_to_commit_ref(&self) -> Result, crate::Error> { gix_object::Data::new(&self.data, self.kind, self.id.kind()) .decode() .or_erased()? @@ -241,7 +235,7 @@ impl<'repo> Object<'repo> { } /// Obtain a fully parsed tag object whose fields reference our data buffer. - pub fn try_to_tag_ref(&self) -> Result, conversion::Error> { + pub fn try_to_tag_ref(&self) -> Result, crate::Error> { gix_object::Data::new(&self.data, self.kind, self.id.kind()) .decode() .or_erased()? diff --git a/gix/src/object/peel.rs b/gix/src/object/peel.rs index 33380527afc..240e4fc44b0 100644 --- a/gix/src/object/peel.rs +++ b/gix/src/object/peel.rs @@ -1,18 +1,6 @@ //! #![allow(clippy::empty_docs)] -use crate::{ - Commit, Object, Tree, object, - object::{Kind, peel}, -}; - -/// -pub mod to_kind { - mod error { - /// The error returned by [`Object::peel_to_kind()`][crate::Object::peel_to_kind()]. - pub type Error = gix_error::Error; - } - pub use error::Error; -} +use crate::{Commit, Object, Tree, object::Kind}; impl<'repo> Object<'repo> { // TODO: tests @@ -20,7 +8,7 @@ impl<'repo> Object<'repo> { /// /// Note that this object doesn't necessarily have to be the end of the chain. /// Typical values are [`Kind::Commit`] or [`Kind::Tree`]. - pub fn peel_to_kind(mut self, kind: Kind) -> Result { + pub fn peel_to_kind(mut self, kind: Kind) -> Result { loop { match self.kind { our_kind if kind == our_kind => { @@ -56,14 +44,14 @@ impl<'repo> Object<'repo> { /// Peel this object into a tree and return it, if this is possible. /// /// This will follow tag objects and commits until their tree is reached. - pub fn peel_to_tree(self) -> Result, peel::to_kind::Error> { + pub fn peel_to_tree(self) -> Result, crate::Error> { Ok(self.peel_to_kind(gix_object::Kind::Tree)?.into_tree()) } /// Peel this object into a commit and return it, if this is possible. /// /// This will follow tag objects until a commit is reached. - pub fn peel_to_commit(self) -> Result, peel::to_kind::Error> { + pub fn peel_to_commit(self) -> Result, crate::Error> { Ok(self.peel_to_kind(gix_object::Kind::Commit)?.into_commit()) } @@ -72,7 +60,7 @@ impl<'repo> Object<'repo> { /// /// Note that this method is different from [`peel_to_kind(…)`][Object::peel_to_kind()] as it won't /// peel commits to their tree, but handles tags only. - pub fn peel_tags_to_end(mut self) -> Result { + pub fn peel_tags_to_end(mut self) -> Result { loop { match self.kind { Kind::Commit | Kind::Tree | Kind::Blob => break Ok(self), diff --git a/gix/src/object/tag.rs b/gix/src/object/tag.rs index df7da2bb50d..f0c3eb9ae3e 100644 --- a/gix/src/object/tag.rs +++ b/gix/src/object/tag.rs @@ -8,19 +8,19 @@ impl<'repo> Tag<'repo> { /// Note that the returned commit object does make lookup easy and should be /// used for successive calls to string-ish information to avoid decoding the object /// more than once. - pub fn decode(&self) -> Result, gix_object::decode::Error> { + pub fn decode(&self) -> Result, gix_error::ValidationError> { gix_object::TagRef::from_bytes(&self.data, self.id.kind()) } /// Decode this tag partially and return the id of its target. - pub fn target_id(&self) -> Result, gix_object::decode::Error> { + pub fn target_id(&self) -> Result, gix_error::ValidationError> { gix_object::TagRefIter::from_bytes(&self.data, self.id.kind()) .target_id() .map(|id| id.attach(self.repo)) } /// Decode this tag partially and return the tagger, if the field exists. - pub fn tagger(&self) -> Result>, gix_object::decode::Error> { + pub fn tagger(&self) -> Result>, gix_error::ValidationError> { gix_object::TagRefIter::from_bytes(&self.data, self.id.kind()).tagger() } } diff --git a/gix/src/object/tree/diff/change.rs b/gix/src/object/tree/diff/change.rs index fee7c827a2a..53e889f76d6 100644 --- a/gix/src/object/tree/diff/change.rs +++ b/gix/src/object/tree/diff/change.rs @@ -20,7 +20,7 @@ impl Change<'_, '_, '_> { pub fn diff<'b>( &self, resource_cache: &'b mut gix_diff::blob::Platform, - ) -> Result, crate::object::blob::diff::init::Error> { + ) -> Result, crate::Error> { resource_cache.set_resource_by_change((*self).into(), &self.id().repo.objects)?; Ok(crate::object::blob::diff::Platform { resource_cache }) } diff --git a/gix/src/object/tree/diff/for_each.rs b/gix/src/object/tree/diff/for_each.rs index 3a942e42725..baa6b929a4f 100644 --- a/gix/src/object/tree/diff/for_each.rs +++ b/gix/src/object/tree/diff/for_each.rs @@ -4,9 +4,6 @@ use gix_object::TreeRefIter; use super::{Action, Change, Platform}; use crate::Tree; -/// The error return by methods on the [diff platform][Platform]. -pub type Error = gix_error::Error; - /// Add the item to compare to. impl<'old> Platform<'_, 'old> { /// Call `for_each` repeatedly with all changes that are needed to convert the source of the diff to the tree to `other`. @@ -17,7 +14,7 @@ impl<'old> Platform<'_, 'old> { &mut self, other: &Tree<'new>, for_each: impl FnMut(Change<'_, 'old, 'new>) -> Result, - ) -> Result, Error> { + ) -> Result, crate::Error> { self.for_each_to_obtain_tree_inner(other, for_each, None) } @@ -36,7 +33,7 @@ impl<'old> Platform<'_, 'old> { other: &Tree<'new>, resource_cache: &mut gix_diff::blob::Platform, for_each: impl FnMut(Change<'_, 'old, 'new>) -> Result, - ) -> Result, Error> { + ) -> Result, crate::Error> { self.for_each_to_obtain_tree_inner(other, for_each, Some(resource_cache)) } @@ -45,7 +42,7 @@ impl<'old> Platform<'_, 'old> { other: &Tree<'new>, mut for_each: impl FnMut(Change<'_, 'old, 'new>) -> Result, resource_cache: Option<&mut gix_diff::blob::Platform>, - ) -> Result, Error> { + ) -> Result, crate::Error> { let repo = self.lhs.repo; let mut storage; let cache = match resource_cache { diff --git a/gix/src/object/tree/diff/mod.rs b/gix/src/object/tree/diff/mod.rs index b3c4ca55f16..714ef276659 100644 --- a/gix/src/object/tree/diff/mod.rs +++ b/gix/src/object/tree/diff/mod.rs @@ -125,7 +125,7 @@ impl<'repo> Tree<'repo> { /// try to access blobs to compute a similarity metric. Thus, it's more compatible to turn rewrite tracking off /// using [`Options::track_rewrites()`](crate::diff::Options::track_rewrites()). #[doc(alias = "diff_tree_to_tree", alias = "git2")] - pub fn changes<'a>(&'a self) -> Result, crate::diff::options::init::Error> { + pub fn changes<'a>(&'a self) -> Result, crate::Error> { Ok(Platform { state: Default::default(), lhs: self, @@ -164,12 +164,6 @@ pub struct Stats { pub files_changed: u64, } -/// -pub mod stats { - /// The error returned by [`stats()`](super::Platform::stats()). - pub type Error = gix_error::Error; -} - /// Convenience impl Platform<'_, '_> { /// Calculate statistics about the lines of the diff between our current and the `other` tree. @@ -180,7 +174,7 @@ impl Platform<'_, '_> { /// rename tracking, an operation that doesn't affect the statistics currently. /// As diffed resources aren't cached, if highly repetitive blobs are expected, performance /// may be diminished. In real-world scenarios where blobs are mostly unique, that's not an issue though. - pub fn stats(&mut self, other: &Tree<'_>) -> Result { + pub fn stats(&mut self, other: &Tree<'_>) -> Result { // let (mut number_of_files, mut lines_added, mut lines_removed) = (0, 0, 0); let mut resource_cache = self.lhs.repo.diff_resource_cache_for_tree_diff()?; diff --git a/gix/src/object/tree/editor.rs b/gix/src/object/tree/editor.rs index b8a701b8ee9..5bccd607308 100644 --- a/gix/src/object/tree/editor.rs +++ b/gix/src/object/tree/editor.rs @@ -8,18 +8,6 @@ use crate::{ prelude::ObjectIdExt, }; -/// -pub mod init { - /// The error returned by [`Editor::new()](crate::object::tree::Editor::new()). - pub type Error = gix_error::Error; -} - -/// -pub mod write { - /// The error returned by [`Editor::write()](crate::object::tree::Editor::write()) and [`Cursor::write()](super::Cursor::write). - pub type Error = gix_error::Error; -} - /// A cursor at a specific portion of a tree to [edit](super::Editor). pub struct Cursor<'a, 'repo> { inner: gix_object::tree::editor::Cursor<'a, 'repo>, @@ -30,7 +18,7 @@ pub struct Cursor<'a, 'repo> { /// Lifecycle impl<'repo> super::Editor<'repo> { /// Initialize a new editor from the given `tree`. - pub fn new(tree: &crate::Tree<'repo>) -> Result { + pub fn new(tree: &crate::Tree<'repo>) -> Result { let tree_ref = tree.decode().or_erased()?; let repo = tree.repo; let validate = repo.config.protect_options().or_erased()?; @@ -52,7 +40,7 @@ impl<'repo> super::Editor<'repo> { impl<'repo> crate::Tree<'repo> { /// Start editing a new tree based on this one. #[doc(alias = "treebuilder", alias = "git2")] - pub fn edit(&self) -> Result, init::Error> { + pub fn edit(&self) -> Result, crate::Error> { super::Editor::new(self) } } @@ -153,7 +141,7 @@ impl<'repo> Cursor<'_, 'repo> { } /// Like [`Editor::write()`](super::Editor::write()), but will write only the subtree of the cursor. - pub fn write(&mut self) -> Result, write::Error> { + pub fn write(&mut self) -> Result, crate::Error> { write_cursor(self) } @@ -180,7 +168,7 @@ impl<'repo> super::Editor<'repo> { /// Note that this erases all previous edits. /// /// This is useful if the same editor is re-used for various trees. - pub fn set_root(&mut self, root: &crate::Tree<'repo>) -> Result<&mut Self, init::Error> { + pub fn set_root(&mut self, root: &crate::Tree<'repo>) -> Result<&mut Self, crate::Error> { let new_editor = super::Editor::new(root)?; self.inner = new_editor.inner; self.repo = new_editor.repo; @@ -242,7 +230,7 @@ impl<'repo> super::Editor<'repo> { /// /// Before writing a tree, all of its entries (not only added ones), will be validated to assure they are /// correct. The objects pointed to by entries also have to exist already. - pub fn write(&mut self) -> Result, write::Error> { + pub fn write(&mut self) -> Result, crate::Error> { write_cursor(&mut self.to_cursor()) } @@ -262,10 +250,10 @@ impl<'repo> super::Editor<'repo> { } } -fn write_cursor<'repo>(cursor: &mut Cursor<'_, 'repo>) -> Result, write::Error> { +fn write_cursor<'repo>(cursor: &mut Cursor<'_, 'repo>) -> Result, crate::Error> { cursor .inner - .write(|tree| -> Result { + .write(|tree| -> Result { for entry in &tree.entries { let kind: EntryKind = entry.mode.into(); gix_validate::path::component( diff --git a/gix/src/object/tree/entry.rs b/gix/src/object/tree/entry.rs index 16085f14d95..bc7445a1d98 100644 --- a/gix/src/object/tree/entry.rs +++ b/gix/src/object/tree/entry.rs @@ -19,7 +19,7 @@ impl<'repo> Entry<'repo> { } /// Return the object this entry points to. - pub fn object(&self) -> Result, crate::object::find::existing::Error> { + pub fn object(&self) -> Result, crate::Error> { self.id().object() } @@ -69,7 +69,7 @@ impl<'repo, 'a> EntryRef<'repo, 'a> { } /// Return the object this entry points to. - pub fn object(&self) -> Result, crate::object::find::existing::Error> { + pub fn object(&self) -> Result, crate::Error> { self.id().object() } diff --git a/gix/src/object/tree/mod.rs b/gix/src/object/tree/mod.rs index 8ad595d1dbc..38ac00a01a0 100644 --- a/gix/src/object/tree/mod.rs +++ b/gix/src/object/tree/mod.rs @@ -4,7 +4,7 @@ use gix_hash::ObjectId; pub use gix_object::tree::{EntryKind, EntryMode}; use gix_object::{FindExt, TreeRefIter, bstr::BStr, tree::next_entry}; -use crate::{Id, ObjectDetached, Repository, Tree, object::find}; +use crate::{Id, ObjectDetached, Repository, Tree}; /// All state needed to conveniently edit a tree, using only [update-or-insert](Editor::upsert()) and [removals](Editor::remove()). #[derive(Clone)] @@ -35,7 +35,7 @@ impl<'repo> Tree<'repo> { } /// Parse our tree data and return the parse tree for direct access to its entries. - pub fn decode(&self) -> Result, gix_object::decode::Error> { + pub fn decode(&self) -> Result, gix_error::ValidationError> { gix_object::TreeRef::from_bytes(&self.data, self.id.kind()) } @@ -72,7 +72,7 @@ impl<'repo> Tree<'repo> { /// to reuse a vector and use a binary search instead, which might be able to improve performance over all. /// However, a benchmark should be created first to have some data and see which trade-off to choose here. /// - pub fn lookup_entry(&self, path: I) -> Result>, find::existing::Error> + pub fn lookup_entry(&self, path: I) -> Result>, crate::Error> where I: IntoIterator, P: PartialEq, @@ -108,7 +108,7 @@ impl<'repo> Tree<'repo> { /// Searching tree entries is currently done in sequence, which allows to the search to be allocation free. /// This is beneficial for most 'common' repositiries, but for other cases an allocation might be preferable /// to allow a bisect. - pub fn peel_to_entry(&mut self, path: I) -> Result>, find::existing::Error> + pub fn peel_to_entry(&mut self, path: I) -> Result>, crate::Error> where I: IntoIterator, P: PartialEq, @@ -181,7 +181,7 @@ impl<'repo> Tree<'repo> { pub fn lookup_entry_by_path( &self, relative_path: impl AsRef, - ) -> Result>, find::existing::Error> { + ) -> Result>, crate::Error> { self.lookup_entry( relative_path .as_ref() @@ -199,7 +199,7 @@ impl<'repo> Tree<'repo> { pub fn peel_to_entry_by_path( &mut self, relative_path: impl AsRef, - ) -> Result>, find::existing::Error> { + ) -> Result>, crate::Error> { self.peel_to_entry( relative_path .as_ref() @@ -225,7 +225,7 @@ mod iter { impl<'repo> Tree<'repo> { /// Return an iterator over tree entries to obtain information about files and directories this tree contains. - pub fn iter(&self) -> impl Iterator, gix_object::decode::Error>> { + pub fn iter(&self) -> impl Iterator, gix_error::ValidationError>> { let repo = self.repo; gix_object::TreeRefIter::from_bytes(&self.data, self.id.kind()) .map(move |e| e.map(|entry| EntryRef { inner: entry, repo })) @@ -261,7 +261,7 @@ mod _impls { use crate::Tree; impl TryFrom> for gix_object::Tree { - type Error = gix_object::decode::Error; + type Error = gix_error::ValidationError; fn try_from(t: Tree<'_>) -> Result { t.decode().map(Into::into) diff --git a/gix/src/open/mod.rs b/gix/src/open/mod.rs index d490a27e821..043e988c772 100644 --- a/gix/src/open/mod.rs +++ b/gix/src/open/mod.rs @@ -50,9 +50,6 @@ pub struct Options { pub(crate) current_dir: Option, } -/// The error returned by [`crate::open()`]. -pub type Error = gix_error::Error; - mod options; pub mod permissions; mod repository; diff --git a/gix/src/open/options.rs b/gix/src/open/options.rs index 35ae5742e2b..4c03d0ed79b 100644 --- a/gix/src/open/options.rs +++ b/gix/src/open/options.rs @@ -1,6 +1,6 @@ use std::path::PathBuf; -use super::{Error, Options}; +use super::Options; use crate::{ThreadSafeRepository, bstr::BString, config, open::Permissions}; impl Default for Options { @@ -168,7 +168,7 @@ impl Options { } /// Open a repository at `path` with the options set so far. - pub fn open(self, path: impl Into) -> Result { + pub fn open(self, path: impl Into) -> Result { ThreadSafeRepository::open_opts(path, self) } } diff --git a/gix/src/open/repository.rs b/gix/src/open/repository.rs index d6874706d2d..17f4e9a0ebf 100644 --- a/gix/src/open/repository.rs +++ b/gix/src/open/repository.rs @@ -12,7 +12,7 @@ use std::{ path::PathBuf, }; -use super::{Error, Options}; +use super::Options; use crate::{ ThreadSafeRepository, bstr::BString, @@ -24,7 +24,7 @@ use crate::{ open::Permissions, }; -fn not_a_repository(source: gix_discover::is_git::Error, path: PathBuf) -> Error { +fn not_a_repository(source: gix_error::Exn, path: PathBuf) -> crate::Error { source .raise(gix_error::NotFoundError::new(format!( "\"{}\" does not appear to be a git repository", @@ -61,7 +61,7 @@ impl EnvironmentOverrides { impl ThreadSafeRepository { /// Open a git repository at the given `path`, possibly expanding it to `path/.git` if `path` is a work tree dir. - pub fn open(path: impl Into) -> Result { + pub fn open(path: impl Into) -> Result { Self::open_opts(path, Options::default()) } @@ -77,7 +77,7 @@ impl ThreadSafeRepository { /// /// Note that opening a repository for implementing custom hooks is also handle specifically in /// [`open_with_environment_overrides()`][Self::open_with_environment_overrides()]. - pub fn open_opts(path: impl Into, mut options: Options) -> Result { + pub fn open_opts(path: impl Into, mut options: Options) -> Result { let _span = gix_trace::coarse!("ThreadSafeRepository::open()"); let (path, kind) = { let path = path.into(); @@ -132,7 +132,7 @@ impl ThreadSafeRepository { pub fn open_with_environment_overrides( fallback_directory: impl Into, trust_map: gix_sec::trust::Mapping, - ) -> Result { + ) -> Result { let _span = gix_trace::coarse!("ThreadSafeRepository::open_with_environment_overrides()"); let overrides = EnvironmentOverrides::from_env().or_erased()?; let (path, path_kind): (PathBuf, _) = match overrides.git_dir { @@ -166,7 +166,7 @@ impl ThreadSafeRepository { mut worktree_dir: Option, mut options: Options, known_common_dir: Option, - ) -> Result { + ) -> Result { let _span = gix_trace::detail!("open_from_paths()"); options.open_path_as_is = false; let Options { @@ -608,7 +608,7 @@ fn replacement_objects_refs_prefix( config: &gix_config::File, lenient: bool, mut filter_config_section: fn(&gix_config::file::Metadata) -> bool, -) -> Result, Error> { +) -> Result, crate::Error> { let is_disabled = config::shared::is_replace_refs_enabled(config, lenient, filter_config_section) .map_err(gix_error::Error::from)? .unwrap_or(true); @@ -633,7 +633,7 @@ fn check_safe_directories( current_dir: &std::path::Path, home: Option<&std::path::Path>, safe_dirs: &[BString], -) -> Result<(), Error> { +) -> Result<(), crate::Error> { let mut is_safe = false; let realpath_or_original = |path: &std::path::Path| { std::fs::canonicalize(path) diff --git a/gix/src/pathspec.rs b/gix/src/pathspec.rs index 5297eb2f5f8..e828a638404 100644 --- a/gix/src/pathspec.rs +++ b/gix/src/pathspec.rs @@ -4,12 +4,6 @@ pub use gix_pathspec::*; use crate::{AttributeStack, Pathspec, PathspecDetached, Repository, bstr::BStr}; use gix_error::ResultExt; -/// -pub mod init { - /// The error returned by [`Pathspec::new()`](super::Pathspec::new()). - pub type Error = gix_error::Error; -} - /// Lifecycle impl<'repo> Pathspec<'repo> { /// Create a new instance by parsing `patterns` into [`Pathspecs`](Pattern) to make them usable for searches. @@ -30,7 +24,7 @@ impl<'repo> Pathspec<'repo> { patterns: impl IntoIterator>, inherit_ignore_case: bool, make_attributes: impl FnOnce() -> Result, - ) -> Result { + ) -> Result { let defaults = repo.pathspec_defaults_inherit_ignore_case(inherit_ignore_case)?; let patterns = patterns .into_iter() diff --git a/gix/src/reference/edits.rs b/gix/src/reference/edits.rs index 6242e82b0d2..e9975688975 100644 --- a/gix/src/reference/edits.rs +++ b/gix/src/reference/edits.rs @@ -4,12 +4,6 @@ pub mod set_target_id { use crate::{Reference, bstr::BString}; - mod error { - /// The error returned by [`Reference::set_target_id()`][super::Reference::set_target_id()]. - pub type Error = gix_error::Error; - } - pub use error::Error; - impl Reference<'_> { /// Set the id of this direct reference to `id` and use `reflog_message` for the reflog (if enabled in the repository). /// @@ -22,7 +16,7 @@ pub mod set_target_id { &mut self, id: impl Into, reflog_message: impl Into, - ) -> Result<(), Error> { + ) -> Result<(), crate::Error> { match &self.inner.target { Target::Symbolic(name) => { return Err(gix_error::Error::from_error(gix_error::message!( @@ -53,7 +47,7 @@ pub mod delete { impl Reference<'_> { /// Delete this reference or fail if it was changed since last observed. /// Note that this instance remains available in memory but probably shouldn't be used anymore. - pub fn delete(&self) -> Result<(), crate::reference::edit::Error> { + pub fn delete(&self) -> Result<(), crate::Error> { self.repo .edit_reference(RefEdit::delete( self.inner.name.clone(), diff --git a/gix/src/reference/errors.rs b/gix/src/reference/errors.rs deleted file mode 100644 index 3e1a47372ce..00000000000 --- a/gix/src/reference/errors.rs +++ /dev/null @@ -1,64 +0,0 @@ -/// -pub mod edit { - /// The error returned by [`edit_references(…)`][crate::Repository::edit_references()], and others - /// which ultimately create a reference. - pub type Error = gix_error::Error; -} - -/// -pub mod peel { - /// The error returned by [`Reference::peel_to_id()`](crate::Reference::peel_to_id()) and - /// [`Reference::into_fully_peeled_id()`](crate::Reference::into_fully_peeled_id()). - pub type Error = gix_error::Error; - - /// - pub mod to_kind { - /// The error returned by [`Reference::peel_to_kind(…)`](crate::Reference::peel_to_kind()). - pub type Error = gix_error::Error; - } -} - -/// -pub mod follow { - /// - pub mod to_object { - /// The error returned by [`Reference::follow_to_object(…)`](crate::Reference::follow_to_object()). - pub type Error = gix_error::Error; - } -} - -/// -pub mod head_id { - /// The error returned by [`Repository::head_id(…)`](crate::Repository::head_id()). - pub type Error = gix_error::Error; -} - -/// -pub mod head_commit { - /// The error returned by [`Repository::head_commit`(…)](crate::Repository::head_commit()). - pub type Error = gix_error::Error; -} - -/// -pub mod head_tree_id { - /// The error returned by [`Repository::head_tree_id`(…)](crate::Repository::head_tree_id()). - pub type Error = gix_error::Error; -} - -/// -pub mod head_tree { - /// The error returned by [`Repository::head_tree`(…)](crate::Repository::head_tree()). - pub type Error = gix_error::Error; -} - -/// -pub mod find { - /// - pub mod existing { - /// The error returned by [`find_reference(…)`][crate::Repository::find_reference()], and others. - pub type Error = gix_error::Error; - } - - /// The error returned by [`try_find_reference(…)`][crate::Repository::try_find_reference()]. - pub type Error = gix_error::Error; -} diff --git a/gix/src/reference/iter.rs b/gix/src/reference/iter.rs index 0a27dcec573..2e5d1444956 100644 --- a/gix/src/reference/iter.rs +++ b/gix/src/reference/iter.rs @@ -39,7 +39,7 @@ impl<'repo> Platform<'repo> { /// /// Even broken or otherwise unparsable or inaccessible references are returned and have to be handled by the caller on a /// case by case basis. - pub fn all(&self) -> Result, init::Error> { + pub fn all(&self) -> Result, crate::Error> { Ok(Iter::new(self.repo, self.platform.all().or_erased()?)) } @@ -48,8 +48,8 @@ impl<'repo> Platform<'repo> { /// These are of the form `refs/heads/` or `refs/remotes/origin`, and must not contain relative paths components like `.` or `..`. pub fn prefixed<'a>( &self, - prefix: impl TryInto<&'a RelativePath, Error = gix_path::relative_path::Error>, - ) -> Result, init::Error> { + prefix: impl TryInto<&'a RelativePath, Error = gix_error::Exn>, + ) -> Result, crate::Error> { let prefix = prefix.try_into().map_err(gix_error::Exn::into_error)?; Ok(Iter::new(self.repo, self.platform.prefixed(prefix).or_erased()?)) } @@ -57,7 +57,7 @@ impl<'repo> Platform<'repo> { /// Return an iterator over all references that are tags. /// /// They are all prefixed with `refs/tags`. - pub fn tags(&self) -> Result, init::Error> { + pub fn tags(&self) -> Result, crate::Error> { let prefix = b"refs/tags/".try_into().map_err(gix_error::Exn::into_error)?; Ok(Iter::new(self.repo, self.platform.prefixed(prefix).or_erased()?)) } @@ -66,14 +66,14 @@ impl<'repo> Platform<'repo> { /// Return an iterator over all local branches. /// /// They are all prefixed with `refs/heads`. - pub fn local_branches(&self) -> Result, init::Error> { + pub fn local_branches(&self) -> Result, crate::Error> { let prefix = b"refs/heads/".try_into().map_err(gix_error::Exn::into_error)?; Ok(Iter::new(self.repo, self.platform.prefixed(prefix).or_erased()?)) } // TODO: tests /// Return an iterator over all local pseudo references. - pub fn pseudo(&self) -> Result, init::Error> { + pub fn pseudo(&self) -> Result, crate::Error> { Ok(Iter::new(self.repo, self.platform.pseudo().or_erased()?)) } @@ -81,7 +81,7 @@ impl<'repo> Platform<'repo> { /// Return an iterator over all remote branches. /// /// They are all prefixed with `refs/remotes`. - pub fn remote_branches(&self) -> Result, init::Error> { + pub fn remote_branches(&self) -> Result, crate::Error> { let prefix = b"refs/remotes/".try_into().map_err(gix_error::Exn::into_error)?; Ok(Iter::new(self.repo, self.platform.prefixed(prefix).or_erased()?)) } @@ -124,11 +124,5 @@ impl<'r> Iterator for Iter<'_, 'r> { } } -/// -pub mod init { - /// The error returned by [`Platform::all()`](super::Platform::all()) or [`Platform::prefixed()`](super::Platform::prefixed()). - pub type Error = gix_error::Error; -} - /// The error returned by [references()][crate::Repository::references()]. pub type Error = gix_ref::packed::buffer::open::Error; diff --git a/gix/src/reference/mod.rs b/gix/src/reference/mod.rs index c2bab2f75df..9fc96afb726 100644 --- a/gix/src/reference/mod.rs +++ b/gix/src/reference/mod.rs @@ -13,9 +13,6 @@ pub mod iter; /// pub mod remote; -mod errors; -pub use errors::{edit, find, follow, head_commit, head_id, head_tree, head_tree_id, peel}; - pub mod log; mod edits; @@ -79,7 +76,7 @@ impl<'repo> Reference<'repo> { /// This is useful to learn where this reference is ultimately pointing to after following /// the chain of symbolic refs and annotated tags. #[deprecated = "Use `peel_to_id()` instead"] - pub fn peel_to_id_in_place(&mut self) -> Result, peel::Error> { + pub fn peel_to_id_in_place(&mut self) -> Result, crate::Error> { let oid = self.inner.peel_to_id(&self.repo.refs, &self.repo.objects).or_erased()?; Ok(Id::from_id(oid, self.repo)) } @@ -108,7 +105,7 @@ impl<'repo> Reference<'repo> { /// assert_eq!(peeled_id, repo.find_reference("b")?.id()); /// # Ok(()) } /// ``` - pub fn peel_to_id(&mut self) -> Result, peel::Error> { + pub fn peel_to_id(&mut self) -> Result, crate::Error> { let oid = self.inner.peel_to_id(&self.repo.refs, &self.repo.objects).or_erased()?; Ok(Id::from_id(oid, self.repo)) } @@ -122,7 +119,7 @@ impl<'repo> Reference<'repo> { pub fn peel_to_id_in_place_packed( &mut self, packed: Option<&gix_ref::packed::Buffer>, - ) -> Result, peel::Error> { + ) -> Result, crate::Error> { let oid = self .inner .peel_to_id_packed(&self.repo.refs, &self.repo.objects, packed) @@ -138,7 +135,7 @@ impl<'repo> Reference<'repo> { /// /// Note that this method mutates `self` in place if it does not already point to a /// non-symbolic object. - pub fn peel_to_id_packed(&mut self, packed: Option<&gix_ref::packed::Buffer>) -> Result, peel::Error> { + pub fn peel_to_id_packed(&mut self, packed: Option<&gix_ref::packed::Buffer>) -> Result, crate::Error> { let oid = self .inner .peel_to_id_packed(&self.repo.refs, &self.repo.objects, packed) @@ -147,7 +144,7 @@ impl<'repo> Reference<'repo> { } /// Similar to [`peel_to_id()`](Reference::peel_to_id()), but consumes this instance. - pub fn into_fully_peeled_id(mut self) -> Result, peel::Error> { + pub fn into_fully_peeled_id(mut self) -> Result, crate::Error> { self.peel_to_id() } @@ -160,7 +157,7 @@ impl<'repo> Reference<'repo> { /// Note that `git2::Reference::peel` does not "peel in place", but returns a new object /// instead. #[doc(alias = "peel", alias = "git2")] - pub fn peel_to_kind(&mut self, kind: gix_object::Kind) -> Result, peel::to_kind::Error> { + pub fn peel_to_kind(&mut self, kind: gix_object::Kind) -> Result, crate::Error> { let packed = self.repo.refs.cached_packed_buffer().or_erased()?; self.peel_to_kind_packed(kind, packed.as_ref().map(|p| &***p)) } @@ -181,21 +178,21 @@ impl<'repo> Reference<'repo> { /// assert_eq!(commit.message_raw()?, "c2\n"); /// # Ok(()) } /// ``` - pub fn peel_to_commit(&mut self) -> Result, peel::to_kind::Error> { + pub fn peel_to_commit(&mut self) -> Result, crate::Error> { Ok(self.peel_to_kind(gix_object::Kind::Commit)?.into_commit()) } /// Peel this ref until the first annotated tag. /// /// For details, see [`peel_to_kind`()](Self::peel_to_kind()). - pub fn peel_to_tag(&mut self) -> Result, peel::to_kind::Error> { + pub fn peel_to_tag(&mut self) -> Result, crate::Error> { Ok(self.peel_to_kind(gix_object::Kind::Tag)?.into_tag()) } /// Peel this ref until the first tree. /// /// For details, see [`peel_to_kind`()](Self::peel_to_kind()). - pub fn peel_to_tree(&mut self) -> Result, peel::to_kind::Error> { + pub fn peel_to_tree(&mut self) -> Result, crate::Error> { Ok(self.peel_to_kind(gix_object::Kind::Tree)?.into_tree()) } @@ -203,7 +200,7 @@ impl<'repo> Reference<'repo> { /// as it would require an annotated tag to point to a blob, instead of a commit. /// /// For details, see [`peel_to_kind`()](Self::peel_to_kind()). - pub fn peel_to_blob(&mut self) -> Result, peel::to_kind::Error> { + pub fn peel_to_blob(&mut self) -> Result, crate::Error> { Ok(self.peel_to_kind(gix_object::Kind::Blob)?.into_blob()) } @@ -213,7 +210,7 @@ impl<'repo> Reference<'repo> { &mut self, kind: gix_object::Kind, packed: Option<&gix_ref::packed::Buffer>, - ) -> Result, peel::to_kind::Error> { + ) -> Result, crate::Error> { let target = self .inner .follow_to_object_packed(&self.repo.refs, packed) @@ -227,7 +224,7 @@ impl<'repo> Reference<'repo> { /// After this call, this ref will be pointing to an object directly, but may still not consider itself 'peeled' unless /// a symbolic target ref was looked up from packed-refs. #[doc(alias = "resolve", alias = "git2")] - pub fn follow_to_object(&mut self) -> Result, follow::to_object::Error> { + pub fn follow_to_object(&mut self) -> Result, crate::Error> { let packed = self.repo.refs.cached_packed_buffer().or_erased()?; self.follow_to_object_packed(packed.as_ref().map(|p| &***p)) } @@ -238,7 +235,7 @@ impl<'repo> Reference<'repo> { pub fn follow_to_object_packed( &mut self, packed: Option<&gix_ref::packed::Buffer>, - ) -> Result, follow::to_object::Error> { + ) -> Result, crate::Error> { Ok(self .inner .follow_to_object_packed(&self.repo.refs, packed) diff --git a/gix/src/reference/remote.rs b/gix/src/reference/remote.rs index 850116e7a7c..e8425370109 100644 --- a/gix/src/reference/remote.rs +++ b/gix/src/reference/remote.rs @@ -4,7 +4,6 @@ use crate::{ Reference, bstr::{BStr, ByteSlice}, remote, - repository::{branch_remote_ref_name, branch_remote_tracking_ref_name}, }; /// Remotes @@ -34,10 +33,7 @@ impl<'repo> Reference<'repo> { /// Find the remote along with all configuration associated with it suitable for handling this reference. /// /// See also [`Repository::branch_remote()`](crate::Repository::branch_remote()) for more details. - pub fn remote( - &self, - direction: remote::Direction, - ) -> Option, remote::find::existing::Error>> { + pub fn remote(&self, direction: remote::Direction) -> Option, crate::Error>> { self.repo.branch_remote(self.name().shorten(), direction) } @@ -45,10 +41,7 @@ impl<'repo> Reference<'repo> { /// /// See [`Repository::branch_remote_ref_name()`](crate::Repository::branch_remote_ref_name()) for details. #[doc(alias = "upstream", alias = "git2")] - pub fn remote_ref_name( - &self, - direction: remote::Direction, - ) -> Option> { + pub fn remote_ref_name(&self, direction: remote::Direction) -> Option> { self.repo.branch_remote_ref_name(self.name(), direction) } @@ -56,10 +49,7 @@ impl<'repo> Reference<'repo> { /// /// See [`Repository::branch_remote_tracking_ref_name()`](crate::Repository::branch_remote_tracking_ref_name()) for details. #[doc(alias = "upstream", alias = "git2")] - pub fn remote_tracking_ref_name( - &self, - direction: remote::Direction, - ) -> Option> { + pub fn remote_tracking_ref_name(&self, direction: remote::Direction) -> Option> { self.repo.branch_remote_tracking_ref_name(self.name(), direction) } } diff --git a/gix/src/remote/access.rs b/gix/src/remote/access.rs index edea10377da..c0716ade65b 100644 --- a/gix/src/remote/access.rs +++ b/gix/src/remote/access.rs @@ -98,7 +98,7 @@ impl Remote<'_> { /// Every URL is attempted non-destructively: successful rewrites remain effective if another rewritten URL is malformed, /// while a failed entry keeps using its original URL. The first error is returned in fetch, push-fallback, explicit-push /// order. See [`urls()`](Self::urls()) for which rules apply to each category. - pub fn rewrite_urls(&mut self) -> Result<&mut Self, remote::init::Error> { + pub fn rewrite_urls(&mut self) -> Result<&mut Self, crate::Error> { let (url_aliases, url_err) = remote::init::rewrite_url_aliases_non_destructive(&self.repo.config, &self.urls, remote::Direction::Fetch); self.url_aliases = url_aliases; diff --git a/gix/src/remote/build.rs b/gix/src/remote/build.rs index 1814001a18d..0388eea0f72 100644 --- a/gix/src/remote/build.rs +++ b/gix/src/remote/build.rs @@ -7,7 +7,7 @@ impl Remote<'_> { /// Override the `url` to be used when fetching data from a remote. /// /// Note that this URL is typically set during instantiation with [`crate::Repository::remote_at()`]. - pub fn with_url(self, url: Url) -> Result + pub fn with_url(self, url: Url) -> Result where Url: TryInto, E: std::error::Error + Send + Sync + 'static, @@ -19,7 +19,7 @@ impl Remote<'_> { /// eliminating one failure mode. /// /// Note that this URL is typically set during instantiation with [`crate::Repository::remote_at_without_url_rewrite()`]. - pub fn with_url_without_url_rewrite(self, url: Url) -> Result + pub fn with_url_without_url_rewrite(self, url: Url) -> Result where Url: TryInto, E: std::error::Error + Send + Sync + 'static, @@ -29,7 +29,7 @@ impl Remote<'_> { /// Set the `url` to be used when pushing data to a remote. #[deprecated = "Use `with_push_url()` instead"] - pub fn push_url(self, url: Url) -> Result + pub fn push_url(self, url: Url) -> Result where Url: TryInto, E: std::error::Error + Send + Sync + 'static, @@ -41,7 +41,7 @@ impl Remote<'_> { /// /// Explicit push URLs are rewritten with `url..insteadOf`; `pushInsteadOf` only applies when a fetch URL is used /// as the push fallback because no explicit push URL is configured. - pub fn with_push_url(self, url: Url) -> Result + pub fn with_push_url(self, url: Url) -> Result where Url: TryInto, E: std::error::Error + Send + Sync + 'static, @@ -52,7 +52,7 @@ impl Remote<'_> { /// Set the `url` to be used when pushing data to a remote, without applying rewrite rules in case these could be faulty, /// eliminating one failure mode. #[deprecated = "Use `with_push_url_without_rewrite()` instead"] - pub fn push_url_without_url_rewrite(self, url: Url) -> Result + pub fn push_url_without_url_rewrite(self, url: Url) -> Result where Url: TryInto, E: std::error::Error + Send + Sync + 'static, @@ -62,7 +62,7 @@ impl Remote<'_> { /// Set the `url` to be used when pushing data to a remote, without applying rewrite rules in case these could be faulty, /// eliminating one failure mode. - pub fn with_push_url_without_url_rewrite(self, url: Url) -> Result + pub fn with_push_url_without_url_rewrite(self, url: Url) -> Result where Url: TryInto, E: std::error::Error + Send + Sync + 'static, @@ -76,11 +76,7 @@ impl Remote<'_> { self } - fn push_url_inner( - mut self, - push_url: gix_url::Url, - should_rewrite_urls: bool, - ) -> Result { + fn push_url_inner(mut self, push_url: gix_url::Url, should_rewrite_urls: bool) -> Result { self.push_urls = vec![push_url]; self.push_url_aliases = if should_rewrite_urls { @@ -98,7 +94,7 @@ impl Remote<'_> { Ok(self) } - fn url_inner(mut self, url: gix_url::Url, should_rewrite_urls: bool) -> Result { + fn url_inner(mut self, url: gix_url::Url, should_rewrite_urls: bool) -> Result { self.urls = vec![url]; self.url_aliases = if should_rewrite_urls { diff --git a/gix/src/remote/connect.rs b/gix/src/remote/connect.rs index 1386c3cb169..14f6bebc41f 100644 --- a/gix/src/remote/connect.rs +++ b/gix/src/remote/connect.rs @@ -10,9 +10,6 @@ use gix_transport::client::blocking_io::{Transport, connect}; use crate::{Remote, config::tree::Protocol, remote::Connection}; -/// The error returned by [connect()][crate::Remote::connect()]. -pub type Error = gix_error::Error; - /// Establishing connections to remote hosts (without performing a git-handshake). impl<'repo> Remote<'repo> { /// Create a new connection using `transport` to communicate, with `progress` to indicate changes. @@ -47,7 +44,7 @@ impl<'repo> Remote<'repo> { pub async fn connect( &self, direction: crate::remote::Direction, - ) -> Result>, Error> { + ) -> Result>, crate::Error> { let (url, version) = self.sanitized_url_and_version(direction)?; #[cfg(feature = "blocking-network-client")] let scheme_is_ssh = url.scheme == gix_url::Scheme::Ssh; @@ -75,8 +72,8 @@ impl<'repo> Remote<'repo> { pub fn sanitized_url_and_version( &self, direction: crate::remote::Direction, - ) -> Result<(gix_url::Url, gix_protocol::transport::Protocol), Error> { - fn sanitize(mut url: gix_url::Url) -> Result { + ) -> Result<(gix_url::Url, gix_protocol::transport::Protocol), crate::Error> { + fn sanitize(mut url: gix_url::Url) -> Result { if url.scheme == gix_url::Scheme::File { let mut dir = gix_path::to_native_path_on_windows(Cow::Borrowed(url.path.as_ref())); let kind = gix_discover::is_git(dir.as_ref()) diff --git a/gix/src/remote/connection/access.rs b/gix/src/remote/connection/access.rs index 6d026c964a5..22e61e95fec 100644 --- a/gix/src/remote/connection/access.rs +++ b/gix/src/remote/connection/access.rs @@ -92,10 +92,7 @@ where /// /// It's meant to be used by users of the [`with_credentials()`](Self::with_credentials()) builder to gain access to the /// default way of handling credentials, which they can call as fallback. - pub fn configured_credentials( - &self, - url: gix_url::Url, - ) -> Result, crate::config::credential_helpers::Error> { + pub fn configured_credentials(&self, url: gix_url::Url) -> Result, crate::Error> { let (mut cascade, _action_with_normalized_url, prompt_opts) = self.remote.repo.config_snapshot().credential_helpers(url)?; Ok(Box::new(move |action| cascade.invoke(action, prompt_opts.clone())) as AuthenticateFn<'_>) diff --git a/gix/src/remote/connection/fetch/config.rs b/gix/src/remote/connection/fetch/config.rs index a3294e89069..cf6793f37ef 100644 --- a/gix/src/remote/connection/fetch/config.rs +++ b/gix/src/remote/connection/fetch/config.rs @@ -1,11 +1,10 @@ -use super::Error; use crate::{ Repository, config::{cache::util::ApplyLeniency, tree::Pack}, }; use gix_error::ErrorExt; -pub fn index_threads(repo: &Repository) -> Result, Error> { +pub fn index_threads(repo: &Repository) -> Result, crate::Error> { Pack::THREADS .try_into_usize( repo.config @@ -20,7 +19,7 @@ pub fn index_threads(repo: &Repository) -> Result, Error> { }) } -pub fn pack_index_version(repo: &Repository) -> Result { +pub fn pack_index_version(repo: &Repository) -> Result { Ok(Pack::INDEX_VERSION .try_into_index_version(repo.config.resolved.integer(Pack::INDEX_VERSION)) .with_leniency(repo.options.lenient_config) diff --git a/gix/src/remote/connection/fetch/error.rs b/gix/src/remote/connection/fetch/error.rs deleted file mode 100644 index b9f45dd4ee6..00000000000 --- a/gix/src/remote/connection/fetch/error.rs +++ /dev/null @@ -1,2 +0,0 @@ -/// The error returned by [`receive()`](super::Prepare::receive()). -pub type Error = gix_error::Error; diff --git a/gix/src/remote/connection/fetch/mod.rs b/gix/src/remote/connection/fetch/mod.rs index db5a002c004..b99e4121c58 100644 --- a/gix/src/remote/connection/fetch/mod.rs +++ b/gix/src/remote/connection/fetch/mod.rs @@ -15,9 +15,6 @@ use crate::{ }, }; -mod error; -pub use error::Error; - use crate::remote::fetch::WritePackedRefs; /// The way reflog messages should be composed whenever a ref is written with recent objects from a remote. @@ -99,12 +96,6 @@ pub mod outcome { pub use gix_protocol::fetch::ProgressId; -/// -pub mod prepare { - /// The error returned by [`prepare_fetch()`][super::Connection::prepare_fetch()]. - pub type Error = gix_error::Error; -} - impl<'auth, 'repo, T> Connection<'_, 'auth, 'repo, T> where T: Transport, @@ -126,7 +117,7 @@ where self, progress: impl Progress, options: ref_map::Options, - ) -> Result, prepare::Error> { + ) -> Result, crate::Error> { let repo = self.remote.repo; let inner = self.into_detached().prepare_fetch(repo, progress, options).await?; Ok(Prepare { inner, repo }) @@ -143,7 +134,7 @@ where repo: &crate::Repository, progress: impl Progress, options: ref_map::Options, - ) -> Result, prepare::Error> { + ) -> Result, crate::Error> { if self.remote.fetch_refspecs().is_empty() && options.extra_refspecs.is_empty() { return Err(gix_error::Error::from_error(gix_error::ValidationError::new( "Cannot perform a meaningful fetch operation without any configured ref-specs", diff --git a/gix/src/remote/connection/fetch/receive_pack.rs b/gix/src/remote/connection/fetch/receive_pack.rs index da796b3325c..b62e4ae5563 100644 --- a/gix/src/remote/connection/fetch/receive_pack.rs +++ b/gix/src/remote/connection/fetch/receive_pack.rs @@ -16,7 +16,7 @@ use crate::{ remote::{ connection::fetch::{PrepareDetached, config}, fetch, - fetch::{Error, Outcome, Prepare, RefLogMessage, Status, negotiate::Algorithm, outcome, refs}, + fetch::{Outcome, Prepare, RefLogMessage, Status, negotiate::Algorithm, outcome, refs}, }, }; @@ -71,7 +71,7 @@ where /// - `gitoxide.userAgent` is read to obtain the application user agent for git servers and for HTTP servers as well. /// #[gix_protocol::bisync::bisync] - pub async fn receive

(self, progress: P, should_interrupt: &AtomicBool) -> Result + pub async fn receive

(self, progress: P, should_interrupt: &AtomicBool) -> Result where P: gix_features::progress::NestedProgress, P::SubProgress: 'static, @@ -91,7 +91,7 @@ where repo: &crate::Repository, progress: P, should_interrupt: &AtomicBool, - ) -> Result + ) -> Result where P: gix_features::progress::NestedProgress, P::SubProgress: 'static, @@ -281,7 +281,7 @@ struct Negotiate<'a, 'b, 'c> { } impl gix_protocol::fetch::Negotiate for Negotiate<'_, '_, '_> { - fn mark_complete_and_common_ref(&mut self) -> Result { + fn mark_complete_and_common_ref(&mut self) -> Result> { negotiate::mark_complete_and_common_ref( &self.objects, self.refs, @@ -323,7 +323,7 @@ impl gix_protocol::fetch::Negotiate for Negotiate<'_, '_, '_> { state: &mut negotiate::one_round::State, arguments: &mut Arguments, previous_response: Option<&gix_protocol::fetch::Response>, - ) -> Result<(negotiate::Round, bool), negotiate::Error> { + ) -> Result<(negotiate::Round, bool), gix_error::Exn> { negotiate::one_round( self.negotiator.deref_mut(), &mut *self.graph, diff --git a/gix/src/remote/connection/fetch/update_refs/mod.rs b/gix/src/remote/connection/fetch/update_refs/mod.rs index 2278180888f..8305a70b798 100644 --- a/gix/src/remote/connection/fetch/update_refs/mod.rs +++ b/gix/src/remote/connection/fetch/update_refs/mod.rs @@ -69,7 +69,7 @@ pub(crate) fn update( fetch_tags: fetch::Tags, dry_run: fetch::DryRun, write_packed_refs: fetch::WritePackedRefs, -) -> Result { +) -> Result { let _span = gix_trace::detail!("update_refs()", mappings = mappings.len()); let mut edits = Vec::new(); let mut updates = Vec::new(); @@ -416,7 +416,7 @@ fn update_needs_adjustment_as_edits_symbolic_target_is_missing( /// /// Born symbolic remote refs are written as direct refs to the advertised target object id. /// Unborn remote refs remain symbolic as there is no object id to write. -fn new_value_by_remote(remote: &Source) -> Result { +fn new_value_by_remote(remote: &Source) -> Result { let remote_id = remote.as_id(); Ok( if let Source::Ref( diff --git a/gix/src/remote/connection/fetch/update_refs/update.rs b/gix/src/remote/connection/fetch/update_refs/update.rs index df39ea51aa0..d5d74fd09b1 100644 --- a/gix/src/remote/connection/fetch/update_refs/update.rs +++ b/gix/src/remote/connection/fetch/update_refs/update.rs @@ -2,13 +2,6 @@ use std::path::PathBuf; use crate::remote::fetch; -mod error { - /// The error returned when updating references. - pub type Error = gix_error::Error; -} - -pub use error::Error; - /// The outcome of the refs-update operation at the end of a fetch. #[derive(Debug, Clone)] pub struct Outcome { diff --git a/gix/src/remote/connection/ref_map.rs b/gix/src/remote/connection/ref_map.rs index 9ff60b126c5..046b8f3b624 100644 --- a/gix/src/remote/connection/ref_map.rs +++ b/gix/src/remote/connection/ref_map.rs @@ -7,9 +7,6 @@ use gix_transport::client::blocking_io::Transport; use crate::remote::{Connection, connection::ConnectionDetached, fetch}; -/// The error returned by [`Connection::ref_map()`]. -pub type Error = gix_error::Error; - /// For use in [`Connection::ref_map()`]. #[derive(Debug, Clone)] pub struct Options { @@ -61,7 +58,7 @@ where self, progress: impl Progress, options: Options, - ) -> Result<(fetch::RefMap, gix_protocol::Handshake), Error> { + ) -> Result<(fetch::RefMap, gix_protocol::Handshake), crate::Error> { let repo = self.remote.repo; self.into_detached().ref_map(repo, progress, options).await } @@ -77,7 +74,7 @@ where repo: &crate::Repository, progress: impl Progress, options: Options, - ) -> Result<(fetch::RefMap, gix_protocol::Handshake), Error> { + ) -> Result<(fetch::RefMap, gix_protocol::Handshake), crate::Error> { let refmap = self.ref_map_by_ref(repo, progress, options).await?; let handshake = self .handshake @@ -95,7 +92,7 @@ where handshake_parameters, mut extra_refspecs, }: Options, - ) -> Result { + ) -> Result { let _span = gix_trace::coarse!("remote::Connection::ref_map()"); if let Some(tag_spec) = self.remote.fetch_tags.to_refspec().map(|spec| spec.to_owned()) && !extra_refspecs.contains(&tag_spec) diff --git a/gix/src/remote/errors.rs b/gix/src/remote/errors.rs deleted file mode 100644 index acc2bf9e362..00000000000 --- a/gix/src/remote/errors.rs +++ /dev/null @@ -1,17 +0,0 @@ -/// -pub mod find { - /// The error returned by [`Repository::find_remote(…)`](crate::Repository::find_remote()). - pub type Error = gix_error::Error; - - /// - pub mod existing { - /// The error returned by [`Repository::find_remote(…)`](crate::Repository::find_remote()). - pub type Error = gix_error::Error; - } - - /// - pub mod for_fetch { - /// The error returned by [`Repository::find_fetch_remote(…)`](crate::Repository::find_fetch_remote()). - pub type Error = gix_error::Error; - } -} diff --git a/gix/src/remote/fetch.rs b/gix/src/remote/fetch.rs index 4e28ea2ba56..115dea179e3 100644 --- a/gix/src/remote/fetch.rs +++ b/gix/src/remote/fetch.rs @@ -2,14 +2,10 @@ pub mod negotiate { #[cfg(feature = "credentials")] pub use gix_negotiate::Algorithm; - #[cfg(any(feature = "blocking-network-client", feature = "async-network-client"))] - pub use gix_protocol::fetch::negotiate::Error; } #[cfg(any(feature = "blocking-network-client", feature = "async-network-client"))] -pub use super::connection::fetch::{ - Error, Outcome, Prepare, ProgressId, RefLogMessage, Status, outcome, prepare, refs, -}; +pub use super::connection::fetch::{Outcome, Prepare, ProgressId, RefLogMessage, Status, outcome, refs}; /// If `Yes`, don't really make changes but do as much as possible to get an idea of what would be done. #[derive(Debug, Copy, Clone, PartialEq, Eq)] diff --git a/gix/src/remote/init.rs b/gix/src/remote/init.rs index 5968cf759f7..88a3828c5ae 100644 --- a/gix/src/remote/init.rs +++ b/gix/src/remote/init.rs @@ -4,9 +4,6 @@ use gix_refspec::RefSpec; use crate::{Remote, Repository, config, remote}; -/// The error returned by [`Repository::remote_at(…)`][crate::Repository::remote_at()]. -pub type Error = gix_error::Error; - use crate::bstr::BString; type UrlAliases = Vec>; @@ -24,7 +21,7 @@ impl<'repo> Remote<'repo> { should_rewrite_urls: bool, fetch_tags: remote::fetch::Tags, repo: &'repo Repository, - ) -> Result { + ) -> Result { let (url_aliases, url_push_aliases, push_url_aliases) = if should_rewrite_urls { rewrite_urls(&repo.config, &urls, &push_urls) } else { @@ -52,7 +49,7 @@ impl<'repo> Remote<'repo> { url: Url, should_rewrite_urls: bool, repo: &'repo Repository, - ) -> Result + ) -> Result where Url: TryInto, E: std::error::Error + Send + Sync + 'static, @@ -64,7 +61,7 @@ impl<'repo> Remote<'repo> { url: gix_url::Url, should_rewrite_urls: bool, repo: &'repo Repository, - ) -> Result { + ) -> Result { let urls = vec![url]; let (url_aliases, url_push_aliases, _) = if should_rewrite_urls { rewrite_urls(&repo.config, &urls, &[]) @@ -91,7 +88,7 @@ pub(crate) fn rewrite_url( url: &gix_url::Url, direction: remote::Direction, error_kind: remote::Direction, -) -> Result, Error> { +) -> Result, crate::Error> { config .url_rewrite() .longest(url, direction) @@ -113,7 +110,7 @@ pub(crate) fn rewrite_url_aliases( config: &config::Cache, urls: &[gix_url::Url], direction: remote::Direction, -) -> Result>, Error> { +) -> Result>, crate::Error> { rewrite_url_aliases_with_error_kind(config, urls, direction, direction) } @@ -122,7 +119,7 @@ pub(crate) fn rewrite_url_aliases_with_error_kind( urls: &[gix_url::Url], direction: remote::Direction, error_kind: remote::Direction, -) -> Result>, Error> { +) -> Result>, crate::Error> { urls.iter() .map(|url| rewrite_url(config, url, direction, error_kind)) .collect() @@ -132,7 +129,7 @@ pub(crate) fn rewrite_url_aliases_non_destructive( config: &config::Cache, urls: &[gix_url::Url], direction: remote::Direction, -) -> (Vec>, Option) { +) -> (Vec>, Option) { rewrite_url_aliases_non_destructive_with_error_kind(config, urls, direction, direction) } @@ -141,7 +138,7 @@ pub(crate) fn rewrite_url_aliases_non_destructive_with_error_kind( urls: &[gix_url::Url], direction: remote::Direction, error_kind: remote::Direction, -) -> (Vec>, Option) { +) -> (Vec>, Option) { let mut first_error = None; let aliases = urls .iter() @@ -161,7 +158,7 @@ pub(crate) fn rewrite_url_aliases_with_fallback_non_destructive( urls: &[gix_url::Url], direction: remote::Direction, fallback: remote::Direction, -) -> (Vec>, Option) { +) -> (Vec>, Option) { let mut first_error = None; let aliases = urls .iter() @@ -187,7 +184,7 @@ pub(crate) fn rewrite_urls( config: &config::Cache, urls: &[gix_url::Url], push_urls: &[gix_url::Url], -) -> Result { +) -> Result { let url_aliases = rewrite_url_aliases(config, urls, remote::Direction::Fetch)?; let url_push_aliases = if push_urls.is_empty() { rewrite_url_aliases_with_fallback_non_destructive( diff --git a/gix/src/remote/mod.rs b/gix/src/remote/mod.rs index 94bdf58cecb..64e3ba11a8d 100644 --- a/gix/src/remote/mod.rs +++ b/gix/src/remote/mod.rs @@ -40,9 +40,6 @@ pub mod name; mod build; -mod errors; -pub use errors::find; - /// pub mod init; diff --git a/gix/src/remote/name.rs b/gix/src/remote/name.rs index 74d273a1974..ad67d0983af 100644 --- a/gix/src/remote/name.rs +++ b/gix/src/remote/name.rs @@ -3,13 +3,10 @@ use std::borrow::Cow; use super::Name; use crate::bstr::{BStr, BString, ByteSlice, ByteVec}; -/// The error returned by [validated()]. -pub type Error = gix_error::Error; - /// Return `name` if it is valid as symbolic remote name. /// /// This means it has to be valid within a the ref path of a tracking branch. -pub fn validated(name: impl Into) -> Result { +pub fn validated(name: impl Into) -> Result { let name = name.into(); match gix_refspec::parse( format!("refs/heads/test:refs/remotes/{name}/test").as_str().into(), diff --git a/gix/src/remote/save.rs b/gix/src/remote/save.rs index 815d3b6131a..c6b031b4e59 100644 --- a/gix/src/remote/save.rs +++ b/gix/src/remote/save.rs @@ -3,12 +3,6 @@ use gix_error::ResultExt; use crate::{Remote, bstr::BStr, config, remote}; use gix_utils::AsBStr; -/// The error returned by [`Remote::save_to()`]. -pub type Error = gix_error::Error; - -/// The error returned by [`Remote::save_as_to()`]. -pub type AsError = gix_error::Error; - /// Serialize into git-config. impl Remote<'_> { /// Save ourselves to the given `config` if we are a named remote or fail otherwise. @@ -16,7 +10,7 @@ impl Remote<'_> { /// Note that all sections named `remote ""` will be cleared of all values we are about to write, /// and the last `remote ""` section will be containing all relevant values so that reloading the remote /// from `config` would yield the same in-memory state. - pub fn save_to(&self, config: &mut gix_config::File) -> Result<(), Error> { + pub fn save_to(&self, config: &mut gix_config::File) -> Result<(), crate::Error> { let name = self.name().ok_or_else(|| { let url = self .urls @@ -128,7 +122,7 @@ impl Remote<'_> { /// Note that this sets a name for anonymous remotes, but overwrites the name for those who were named before. /// If this name is different from the current one, the git configuration will still contain the previous name, /// and the caller should account for that. - pub fn save_as_to(&mut self, name: impl AsBStr, config: &mut gix_config::File) -> Result<(), AsError> { + pub fn save_as_to(&mut self, name: impl AsBStr, config: &mut gix_config::File) -> Result<(), crate::Error> { let name = crate::remote::name::validated(name.as_bstr().to_owned())?; let prev_name = self.name.take(); self.name = Some(name.into()); diff --git a/gix/src/remote/url/scheme_permission.rs b/gix/src/remote/url/scheme_permission.rs index f508db7fec2..b60f448a90b 100644 --- a/gix/src/remote/url/scheme_permission.rs +++ b/gix/src/remote/url/scheme_permission.rs @@ -18,9 +18,6 @@ pub enum Allow { User, } -/// The error returned when obtaining transport permissions from configuration. -pub type Error = gix_error::Error; - impl Allow { /// Return true if we represent something like 'allow == true'. pub fn to_bool(self, user_allowed: Option) -> bool { @@ -61,7 +58,7 @@ impl SchemePermission { pub fn from_config( config: &gix_config::File, mut filter: fn(&gix_config::file::Metadata) -> bool, - ) -> Result { + ) -> Result { if let Some(allow_protocol) = config.string_filter(gitoxide::Allow::PROTOCOL, &mut filter) { return Ok(SchemePermission { user_allowed: None, diff --git a/gix/src/repository/attributes.rs b/gix/src/repository/attributes.rs index 5986b9030e0..72727d4b15c 100644 --- a/gix/src/repository/attributes.rs +++ b/gix/src/repository/attributes.rs @@ -1,10 +1,7 @@ //! exclude information use gix_error::ResultExt; -use crate::{AttributeStack, Repository, config}; - -/// The error returned by [`Repository::attributes()`]. -pub type Error = gix_error::Error; +use crate::{AttributeStack, Repository}; impl Repository { /// Configure a file-system cache for accessing git attributes *and* excludes on a per-path basis. @@ -27,7 +24,7 @@ impl Repository { attributes_source: gix_worktree::stack::state::attributes::Source, ignore_source: gix_worktree::stack::state::ignore::Source, exclude_overrides: Option, - ) -> Result, Error> { + ) -> Result, crate::Error> { let case = if self.config.ignore_case { gix_glob::pattern::Case::Fold } else { @@ -66,7 +63,7 @@ impl Repository { &self, index: &gix_index::State, attributes_source: gix_worktree::stack::state::attributes::Source, - ) -> Result, config::attribute_stack::Error> { + ) -> Result, crate::Error> { let case = if self.config.ignore_case { gix_glob::pattern::Case::Fold } else { @@ -112,7 +109,7 @@ impl Repository { index: &gix_index::State, overrides: Option, source: gix_worktree::stack::state::ignore::Source, - ) -> Result, config::exclude_stack::Error> { + ) -> Result, crate::Error> { let case = if self.config.ignore_case { gix_glob::pattern::Case::Fold } else { diff --git a/gix/src/repository/blame.rs b/gix/src/repository/blame.rs index 35f5b33ec7c..b1f49e5bb1b 100644 --- a/gix/src/repository/blame.rs +++ b/gix/src/repository/blame.rs @@ -17,7 +17,7 @@ impl Repository { file_path: &BStr, suspect: impl Into, options: blame_file::Options, - ) -> Result { + ) -> Result { let cache = self.commit_graph_if_enabled()?; let mut resource_cache = self.diff_resource_cache_for_tree_diff()?; diff --git a/gix/src/repository/branch.rs b/gix/src/repository/branch.rs index ac5ba3a1087..bc8722ba0ef 100644 --- a/gix/src/repository/branch.rs +++ b/gix/src/repository/branch.rs @@ -56,9 +56,6 @@ pub mod delete { } impl std::error::Error for CleanupError {} - - /// The error returned by [`Repository::delete_local_branches()`][crate::Repository::delete_local_branches()]. - pub type Error = gix_error::Error; } impl crate::Repository { @@ -83,7 +80,7 @@ impl crate::Repository { pub fn delete_local_branches( &mut self, names: impl IntoIterator, - ) -> Result, delete::Error> { + ) -> Result, crate::Error> { let mut names: Vec<_> = names.into_iter().collect(); names.sort(); names.dedup(); diff --git a/gix/src/repository/checkout.rs b/gix/src/repository/checkout.rs index c77ff99da85..34bafbbe67f 100644 --- a/gix/src/repository/checkout.rs +++ b/gix/src/repository/checkout.rs @@ -1,4 +1,4 @@ -use crate::{Repository, config}; +use crate::Repository; impl Repository { /// Return options that can be used to drive a low-level checkout operation. @@ -8,7 +8,7 @@ impl Repository { pub fn checkout_options( &self, attributes_source: gix_worktree::stack::state::attributes::Source, - ) -> Result { + ) -> Result { self.config.checkout_options(self, attributes_source) } } diff --git a/gix/src/repository/config/branch.rs b/gix/src/repository/config/branch.rs index de1ddc2b3f1..6e148fc1e44 100644 --- a/gix/src/repository/config/branch.rs +++ b/gix/src/repository/config/branch.rs @@ -10,9 +10,6 @@ use crate::{ tree::{Branch, Push}, }, push, remote, - repository::{ - branch_remote_ref_name, branch_remote_tracking_ref_name, upstream_branch_and_remote_name_for_tracking_branch, - }, }; /// Query configuration related to branches. @@ -46,7 +43,7 @@ impl crate::Repository { &self, name: &FullNameRef, direction: remote::Direction, - ) -> Option> { + ) -> Option> { match direction { remote::Direction::Fetch => { let short_name = name.shorten(); @@ -125,7 +122,7 @@ impl crate::Repository { &self, name: &FullNameRef, direction: remote::Direction, - ) -> Option> { + ) -> Option> { let remote_ref = match self.branch_remote_ref_name(name, direction)? { Ok(r) => r, Err(err) => { @@ -164,7 +161,7 @@ impl crate::Repository { pub fn upstream_branch_and_remote_for_tracking_branch( &self, tracking_branch: &FullNameRef, - ) -> Result)>, upstream_branch_and_remote_name_for_tracking_branch::Error> { + ) -> Result)>, crate::Error> { if tracking_branch.category() != Some(gix_ref::Category::RemoteBranch) { return Err(gix_error::Error::from_error(gix_error::ValidationError::new(format!( "The input branch '{}' needs to be a remote tracking branch", @@ -258,7 +255,7 @@ impl crate::Repository { &self, short_branch_name: impl Into<&'a BStr>, direction: remote::Direction, - ) -> Option, remote::find::existing::Error>> { + ) -> Option, crate::Error>> { let name = self.branch_remote_name(short_branch_name, direction)?; self.try_find_remote(name.as_bstr()).or_else(|| match name { remote::Name::Url(url) => gix_url::parse(&url) diff --git a/gix/src/repository/config/mod.rs b/gix/src/repository/config/mod.rs index f393ecfca03..e0b6c83e0ee 100644 --- a/gix/src/repository/config/mod.rs +++ b/gix/src/repository/config/mod.rs @@ -9,7 +9,7 @@ impl crate::Repository { } /// Return the effective compression level used when writing pack entries. - pub fn pack_compression(&self) -> Result { + pub fn pack_compression(&self) -> Result { config::cache::access::pack_compression( &self.config.resolved, self.config.lenient_config, @@ -75,7 +75,7 @@ impl crate::Repository { pub fn config_file_mut( &self, path: impl Into, - ) -> Result { + ) -> Result { let path = path.into(); let path = if path.is_absolute() { path @@ -136,7 +136,7 @@ impl crate::Repository { /// The returned command has repository context and inherited standard streams. Add the paths to edit as arguments /// before spawning it. #[cfg(feature = "command")] - pub fn editor_command(&self) -> Result, config::command_context::Error> { + pub fn editor_command(&self) -> Result, crate::Error> { use std::{path::Path, process::Stdio}; let Some(editor) = self.editor() else { @@ -163,9 +163,7 @@ impl crate::Repository { /// The returned plumbing options may be adjusted before use, for example to disable GPG pinentry by adding /// `--pinentry-mode=error` to `program_arguments`. #[cfg(feature = "command")] - pub fn commit_signing_options( - &self, - ) -> Result { + pub fn commit_signing_options(&self) -> Result { crate::commit::sign::signing_options(self) } @@ -175,7 +173,7 @@ impl crate::Repository { #[cfg(feature = "command")] pub fn commit_signing_options_if_enabled( &self, - ) -> Result, crate::commit::sign::options::Error> { + ) -> Result, crate::Error> { crate::commit::sign::signing_options_if_enabled(self) } @@ -209,7 +207,7 @@ impl crate::Repository { /// /// Note that these values have not been [probed](gix_fs::Capabilities::probe()). #[cfg(feature = "index")] - pub fn stat_options(&self) -> Result { + pub fn stat_options(&self) -> Result { self.config.stat_options() } @@ -237,8 +235,7 @@ impl crate::Repository { #[cfg(feature = "blocking-network-client")] pub fn ssh_connect_options( &self, - ) -> Result - { + ) -> Result { use crate::config::{ cache::util::ApplyLeniency, tree::{Core, Ssh, gitoxide}, @@ -270,7 +267,7 @@ impl crate::Repository { /// Return the context to be passed to any spawned program that is supposed to interact with the repository, like /// hooks or filters. #[cfg(feature = "command")] - pub fn command_context(&self) -> Result { + pub fn command_context(&self) -> Result { use crate::config::{cache::util::ApplyLeniency, tree::gitoxide}; let pathspec_boolean = |key: &'static config::tree::keys::Boolean| { diff --git a/gix/src/repository/config/transport.rs b/gix/src/repository/config/transport.rs index 14d34034050..18fa1a9a548 100644 --- a/gix/src/repository/config/transport.rs +++ b/gix/src/repository/config/transport.rs @@ -26,13 +26,13 @@ impl crate::Repository { &self, url: impl Into<&'a BStr>, remote_name: Option<&BStr>, - ) -> Result>, crate::config::transport::Error> { + ) -> Result>, crate::Error> { let url = gix_url::parse(url.into()).or_raise(|| gix_error::message("Invalid URL passed for configuration"))?; use gix_url::Scheme::*; match &url.scheme { Http | Https => { - let options: Result>, crate::config::transport::http::Error> = { + let options: Result>, crate::Error> = { #[cfg(not(any( feature = "blocking-http-transport-reqwest", feature = "blocking-http-transport-curl" @@ -66,7 +66,7 @@ impl crate::Repository { lenient: bool, key_str: impl Into, key: &'static config::tree::keys::String, - ) -> Result, config::transport::Error> { + ) -> Result, crate::Error> { let key_str = key_str.into(); key.try_into_string(v) .map_err(|err| { @@ -81,7 +81,7 @@ impl crate::Repository { fn proxy_auth_method( value_and_key: Option<(BString, BString, &'static config::tree::http::ProxyAuthMethod)>, - ) -> Result { + ) -> Result { let value = value_and_key .map(|(method, key, key_type)| { let _ = &key; // CodeQL doesn't inspect formatting macro arguments. @@ -102,7 +102,7 @@ impl crate::Repository { key: &'static config::tree::http::SslVersion, mut filter: fn(&gix_config::file::Metadata) -> bool, lenient: bool, - ) -> Result, config::transport::Error> { + ) -> Result, crate::Error> { debug_assert_eq!( key_str, key.logical_name(), @@ -119,7 +119,7 @@ impl crate::Repository { fn proxy( value: Option<(BString, BString, &'static config::tree::keys::String)>, lenient: bool, - ) -> Result, config::transport::Error> { + ) -> Result, crate::Error> { Ok(value .and_then(|(v, k, key)| try_to_string(v, lenient, k.clone(), key).transpose()) .transpose()? @@ -268,7 +268,7 @@ impl crate::Repository { .transpose() .or_raise(|| gix_error::message("Invalid URL passed for configuration"))? .filter(|url| url.user().is_some()) - .map(|url| -> Result<_, config::transport::http::Error> { + .map(|url| -> Result<_, crate::Error> { let (mut cascade, action_with_normalized_url, prompt_opts) = self.config_snapshot().credential_helpers(url).or_raise(|| { gix_error::message( diff --git a/gix/src/repository/diff.rs b/gix/src/repository/diff.rs index 9e31dbbf7d9..9eb3115150a 100644 --- a/gix/src/repository/diff.rs +++ b/gix/src/repository/diff.rs @@ -1,10 +1,7 @@ use gix_error::ResultExt; use gix_object::TreeRefIter; -use crate::{ - Repository, Tree, - repository::{diff_resource_cache, diff_tree_to_tree}, -}; +use crate::{Repository, Tree}; /// Diff-utilities impl Repository { @@ -22,7 +19,7 @@ impl Repository { &self, mode: gix_diff::blob::pipeline::Mode, worktree_roots: gix_diff::blob::pipeline::WorktreeRoots, - ) -> Result { + ) -> Result { let index = self.index_or_load_from_head_or_empty()?; crate::diff::resource_cache( self, @@ -54,7 +51,7 @@ impl Repository { old_tree: impl Into>>, new_tree: impl Into>>, options: impl Into>, - ) -> Result, diff_tree_to_tree::Error> { + ) -> Result, crate::Error> { let mut cache = self.diff_resource_cache(gix_diff::blob::pipeline::Mode::ToGit, Default::default())?; let opts = options .into() @@ -84,7 +81,7 @@ impl Repository { /// Return a resource cache suitable for diffing blobs from trees directly, where no worktree checkout exists. /// /// For more control, see [`diff_resource_cache()`](Self::diff_resource_cache). - pub fn diff_resource_cache_for_tree_diff(&self) -> Result { + pub fn diff_resource_cache_for_tree_diff(&self) -> Result { self.diff_resource_cache( gix_diff::blob::pipeline::Mode::ToGit, gix_diff::blob::pipeline::WorktreeRoots::default(), diff --git a/gix/src/repository/dirwalk.rs b/gix/src/repository/dirwalk.rs index 688e5d175e5..e463aec8347 100644 --- a/gix/src/repository/dirwalk.rs +++ b/gix/src/repository/dirwalk.rs @@ -39,7 +39,7 @@ impl Repository { should_interrupt: &AtomicBool, options: dirwalk::Options, delegate: &mut dyn gix_dir::walk::Delegate, - ) -> Result, dirwalk::Error> { + ) -> Result, crate::Error> { let _span = gix_trace::coarse!("gix::dirwalk"); let workdir = self.workdir().ok_or_else(|| { gix_error::Error::from_error(gix_error::message( @@ -135,7 +135,7 @@ impl Repository { patterns: impl IntoIterator>, should_interrupt: OwnedOrStaticAtomicBool, options: dirwalk::Options, - ) -> Result { + ) -> Result { dirwalk::Iter::new( self, index.into(), diff --git a/gix/src/repository/filter.rs b/gix/src/repository/filter.rs index a349e14ef0f..49752fdfd8f 100644 --- a/gix/src/repository/filter.rs +++ b/gix/src/repository/filter.rs @@ -2,12 +2,6 @@ use gix_error::ResultExt; use crate::{Repository, filter, worktree::IndexPersistedOrInMemory}; -/// -pub mod pipeline { - /// The error returned by [Repository::filter_pipeline()](super::Repository::filter_pipeline()). - pub type Error = gix_error::Error; -} - impl Repository { /// Configure a pipeline for converting byte buffers to the worktree representation, and byte streams to the git-internal /// representation. Also return the index that was used when initializing the pipeline as it may be useful when calling @@ -26,7 +20,7 @@ impl Repository { pub fn filter_pipeline( &self, tree_if_bare: Option, - ) -> Result<(filter::Pipeline<'_>, IndexPersistedOrInMemory), pipeline::Error> { + ) -> Result<(filter::Pipeline<'_>, IndexPersistedOrInMemory), crate::Error> { let (cache, index) = if self.is_bare() { let tree = tree_if_bare.map_or_else( || -> Result<_, gix_error::Error> { diff --git a/gix/src/repository/graph.rs b/gix/src/repository/graph.rs index b2c7e4f636b..fa17a2793bc 100644 --- a/gix/src/repository/graph.rs +++ b/gix/src/repository/graph.rs @@ -31,9 +31,7 @@ impl crate::Repository { } /// Return a newly opened commit-graph if it is available *and* enabled in the Git configuration. - pub fn commit_graph_if_enabled( - &self, - ) -> Result, super::commit_graph_if_enabled::Error> { + pub fn commit_graph_if_enabled(&self) -> Result, crate::Error> { self.config .may_use_commit_graph()? .then(|| gix_commitgraph::at(self.objects.store_ref().path().join("info"))) diff --git a/gix/src/repository/identity.rs b/gix/src/repository/identity.rs index 8c55bc04f60..bcd4d280eb0 100644 --- a/gix/src/repository/identity.rs +++ b/gix/src/repository/identity.rs @@ -63,7 +63,7 @@ impl crate::Repository { &mut self, name: impl gix_utils::AsBStr, email: impl gix_utils::AsBStr, - ) -> Result, config::commit_signature::Error> { + ) -> Result, crate::Error> { if self.committer().is_none() { let mut config = gix_config::File::new(gix_config::file::Metadata::api()); config.set_raw_value(gitoxide::Committer::NAME_FALLBACK, name)?; @@ -78,9 +78,7 @@ impl crate::Repository { /// /// This is equivalent to calling [`committer_or_set_fallback()`](Self::committer_or_set_fallback()) with /// `no name configured `. - pub fn committer_or_set_generic_fallback( - &mut self, - ) -> Result, config::commit_signature::Error> { + pub fn committer_or_set_generic_fallback(&mut self) -> Result, crate::Error> { self.committer_or_set_fallback("no name configured", "noEmailAvailable@example.com") } diff --git a/gix/src/repository/impls.rs b/gix/src/repository/impls.rs index cc37c863458..ebc1c44b5c0 100644 --- a/gix/src/repository/impls.rs +++ b/gix/src/repository/impls.rs @@ -108,13 +108,13 @@ impl From for crate::ThreadSafeRepository { } impl gix_object::Write for crate::Repository { - fn write(&self, object: &dyn gix_object::WriteTo) -> Result { + fn write(&self, object: &dyn gix_object::WriteTo) -> Result { let mut buf = self.empty_reusable_buffer(); object.write_to(buf.deref_mut()).or_erased()?; self.write_buf(object.kind(), &buf) } - fn write_buf(&self, object: gix_object::Kind, from: &[u8]) -> Result { + fn write_buf(&self, object: gix_object::Kind, from: &[u8]) -> Result { let oid = gix_object::compute_hash(self.object_hash(), object, from).or_erased()?; if self.objects.exists(&oid) { return Ok(oid); @@ -127,7 +127,7 @@ impl gix_object::Write for crate::Repository { kind: gix_object::Kind, size: u64, from: &mut dyn std::io::Read, - ) -> Result { + ) -> Result { let mut buf = self.empty_reusable_buffer(); let bytes = std::io::copy(from, buf.deref_mut()).or_erased()?; if size != bytes { @@ -143,7 +143,7 @@ impl gix_object::Write for crate::Repository { object: gix_object::Kind, from: &[u8], id: gix_hash::ObjectId, - ) -> Result { + ) -> Result { if self.objects.exists(&id) { return Ok(id); } @@ -156,7 +156,7 @@ impl gix_object::Write for crate::Repository { size: u64, from: &mut dyn std::io::Read, id: gix_hash::ObjectId, - ) -> Result { + ) -> Result { let mut buf = self.empty_reusable_buffer(); let bytes = std::io::copy(from, buf.deref_mut()).or_erased()?; if size != bytes { @@ -169,7 +169,7 @@ impl gix_object::Write for crate::Repository { } impl gix_object::FindHeader for crate::Repository { - fn try_header(&self, id: &gix_hash::oid) -> Result, gix_object::find::Error> { + fn try_header(&self, id: &gix_hash::oid) -> Result, gix_error::Exn> { if id == ObjectId::empty_tree(self.object_hash()) { return Ok(Some(gix_object::Header { kind: gix_object::Kind::Tree, @@ -185,7 +185,7 @@ impl gix_object::Find for crate::Repository { &self, id: &gix_hash::oid, buffer: &'a mut Vec, - ) -> Result>, gix_object::find::Error> { + ) -> Result>, gix_error::Exn> { if id == ObjectId::empty_tree(self.object_hash()) { buffer.clear(); return Ok(Some(gix_object::Data { diff --git a/gix/src/repository/index.rs b/gix/src/repository/index.rs index 426bd6cf5e0..6cf4c44ffcd 100644 --- a/gix/src/repository/index.rs +++ b/gix/src/repository/index.rs @@ -24,7 +24,7 @@ impl crate::Repository { /// assert_eq!(index.entries().len(), 1); /// # Ok(()) } /// ``` - pub fn open_index(&self) -> Result { + pub fn open_index(&self) -> Result { let thread_limit = self .config .resolved @@ -74,7 +74,7 @@ impl crate::Repository { /// assert_eq!(index.entries().len(), 1); /// # Ok(()) } /// ``` - pub fn index(&self) -> Result { + pub fn index(&self) -> Result { self.try_index().and_then(|opt| match opt { Some(index) => Ok(index), None => Err(gix_error::Error::from_error(gix_error::NotFoundError::new(format!( @@ -98,7 +98,7 @@ impl crate::Repository { /// assert!(index.entries().is_empty()); /// # Ok(()) } /// ``` - pub fn index_or_empty(&self) -> Result { + pub fn index_or_empty(&self) -> Result { Ok(self.try_index()?.unwrap_or_else(|| { worktree::Index::new(gix_fs::FileSnapshot::new(gix_index::File::from_state( gix_index::State::new(self.object_hash()), @@ -111,7 +111,7 @@ impl crate::Repository { /// on disk has changed, or `None` if no such file exists. /// /// The index file is shared across all clones of this repository. - pub fn try_index(&self) -> Result, worktree::open_index::Error> { + pub fn try_index(&self) -> Result, crate::Error> { self.index.recent_snapshot( || self.index_path().metadata().and_then(|m| m.modified()).ok(), || { @@ -154,9 +154,7 @@ impl crate::Repository { /// assert_eq!(index.entries().len(), 1); /// # Ok(()) } /// ``` - pub fn index_or_load_from_head( - &self, - ) -> Result { + pub fn index_or_load_from_head(&self) -> Result { Ok(match self.try_index()? { Some(index) => IndexPersistedOrInMemory::Persisted(index), None => { @@ -184,9 +182,7 @@ impl crate::Repository { /// assert!(index.entries().is_empty()); /// # Ok(()) } /// ``` - pub fn index_or_load_from_head_or_empty( - &self, - ) -> Result { + pub fn index_or_load_from_head_or_empty(&self) -> Result { Ok(match self.try_index()? { Some(index) => IndexPersistedOrInMemory::Persisted(index), None => match self.head()?.id() { @@ -205,7 +201,7 @@ impl crate::Repository { /// Create new index-file, which would live at the correct location, in memory from the given `tree`. /// /// Note that this is an expensive operation as it requires recursively traversing the entire tree to unpack it into the index. - pub fn index_from_tree(&self, tree: &gix_hash::oid) -> Result { + pub fn index_from_tree(&self, tree: &gix_hash::oid) -> Result { Ok(gix_index::File::from_state( gix_index::State::from_tree( tree, diff --git a/gix/src/repository/init.rs b/gix/src/repository/init.rs index 26b1e064137..6ef66d95709 100644 --- a/gix/src/repository/init.rs +++ b/gix/src/repository/init.rs @@ -49,7 +49,7 @@ impl crate::Repository { /// When the [`git_dir`](Self::git_dir()) is relative and the current working dir changed, /// then a reload will be performed on the joined path of both to make it succeed, which makes /// the reloaded repository git-dir absolute. - pub fn reload(&mut self) -> Result<&mut Self, crate::open::Error> { + pub fn reload(&mut self) -> Result<&mut Self, crate::Error> { let mut git_dir = self.git_dir().to_owned(); let options = self.options.clone().open_path_as_is(true); if git_dir.is_relative() diff --git a/gix/src/repository/location.rs b/gix/src/repository/location.rs index 36f67650646..09393ae7c25 100644 --- a/gix/src/repository/location.rs +++ b/gix/src/repository/location.rs @@ -129,7 +129,7 @@ impl crate::Repository { pub fn normalize_path<'a>( &self, path: &'a (impl gix_utils::AsBStr + ?Sized), - ) -> Result, crate::repository::normalize_path::Error> { + ) -> Result, crate::Error> { let path = gix_path::from_bstr(Cow::Borrowed(path.as_bstr())); let path = if gix_path::is_absolute(path.as_ref()) { let root = gix_path::realpath_opts( @@ -199,7 +199,7 @@ impl crate::Repository { /// /// Note that the CWD is obtained once upon instantiation of the repository. // TODO: tests, details - there is a lot about environment variables to change things around. - pub fn prefix(&self) -> Result, gix_path::realpath::Error> { + pub fn prefix(&self) -> Result, gix_error::Exn> { let (root, current_dir) = match self.workdir().zip(self.options.current_dir.as_deref()) { Some((work_dir, cwd)) => (work_dir, cwd), None => return Ok(None), diff --git a/gix/src/repository/mailmap.rs b/gix/src/repository/mailmap.rs index ac9b8b5c284..7ca7b6ad9dd 100644 --- a/gix/src/repository/mailmap.rs +++ b/gix/src/repository/mailmap.rs @@ -22,8 +22,8 @@ impl crate::Repository { /// /// Only the first error will be reported, and as many source mailmaps will be merged into `target` as possible. /// Parsing errors will be ignored. - pub fn open_mailmap_into(&self, target: &mut gix_mailmap::Snapshot) -> Result<(), crate::mailmap::load::Error> { - let mut err = None::; + pub fn open_mailmap_into(&self, target: &mut gix_mailmap::Snapshot) -> Result<(), crate::Error> { + let mut err = None::; let mut buf = Vec::new(); let mut blob_id = self.config.resolved.string(Mailmap::BLOB).and_then(|spec| { self.rev_parse_single(spec.as_bstr()) diff --git a/gix/src/repository/merge.rs b/gix/src/repository/merge.rs index 38f13e9fe77..9e1a82917cb 100644 --- a/gix/src/repository/merge.rs +++ b/gix/src/repository/merge.rs @@ -7,10 +7,6 @@ use crate::{ Repository, config::{cache::util::ApplyLeniencyDefault, tree}, prelude::ObjectIdExt, - repository::{ - blob_merge_options, merge_commits, merge_resource_cache, merge_trees, tree_merge_options, virtual_merge_base, - virtual_merge_base_with_graph, - }, }; /// Merge-utilities @@ -24,7 +20,7 @@ impl Repository { pub fn merge_resource_cache( &self, worktree_roots: gix_merge::blob::pipeline::WorktreeRoots, - ) -> Result { + ) -> Result { let index = self.index_or_load_from_head_or_empty()?; let mode = { let renormalize = tree::Merge::RENORMALIZE @@ -60,7 +56,7 @@ impl Repository { /// Return options for use with [`gix_merge::blob::PlatformRef::merge()`], accessible through /// [merge_resource_cache()](Self::merge_resource_cache). - pub fn blob_merge_options(&self) -> Result { + pub fn blob_merge_options(&self) -> Result { Ok(gix_merge::blob::platform::merge::Options { is_virtual_ancestor: false, resolve_binary_with: None, @@ -86,7 +82,7 @@ impl Repository { } /// Read all relevant configuration options to instantiate options for use in [`merge_trees()`](Self::merge_trees). - pub fn tree_merge_options(&self) -> Result { + pub fn tree_merge_options(&self) -> Result { let (mut rewrites, mut is_configured) = crate::diff::utils::new_rewrites_inner( &self.config.resolved, self.config.lenient_config, @@ -135,7 +131,7 @@ impl Repository { their_tree: impl AsRef, labels: gix_merge::blob::builtin_driver::text::Labels<'_>, options: crate::merge::tree::Options, - ) -> Result, merge_trees::Error> { + ) -> Result, crate::Error> { let mut diff_cache = self.diff_resource_cache_for_tree_diff()?; let mut blob_merge = self.merge_resource_cache(Default::default())?; let gix_merge::tree::Outcome { @@ -190,7 +186,7 @@ impl Repository { their_commit: impl Into, labels: gix_merge::blob::builtin_driver::text::Labels<'_>, options: crate::merge::commit::Options, - ) -> Result, merge_commits::Error> { + ) -> Result, crate::Error> { let mut diff_cache = self.diff_resource_cache_for_tree_diff()?; let mut blob_merge = self.merge_resource_cache(Default::default())?; let commit_graph = self.commit_graph_if_enabled()?; @@ -249,7 +245,7 @@ impl Repository { &self, merge_bases: impl IntoIterator>, options: crate::merge::tree::Options, - ) -> Result, virtual_merge_base::Error> { + ) -> Result, crate::Error> { let commit_graph = self.commit_graph_if_enabled()?; let mut graph = self.revision_graph(commit_graph.as_ref()); self.virtual_merge_base_with_graph(merge_bases, &mut graph, options) @@ -262,7 +258,7 @@ impl Repository { merge_bases: impl IntoIterator>, graph: &mut gix_revwalk::Graph<'_, '_, gix_revwalk::graph::Commit>, options: crate::merge::tree::Options, - ) -> Result, virtual_merge_base_with_graph::Error> { + ) -> Result, crate::Error> { let mut merge_bases: Vec<_> = merge_bases.into_iter().map(Into::into).collect(); let first = merge_bases .pop() diff --git a/gix/src/repository/mod.rs b/gix/src/repository/mod.rs index 8d3ba4a2bfa..2f7de6323a9 100644 --- a/gix/src/repository/mod.rs +++ b/gix/src/repository/mod.rs @@ -64,18 +64,6 @@ mod submodule; mod thread_safe; pub(crate) mod worktree; -/// -mod new_commit { - /// The error returned by [`new_commit(…)`](crate::Repository::new_commit()). - pub type Error = gix_error::Error; -} - -/// -mod new_commit_as { - /// The error returned by [`new_commit_as(…)`](crate::Repository::new_commit_as()). - pub type Error = gix_error::Error; -} - /// #[cfg(feature = "blame")] pub mod blame_file { @@ -91,183 +79,4 @@ pub mod blame_file { /// Determine if rename tracking should be performed, and how. pub rewrites: Option, } - - /// The error returned by [Repository::blame_file()](crate::Repository::blame_file()). - pub type Error = gix_error::Error; -} - -/// -#[cfg(feature = "blob-diff")] -pub mod diff_tree_to_tree { - /// The error returned by [Repository::diff_tree_to_tree()](crate::Repository::diff_tree_to_tree()). - pub type Error = gix_error::Error; -} - -/// -#[cfg(feature = "merge")] -pub mod blob_merge_options { - /// The error returned by [Repository::blob_merge_options()](crate::Repository::blob_merge_options()). - pub type Error = gix_error::Error; -} - -/// -#[cfg(feature = "merge")] -pub mod merge_resource_cache { - /// The error returned by [Repository::merge_resource_cache()](crate::Repository::merge_resource_cache()). - pub type Error = gix_error::Error; -} - -/// -#[cfg(feature = "merge")] -pub mod merge_trees { - /// The error returned by [Repository::merge_trees()](crate::Repository::merge_trees()). - pub type Error = gix_error::Error; -} - -/// -#[cfg(feature = "merge")] -pub mod merge_commits { - /// The error returned by [Repository::merge_commits()](crate::Repository::merge_commits()). - pub type Error = gix_error::Error; -} - -/// -#[cfg(feature = "merge")] -pub mod virtual_merge_base { - /// The error returned by [Repository::virtual_merge_base()](crate::Repository::virtual_merge_base()). - pub type Error = gix_error::Error; -} - -/// -#[cfg(feature = "merge")] -pub mod virtual_merge_base_with_graph { - /// The error returned by [Repository::virtual_merge_base_with_graph()](crate::Repository::virtual_merge_base_with_graph()). - pub type Error = gix_error::Error; -} - -/// -#[cfg(feature = "revision")] -pub mod merge_base_octopus_with_graph { - /// The error returned by [Repository::merge_base_octopus_with_graph()](crate::Repository::merge_base_octopus_with_graph()). - pub type Error = gix_error::Error; -} - -/// -#[cfg(feature = "revision")] -pub mod merge_base_octopus { - /// The error returned by [Repository::merge_base_octopus()](crate::Repository::merge_base_octopus()). - pub type Error = gix_error::Error; -} - -/// -#[cfg(feature = "revision")] -pub mod merge_bases_many { - /// The error returned by [Repository::merge_bases_many()](crate::Repository::merge_bases_many()). - pub type Error = gix_error::Error; -} - -/// -#[cfg(feature = "merge")] -pub mod tree_merge_options { - /// The error returned by [Repository::tree_merge_options()](crate::Repository::tree_merge_options()). - pub type Error = gix_error::Error; -} - -/// -#[cfg(feature = "blob-diff")] -pub mod diff_resource_cache { - /// The error returned by [Repository::diff_resource_cache()](crate::Repository::diff_resource_cache()). - pub type Error = gix_error::Error; -} - -/// -pub mod edit_tree { - /// The error returned by [Repository::edit_tree()](crate::Repository::edit_tree). - pub type Error = gix_error::Error; -} - -/// -#[cfg(feature = "revision")] -pub mod merge_base { - /// The error returned by [Repository::merge_base()](crate::Repository::merge_base()). - pub type Error = gix_error::Error; -} - -/// -#[cfg(feature = "revision")] -pub mod merge_base_with_graph { - /// The error returned by [Repository::merge_base_with_cache()](crate::Repository::merge_base_with_graph()). - pub type Error = gix_error::Error; -} - -/// -pub mod commit_graph_if_enabled { - /// The error returned by [Repository::commit_graph_if_enabled()](crate::Repository::commit_graph_if_enabled()). - pub type Error = gix_error::Error; -} - -/// -#[cfg(feature = "index")] -pub mod index_from_tree { - /// The error returned by [Repository::index_from_tree()](crate::Repository::index_from_tree). - pub type Error = gix_error::Error; -} - -/// -pub mod branch_remote_ref_name { - /// The error returned by [Repository::branch_remote_ref_name()](crate::Repository::branch_remote_ref_name()). - pub type Error = gix_error::Error; -} - -/// -pub mod branch_remote_tracking_ref_name { - /// The error returned by [Repository::branch_remote_tracking_ref_name()](crate::Repository::branch_remote_tracking_ref_name()). - pub type Error = gix_error::Error; -} - -/// -pub mod upstream_branch_and_remote_name_for_tracking_branch { - /// The error returned by [Repository::upstream_branch_and_remote_name_for_tracking_branch()](crate::Repository::upstream_branch_and_remote_for_tracking_branch()). - pub type Error = gix_error::Error; -} - -/// -pub mod normalize_path { - /// The error returned by [Repository::normalize_path()](crate::Repository::normalize_path()). - pub type Error = gix_error::Error; -} - -/// -#[cfg(feature = "attributes")] -pub mod pathspec_defaults_ignore_case { - /// The error returned by [Repository::pathspec_defaults_ignore_case()](crate::Repository::pathspec_defaults_inherit_ignore_case()). - pub type Error = gix_error::Error; -} - -/// -#[cfg(feature = "index")] -pub mod index_or_load_from_head { - /// The error returned by [`Repository::index_or_load_from_head()`](crate::Repository::index_or_load_from_head()). - pub type Error = gix_error::Error; -} - -/// -#[cfg(feature = "index")] -pub mod index_or_load_from_head_or_empty { - /// The error returned by [`Repository::index_or_load_from_head_or_empty()`](crate::Repository::index_or_load_from_head_or_empty()). - pub type Error = gix_error::Error; -} - -/// -#[cfg(feature = "worktree-stream")] -pub mod worktree_stream { - /// The error returned by [`Repository::worktree_stream()`](crate::Repository::worktree_stream()). - pub type Error = gix_error::Error; -} - -/// -#[cfg(feature = "worktree-archive")] -pub mod worktree_archive { - /// The error returned by [`Repository::worktree_archive()`](crate::Repository::worktree_archive()). - pub type Error = gix_error::Error; } diff --git a/gix/src/repository/object.rs b/gix/src/repository/object.rs index 0721550d20f..37feabee8f9 100644 --- a/gix/src/repository/object.rs +++ b/gix/src/repository/object.rs @@ -9,8 +9,7 @@ use gix_odb::{Header, HeaderExt}; use gix_ref::{FullName, transaction::PreviousValue}; use smallvec::SmallVec; -use crate::repository::{new_commit, new_commit_as}; -use crate::{Blob, Commit, Id, Object, Reference, Tag, Tree, commit, ext::ObjectIdExt, object, tag}; +use crate::{Blob, Commit, Id, Object, Reference, Tag, Tree, ext::ObjectIdExt, object}; /// Tree editing impl crate::Repository { @@ -18,10 +17,7 @@ impl crate::Repository { /// /// This can be the [empty tree id](ObjectId::empty_tree) to build a tree from scratch. #[doc(alias = "treebuilder", alias = "git2")] - pub fn edit_tree( - &self, - id: impl Into, - ) -> Result, crate::repository::edit_tree::Error> { + pub fn edit_tree(&self, id: impl Into) -> Result, crate::Error> { let tree = self.find_tree(id)?; tree.edit() } @@ -51,7 +47,7 @@ impl crate::Repository { /// assert_eq!(object.into_commit().message_raw()?, "c2\n"); /// # Ok(()) } /// ``` - pub fn find_object(&self, id: impl Into) -> Result, object::find::existing::Error> { + pub fn find_object(&self, id: impl Into) -> Result, crate::Error> { let id = id.into(); if id == ObjectId::empty_tree(self.object_hash()) { return Ok(Object { @@ -83,10 +79,7 @@ impl crate::Repository { /// assert_eq!(commit.message_raw()?, "c2\n"); /// # Ok(()) } /// ``` - pub fn find_commit( - &self, - id: impl Into, - ) -> Result, object::find::existing::with_conversion::Error> { + pub fn find_commit(&self, id: impl Into) -> Result, crate::Error> { Ok(self.find_object(id)?.try_into_commit().or_erased()?) } @@ -103,23 +96,17 @@ impl crate::Repository { /// assert_eq!(tree.find_entry("this").expect("present").filename(), "this"); /// # Ok(()) } /// ``` - pub fn find_tree( - &self, - id: impl Into, - ) -> Result, object::find::existing::with_conversion::Error> { + pub fn find_tree(&self, id: impl Into) -> Result, crate::Error> { Ok(self.find_object(id)?.try_into_tree().or_erased()?) } /// Find an annotated tag with `id` or fail if there was no object or the object wasn't a tag. - pub fn find_tag(&self, id: impl Into) -> Result, object::find::existing::with_conversion::Error> { + pub fn find_tag(&self, id: impl Into) -> Result, crate::Error> { Ok(self.find_object(id)?.try_into_tag().or_erased()?) } /// Find a blob with `id` or fail if there was no object or the object wasn't a blob. - pub fn find_blob( - &self, - id: impl Into, - ) -> Result, object::find::existing::with_conversion::Error> { + pub fn find_blob(&self, id: impl Into) -> Result, crate::Error> { Ok(self.find_object(id)?.try_into_blob().or_erased()?) } @@ -141,7 +128,7 @@ impl crate::Repository { /// assert_eq!(header.kind(), gix::objs::Kind::Commit); /// # Ok(()) } /// ``` - pub fn find_header(&self, id: impl Into) -> Result { + pub fn find_header(&self, id: impl Into) -> Result { let id = id.into(); if id == ObjectId::empty_tree(self.object_hash()) { return Ok(gix_odb::find::Header::Loose { @@ -195,10 +182,7 @@ impl crate::Repository { /// assert_eq!(header.kind(), gix::objs::Kind::Commit); /// # Ok(()) } /// ``` - pub fn try_find_header( - &self, - id: impl Into, - ) -> Result, object::find::Error> { + pub fn try_find_header(&self, id: impl Into) -> Result, crate::Error> { let id = id.into(); if id == ObjectId::empty_tree(self.object_hash()) { return Ok(Some(gix_odb::find::Header::Loose { @@ -222,7 +206,7 @@ impl crate::Repository { /// assert_eq!(object.kind, gix::objs::Kind::Commit); /// # Ok(()) } /// ``` - pub fn try_find_object(&self, id: impl Into) -> Result>, object::find::Error> { + pub fn try_find_object(&self, id: impl Into) -> Result>, crate::Error> { let id = id.into(); if id == ObjectId::empty_tree(self.object_hash()) { return Ok(Some(Object { @@ -250,14 +234,14 @@ impl crate::Repository { /// /// Note that we hash the object in memory to avoid storing objects that are already present. That way, /// we avoid writing duplicate objects using slow disks that will eventually have to be garbage collected. - pub fn write_object(&self, object: impl gix_object::WriteTo) -> Result, object::write::Error> { + pub fn write_object(&self, object: impl gix_object::WriteTo) -> Result, crate::Error> { let mut buf = self.empty_reusable_buffer(); object.write_to(buf.deref_mut()).or_erased()?; self.write_object_inner(&buf, object.kind()) } - fn write_object_inner(&self, buf: &[u8], kind: gix_object::Kind) -> Result, object::write::Error> { + fn write_object_inner(&self, buf: &[u8], kind: gix_object::Kind) -> Result, crate::Error> { let oid = gix_object::compute_hash(self.object_hash(), kind, buf).or_erased()?; if self.objects.exists(&oid) { return Ok(oid.attach(self)); @@ -287,7 +271,7 @@ impl crate::Repository { /// assert_eq!(repo.find_blob(blob_id)?.data, b"hello world"); /// # Ok(()) } /// ``` - pub fn write_blob(&self, bytes: impl AsRef<[u8]>) -> Result, object::write::Error> { + pub fn write_blob(&self, bytes: impl AsRef<[u8]>) -> Result, crate::Error> { let bytes = bytes.as_ref(); let oid = gix_object::compute_hash(self.object_hash(), gix_object::Kind::Blob, bytes).or_erased()?; if self.objects.exists(&oid) { @@ -306,14 +290,14 @@ impl crate::Repository { /// we avoid writing duplicate objects using slow disks that will eventually have to be garbage collected. /// /// If that is prohibitive, use the object database directly. - pub fn write_blob_stream(&self, mut bytes: impl std::io::Read) -> Result, object::write::Error> { + pub fn write_blob_stream(&self, mut bytes: impl std::io::Read) -> Result, crate::Error> { let mut buf = self.empty_reusable_buffer(); std::io::copy(&mut bytes, buf.deref_mut()).or_erased()?; self.write_blob_stream_inner(&buf) } - fn write_blob_stream_inner(&self, buf: &[u8]) -> Result, object::write::Error> { + fn write_blob_stream_inner(&self, buf: &[u8]) -> Result, crate::Error> { let oid = gix_object::compute_hash(self.object_hash(), gix_object::Kind::Blob, buf).or_erased()?; if self.objects.exists(&oid) { return Ok(oid.attach(self)); @@ -342,7 +326,7 @@ impl crate::Repository { tagger: Option>, message: impl AsRef, constraint: PreviousValue, - ) -> Result, tag::Error> { + ) -> Result, crate::Error> { let tag = gix_object::Tag { target: target.as_ref().into(), target_kind, @@ -366,7 +350,7 @@ impl crate::Repository { message: impl AsRef, tree: impl Into, parents: impl IntoIterator>, - ) -> Result, commit::Error> + ) -> Result, crate::Error> where Name: TryInto, E: std::error::Error + Send + Sync + 'static, @@ -389,7 +373,7 @@ impl crate::Repository { message: &str, tree: ObjectId, parents: SmallVec<[ObjectId; 1]>, - ) -> Result, commit::Error> { + ) -> Result, crate::Error> { use gix_ref::{Target, transaction::RefEdit}; // TODO: possibly use CommitRef to save a few allocations (but will have to allocate for object ids anyway. @@ -454,7 +438,7 @@ impl crate::Repository { message: impl AsRef, tree: impl Into, parents: impl IntoIterator>, - ) -> Result, commit::Error> + ) -> Result, crate::Error> where Name: TryInto, E: std::error::Error + Send + Sync + 'static, @@ -481,7 +465,7 @@ impl crate::Repository { message: impl AsRef, tree: impl Into, parents: impl IntoIterator>, - ) -> Result, new_commit::Error> { + ) -> Result, crate::Error> { let author = self .author() .ok_or_else(|| { @@ -509,7 +493,7 @@ impl crate::Repository { message: impl AsRef, tree: impl Into, parents: impl IntoIterator>, - ) -> Result, new_commit_as::Error> { + ) -> Result, crate::Error> { let commit = gix_object::Commit { message: message.as_ref().into(), tree: tree.into(), diff --git a/gix/src/repository/pathspec.rs b/gix/src/repository/pathspec.rs index 7d5478e1906..230c7130b2b 100644 --- a/gix/src/repository/pathspec.rs +++ b/gix/src/repository/pathspec.rs @@ -22,7 +22,7 @@ impl Repository { inherit_ignore_case: bool, index: &gix_index::State, attributes_source: gix_worktree::stack::state::attributes::Source, - ) -> Result, crate::pathspec::init::Error> { + ) -> Result, crate::Error> { Pathspec::new(self, empty_patterns_match_prefix, patterns, inherit_ignore_case, || { self.attributes_only(index, attributes_source) .map(AttributeStack::detach) @@ -43,7 +43,7 @@ impl Repository { pub fn pathspec_defaults_inherit_ignore_case( &self, inherit_ignore_case: bool, - ) -> Result { + ) -> Result { let mut defaults = self.config.pathspec_defaults().map_err(gix_error::Exn::into_error)?; if inherit_ignore_case && self diff --git a/gix/src/repository/reference.rs b/gix/src/repository/reference.rs index 3ecf6153ccb..fc82c968d71 100644 --- a/gix/src/repository/reference.rs +++ b/gix/src/repository/reference.rs @@ -18,7 +18,7 @@ impl crate::Repository { name: impl AsRef, target: impl Into, constraint: PreviousValue, - ) -> Result, reference::edit::Error> { + ) -> Result, crate::Error> { let id = target.into(); let name = format!("refs/tags/{}", name.as_ref()).try_into().map_err( |err: gix_validate::reference::name::Error| { @@ -82,7 +82,7 @@ impl crate::Repository { target: impl Into, constraint: PreviousValue, log_message: impl Into, - ) -> Result, reference::edit::Error> + ) -> Result, crate::Error> where Name: TryInto, gix_validate::reference::name::Error: From, @@ -107,7 +107,7 @@ impl crate::Repository { id: ObjectId, constraint: PreviousValue, log_message: BString, - ) -> Result, reference::edit::Error> { + ) -> Result, crate::Error> { let mut edits = self.edit_reference(RefEdit::update(name, id, constraint, log_message))?; assert_eq!( edits.len(), @@ -127,7 +127,7 @@ impl crate::Repository { /// /// One or more `RefEdit`s are returned - symbolic reference splits can cause more edits to be performed. All edits have the previous /// reference values set to the ones encountered at rest after acquiring the respective reference's lock. - pub fn edit_reference(&self, edit: RefEdit) -> Result, reference::edit::Error> { + pub fn edit_reference(&self, edit: RefEdit) -> Result, crate::Error> { self.edit_references(Some(edit)) } @@ -138,10 +138,7 @@ impl crate::Repository { /// /// Returns all reference edits, which might be more than where provided due the splitting of symbolic references, and /// whose previous (_old_) values are the ones seen on in storage after the reference was locked. - pub fn edit_references( - &self, - edits: impl IntoIterator, - ) -> Result, reference::edit::Error> { + pub fn edit_references(&self, edits: impl IntoIterator) -> Result, crate::Error> { self.edit_references_as(edits, self.committer().transpose()?) } @@ -152,7 +149,7 @@ impl crate::Repository { &self, edits: impl IntoIterator, committer: Option>, - ) -> Result, reference::edit::Error> { + ) -> Result, crate::Error> { let (file_lock_fail, packed_refs_lock_fail) = self.config.lock_timeout().map_err(|err| { gix_error::Error::from(err.and_raise(gix_error::message( "Could not interpret core.filesRefLockTimeout or core.packedRefsTimeout, it must be the number in \ @@ -185,7 +182,7 @@ impl crate::Repository { /// assert!(!head.is_unborn()); /// # Ok(()) } /// ``` - pub fn head(&self) -> Result, reference::find::existing::Error> { + pub fn head(&self) -> Result, crate::Error> { let head = self.find_reference("HEAD")?; Ok(match head.inner.target { Target::Symbolic(branch) => match self.find_reference(&branch) { @@ -209,7 +206,7 @@ impl crate::Repository { /// /// Also note that the returned id is likely to point to a commit, but could also /// point to a tree or blob. It won't, however, point to a tag as these are always peeled. - pub fn head_id(&self) -> Result, reference::head_id::Error> { + pub fn head_id(&self) -> Result, crate::Error> { self.head()?.into_peeled_id() } @@ -217,12 +214,12 @@ impl crate::Repository { /// /// The difference to [`head_ref()`](Self::head_ref()) is that the latter requires the reference to exist, /// whereas here we merely return a the name of the possibly unborn reference. - pub fn head_name(&self) -> Result, reference::find::existing::Error> { + pub fn head_name(&self) -> Result, crate::Error> { Ok(self.head()?.referent_name().map(std::borrow::ToOwned::to_owned)) } /// Return the reference that `HEAD` points to, or `None` if the head is detached or unborn. - pub fn head_ref(&self) -> Result>, reference::find::existing::Error> { + pub fn head_ref(&self) -> Result>, crate::Error> { Ok(self.head()?.try_into_referent()) } @@ -251,7 +248,7 @@ impl crate::Repository { /// } /// # Ok(()) } /// ``` - pub fn head_commit(&self) -> Result, reference::head_commit::Error> { + pub fn head_commit(&self) -> Result, crate::Error> { self.head()?.peel_to_commit() } @@ -261,12 +258,12 @@ impl crate::Repository { /// Note that this may fail for various reasons, most notably because the repository /// is freshly initialized and doesn't have any commits yet. It could also fail if the /// head does not point to a commit. - pub fn head_tree_id(&self) -> Result, reference::head_tree_id::Error> { + pub fn head_tree_id(&self) -> Result, crate::Error> { Ok(self.head_commit()?.tree_id().or_erased()?) } /// Like [`Self::head_tree_id()`], but will return an empty tree hash if the repository HEAD is unborn. - pub fn head_tree_id_or_empty(&self) -> Result, reference::head_tree_id::Error> { + pub fn head_tree_id_or_empty(&self) -> Result, crate::Error> { let mut head = self.head()?; if head.is_unborn() { Ok(self.empty_tree().id()) @@ -293,7 +290,7 @@ impl crate::Repository { /// assert_eq!(tree.find_entry("this").expect("present").filename(), "this"); /// # Ok(()) } /// ``` - pub fn head_tree(&self) -> Result, reference::head_tree::Error> { + pub fn head_tree(&self) -> Result, crate::Error> { self.head_commit()?.tree() } @@ -315,7 +312,7 @@ impl crate::Repository { /// assert_eq!(reference.peel_to_commit()?.message()?.title, "c2"); /// # Ok(()) } /// ``` - pub fn find_reference<'a, Name, E>(&self, name: Name) -> Result, reference::find::existing::Error> + pub fn find_reference<'a, Name, E>(&self, name: Name) -> Result, crate::Error> where Name: TryInto<&'a PartialNameRef, Error = E> + Clone, gix_ref::file::find::Error: From, @@ -365,7 +362,7 @@ impl crate::Repository { /// /// Otherwise return `None` if the reference wasn't found. /// If the reference is expected to exist, use [`find_reference()`](crate::Repository::find_reference()). - pub fn try_find_reference<'a, Name, E>(&self, name: Name) -> Result>, reference::find::Error> + pub fn try_find_reference<'a, Name, E>(&self, name: Name) -> Result>, crate::Error> where Name: TryInto<&'a PartialNameRef, Error = E>, gix_ref::file::find::Error: From, diff --git a/gix/src/repository/remote.rs b/gix/src/repository/remote.rs index fcfb09264af..9756170faf5 100644 --- a/gix/src/repository/remote.rs +++ b/gix/src/repository/remote.rs @@ -1,5 +1,5 @@ #![allow(clippy::result_large_err)] -use crate::{Remote, bstr::BStr, config, remote, remote::find}; +use crate::{Remote, bstr::BStr, config, remote}; use gix_error::{ErrorExt, ResultExt}; impl crate::Repository { @@ -29,7 +29,7 @@ impl crate::Repository { /// ); /// # Ok(()) } /// ``` - pub fn remote_at(&self, url: Url) -> Result, remote::init::Error> + pub fn remote_at(&self, url: Url) -> Result, crate::Error> where Url: TryInto, E: std::error::Error + Send + Sync + 'static, @@ -41,7 +41,7 @@ impl crate::Repository { /// but don't rewrite the url according to rewrite rules. /// This eliminates a failure mode in case the rewritten URL is faulty, allowing to selectively [apply rewrite /// rules][Remote::rewrite_urls()] later and do so non-destructively. - pub fn remote_at_without_url_rewrite(&self, url: Url) -> Result, remote::init::Error> + pub fn remote_at_without_url_rewrite(&self, url: Url) -> Result, crate::Error> where Url: TryInto, E: std::error::Error + Send + Sync + 'static, @@ -66,7 +66,7 @@ impl crate::Repository { /// assert_eq!(remote.refspecs(gix::remote::Direction::Fetch).len(), 1); /// # Ok(()) } /// ``` - pub fn find_remote(&self, name_or_url: impl gix_utils::AsBStr) -> Result, find::existing::Error> { + pub fn find_remote(&self, name_or_url: impl gix_utils::AsBStr) -> Result, crate::Error> { let name_or_url = name_or_url.as_bstr(); self.try_find_remote(name_or_url).ok_or_else(|| { gix_error::Error::from_error(gix_error::NotFoundError::new(format!( @@ -92,10 +92,7 @@ impl crate::Repository { /// assert_eq!(remote.name().expect("named").as_bstr(), "origin"); /// # Ok(()) } /// ``` - pub fn find_default_remote( - &self, - direction: remote::Direction, - ) -> Option, find::existing::Error>> { + pub fn find_default_remote(&self, direction: remote::Direction) -> Option, crate::Error>> { self.remote_default_name(direction).map(|name| self.find_remote(name)) } @@ -117,7 +114,7 @@ impl crate::Repository { /// as negations/excludes are applied after includes. /// /// We will only include information if we deem it [trustworthy][crate::open::Options::filter_config_section()]. - pub fn try_find_remote<'a>(&self, name_or_url: impl Into<&'a BStr>) -> Option, find::Error>> { + pub fn try_find_remote<'a>(&self, name_or_url: impl Into<&'a BStr>) -> Option, crate::Error>> { self.try_find_remote_inner(name_or_url.into(), true) } @@ -148,7 +145,7 @@ impl crate::Repository { /// assert_eq!(remote.name().expect("configured").as_bstr(), "origin"); /// # Ok(()) } /// ``` - pub fn find_fetch_remote(&self, name_or_url: Option<&BStr>) -> Result, find::for_fetch::Error> { + pub fn find_fetch_remote(&self, name_or_url: Option<&BStr>) -> Result, crate::Error> { Ok(match name_or_url { Some(name) => match self.try_find_remote(name).and_then(Result::ok) { Some(remote) => remote, @@ -176,7 +173,7 @@ impl crate::Repository { pub fn try_find_remote_without_url_rewrite<'a>( &self, name_or_url: impl Into<&'a BStr>, - ) -> Option, find::Error>> { + ) -> Option, crate::Error>> { self.try_find_remote_inner(name_or_url.into(), false) } @@ -184,13 +181,13 @@ impl crate::Repository { &self, name_or_url: impl Into<&'a BStr>, rewrite_urls: bool, - ) -> Option, find::Error>> { + ) -> Option, crate::Error>> { fn config_spec( specs: Vec, name_or_url: &BStr, key: &'static config::tree::keys::Any, op: gix_refspec::parse::Operation, - ) -> Result, find::Error> { + ) -> Result, crate::Error> { let kind = key.name; specs .into_iter() diff --git a/gix/src/repository/revision.rs b/gix/src/repository/revision.rs index df92c16ccd7..8fc2c7a52e6 100644 --- a/gix/src/repository/revision.rs +++ b/gix/src/repository/revision.rs @@ -42,10 +42,7 @@ impl crate::Repository { /// ``` #[doc(alias = "revparse_single", alias = "git2")] #[cfg(feature = "revision")] - pub fn rev_parse_single<'repo, 'a>( - &'repo self, - spec: impl Into<&'a BStr>, - ) -> Result, revision::spec::parse::single::Error> { + pub fn rev_parse_single<'repo, 'a>(&'repo self, spec: impl Into<&'a BStr>) -> Result, crate::Error> { let spec = spec.into(); self.rev_parse(spec)?.single().ok_or_else(|| { let spec: crate::bstr::BString = spec.into(); @@ -65,7 +62,7 @@ impl crate::Repository { &self, one: impl Into, two: impl Into, - ) -> Result, super::merge_base::Error> { + ) -> Result, crate::Error> { use crate::prelude::ObjectIdExt; let one = one.into(); let two = two.into(); @@ -92,7 +89,7 @@ impl crate::Repository { one: impl Into, two: impl Into, graph: &mut gix_revwalk::Graph<'_, '_, gix_revwalk::graph::Commit>, - ) -> Result, super::merge_base_with_graph::Error> { + ) -> Result, crate::Error> { use crate::prelude::ObjectIdExt; let one = one.into(); let two = two.into(); @@ -139,7 +136,7 @@ impl crate::Repository { &self, one: impl Into, others: &[gix_hash::ObjectId], - ) -> Result>, crate::repository::merge_bases_many::Error> { + ) -> Result>, crate::Error> { let cache = self.commit_graph_if_enabled()?; let mut graph = self.revision_graph(cache.as_ref()); Ok(self.merge_bases_many_with_graph(one, others, &mut graph).or_erased()?) @@ -153,7 +150,7 @@ impl crate::Repository { &self, commits: impl IntoIterator>, graph: &mut gix_revwalk::Graph<'_, '_, gix_revwalk::graph::Commit>, - ) -> Result, crate::repository::merge_base_octopus_with_graph::Error> { + ) -> Result, crate::Error> { use crate::prelude::ObjectIdExt; let commits: Vec<_> = commits.into_iter().map(Into::into).collect(); let first = commits @@ -175,7 +172,7 @@ impl crate::Repository { pub fn merge_base_octopus( &self, commits: impl IntoIterator>, - ) -> Result, crate::repository::merge_base_octopus::Error> { + ) -> Result, crate::Error> { let cache = self.commit_graph_if_enabled()?; let mut graph = self.revision_graph(cache.as_ref()); self.merge_base_octopus_with_graph(commits, &mut graph) diff --git a/gix/src/repository/submodule.rs b/gix/src/repository/submodule.rs index 10b1eca7291..1aea20ac686 100644 --- a/gix/src/repository/submodule.rs +++ b/gix/src/repository/submodule.rs @@ -11,7 +11,7 @@ impl Repository { /// /// Note that his method will not look in other places, like the index or the `HEAD` tree. // TODO(submodule): make it use an updated snapshot instead once we have `config()`. - pub fn open_modules_file(&self) -> Result, submodule::open_modules_file::Error> { + pub fn open_modules_file(&self) -> Result, crate::Error> { let path = match self.modules_path() { Some(path) => path, None => return Ok(None), @@ -44,7 +44,7 @@ impl Repository { /// Note that git configuration is also contributing to the result based on the current snapshot. /// // TODO(submodule): make it use an updated snapshot instead once we have `config()`. - pub fn modules(&self) -> Result, submodule::modules::Error> { + pub fn modules(&self) -> Result, crate::Error> { match self .modules .recent_snapshot( @@ -68,7 +68,7 @@ impl Repository { .head() .or_erased()? .try_peel_to_id()? - .map(|id| -> Result, submodule::modules::Error> { + .map(|id| -> Result, crate::Error> { Ok(id .object()? .peel_to_commit()? @@ -103,7 +103,7 @@ impl Repository { /// Return the list of available submodules, or `None` if there is no submodule configuration. #[doc(alias = "git2")] - pub fn submodules(&self) -> Result>>, submodule::modules::Error> { + pub fn submodules(&self) -> Result>>, crate::Error> { let modules = match self.modules()? { None => return Ok(None), Some(m) => m, diff --git a/gix/src/repository/worktree.rs b/gix/src/repository/worktree.rs index 1d4e360221e..ad21647ccc2 100644 --- a/gix/src/repository/worktree.rs +++ b/gix/src/repository/worktree.rs @@ -67,7 +67,7 @@ impl crate::Repository { /// /// Note that it might be the one that is currently open if this repository doesn't point to a linked worktree. /// Also note that the main repo might be bare. - pub fn main_repo(&self) -> Result { + pub fn main_repo(&self) -> Result { let options = match (self.kind(), self.options.clone()) { (crate::repository::Kind::LinkedWorkTree, opts) => opts.without_repository_environment_overrides(), (_, opts) => opts, @@ -101,7 +101,7 @@ impl crate::Repository { pub fn worktree_stream( &self, id: impl Into, - ) -> Result<(gix_worktree_stream::Stream, gix_index::File), crate::repository::worktree_stream::Error> { + ) -> Result<(gix_worktree_stream::Stream, gix_index::File), crate::Error> { use gix_odb::HeaderExt; let id = id.into(); let header = self.objects.header(id).map_err(gix_error::Exn::into_error)?; @@ -155,7 +155,7 @@ impl crate::Repository { blobs: impl gix_features::progress::Count, should_interrupt: &std::sync::atomic::AtomicBool, options: gix_archive::Options, - ) -> Result<(), crate::repository::worktree_archive::Error> { + ) -> Result<(), crate::Error> { let mut out = gix_features::interrupt::Write { inner: out, should_interrupt, diff --git a/gix/src/revision/spec/parse/error.rs b/gix/src/revision/spec/parse/error.rs index 9ce5642edfd..1e54827d06e 100644 --- a/gix/src/revision/spec/parse/error.rs +++ b/gix/src/revision/spec/parse/error.rs @@ -9,7 +9,7 @@ pub enum CandidateInfo { /// An error occurred when looking up the object in the database. FindError { /// The reported error. - source: crate::object::find::existing::Error, + source: crate::Error, }, /// The candidate is an object of the given `kind`. Object { diff --git a/gix/src/revision/spec/parse/mod.rs b/gix/src/revision/spec/parse/mod.rs index d29bc106814..fbf11569c68 100644 --- a/gix/src/revision/spec/parse/mod.rs +++ b/gix/src/revision/spec/parse/mod.rs @@ -7,12 +7,6 @@ pub use types::{ObjectKindHint, Options, RefsHint}; use crate::bstr::BString; -/// -pub mod single { - /// The error returned by [`crate::Repository::rev_parse_single()`]. - pub type Error = gix_error::Error; -} - /// pub mod error; diff --git a/gix/src/revision/walk.rs b/gix/src/revision/walk.rs index 216463ac8b0..7964681efd4 100644 --- a/gix/src/revision/walk.rs +++ b/gix/src/revision/walk.rs @@ -6,9 +6,6 @@ use gix_traverse::commit::simple::CommitTimeOrder; use crate::{Repository, ext::ObjectIdExt, revision}; -/// The error returned by [`Platform::all()`] and [`Platform::selected()`]. -pub type Error = gix_error::Error; - /// Specify how to sort commits during a [revision::Walk] traversal. /// /// ### Sample History @@ -98,7 +95,7 @@ impl<'repo> Info<'repo> { /// /// Note that this is an expensive operation which shouldn't be performed unless one needs more than parent ids /// and commit time. - pub fn object(&self) -> Result, crate::object::find::existing::Error> { + pub fn object(&self) -> Result, crate::Error> { Ok(self.id().object()?.into_commit()) } @@ -266,7 +263,7 @@ impl<'repo> Platform<'repo> { pub fn selected( self, mut filter: impl FnMut(&gix_hash::oid) -> bool + 'repo, - ) -> Result, Error> { + ) -> Result, crate::Error> { let Platform { repo, tips, @@ -335,27 +332,21 @@ impl<'repo> Platform<'repo> { /// /// It's highly recommended to set an [`object cache`](Repository::object_cache_size()) on the parent repo /// to greatly speed up performance if the returned id is supposed to be looked up right after. - pub fn all(self) -> Result, Error> { + pub fn all(self) -> Result, crate::Error> { self.selected(|_| true) } } -/// -pub mod iter { - /// The error returned by the [Walk](crate::revision::Walk) iterator. - pub type Error = gix_error::Error; -} - pub(crate) mod iter_impl { /// The iterator returned by [`crate::revision::walk::Platform::all()`]. pub struct Walk<'repo> { /// The owning repository. pub repo: &'repo crate::Repository, - pub(crate) inner: Box> + 'repo>, + pub(crate) inner: Box> + 'repo>, } impl<'repo> Iterator for Walk<'repo> { - type Item = Result, super::iter::Error>; + type Item = Result, crate::Error>; fn next(&mut self) -> Option { self.inner diff --git a/gix/src/shallow.rs b/gix/src/shallow.rs index bd85cda8e17..34413d54bec 100644 --- a/gix/src/shallow.rs +++ b/gix/src/shallow.rs @@ -3,13 +3,3 @@ pub(crate) type CommitsStorage = /// A lazily loaded and auto-updated list of commits which are at the shallow boundary (behind which there are no commits available), /// sorted to allow bisecting. pub type Commits = gix_fs::SharedFileSnapshot>; - -/// -pub mod read { - pub use gix_shallow::read::Error; -} - -/// -pub mod write { - pub use gix_shallow::write::Error; -} diff --git a/gix/src/status/index_worktree.rs b/gix/src/status/index_worktree.rs index 74eb68e5681..8a72cabcf58 100644 --- a/gix/src/status/index_worktree.rs +++ b/gix/src/status/index_worktree.rs @@ -10,9 +10,6 @@ use crate::{ }; use gix_status::index_as_worktree::traits::{CompareBlobs, SubmoduleStatus}; -/// The error returned by [Repository::index_worktree_status()]. -pub type Error = gix_error::Error; - /// Options for use with [Repository::index_worktree_status()]. #[derive(Default, Debug, Clone, Copy, PartialEq)] pub struct Options { @@ -84,7 +81,7 @@ impl Repository { progress: &mut dyn gix_features::progress::Progress, should_interrupt: &AtomicBool, options: Options, - ) -> Result + ) -> Result where T: Send + Clone, U: Send + Clone, @@ -167,7 +164,7 @@ impl Repository { patterns: impl IntoIterator>, index: &gix_index::State, options: Option<&crate::dirwalk::Options>, - ) -> Result, Error> { + ) -> Result, crate::Error> { let empty_patterns_match_prefix = options.is_some_and(|opts| opts.empty_patterns_match_prefix); let attrs_and_excludes = self .attributes( @@ -213,10 +210,7 @@ mod submodule_status { impl BuiltinSubmoduleStatus { /// Create a new instance from a `repo` and a `mode` to control how the submodule status will be obtained. - pub fn new( - repo: crate::ThreadSafeRepository, - mode: Submodule, - ) -> Result { + pub fn new(repo: crate::ThreadSafeRepository, mode: Submodule) -> Result { let local_repo = repo.to_thread_local(); let submodule_paths = match local_repo.submodules() { Ok(Some(sm)) => { @@ -570,7 +564,7 @@ pub mod iter { pub fn into_index_worktree_iter( mut self, patterns: impl IntoIterator, - ) -> Result { + ) -> Result { // deactivate the tree-iteration self.head_tree = None; Ok(index_worktree::Iter { @@ -580,7 +574,7 @@ pub mod iter { } impl Iterator for super::Iter { - type Item = Result; + type Item = Result; fn next(&mut self) -> Option { self.inner.next().map(|res| { diff --git a/gix/src/status/iter/mod.rs b/gix/src/status/iter/mod.rs index 46a2790763e..51e762eb7c3 100644 --- a/gix/src/status/iter/mod.rs +++ b/gix/src/status/iter/mod.rs @@ -40,16 +40,13 @@ where /// This isn't feasible to do here as it would mean that returned items would have to be delayed, /// degrading performance for everyone who isn't order-dependent. #[doc(alias = "diff_index_to_workdir", alias = "git2")] - pub fn into_iter( - self, - patterns: impl IntoIterator, - ) -> Result { + pub fn into_iter(self, patterns: impl IntoIterator) -> Result { let index = match self.index { None => IndexPersistedOrInMemory::Persisted(self.repo.index_or_empty()?), Some(index) => index, }; - let obtain_tree_id = || -> Result, crate::status::into_iter::Error> { + let obtain_tree_id = || -> Result, crate::Error> { Ok(match self.head_tree { Some(None) => Some( self.repo @@ -127,7 +124,7 @@ where let options = self.index_worktree_options; let should_interrupt = should_interrupt.clone(); let mut progress = self.progress; - move || -> Result<_, index_worktree::Error> { + move || -> Result<_, crate::Error> { let repo = repo.to_thread_local(); let out = repo.index_worktree_status( &index, @@ -230,11 +227,8 @@ where } } -/// The error returned for each item returned by [`Iter`]. -pub type Error = gix_error::Error; - impl Iterator for Iter { - type Item = Result; + type Item = Result; fn next(&mut self) -> Option { #[cfg(feature = "parallel")] diff --git a/gix/src/status/iter/types.rs b/gix/src/status/iter/types.rs index 3aa080d0b59..def2409e64f 100644 --- a/gix/src/status/iter/types.rs +++ b/gix/src/status/iter/types.rs @@ -35,8 +35,8 @@ pub struct Iter { #[expect(clippy::type_complexity)] pub(super) rx_and_join: Option<( std::sync::mpsc::Receiver, - std::thread::JoinHandle>, - Option>>, + std::thread::JoinHandle>, + Option>>, )>, #[cfg(feature = "parallel")] pub(super) should_interrupt: crate::status::OwnedOrStaticAtomicBool, diff --git a/gix/src/status/mod.rs b/gix/src/status/mod.rs index d0dfe4bc180..6532980c9fd 100644 --- a/gix/src/status/mod.rs +++ b/gix/src/status/mod.rs @@ -63,9 +63,6 @@ impl Default for Submodule { } } -/// The error returned by [status()](Repository::status). -pub type Error = gix_error::Error; - /// Status impl Repository { /// Obtain a platform for configuring iterators for traversing git repository status information. @@ -89,7 +86,7 @@ impl Repository { /// Whereas Git runs the index-modified check before the directory walk to set entries /// as up-to-date to (potentially) safe some disk-access, we run both in parallel which /// ultimately is much faster. - pub fn status

(&self, progress: P) -> Result, Error> + pub fn status

(&self, progress: P) -> Result, crate::Error> where P: gix_features::progress::Progress + 'static, { @@ -128,9 +125,6 @@ impl Repository { pub mod is_dirty { use crate::Repository; - /// The error returned by [Repository::is_dirty()]. - pub type Error = gix_error::Error; - impl Repository { /// Returns `true` if the repository is dirty. /// This means it's changed in one of the following ways: @@ -143,7 +137,7 @@ pub mod is_dirty { // TODO(performance): this could be its very own implementation with parallelism and the special: // stop once there is a change flag, but without using the iterator for // optimal resource usage. - pub fn is_dirty(&self) -> Result { + pub fn is_dirty(&self) -> Result { { let head_tree_id = self.head_tree_id_or_empty()?; let mut index_is_dirty = false; @@ -189,12 +183,6 @@ pub mod is_dirty { } } -/// -pub mod into_iter { - /// The error returned by [status::Platform::into_iter()](crate::status::Platform::into_iter()). - pub type Error = gix_error::Error; -} - mod platform; /// diff --git a/gix/src/status/tree_index.rs b/gix/src/status/tree_index.rs index df5e5f823e7..7394400587f 100644 --- a/gix/src/status/tree_index.rs +++ b/gix/src/status/tree_index.rs @@ -1,9 +1,6 @@ use crate::{Repository, config::tree}; use gix_error::ResultExt; -/// The error returned by [Repository::tree_index_status()]. -pub type Error = gix_error::Error; - /// Specify how to perform rewrite tracking [Repository::tree_index_status()]. #[derive(Default, Debug, Copy, Clone)] pub enum TrackRenames { @@ -53,7 +50,7 @@ impl Repository { &gix_index::State, &gix_index::State, ) -> Result, - ) -> Result { + ) -> Result { let _span = gix_trace::coarse!("gix::tree_index_status"); let tree_index: gix_index::State = self.index_from_tree(tree_id)?.into(); let rewrites = match renames { diff --git a/gix/src/submodule/errors.rs b/gix/src/submodule/errors.rs deleted file mode 100644 index e05e29cb3c1..00000000000 --- a/gix/src/submodule/errors.rs +++ /dev/null @@ -1,53 +0,0 @@ -/// -pub mod open_modules_file { - /// The error returned by [Repository::open_modules_file()](crate::Repository::open_modules_file()). - pub type Error = gix_error::Error; -} - -/// -pub mod modules { - /// The error returned by [Repository::modules()](crate::Repository::modules()). - pub type Error = gix_error::Error; -} - -/// -pub mod is_active { - /// The error returned by [Submodule::is_active()](crate::Submodule::is_active()). - pub type Error = gix_error::Error; -} - -/// -pub mod fetch_recurse { - /// The error returned by [Submodule::fetch_recurse()](crate::Submodule::fetch_recurse()). - pub type Error = gix_error::Error; -} - -/// -pub mod open { - /// The error returned by [Submodule::open()](crate::Submodule::open()). - pub type Error = gix_error::Error; -} - -/// -pub mod git_dir_try_old_form { - /// The error returned by [Submodule::git_dir_try_old_form()](crate::Submodule::git_dir_try_old_form()). - pub type Error = gix_error::Error; -} - -/// -pub mod state { - /// The error returned by [Submodule::state()](crate::Submodule::state()). - pub type Error = gix_error::Error; -} - -/// -pub mod index_id { - /// The error returned by [Submodule::index_id()](crate::Submodule::index_id()). - pub type Error = gix_error::Error; -} - -/// -pub mod head_id { - /// The error returned by [Submodule::head_id()](crate::Submodule::head_id()). - pub type Error = gix_error::Error; -} diff --git a/gix/src/submodule/mod.rs b/gix/src/submodule/mod.rs index 47de241cc21..f65ecfc9ee2 100644 --- a/gix/src/submodule/mod.rs +++ b/gix/src/submodule/mod.rs @@ -23,9 +23,6 @@ pub type ModulesSnapshot = gix_fs::SharedFileSnapshot; /// The name of the file containing (sub) module information. pub(crate) const MODULES_FILE: &str = ".gitmodules"; -mod errors; -pub use errors::*; - /// A platform maintaining state needed to interact with submodules, created by [`Repository::submodules()]. pub(crate) struct SharedState<'repo> { pub repo: &'repo Repository, @@ -44,7 +41,7 @@ impl<'repo> SharedState<'repo> { } } - fn index(&self) -> Result, crate::repository::index_or_load_from_head::Error> { + fn index(&self) -> Result, crate::Error> { { let mut state = self.index.borrow_mut(); if state.is_none() { @@ -58,7 +55,7 @@ impl<'repo> SharedState<'repo> { fn active_state_mut( &self, - ) -> Result<(RefMut<'_, IsActivePlatform>, RefMut<'_, gix_worktree::Stack>), is_active::Error> { + ) -> Result<(RefMut<'_, IsActivePlatform>, RefMut<'_, gix_worktree::Stack>), crate::Error> { let mut state = self.is_active.borrow_mut(); if state.is_none() { let platform = self @@ -113,7 +110,7 @@ impl Submodule<'_> { /// Return the path at which the submodule can be found, relative to the repository. /// /// For details, see [gix_submodule::File::path()]. - pub fn path(&self) -> Result { + pub fn path(&self) -> Result { self.state.modules.path(self.name()) } @@ -127,7 +124,7 @@ impl Submodule<'_> { /// Return the `update` field from this submodule's configuration, if present, or `None`. /// /// This method takes into consideration submodule configuration overrides. - pub fn update(&self) -> Result, config::update::Error> { + pub fn update(&self) -> Result, gix_error::ValidationError> { self.state.modules.update(self.name()) } @@ -139,7 +136,7 @@ impl Submodule<'_> { } /// Return the `fetchRecurseSubmodules` field from this submodule's configuration, or retrieve the value from `fetch.recurseSubmodules` if unset. - pub fn fetch_recurse(&self) -> Result, fetch_recurse::Error> { + pub fn fetch_recurse(&self) -> Result, crate::Error> { Ok(match self.state.modules.fetch_recurse(self.name()).or_erased()? { Some(val) => Some(val), None => crate::config::tree::Fetch::RECURSE_SUBMODULES @@ -151,7 +148,7 @@ impl Submodule<'_> { /// Return the `ignore` field from this submodule's configuration, if present, or `None`. /// /// This method takes into consideration submodule configuration overrides. - pub fn ignore(&self) -> Result, config::Error> { + pub fn ignore(&self) -> Result, gix_error::ValidationError> { self.state.modules.ignore(self.name()) } @@ -165,7 +162,7 @@ impl Submodule<'_> { /// Returns true if this submodule is considered active and can thus participate in an operation. /// /// Please see the [plumbing crate documentation](gix_submodule::IsActivePlatform::is_active()) for details. - pub fn is_active(&self) -> Result { + pub fn is_active(&self) -> Result { let (mut platform, mut attributes) = self.state.active_state_mut()?; let is_active = platform .is_active( @@ -188,7 +185,7 @@ impl Submodule<'_> { /// If `None`, but `Some()` when calling [`Self::head_id()`], then the submodule was just deleted but the change /// wasn't yet committed. Note that `None` is also returned if the entry at the submodule path isn't a submodule. /// If `Some()`, but `None` when calling [`Self::head_id()`], then the submodule was just added without having committed the change. - pub fn index_id(&self) -> Result, index_id::Error> { + pub fn index_id(&self) -> Result, crate::Error> { let path = self.path().or_erased()?; Ok(self .state @@ -202,7 +199,7 @@ impl Submodule<'_> { /// If `Some()`, but `None` when calling [`Self::index_id()`], then the submodule was just deleted but the change /// wasn't yet committed. Note that `None` is also returned if the entry at the submodule path isn't a submodule. /// If `None`, but `Some()` when calling [`Self::index_id()`], then the submodule was just added without having committed the change. - pub fn head_id(&self) -> Result, head_id::Error> { + pub fn head_id(&self) -> Result, crate::Error> { let path = self.path().or_erased()?; Ok(self .state @@ -226,7 +223,7 @@ impl Submodule<'_> { /// /// Note that it may be a path relative to the repository if, for some reason, the parent directory /// doesn't have a working dir set. - pub fn work_dir(&self) -> Result { + pub fn work_dir(&self) -> Result { let worktree_git = gix_path::from_bstr(self.path()?); Ok(match self.state.repo.workdir() { None => worktree_git.into_owned(), @@ -243,7 +240,7 @@ impl Submodule<'_> { /// is required to be a directory though, as a malformed gitdir file means a broken submodule checkout. /// /// Also note that the fallback path returned for uninitialized submodules may not actually exist. - pub fn git_dir_try_old_form(&self) -> Result { + pub fn git_dir_try_old_form(&self) -> Result { self.git_dir_try_old_form_inner(true) } @@ -252,10 +249,7 @@ impl Submodule<'_> { /// /// Validation may be skipped when callers only need coarse state, like `status_opts()` with /// `Ignore::All`, which returns before opening or inspecting the submodule repository. - fn git_dir_try_old_form_inner( - &self, - validate_gitdir_file_target: bool, - ) -> Result { + fn git_dir_try_old_form_inner(&self, validate_gitdir_file_target: bool) -> Result { let git_dir = self.git_dir().map_err(|err| { gix_error::Error::from(err.and_raise(gix_error::ValidationError::new("The submodule name is invalid"))) })?; @@ -287,7 +281,7 @@ impl Submodule<'_> { Ok(git_dir) } - fn state_inner(&self, validate_gitdir_file_target: bool) -> Result { + fn state_inner(&self, validate_gitdir_file_target: bool) -> Result { let maybe_old_path = self.git_dir_try_old_form_inner(validate_gitdir_file_target)?; let worktree_git = self.worktree_gitdir().or_erased()?; let superproject_configuration = self @@ -309,7 +303,7 @@ impl Submodule<'_> { /// Query various parts of the submodule and assemble it into state information. #[doc(alias = "status", alias = "git2")] - pub fn state(&self) -> Result { + pub fn state(&self) -> Result { self.state_inner(true) } @@ -324,7 +318,7 @@ impl Submodule<'_> { /// Also see the [state()](Self::state()) method for learning about the submodule. /// The repository can also be used to learn about the submodule `HEAD`, i.e. where its working tree is at, /// which may differ compared to the superproject's index or `HEAD` commit. - pub fn open(&self) -> Result, open::Error> { + pub fn open(&self) -> Result, crate::Error> { let mut options = self .state .repo @@ -352,7 +346,7 @@ impl Submodule<'_> { } } - fn worktree_gitdir(&self) -> Result { + fn worktree_gitdir(&self) -> Result { Ok(self.work_dir()?.join(gix_discover::DOT_GIT_DIR)) } } @@ -407,9 +401,6 @@ pub mod status { use super::Status; use crate::Submodule; - /// The error returned by [Submodule::status()]. - pub type Error = gix_error::Error; - impl Submodule<'_> { /// Return the status of the submodule. /// @@ -423,7 +414,7 @@ pub mod status { &self, ignore: config::Ignore, check_dirty: bool, - ) -> Result { + ) -> Result { self.status_opts(ignore, check_dirty, &mut |s| s) } /// Return the status of the submodule, just like [`status`](Self::status), but allows to adjust options @@ -447,7 +438,7 @@ pub mod status { crate::status::Platform<'a, gix_features::progress::Discard>, ) -> crate::status::Platform<'a, gix_features::progress::Discard>, - ) -> Result { + ) -> Result { let mut state = self.state_inner(ignore != config::Ignore::All)?; if ignore == config::Ignore::All { return Ok(Status { diff --git a/gix/src/tag.rs b/gix/src/tag.rs index 97d6dd6082b..53bf3e336d9 100644 --- a/gix/src/tag.rs +++ b/gix/src/tag.rs @@ -1,8 +1,2 @@ //! #![allow(clippy::empty_docs)] -mod error { - - /// The error returned by [`tag(…)`][crate::Repository::tag()]. - pub type Error = gix_error::Error; -} -pub use error::Error; diff --git a/gix/src/worktree/mod.rs b/gix/src/worktree/mod.rs index 104fcc24929..5d2146e7b2a 100644 --- a/gix/src/worktree/mod.rs +++ b/gix/src/worktree/mod.rs @@ -118,17 +118,15 @@ pub mod proxy; /// #[cfg(feature = "index")] pub mod open_index { - /// The error returned by [`Worktree::open_index()`][crate::Worktree::open_index()]. - pub type Error = gix_error::Error; impl crate::Worktree<'_> { /// A shortcut to [`crate::Repository::open_index()`]. - pub fn open_index(&self) -> Result { + pub fn open_index(&self) -> Result { self.parent.open_index() } /// A shortcut to [`crate::Repository::index()`]. - pub fn index(&self) -> Result { + pub fn index(&self) -> Result { self.parent.index() } } @@ -139,9 +137,6 @@ pub mod open_index { pub mod excludes { use crate::AttributeStack; - /// The error returned by [`Worktree::excludes()`][crate::Worktree::excludes()]. - pub type Error = gix_error::Error; - impl crate::Worktree<'_> { /// Configure a file-system cache checking if files below the repository are excluded. /// @@ -152,7 +147,7 @@ pub mod excludes { /// /// When only excludes are desired, this is the most efficient way to obtain them. Otherwise use /// [`Worktree::attributes()`][crate::Worktree::attributes()] for accessing both attributes and excludes. - pub fn excludes(&self, overrides: Option) -> Result, Error> { + pub fn excludes(&self, overrides: Option) -> Result, crate::Error> { let index = self.index()?; self.parent.excludes( &index, @@ -168,9 +163,6 @@ pub mod excludes { pub mod attributes { use crate::{AttributeStack, Worktree}; - /// The error returned by [`Worktree::attributes()`]. - pub type Error = gix_error::Error; - impl<'repo> Worktree<'repo> { /// Configure a file-system cache checking if files below the repository are excluded or for querying their attributes. /// @@ -178,7 +170,7 @@ pub mod attributes { /// /// * `$XDG_CONFIG_HOME/…/ignore|attributes` if `core.excludesFile|attributesFile` is *not* set, otherwise use the configured file. /// * `$GIT_DIR/info/exclude|attributes` if present. - pub fn attributes(&self, overrides: Option) -> Result, Error> { + pub fn attributes(&self, overrides: Option) -> Result, crate::Error> { let index = self.index()?; self.parent.attributes( &index, @@ -189,7 +181,7 @@ pub mod attributes { } /// Like [attributes()][Self::attributes()], but without access to exclude/ignore information. - pub fn attributes_only(&self) -> Result, Error> { + pub fn attributes_only(&self) -> Result, crate::Error> { let index = self.index()?; self.parent.attributes_only( &index, @@ -208,9 +200,6 @@ pub mod pathspec { config::{cache::util::ApplyLeniencyDefaultValue, tree::gitoxide}, }; - /// The error returned by [`Worktree::pathspec()`]. - pub type Error = gix_error::Error; - impl<'repo> Worktree<'repo> { /// Configure pathspecs `patterns` to be matched against, with pathspec attributes read from the worktree and then from the index /// if needed. @@ -223,7 +212,7 @@ pub mod pathspec { pub fn pathspec( &self, patterns: impl IntoIterator>, - ) -> Result, Error> { + ) -> Result, crate::Error> { let index = self.index()?; let inherit_ignore_case = gitoxide::Pathspec::INHERIT_IGNORE_CASE .enrich_error( diff --git a/gix/src/worktree/proxy.rs b/gix/src/worktree/proxy.rs index e1b8fbb29d0..144989f6505 100644 --- a/gix/src/worktree/proxy.rs +++ b/gix/src/worktree/proxy.rs @@ -9,12 +9,6 @@ use crate::{ worktree::Proxy, }; -#[expect(missing_docs)] -pub mod into_repo { - /// The error returned by [`Proxy::into_repo()`][super::Proxy::into_repo()]. - pub type Error = gix_error::Error; -} - impl<'repo> Proxy<'repo> { pub(crate) fn new(parent: &'repo Repository, git_dir: impl Into) -> Self { Proxy { @@ -93,7 +87,7 @@ impl Proxy<'_> { /// was removed or moved in the mean time or is unavailable for other reasons. /// The caller will encounter io errors if it's used like the work tree is guaranteed to be present, but can still access /// a lot of information if work tree access is avoided. - pub fn into_repo_with_possibly_inaccessible_worktree(self) -> Result { + pub fn into_repo_with_possibly_inaccessible_worktree(self) -> Result { let base = self.base().ok(); let options = self.parent.options.clone().without_repository_environment_overrides(); let common_dir = self.parent.common_dir().to_owned(); @@ -105,7 +99,7 @@ impl Proxy<'_> { /// if the worktree doesn't exist. /// /// Note that it won't fail if the worktree doesn't exist. - pub fn into_repo(self) -> Result { + pub fn into_repo(self) -> Result { let base = self.base().or_erased()?; if !base.is_dir() { return Err(gix_error::Error::from_error(gix_error::message!( diff --git a/gix/tests/doctest.rs b/gix/tests/doctest.rs index 308aaada474..98f4be77b83 100644 --- a/gix/tests/doctest.rs +++ b/gix/tests/doctest.rs @@ -2,11 +2,11 @@ pub fn basic_repo_dir() -> Result) -> Result { +pub fn open_repo(path: impl Into) -> Result { gix::open_opts(path, gix::open::Options::isolated()) } -pub fn discover_repo(path: impl AsRef) -> Result { +pub fn discover_repo(path: impl AsRef) -> Result { let opts = gix::open::Options::isolated(); gix::ThreadSafeRepository::discover_opts( path, diff --git a/gix/tests/gix-init.rs b/gix/tests/gix-init.rs index 8d75a32e682..56fc6ebc9ba 100644 --- a/gix/tests/gix-init.rs +++ b/gix/tests/gix-init.rs @@ -11,14 +11,12 @@ pub fn named_subrepo_opts( fixture: &str, name: &str, opts: gix::open::Options, -) -> std::result::Result { +) -> std::result::Result { let repo_path = gix_testtools::scripted_fixture_read_only(fixture).unwrap().join(name); Ok(ThreadSafeRepository::open_opts(repo_path, opts)?.to_thread_local()) } -fn discover_with_environment_overrides_isolated( - directory: impl AsRef, -) -> Result { +fn discover_with_environment_overrides_isolated(directory: impl AsRef) -> Result { let mut options = gix::open::Options::isolated(); options.permissions.env.git_prefix = Permission::Allow; ThreadSafeRepository::discover_with_environment_overrides_opts( diff --git a/gix/tests/gix/config/tree.rs b/gix/tests/gix/config/tree.rs index 994963db7e5..81d99c7f75e 100644 --- a/gix/tests/gix/config/tree.rs +++ b/gix/tests/gix/config/tree.rs @@ -1,4 +1,4 @@ -fn config_value_error(message: &'static str, input: &'static str) -> gix_config::value::Error { +fn config_value_error(message: &'static str, input: &'static str) -> gix_error::Exn { use gix_error::ErrorExt; gix_error::ValidationError::new_with_input(message, input).raise() } @@ -470,7 +470,7 @@ mod core { use gix::config::tree::{Core, Key}; use gix_lock::acquire::Fail; - fn signed(value: i64) -> Result, gix_config::value::Error> { + fn signed(value: i64) -> Result, gix_error::Exn> { Ok(Some(value)) } @@ -837,7 +837,7 @@ mod extensions { mod checkout { use gix::config::tree::{Checkout, Key}; - fn int(value: i64) -> Result, gix_config::value::Error> { + fn int(value: i64) -> Result, gix_error::Exn> { Ok(Some(value)) } diff --git a/gix/tests/gix/object/tree/mod.rs b/gix/tests/gix/object/tree/mod.rs index b3280ce9105..205af0915b1 100644 --- a/gix/tests/gix/object/tree/mod.rs +++ b/gix/tests/gix/object/tree/mod.rs @@ -3,7 +3,7 @@ use crate::util::{named_repo, named_subrepo_opts}; #[cfg(all(feature = "blob-diff", feature = "revision"))] mod diff; -fn worktree_repo() -> Result { +fn worktree_repo() -> Result { named_subrepo_opts("make_worktree_repo.sh", "repo", gix::open::Options::isolated()) } diff --git a/gix/tests/gix/remote/connect.rs b/gix/tests/gix/remote/connect.rs index c3d61a136d0..512c336925b 100644 --- a/gix/tests/gix/remote/connect.rs +++ b/gix/tests/gix/remote/connect.rs @@ -3,9 +3,8 @@ feature = "blocking-http-transport-reqwest" ))] mod http_authentication { - use std::io::{BufRead, Write}; - use gix_error::ErrorExt; + use std::io::{BufRead, Write}; #[test] fn cached_credentials_are_selected_without_prompting() -> crate::Result { @@ -67,7 +66,7 @@ mod http_authentication { .with_credentials(|action| { obtained = Some(authenticate(action)); // Stop after credential lookup, before the transport sends these dummy credentials. - Err(gix_error::message("stop after credential lookup").raise_erased()) + Err(gix_error::message("The handler asked to stop trying to obtain credentials").raise_erased()) }) .ref_map(gix::progress::Discard, Default::default()); server.join().expect("the HTTP fixture thread does not panic")?; diff --git a/gix/tests/gix/remote/fetch.rs b/gix/tests/gix/remote/fetch.rs index a79ac446fb7..b7b4dd7e8a2 100644 --- a/gix/tests/gix/remote/fetch.rs +++ b/gix/tests/gix/remote/fetch.rs @@ -83,7 +83,7 @@ mod blocking_and_async_io { pub(crate) fn try_repo_rw( name: &str, - ) -> Result<(gix::Repository, gix_testtools::tempfile::TempDir), gix::open::Error> { + ) -> Result<(gix::Repository, gix_testtools::tempfile::TempDir), gix_error::Error> { try_repo_rw_args(name, Vec::::new(), Mode::FastClone) } @@ -96,7 +96,7 @@ mod blocking_and_async_io { name: &str, args: impl IntoIterator, mode: Mode, - ) -> Result<(gix::Repository, gix_testtools::tempfile::TempDir), gix::open::Error> { + ) -> Result<(gix::Repository, gix_testtools::tempfile::TempDir), gix_error::Error> { let dir = gix_testtools::scripted_fixture_writable_with_args_single_archive( "make_fetch_repos.sh", [{ @@ -264,7 +264,7 @@ mod blocking_and_async_io { #[test] #[cfg(feature = "blocking-network-client")] - fn collate_fetch_error() -> Result<(), gix::env::collate::fetch::Error> { + fn collate_fetch_error() -> Result<(), gix_error::Error> { let (repo, _tmp) = try_repo_rw("two-origins")?; let remote = repo .head()? diff --git a/gix/tests/gix/repository/config/remote.rs b/gix/tests/gix/repository/config/remote.rs index ae9ea7c1f9b..1903f8b4e6b 100644 --- a/gix/tests/gix/repository/config/remote.rs +++ b/gix/tests/gix/repository/config/remote.rs @@ -395,7 +395,7 @@ mod branch_remote { Ok(()) } - fn repo(name: &str) -> Result { + fn repo(name: &str) -> Result { named_subrepo_opts("make_remote_config_repos.sh", name, gix::open::Options::isolated()) } } diff --git a/gix/tests/gix/util.rs b/gix/tests/gix/util.rs index 7ae1c63c7e4..812063065a8 100644 --- a/gix/tests/gix/util.rs +++ b/gix/tests/gix/util.rs @@ -214,7 +214,7 @@ pub fn freeze_time() -> gix_testtools::Env<'static> { pub fn init_repo_isolated( path: impl AsRef, kind: gix::create::Kind, -) -> std::result::Result { +) -> std::result::Result { ThreadSafeRepository::init_opts(path, kind, Default::default(), open::Options::isolated()) } @@ -223,7 +223,7 @@ pub fn repo(name: &str) -> Result { Ok(ThreadSafeRepository::open_opts(repo_path, restricted())?) } -pub fn repo_opts(name: &str, opts: open::Options) -> std::result::Result { +pub fn repo_opts(name: &str, opts: open::Options) -> std::result::Result { let repo_path = gix_testtools::scripted_fixture_read_only(name).unwrap(); ThreadSafeRepository::open_opts(repo_path, opts) } @@ -237,9 +237,9 @@ pub fn named_subrepo_opts( fixture: &str, name: &str, opts: open::Options, -) -> std::result::Result { +) -> std::result::Result { let repo_path = gix_testtools::scripted_fixture_read_only(fixture) - .map_err(gix::open::Error::from_boxed)? + .map_err(gix_error::Error::from_boxed)? .join(name); Ok(ThreadSafeRepository::open_opts(repo_path, opts)?.to_thread_local()) } diff --git a/gix/tests/repository.rs b/gix/tests/repository.rs index 719456225de..ae0a90ff179 100644 --- a/gix/tests/repository.rs +++ b/gix/tests/repository.rs @@ -450,7 +450,7 @@ fn open_options_preset_system_config_paths_avoid_running_git() -> gix_testtools: } #[cfg(feature = "revision")] -fn probable_cause(res: Result, gix::revision::spec::parse::single::Error>) -> String { +fn probable_cause(res: Result, gix_error::Error>) -> String { res.expect_err("the revspec must not resolve") .probable_cause() .to_string() diff --git a/src/porcelain/options.rs b/src/porcelain/options.rs index 0dcb77c3fa3..1c65657034a 100644 --- a/src/porcelain/options.rs +++ b/src/porcelain/options.rs @@ -210,7 +210,7 @@ pub mod tools { fn assure_is_repo(dir: &OsStr) -> anyhow::Result<()> { let git_dir = PathBuf::from(dir).join(".git"); let p = gix::path::realpath(&git_dir) - .map_err(gix::path::realpath::Error::into_error) + .map_err(gix::Exn::into_error) .with_context(|| format!("Could not canonicalize git repository at '{}'", git_dir.display()))?; if p.extension().unwrap_or_default() == "git" || p.file_name().unwrap_or_default() == ".git" diff --git a/src/shared.rs b/src/shared.rs index 67154b0c13f..bd31fc45abc 100644 --- a/src/shared.rs +++ b/src/shared.rs @@ -332,7 +332,7 @@ mod clap { fn parse_ref(&self, cmd: &Command, arg: Option<&Arg>, value: &OsStr) -> Result { OsStringValueParser::new() - .try_map(|arg| -> Result<_, gix::pathspec::parse::Error> { + .try_map(|arg| -> Result<_, gix::error::ValidationError> { let arg = gix::path::into_bstr(std::path::PathBuf::from(arg)); gix::pathspec::parse(arg.as_ref(), *PATHSPEC_DEFAULTS)?; Ok(arg.into_owned()) @@ -354,7 +354,7 @@ mod clap { fn parse_ref(&self, cmd: &Command, arg: Option<&Arg>, value: &OsStr) -> Result { OsStringValueParser::new() - .try_map(|arg| -> Result<_, gix::pathspec::parse::Error> { + .try_map(|arg| -> Result<_, gix::error::ValidationError> { let arg = gix::path::into_bstr(std::path::PathBuf::from(arg)); gix::pathspec::parse(arg.as_ref(), Default::default())?; Ok(arg.into_owned()) diff --git a/tests/tools/src/lib.rs b/tests/tools/src/lib.rs index cb45612b415..3a8f0bf3cc9 100644 --- a/tests/tools/src/lib.rs +++ b/tests/tools/src/lib.rs @@ -52,9 +52,6 @@ pub mod repository; const ARCHIVE_DIR_NAME: &str = "generated-archives"; -/// The error returned by test functions. -pub use gix_error::TestError as Error; - /// A result type to allow using the try operator `?` in unit tests. /// /// Use it like so: From daf73b5fe5a21e3ddcc58f0882c2d880f48b7860 Mon Sep 17 00:00:00 2001 From: Codex Date: Tue, 15 Sep 2026 12:10:49 +0200 Subject: [PATCH 31/51] Use borrowed error inspection throughout the workspace rubberstamp Replace classification-only downcasts with semantic predicates for retryability, missing resources, invalid input, corruption, and resource exhaustion. Use borrowed probable-cause inspection where callers only need the underlying error, removing temporary ownership conversions. --- gix-commitgraph/tests/commitgraph.rs | 9 +++------ gix-config-value/tests/value/path.rs | 6 +++--- gix-config/src/file/includes/mod.rs | 2 +- gix-config/tests/config/file/access/mutate.rs | 12 ++++++------ .../tests/config/file/access/raw/raw_multi_value.rs | 6 +++--- gix-config/tests/config/file/access/raw/raw_value.rs | 10 +++++----- .../tests/config/file/access/raw/set_raw_value.rs | 2 +- gix-config/tests/config/file/access/read_only.rs | 11 ++++------- gix-credentials/src/helper/cascade.rs | 4 ++-- gix-credentials/tests/helper/invoke.rs | 2 +- gix-credentials/tests/helper/mod.rs | 4 ++-- gix-credentials/tests/program/main.rs | 6 +++--- gix-discover/tests/discover/is_git.rs | 2 +- gix-discover/tests/discover/upwards/ceiling_dirs.rs | 10 +++++----- gix-discover/tests/discover/upwards/mod.rs | 2 +- gix-discover/tests/isolated.rs | 4 ++-- .../tests/filter/pipeline/convert_to_worktree.rs | 5 +---- gix-index/src/extension/tree/verify.rs | 2 +- gix-index/tests/index/file/init.rs | 4 ++-- gix-index/tests/index/file/read.rs | 2 +- gix-index/tests/index/init.rs | 3 +-- gix-lock/tests/lock/marker.rs | 5 +---- gix-merge/tests/merge/blob/pipeline.rs | 7 ++----- gix-merge/tests/merge/blob/platform.rs | 2 +- gix-note/tests/note.rs | 4 ---- gix-object/src/signature/verify.rs | 4 ++-- gix-pack/tests/pack/bundle.rs | 5 +---- gix-pack/tests/pack/data/file.rs | 8 ++------ gix-pack/tests/pack/data/input.rs | 2 +- gix-pack/tests/pack/data/output/count_and_entries.rs | 5 +---- gix-pack/tests/pack/multi_index/fuzzed.rs | 4 ++-- gix-protocol/tests/protocol/fetch/response.rs | 1 - gix-ref/src/store/file/transaction/prepare.rs | 2 +- gix-url/tests/url/expand_path.rs | 2 +- gix-zlib/src/stream/inflate.rs | 2 +- gix-zlib/tests/zlib/inflate.rs | 2 +- gix/src/repository/branch.rs | 8 +------- gix/src/repository/graph.rs | 9 ++++++--- gix/tests/gix-init.rs | 4 +--- .../config/config_snapshot/credential_helpers.rs | 2 +- gix/tests/gix/submodule.rs | 2 +- 41 files changed, 76 insertions(+), 112 deletions(-) diff --git a/gix-commitgraph/tests/commitgraph.rs b/gix-commitgraph/tests/commitgraph.rs index 07aa851dcc6..5d9142a9d19 100644 --- a/gix-commitgraph/tests/commitgraph.rs +++ b/gix-commitgraph/tests/commitgraph.rs @@ -11,16 +11,13 @@ use gix_testtools::scripted_fixture_read_only; mod access; #[test] -fn missing_path_keeps_io_error() -> gix_testtools::Result { +fn missing_path_is_not_found() -> gix_testtools::Result { let dir = gix_testtools::tempfile::tempdir()?; let err = gix_commitgraph::at(dir.path().join("missing")) .err() .expect("a missing path cannot contain a commit-graph"); - assert_eq!( - err.downcast_any_ref::() - .expect("the filesystem error is preserved") - .kind(), - std::io::ErrorKind::NotFound, + assert!( + err.is_not_found(), "callers can distinguish a missing optional cache from other failures" ); Ok(()) diff --git a/gix-config-value/tests/value/path.rs b/gix-config-value/tests/value/path.rs index b8ce08b2477..ee199d98894 100644 --- a/gix-config-value/tests/value/path.rs +++ b/gix-config-value/tests/value/path.rs @@ -3,7 +3,7 @@ mod interpolate { use bstr::BString; use gix_config_value::path; - use gix_error::{NotFoundError, ValidationError}; + use gix_error::NotFoundError; #[test] fn backslash_is_not_special_and_they_are_not_escaping_anything() -> gix_error::Result { @@ -17,7 +17,7 @@ mod interpolate { #[test] fn empty_path_is_error() { let err = interpolate_without_context("").expect_err("empty paths are invalid"); - assert!(err.downcast_any_ref::().is_some()); + assert!(err.is_not_found()); } #[test] @@ -137,7 +137,7 @@ mod interpolate { ..Default::default() }) .expect_err("the username is not UTF-8"); - assert!(err.downcast_any_ref::().is_some()); + assert!(err.is_validation()); assert!(err.downcast_any_ref::().is_some()); } diff --git a/gix-config/src/file/includes/mod.rs b/gix-config/src/file/includes/mod.rs index 4e142dbd534..8ce312f0b7d 100644 --- a/gix-config/src/file/includes/mod.rs +++ b/gix-config/src/file/includes/mod.rs @@ -319,7 +319,7 @@ fn check_interpolation_result( } match res { Ok(good) => Ok(Some(good.into())), - Err(err) if err.downcast_any_ref::().is_some() => Err(err), + Err(err) if err.is_validation() => Err(err), Err(_) => Ok(None), } } diff --git a/gix-config/tests/config/file/access/mutate.rs b/gix-config/tests/config/file/access/mutate.rs index 83bee8b256a..c6b2025dca8 100644 --- a/gix-config/tests/config/file/access/mutate.rs +++ b/gix-config/tests/config/file/access/mutate.rs @@ -80,7 +80,7 @@ mod remove_section { file.remove_section("core", None).expect("plain section exists"); let err = file.section("core", None).unwrap_err(); - assert!(err.downcast_any_ref::().is_some()); + assert!(err.is_not_found()); assert_eq!( err.to_string(), "The requested subsection does not exist", @@ -93,7 +93,7 @@ mod remove_section { file.remove_section("core", "b").expect("final subsection exists"); let err = file.section("core", "b").unwrap_err(); - assert!(err.downcast_any_ref::().is_some()); + assert!(err.is_not_found()); assert_eq!(err.to_string(), "The requested section does not exist"); Ok(()) } @@ -149,11 +149,11 @@ mod rename_section { fn section_renaming_validates_new_name() { let mut file = gix_config::File::try_from("[core] a = b").unwrap(); let err = file.rename_section("core", None, "new_core", None).unwrap_err(); - assert!(err.downcast_any_ref::().is_some()); + assert!(err.is_validation()); assert_eq!(err.to_string(), "section names can only be ascii, '-': \"new_core\""); let err = file.rename_section("core", None, "new-core", "a\nb").unwrap_err(); - assert!(err.downcast_any_ref::().is_some()); + assert!(err.is_validation()); assert_eq!( err.to_string(), "sub-section names must not contain newlines or null bytes: \"a\\nb\"" @@ -238,7 +238,7 @@ mod rename_section { let err = file .rename_section_filter("branch", "source", "branch", "other", |_| false) .unwrap_err(); - assert!(err.downcast_any_ref::().is_some()); + assert!(err.is_not_found()); assert_eq!( err.to_string(), "The key does not exist in the requested section", @@ -273,7 +273,7 @@ mod rename_section { let mut file = gix_config::File::try_from("[core] key = value\n")?; file.remove_section("core", None).expect("section exists"); let err = file.rename_section("core", None, "other", None).unwrap_err(); - assert!(err.downcast_any_ref::().is_some()); + assert!(err.is_not_found()); assert_eq!(err.to_string(), "The requested section does not exist"); Ok(()) } diff --git a/gix-config/tests/config/file/access/raw/raw_multi_value.rs b/gix-config/tests/config/file/access/raw/raw_multi_value.rs index 0044d1c8b70..4ee7fac3a2c 100644 --- a/gix-config/tests/config/file/access/raw/raw_multi_value.rs +++ b/gix-config/tests/config/file/access/raw/raw_multi_value.rs @@ -95,7 +95,7 @@ fn values_with_sections_filter_returns_values_from_accepted_sections() -> crate: fn section_not_found() -> crate::Result { let config = File::try_from("[core]\na=b\nc=d")?; let err = config.raw_values("foo.a").unwrap_err(); - assert!(err.downcast_any_ref::().is_some()); + assert!(err.is_not_found()); assert_eq!(err.to_string(), "The requested section does not exist"); Ok(()) } @@ -104,7 +104,7 @@ fn section_not_found() -> crate::Result { fn subsection_not_found() -> crate::Result { let config = File::try_from("[core]\na=b\nc=d")?; let err = config.raw_values("core.a.a").unwrap_err(); - assert!(err.downcast_any_ref::().is_some()); + assert!(err.is_not_found()); assert_eq!(err.to_string(), "The requested subsection does not exist"); Ok(()) } @@ -113,7 +113,7 @@ fn subsection_not_found() -> crate::Result { fn key_not_found() -> crate::Result { let config = File::try_from("[core]\na=b\nc=d")?; let err = config.raw_values("core.aaaaaa").unwrap_err(); - assert!(err.downcast_any_ref::().is_some()); + assert!(err.is_not_found()); assert_eq!(err.to_string(), "The key does not exist in the requested section"); Ok(()) } diff --git a/gix-config/tests/config/file/access/raw/raw_value.rs b/gix-config/tests/config/file/access/raw/raw_value.rs index 67831d539a5..c2363a6ef15 100644 --- a/gix-config/tests/config/file/access/raw/raw_value.rs +++ b/gix-config/tests/config/file/access/raw/raw_value.rs @@ -116,7 +116,7 @@ fn mutable_value_filters_have_key_and_component_variants() -> crate::Result { fn section_not_found() -> crate::Result { let config = File::try_from("[core]\na=b\nc=d")?; let err = config.raw_value("foo.a").unwrap_err(); - assert!(err.downcast_any_ref::().is_some()); + assert!(err.is_not_found()); assert_eq!(err.to_string(), "The requested section does not exist"); Ok(()) } @@ -125,7 +125,7 @@ fn section_not_found() -> crate::Result { fn subsection_not_found() -> crate::Result { let config = File::try_from("[core]\na=b\nc=d")?; let err = config.raw_value("core.a.a").unwrap_err(); - assert!(err.downcast_any_ref::().is_some()); + assert!(err.is_not_found()); assert_eq!(err.to_string(), "The requested subsection does not exist"); Ok(()) } @@ -134,7 +134,7 @@ fn subsection_not_found() -> crate::Result { fn key_not_found() -> crate::Result { let config = File::try_from("[core]\na=b\nc=d")?; let err = config.raw_value("core.aaaaaa").unwrap_err(); - assert!(err.downcast_any_ref::().is_some()); + assert!(err.is_not_found()); assert_eq!(err.to_string(), "The key does not exist in the requested section"); Ok(()) } @@ -143,13 +143,13 @@ fn key_not_found() -> crate::Result { fn invalid_value_names_are_reported_by_mutable_lookups() -> crate::Result { let mut config = File::try_from("[core]\na=b")?; let err = config.raw_value_mut_by("core", None, "1invalid").unwrap_err(); - assert!(err.downcast_any_ref::().is_some()); + assert!(err.is_validation()); assert_eq!( err.to_string(), "Valid value names consist of alphanumeric characters or dashes, starting with an alphabetic character.: \"1invalid\"" ); let err = config.raw_values_mut_by("core", None, "contains.dot").unwrap_err(); - assert!(err.downcast_any_ref::().is_some()); + assert!(err.is_validation()); assert_eq!( err.to_string(), "Valid value names consist of alphanumeric characters or dashes, starting with an alphabetic character.: \"contains.dot\"" diff --git a/gix-config/tests/config/file/access/raw/set_raw_value.rs b/gix-config/tests/config/file/access/raw/set_raw_value.rs index 80d201c5947..83d40c6576b 100644 --- a/gix-config/tests/config/file/access/raw/set_raw_value.rs +++ b/gix-config/tests/config/file/access/raw/set_raw_value.rs @@ -80,7 +80,7 @@ fn accepts_short_lived_keys() -> crate::Result { fn invalid_value_names_fail_without_creating_a_section() { let mut file = gix_config::File::default(); let err = file.set_raw_value_by("new", None, "not.valid", "value").unwrap_err(); - assert!(err.downcast_any_ref::().is_some()); + assert!(err.is_validation()); assert_eq!( err.to_string(), "Valid value names consist of alphanumeric characters or dashes, starting with an alphabetic character.: \"not.valid\"" diff --git a/gix-config/tests/config/file/access/read_only.rs b/gix-config/tests/config/file/access/read_only.rs index a2a4f1abe42..08eac67b1c8 100644 --- a/gix-config/tests/config/file/access/read_only.rs +++ b/gix-config/tests/config/file/access/read_only.rs @@ -31,10 +31,7 @@ fn typed_lookup_errors_can_be_erased() -> crate::Result { .or_erased() .expect_err("invalid typed values must fail conversion") .into_error(); - assert!( - err.downcast_any_ref::().is_some(), - "erasure retains the conversion error" - ); + assert!(err.is_validation(), "erasure retains the conversion error"); } let err = config .value::("core.missing") @@ -397,7 +394,7 @@ fn sections_by_name_ignores_subsections_and_preserves_file_order() -> crate::Res fn unknown_section() -> crate::Result { let config = File::default(); let err = config.section("missing", None).unwrap_err(); - assert!(err.downcast_any_ref::().is_some()); + assert!(err.is_not_found()); assert_eq!(err.to_string(), "The requested section does not exist"); let config = r#" @@ -406,7 +403,7 @@ fn unknown_section() -> crate::Result { "#; let mut config = File::try_from(config)?; let err = config.section("present", Some("subsection".into())).unwrap_err(); - assert!(err.downcast_any_ref::().is_some()); + assert!(err.is_not_found()); assert_eq!(err.to_string(), "The requested subsection does not exist"); config.set_raw_value_by("present", "subsection", "key", "value")?; @@ -419,7 +416,7 @@ fn unknown_section() -> crate::Result { assert!(config.remove_section_by_id(id).is_some()); } let err = config.section("present", None).unwrap_err(); - assert!(err.downcast_any_ref::().is_some()); + assert!(err.is_not_found()); assert_eq!(err.to_string(), "The requested section does not exist"); Ok(()) diff --git a/gix-credentials/src/helper/cascade.rs b/gix-credentials/src/helper/cascade.rs index 904a2afb164..74126888c59 100644 --- a/gix-credentials/src/helper/cascade.rs +++ b/gix-credentials/src/helper/cascade.rs @@ -4,7 +4,7 @@ use crate::{ protocol, protocol::{Context, ContextOptions}, }; -use gix_error::{ResultExt, RetryableError}; +use gix_error::ResultExt; impl Default for Cascade { fn default() -> Self { @@ -155,7 +155,7 @@ impl Cascade { } } } - Err(err) if err.downcast_any_ref::().is_some() => continue, + Err(err) if err.is_retryable() => continue, Err(err) if action.context().is_some() => return Err(err), // communication errors are fatal when getting credentials Err(_) => {} // for other actions, ignore everything, try the operation } diff --git a/gix-credentials/tests/helper/invoke.rs b/gix-credentials/tests/helper/invoke.rs index 9b71cd3d34a..b92f8f96896 100644 --- a/gix-credentials/tests/helper/invoke.rs +++ b/gix-credentials/tests/helper/invoke.rs @@ -85,7 +85,7 @@ mod program { ) .expect_err("the builtin helper rejects a URL without a scheme"); assert!( - err.downcast_any_ref::().is_some(), + err.is_retryable(), "this failure indicates we could launch the helper, even though it wasn't happy which is fine. It doesn't like the URL" ); Ok(()) diff --git a/gix-credentials/tests/helper/mod.rs b/gix-credentials/tests/helper/mod.rs index 9593558abd1..efbafc2d8ab 100644 --- a/gix-credentials/tests/helper/mod.rs +++ b/gix-credentials/tests/helper/mod.rs @@ -19,7 +19,7 @@ mod invoke_outcome_to_helper_result { action, ) .unwrap_err(); - assert!(err.downcast_any_ref::().is_some()); + assert!(err.is_not_found()); } #[test] @@ -35,7 +35,7 @@ mod invoke_outcome_to_helper_result { let err = helper_outcome_to_result(None, helper::Action::Get(context)) .expect_err("Missing credentials must return an error even when the context is invalid"); assert!( - err.downcast_any_ref::().is_some(), + err.is_not_found(), "Invalid context must not replace the missing-credentials classification" ); } diff --git a/gix-credentials/tests/program/main.rs b/gix-credentials/tests/program/main.rs index 0b1f5cc4176..dd5b4f513d9 100644 --- a/gix-credentials/tests/program/main.rs +++ b/gix-credentials/tests/program/main.rs @@ -63,7 +63,7 @@ fn protocol_and_host_without_url_is_valid() { // This should fail because our mock helper returned None (no credentials found) // but it should NOT fail because of missing URL let err = result.expect_err("missing credentials must fail"); - assert!(err.downcast_any_ref::().is_some()); + assert!(err.is_not_found()); assert!( called, "The helper gets called, but as nothing is provided in the function it ultimately fails" @@ -88,7 +88,7 @@ fn missing_protocol_with_only_host_or_protocol_fails() { ); let err = result.expect_err("incomplete URL must fail validation"); - assert!(err.downcast_any_ref::().is_some()); + assert!(err.is_validation()); assert!(!called, "the context is lacking, hence nothing gets called"); } } @@ -117,6 +117,6 @@ fn url_alone_is_valid() { // This should fail because our mock helper returned None (no credentials found) // but it should NOT fail because of missing URL let err = result.expect_err("missing credentials must fail"); - assert!(err.downcast_any_ref::().is_some()); + assert!(err.is_not_found()); assert!(called); } diff --git a/gix-discover/tests/discover/is_git.rs b/gix-discover/tests/discover/is_git.rs index 92339e38809..a97785c9dd8 100644 --- a/gix-discover/tests/discover/is_git.rs +++ b/gix-discover/tests/discover/is_git.rs @@ -110,7 +110,7 @@ fn non_bare_repo_with_git_extension_is_not_a_worktree() -> crate::Result { "repo.git isn't a .git directory after all" ); assert!( - err.downcast_any_ref::().is_some(), + err.is_not_found(), "a missing repository marker is classified as not found" ); Ok(()) diff --git a/gix-discover/tests/discover/upwards/ceiling_dirs.rs b/gix-discover/tests/discover/upwards/ceiling_dirs.rs index 8c9eb9b5ac6..341aa88761f 100644 --- a/gix-discover/tests/discover/upwards/ceiling_dirs.rs +++ b/gix-discover/tests/discover/upwards/ceiling_dirs.rs @@ -67,7 +67,7 @@ fn discovery_fails_if_we_require_a_matching_ceiling_dir_but_are_standing_on_it() .unwrap_err(); assert!( - err.downcast_any_ref::().is_some(), + err.is_validation(), "since standing on the ceiling dir doesn't match it, we get exactly the semantically correct error" ); Ok(()) @@ -86,7 +86,7 @@ fn ceiling_dir_limits_are_respected_and_prevent_discovery() -> crate::Result { }, ) .expect_err("ceiling dir prevents discovery as it ends on level too early, and they are also absolutized"); - assert!(err.downcast_any_ref::().is_some()); + assert!(err.is_not_found()); assert!(err.to_string().contains("ceiling height of 5")); Ok(()) @@ -127,7 +127,7 @@ fn more_restrictive_ceiling_dirs_overrule_less_restrictive_ones() -> crate::Resu }, ) .expect_err("more restrictive ceiling dirs overrule less restrictive ones"); - assert!(err.downcast_any_ref::().is_some()); + assert!(err.is_not_found()); assert!(err.to_string().contains("ceiling height of 5")); Ok(()) @@ -170,7 +170,7 @@ fn no_matching_ceiling_dirs_errors_by_default() -> crate::Result { let err = res.expect_err("an unrelated ceiling directory cannot match"); assert!( - err.downcast_any_ref::().is_some(), + err.is_validation(), "the canonicalized ceiling dir doesn't have the same root as the git dir candidate, and can never match." ); Ok(()) @@ -220,7 +220,7 @@ fn ceiling_dirs_limit_the_physical_symlink_target() -> crate::Result { .expect_err("the physical ceiling prevents discovery of the repository above it"); assert!( - err.downcast_any_ref::().is_some(), + err.is_not_found(), "the symlink target matches the ceiling before traversal reaches the repository" ); Ok(()) diff --git a/gix-discover/tests/discover/upwards/mod.rs b/gix-discover/tests/discover/upwards/mod.rs index 615c8522abb..d767bc8e120 100644 --- a/gix-discover/tests/discover/upwards/mod.rs +++ b/gix-discover/tests/discover/upwards/mod.rs @@ -470,7 +470,7 @@ fn cross_fs() -> crate::Result { let res = gix_discover::upwards(&top_level_repo.path().join("remote")) .expect_err("the cross-fs option should prevent us from discovering the repo"); - assert!(res.downcast_any_ref::().is_some()); + assert!(res.is_not_found()); let (repo_path, _trust) = gix_discover::upwards_opts( &top_level_repo.path().join("remote"), diff --git a/gix-discover/tests/isolated.rs b/gix-discover/tests/isolated.rs index 6a1bde0f1ac..691ed92848e 100644 --- a/gix-discover/tests/isolated.rs +++ b/gix-discover/tests/isolated.rs @@ -146,9 +146,9 @@ fn upwards_with_relative_directories_and_optional_ceiling() -> gix_testtools::Re .unwrap_err(); if search_dir.parent() == Some(".".as_ref()) || search_dir.parent() == Some("".as_ref()) { - assert!(err.downcast_any_ref::().is_some()); + assert!(err.is_validation()); } else { - assert!(err.downcast_any_ref::().is_some()); + assert!(err.is_not_found()); } } diff --git a/gix-filter/tests/filter/pipeline/convert_to_worktree.rs b/gix-filter/tests/filter/pipeline/convert_to_worktree.rs index b89a669df81..f84a3caa926 100644 --- a/gix-filter/tests/filter/pipeline/convert_to_worktree.rs +++ b/gix-filter/tests/filter/pipeline/convert_to_worktree.rs @@ -225,9 +225,6 @@ fn unknown_encoding_can_be_an_error() -> gix_testtools::Result { .err() .expect("unknown encodings can be rejected explicitly"); assert_eq!(err.to_string(), "The encoding named 'not-an-encoding' isn't available"); - assert!( - err.downcast_any_ref::().is_some(), - "unknown encodings are validation failures" - ); + assert!(err.is_validation(), "unknown encodings are validation failures"); Ok(()) } diff --git a/gix-index/src/extension/tree/verify.rs b/gix-index/src/extension/tree/verify.rs index d1312dfcc26..6ee52e7fd3a 100644 --- a/gix-index/src/extension/tree/verify.rs +++ b/gix-index/src/extension/tree/verify.rs @@ -175,6 +175,6 @@ mod tests { let err = tree.verify(true, MalformedTree).expect_err("malformed entry must fail"); assert_eq!(err.to_string(), format!("Could not decode an entry in tree {root_id}")); - assert!(err.downcast_any_ref::().is_some()); + assert!(err.is_validation()); } } diff --git a/gix-index/tests/index/file/init.rs b/gix-index/tests/index/file/init.rs index bcc31e27c9b..07a0d09eeea 100644 --- a/gix-index/tests/index/file/init.rs +++ b/gix-index/tests/index/file/init.rs @@ -12,7 +12,7 @@ mod at { // Some platforms cannot memory-map an empty file and return an IO error instead. if len != 0 { assert!( - err.downcast_any_ref::().is_some(), + err.is_corrupted(), "expected a corrupt header for {object_hash:?}, {len} bytes, skip_hash={skip_hash}: {err}" ); } @@ -46,7 +46,7 @@ mod at_or_new { let err = gix_index::File::at_or_default(index_path, gix_testtools::object_hash(), false, Default::default()) .expect_err("a missing shared index must not produce an empty index"); - assert!(err.into_error().is_not_found(), "the missing-file cause is preserved"); + assert!(err.is_not_found(), "the missing-file cause is preserved"); Ok(()) } diff --git a/gix-index/tests/index/file/read.rs b/gix-index/tests/index/file/read.rs index 901d2550f49..3ef515c299d 100644 --- a/gix-index/tests/index/file/read.rs +++ b/gix-index/tests/index/file/read.rs @@ -690,7 +690,7 @@ fn v2_split_index() { fn v2_split_index_recursion_is_handled_gracefully() { let err = try_file("v2_split_index_recursive", false).expect_err("recursion fails gracefully"); assert_eq!(err.to_string(), "Shared index checksum mismatch"); - assert!(err.downcast_any_ref::().is_some()); + assert!(err.is_corrupted()); } #[test] diff --git a/gix-index/tests/index/init.rs b/gix-index/tests/index/init.rs index bbbd61d8a02..b7939d41307 100644 --- a/gix-index/tests/index/init.rs +++ b/gix-index/tests/index/init.rs @@ -47,8 +47,7 @@ fn from_tree_validation() -> crate::Result { let odb = odb_at(git_dir.join("objects"))?; let err = State::from_tree(&tree_id, &odb, Default::default()) - .unwrap_err() - .into_error(); + .expect_err("tree entries with path separators must fail validation"); assert!(err.is_validation(), "invalid path components are validation errors"); assert_eq!( err.probable_cause().to_string(), diff --git a/gix-lock/tests/lock/marker.rs b/gix-lock/tests/lock/marker.rs index 7bc164a656b..a262b62c8b7 100644 --- a/gix-lock/tests/lock/marker.rs +++ b/gix-lock/tests/lock/marker.rs @@ -12,10 +12,7 @@ mod acquire { assert!(guard.resource_path().ends_with("the-resource")); let err = gix_lock::Marker::acquire_to_hold_resource(resource, Fail::Immediately, None) .expect_err("the lock is taken and there is a failure obtaining it again"); - assert!( - err.downcast_any_ref::().is_some(), - "lock contention is retryable" - ); + assert!(err.is_retryable(), "lock contention is retryable"); let err_str = err.to_string(); assert!(err_str.contains("the-resource' could not be obtained immediately")); diff --git a/gix-merge/tests/merge/blob/pipeline.rs b/gix-merge/tests/merge/blob/pipeline.rs index 0520e90d755..94dad391115 100644 --- a/gix-merge/tests/merge/blob/pipeline.rs +++ b/gix-merge/tests/merge/blob/pipeline.rs @@ -60,10 +60,7 @@ fn without_transformation() -> crate::Result { err.to_string(), "Entry at 'link' must be regular file or symlink, but was Link" ); - assert!( - err.downcast_any_ref::().is_some(), - "an unsupported entry kind is a validation failure" - ); + assert!(err.is_validation(), "an unsupported entry kind is a validation failure"); assert_eq!( buf.len(), 9, @@ -301,7 +298,7 @@ fn non_existing() -> crate::Result { ) .unwrap_err(); assert!( - err.downcast_any_ref::().is_some(), + err.is_not_found(), "missing object database ids are always an error (even though missing objects on disk are allowed)" ); Ok(()) diff --git a/gix-merge/tests/merge/blob/platform.rs b/gix-merge/tests/merge/blob/platform.rs index a209c96197c..7ed870899fa 100644 --- a/gix-merge/tests/merge/blob/platform.rs +++ b/gix-merge/tests/merge/blob/platform.rs @@ -570,7 +570,7 @@ cat "%B" >> "%A""# .prepare_external_driver("bogus".into(), Default::default(), Default::default()) .unwrap_err(); assert!( - err.downcast_any_ref::().is_some(), + err.is_validation(), "however, for external drivers, resources can still be too much to handle, until we learn how to stream them" ); Ok(()) diff --git a/gix-note/tests/note.rs b/gix-note/tests/note.rs index b2936341f72..1b300a0bc0c 100644 --- a/gix-note/tests/note.rs +++ b/gix-note/tests/note.rs @@ -868,7 +868,6 @@ fn mutations_reject_mixed_hash_kinds() -> gix_testtools::Result { let err = one_shot::replace(root, sha256, sha1, &objects).expect_err("the annotated object has the wrong hash kind"); - let err = err.into_error(); assert!( err.is_validation(), "an annotated-object hash mismatch is a validation error" @@ -879,7 +878,6 @@ fn mutations_reject_mixed_hash_kinds() -> gix_testtools::Result { "replace reports an annotated-object hash mismatch" ); let err = one_shot::replace(root, sha1, sha256, &objects).expect_err("the note has the wrong hash kind"); - let err = err.into_error(); assert!(err.is_validation(), "a note hash mismatch is a validation error"); assert_eq!( err.probable_cause().to_string(), @@ -887,7 +885,6 @@ fn mutations_reject_mixed_hash_kinds() -> gix_testtools::Result { "replace reports a note hash mismatch" ); let err = one_shot::remove(root, sha256, &objects).expect_err("the annotated object has the wrong hash kind"); - let err = err.into_error(); assert!( err.is_validation(), "an annotated-object hash mismatch is a validation error" @@ -960,7 +957,6 @@ fn mutations_reject_duplicate_mappings_across_layouts() -> gix_testtools::Result let err = one_shot::replace(root, other, flat_note, &objects) .expect_err("ambiguous existing mappings cannot be rewritten losslessly"); - let err = err.into_error(); assert!(err.is_corrupted(), "duplicate mappings indicate a corrupt notes tree"); assert_eq!( err.probable_cause().to_string(), diff --git a/gix-object/src/signature/verify.rs b/gix-object/src/signature/verify.rs index 640507f28f7..69c9713555c 100644 --- a/gix-object/src/signature/verify.rs +++ b/gix-object/src/signature/verify.rs @@ -728,14 +728,14 @@ mod tests { .verify(BStr::new(b"not a signature"), options.clone()) .expect_err("the unsupported signature is rejected"); assert!( - unsupported.downcast_any_ref::().is_some(), + unsupported.is_corrupted(), "an unrecognized object signature is corrupt" ); let mismatch = signed .verify(BStr::new(b"-----BEGIN SSH SIGNATURE-----\n"), options) .expect_err("the mismatched signature is rejected"); assert!( - mismatch.downcast_any_ref::().is_some(), + mismatch.is_validation(), "a configured verifier which cannot handle the signature is invalid" ); let mismatch = mismatch.to_string(); diff --git a/gix-pack/tests/pack/bundle.rs b/gix-pack/tests/pack/bundle.rs index 445affea129..2197a4c2ee8 100644 --- a/gix-pack/tests/pack/bundle.rs +++ b/gix-pack/tests/pack/bundle.rs @@ -274,10 +274,7 @@ mod write_to_directory { }, ) .expect_err("a ref-delta without an in-pack or external base cannot be indexed"); - assert!( - err.downcast_any_ref::().is_some(), - "an unresolved base is classified as not found" - ); + assert!(err.is_not_found(), "an unresolved base is classified as not found"); let err = err.into_error(); let expected = format!("The ref-delta base object {base_id} could not be found"); assert!( diff --git a/gix-pack/tests/pack/data/file.rs b/gix-pack/tests/pack/data/file.rs index 9b22b6941c1..c265fdabf0f 100644 --- a/gix-pack/tests/pack/data/file.rs +++ b/gix-pack/tests/pack/data/file.rs @@ -291,12 +291,8 @@ mod decompress_entry { let err = p .decompress_entry(&entry, &mut Default::default(), &mut buf) .expect_err("an undersized caller-provided buffer is invalid input"); - assert!(err.downcast_any_ref::().is_some()); - assert!( - !err.into_error() - .classify() - .any(|classification| matches!(classification.class(), gix_error::Class::ResourceExhaustion(_))) - ); + assert!(err.is_validation()); + assert!(!err.is_resource_exhausted()); } fn decompress_entry_at_offset(offset: u64) -> Vec { diff --git a/gix-pack/tests/pack/data/input.rs b/gix-pack/tests/pack/data/input.rs index de1c7c12b9d..bb95650f98a 100644 --- a/gix-pack/tests/pack/data/input.rs +++ b/gix-pack/tests/pack/data/input.rs @@ -197,7 +197,7 @@ mod lookup_ref_delta_objects { let err = result.expect_err("zero and out-of-bounds base distances must be rejected"); assert!( - err.downcast_any_ref::().is_some(), + err.is_corrupted(), "zero and out-of-bounds base distances are rejected as corrupt pack data" ); assert!( 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 264cdb1115f..ef57b5d39cd 100644 --- a/gix-pack/tests/pack/data/output/count_and_entries.rs +++ b/gix-pack/tests/pack/data/output/count_and_entries.rs @@ -42,10 +42,7 @@ fn invalid_ofs_delta_base_distance_is_an_error() -> crate::Result { let err = result .and_then(Result::err) .expect("an invalid OFS_DELTA base distance must fail"); - assert!( - err.downcast_any_ref::().is_some(), - "an invalid packed delta is reported as corrupt" - ); + assert!(err.is_corrupted(), "an invalid packed delta is reported as corrupt"); } Ok(()) } diff --git a/gix-pack/tests/pack/multi_index/fuzzed.rs b/gix-pack/tests/pack/multi_index/fuzzed.rs index cd5f99c7e68..d988692231c 100644 --- a/gix-pack/tests/pack/multi_index/fuzzed.rs +++ b/gix-pack/tests/pack/multi_index/fuzzed.rs @@ -67,7 +67,7 @@ fn long_pack_names_over_alloc_limit_bytes_are_resource_exhaustion() { err.to_string().contains("more memory than allowed"), "the error explains the allocation-limit failure: {err}" ); - assert!(!err.into_error().is_corrupted()); + assert!(!err.is_corrupted()); } /// Reproducer for the fuzz target OOM case: the harness uses an allocation cap so attacker-controlled @@ -82,7 +82,7 @@ fn absurd_pack_count_is_rejected_with_fuzz_alloc_limit() { .err() .expect("an absurd pack count must be rejected under a zero allocation limit"); assert!( - err.downcast_any_ref::().is_some(), + err.is_corrupted(), "a pack count which cannot fit in the available name data is corrupt input" ); assert!( diff --git a/gix-protocol/tests/protocol/fetch/response.rs b/gix-protocol/tests/protocol/fetch/response.rs index 502fe48420c..be5f2439fdc 100644 --- a/gix-protocol/tests/protocol/fetch/response.rs +++ b/gix-protocol/tests/protocol/fetch/response.rs @@ -382,7 +382,6 @@ mod v2 { match fetch::Response::from_line_reader(Protocol::V2, &mut sidebands, true, true).await { Ok(_) => panic!("need error response"), Err(err) => { - let err = err.into_error(); let packetline_err = err .downcast_any_ref::() .unwrap_or_else(|| panic!("the remote ERR packet is retained in the error chain: {err:#?}")); diff --git a/gix-ref/src/store/file/transaction/prepare.rs b/gix-ref/src/store/file/transaction/prepare.rs index b80bde815d8..c5edaa30300 100644 --- a/gix-ref/src/store/file/transaction/prepare.rs +++ b/gix-ref/src/store/file/transaction/prepare.rs @@ -53,7 +53,7 @@ impl Transaction<'_, '_> { // This happens for path collisions where `a` is a ref file, and `a/b` is the lock to be created. fn lock_acquire_error(err: gix_error::Exn, full_name: &str) -> Error { match ( - err.downcast_any_ref::().is_some(), + err.is_retryable(), err.downcast_any_ref::().map(std::io::Error::kind), ) { (false, Some(kind)) => Error::Io(std::io::Error::new(kind, err.into_error())), diff --git a/gix-url/tests/url/expand_path.rs b/gix-url/tests/url/expand_path.rs index 11a409fc32d..8877ac5684f 100644 --- a/gix-url/tests/url/expand_path.rs +++ b/gix-url/tests/url/expand_path.rs @@ -51,7 +51,7 @@ fn missing_home_is_not_found() { let user = ForUser::Current; let err = expand_path::with(Some(&user), b"/repo".as_bstr(), |_| None).expect_err("home is missing"); assert!( - err.downcast_any_ref::().is_some(), + err.is_not_found(), "a missing home directory is classified as not found" ); } diff --git a/gix-zlib/src/stream/inflate.rs b/gix-zlib/src/stream/inflate.rs index 5016a353c75..77369c98a7c 100644 --- a/gix-zlib/src/stream/inflate.rs +++ b/gix-zlib/src/stream/inflate.rs @@ -38,7 +38,7 @@ pub fn read(rd: &mut impl BufRead, state: &mut Decompress, mut dst: &mut [u8]) - Ok(Status::Ok | Status::BufError) => unreachable!("Definitely a bug somewhere"), // Keep the underlying zlib error so callers can tell a checksum mismatch // (`incorrect data check`) apart from genuine stream corruption. - Err(err) if err.downcast_any_ref::().is_some() => { + Err(err) if err.is_resource_exhausted() => { return Err(io::Error::other(err.into_error())); } Err(err) => { diff --git a/gix-zlib/tests/zlib/inflate.rs b/gix-zlib/tests/zlib/inflate.rs index 6f0506c3e4b..2449b4a92a0 100644 --- a/gix-zlib/tests/zlib/inflate.rs +++ b/gix-zlib/tests/zlib/inflate.rs @@ -29,7 +29,7 @@ fn corrupt_streams_keep_classification_and_context() { assert_eq!(err, "Could not decode zip stream"); assert!( - err.downcast_any_ref::().is_some(), + err.is_corrupted(), "the underlying invalid stream should be classified as corruption" ); } diff --git a/gix/src/repository/branch.rs b/gix/src/repository/branch.rs index bc8722ba0ef..892be8cb7b4 100644 --- a/gix/src/repository/branch.rs +++ b/gix/src/repository/branch.rs @@ -117,13 +117,7 @@ impl crate::Repository { .or_raise(|| gix_error::message("Could not acquire the local configuration lock"))?; let mut config = match gix_config::File::from_path_no_includes(config_path.clone(), gix_config::Source::Local) { Ok(config) => Some(config), - Err(err) - if err - .downcast_any_ref::() - .is_some_and(|source| source.kind() == std::io::ErrorKind::NotFound) => - { - None - } + Err(err) if err.is_not_found() => None, Err(err) => { return Err(err .raise(gix_error::message("Could not read the local configuration")) diff --git a/gix/src/repository/graph.rs b/gix/src/repository/graph.rs index fa17a2793bc..4cbd3ec45e4 100644 --- a/gix/src/repository/graph.rs +++ b/gix/src/repository/graph.rs @@ -36,9 +36,12 @@ impl crate::Repository { .may_use_commit_graph()? .then(|| gix_commitgraph::at(self.objects.store_ref().path().join("info"))) .transpose() - .or_else(|err| match err.downcast_any_ref::() { - Some(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None), - _ => Err(err.into_error()), + .or_else(|err| { + if err.is_not_found() { + Ok(None) + } else { + Err(err.into_error()) + } }) } } diff --git a/gix/tests/gix-init.rs b/gix/tests/gix-init.rs index 56fc6ebc9ba..73f3597ace5 100644 --- a/gix/tests/gix-init.rs +++ b/gix/tests/gix-init.rs @@ -447,9 +447,7 @@ mod config_mut { let path = temp.path().join("missing/global.config"); let options = options_for(Source::System).system_config_path(&path); assert!( - gix::config_mut(Source::System, &options).is_err_and(|err| err - .downcast_any_ref::() - .is_some_and(|err| err.kind() == std::io::ErrorKind::NotFound)), + gix::config_mut(Source::System, &options).is_err_and(|err| err.is_not_found()), "the parent directory must already exist" ); assert!( diff --git a/gix/tests/gix/repository/config/config_snapshot/credential_helpers.rs b/gix/tests/gix/repository/config/config_snapshot/credential_helpers.rs index 21cb3d6b252..341e4caa238 100644 --- a/gix/tests/gix/repository/config/config_snapshot/credential_helpers.rs +++ b/gix/tests/gix/repository/config/config_snapshot/credential_helpers.rs @@ -268,7 +268,7 @@ fn core_askpass_interpolation_errors_are_not_ignored() -> crate::Result { .err() .expect("the isolated repository cannot resolve the home directory"); assert!( - gix_error::Error::from_error(err).is_not_found(), + err.is_not_found(), "the missing interpolation input remains available in the error chain" ); } diff --git a/gix/tests/gix/submodule.rs b/gix/tests/gix/submodule.rs index 96adb7b3081..b4b4a386292 100644 --- a/gix/tests/gix/submodule.rs +++ b/gix/tests/gix/submodule.rs @@ -42,7 +42,7 @@ mod open { "the failure is classified as invalid configuration: {err:?}" ); assert!( - err.downcast_any_ref::().is_some(), + err.is_not_found(), "the nested missing-path error must not hide the configuration failure" ); Ok(()) From ba2c7f2d138f26b0eef140c2440683a58f4e362f Mon Sep 17 00:00:00 2001 From: Codex Date: Tue, 15 Sep 2026 18:11:45 +0100 Subject: [PATCH 32/51] Use implicit error conversion at `gix` API boundaries rubberstamp The central `gix::Error` already implements the conversions used by `?`. Remove redundant `map_err(Exn::into_error)` and equivalent `Error::from` calls, including conversions inside context closures, while retaining the context itself and explicit conversions at returned-result boundaries. --- gix/src/clone/checkout.rs | 5 +- gix/src/clone/fetch/mod.rs | 13 ++--- gix/src/clone/mod.rs | 4 +- gix/src/config/cache/access.rs | 53 ++++++++----------- gix/src/config/cache/incubate.rs | 6 +-- gix/src/config/cache/init.rs | 19 +++---- gix/src/config/snapshot/access.rs | 11 ++-- gix/src/config/snapshot/credential_helpers.rs | 4 +- gix/src/discover.rs | 10 ++-- gix/src/filter.rs | 12 ++--- gix/src/init.rs | 8 +-- gix/src/object/tree/editor.rs | 4 +- gix/src/open/repository.rs | 20 +++---- gix/src/pathspec.rs | 5 +- gix/src/reference/iter.rs | 8 +-- gix/src/remote/connect.rs | 11 ++-- gix/src/remote/connection/fetch/config.rs | 4 +- .../remote/connection/fetch/receive_pack.rs | 3 +- gix/src/remote/connection/ref_map.rs | 26 ++++----- gix/src/repository/blame.rs | 3 +- gix/src/repository/config/mod.rs | 18 +++---- gix/src/repository/config/transport.rs | 39 ++++++-------- gix/src/repository/dirwalk.rs | 11 ++-- gix/src/repository/index.rs | 12 ++--- gix/src/repository/location.rs | 12 ++--- gix/src/repository/merge.rs | 9 ++-- gix/src/repository/object.rs | 18 +++---- gix/src/repository/pathspec.rs | 6 +-- gix/src/repository/reference.rs | 14 ++--- gix/src/repository/submodule.rs | 11 ++-- gix/src/repository/worktree.rs | 2 +- gix/src/revision/walk.rs | 6 +-- gix/src/status/index_worktree.rs | 6 +-- gix/src/status/iter/mod.rs | 8 +-- gix/src/status/tree_index.rs | 3 +- gix/src/submodule/mod.rs | 43 ++++++--------- gix/src/worktree/mod.rs | 3 +- 37 files changed, 175 insertions(+), 275 deletions(-) diff --git a/gix/src/clone/checkout.rs b/gix/src/clone/checkout.rs index f3e281d68ba..26a64cb4cfe 100644 --- a/gix/src/clone/checkout.rs +++ b/gix/src/clone/checkout.rs @@ -130,12 +130,11 @@ pub mod main_worktree { &bytes, should_interrupt, opts, - ) - .map_err(gix_error::Exn::into_error)?; + )?; files.show_throughput(start); bytes.show_throughput(start); - index.write(Default::default()).map_err(gix_error::Error::from)?; + index.write(Default::default())?; Ok((self.repo.take().expect("still present").clone(), outcome)) } } diff --git a/gix/src/clone/fetch/mod.rs b/gix/src/clone/fetch/mod.rs index d285d55e429..46ae5d269c7 100644 --- a/gix/src/clone/fetch/mod.rs +++ b/gix/src/clone/fetch/mod.rs @@ -318,14 +318,11 @@ impl PrepareFetch { // Maybe make API changes available as overlay, just as utility over // Api sections. resolved_config - .append( - gix_config::File::from_bytes_owned( - &mut in_memory_config, - gix_config::file::Metadata::api(), - Default::default(), - ) - .map_err(gix_error::Exn::into_error)?, - ) + .append(gix_config::File::from_bytes_owned( + &mut in_memory_config, + gix_config::file::Metadata::api(), + Default::default(), + )?) .or_raise(|| { gix_error::message( "Failed to transfer in-memory configuration after adopting the remote's object format", diff --git a/gix/src/clone/mod.rs b/gix/src/clone/mod.rs index 904b1772e5f..23b68c3c9e5 100644 --- a/gix/src/clone/mod.rs +++ b/gix/src/clone/mod.rs @@ -147,10 +147,10 @@ impl PrepareFetch { let mut repo = crate::ThreadSafeRepository::init_opts(path, kind, create_opts, open_opts)?.to_thread_local(); url.canonicalize(repo.options.current_dir_or_empty()).map_err(|err| { - gix_error::Error::from(err.raise(gix_error::message!( + err.raise(gix_error::message!( "Failed to turn the relative file url {:?} into an absolute one", url.to_bstring() - ))) + )) })?; repo.committer_or_set_generic_fallback()?; Ok(PrepareFetch { diff --git a/gix/src/config/cache/access.rs b/gix/src/config/cache/access.rs index 90978926861..7ff1c36c4c3 100644 --- a/gix/src/config/cache/access.rs +++ b/gix/src/config/cache/access.rs @@ -72,10 +72,10 @@ impl Cache { .try_into_binary(binary) .with_leniency(self.lenient_config) .map_err(|err| { - gix_error::Error::from(err.and_raise(gix_error::message!( + err.and_raise(gix_error::message!( "Failed to parse value of 'diff.{}.binary'", driver.name - ))) + )) })?; } if let Some(command) = section.value(config::tree::Diff::DRIVER_COMMAND.name) { @@ -95,10 +95,10 @@ impl Cache { }) .with_lenient_default(self.lenient_config) .map_err(|err| { - gix_error::Error::from(err.and_raise(gix_error::message!( + err.and_raise(gix_error::message!( "Failed to parse value of 'diff.{}.algorithm'", driver.name - ))) + )) })? .into(); } @@ -145,15 +145,15 @@ impl Cache { #[cfg(feature = "merge")] pub(crate) fn merge_pipeline_options(&self) -> Result { Ok(gix_merge::blob::pipeline::Options { - large_file_threshold_bytes: self.big_file_threshold().map_err(gix_error::Error::from)?, + large_file_threshold_bytes: self.big_file_threshold()?, }) } #[cfg(feature = "blob-diff")] pub(crate) fn diff_pipeline_options(&self) -> Result { Ok(gix_diff::blob::pipeline::Options { - large_file_threshold_bytes: self.big_file_threshold().map_err(gix_error::Error::from)?, - fs: self.fs_capabilities().map_err(gix_error::Error::from)?, + large_file_threshold_bytes: self.big_file_threshold()?, + fs: self.fs_capabilities()?, }) } @@ -293,19 +293,16 @@ impl Cache { pub(crate) fn stat_options(&self) -> Result { use crate::config::tree::gitoxide; Ok(gix_index::entry::stat::Options { - trust_ctime: boolean(self, "core.trustCTime", &Core::TRUST_C_TIME, true).map_err(gix_error::Error::from)?, - use_nsec: boolean(self, "gitoxide.core.useNsec", &gitoxide::Core::USE_NSEC, false) - .map_err(gix_error::Error::from)?, - use_stdev: boolean(self, "gitoxide.core.useStdev", &gitoxide::Core::USE_STDEV, false) - .map_err(gix_error::Error::from)?, + trust_ctime: boolean(self, "core.trustCTime", &Core::TRUST_C_TIME, true)?, + use_nsec: boolean(self, "gitoxide.core.useNsec", &gitoxide::Core::USE_NSEC, false)?, + use_stdev: boolean(self, "gitoxide.core.useStdev", &gitoxide::Core::USE_STDEV, false)?, check_stat: self .apply_leniency( self.resolved .string(Core::CHECK_STAT) .map(|v| Core::CHECK_STAT.try_into_checkstat(v)) .transpose(), - ) - .map_err(gix_error::Error::from)? + )? .unwrap_or(true), }) } @@ -341,15 +338,13 @@ impl Cache { ) -> Result { use crate::config::tree::gitoxide; let git_dir = repo.git_dir(); - let thread_limit = self - .apply_leniency( - crate::config::tree::Checkout::WORKERS.try_from_workers( - self.resolved - .integer_filter("checkout.workers", &mut self.filter_config_section.clone()), - ), - ) - .map_err(gix_error::Error::from)?; - let capabilities = self.fs_capabilities().map_err(gix_error::Error::from)?; + let thread_limit = self.apply_leniency( + crate::config::tree::Checkout::WORKERS.try_from_workers( + self.resolved + .integer_filter("checkout.workers", &mut self.filter_config_section.clone()), + ), + )?; + let capabilities = self.fs_capabilities()?; let filters = { let mut filters = gix_filter::Pipeline::new(repo.command_context()?, crate::filter::Pipeline::options(repo)?); @@ -365,16 +360,14 @@ impl Cache { "gitoxide.core.filterProcessDelay", &gitoxide::Core::FILTER_PROCESS_DELAY, true, - ) - .map_err(gix_error::Error::from)? - { + )? { gix_filter::driver::apply::Delay::Allow } else { gix_filter::driver::apply::Delay::Forbid }; Ok(gix_worktree_state::checkout::Options { filter_process_delay, - validate: self.protect_options().map_err(gix_error::Error::from)?, + validate: self.protect_options()?, filters, attributes: self .assemble_attribute_globals(git_dir, attributes_source, self.attributes)? @@ -409,14 +402,14 @@ impl Cache { buf: &mut Vec, ) -> Result { let excludes_file = match self.excludes_file().map_err(|err| { - gix_error::Error::from(err.raise(gix_error::message( + err.raise(gix_error::message( "The value for `core.excludesFile` could not be read from configuration", - ))) + )) })? { Some(user_path) => Some(user_path), None => self.xdg_config_path("ignore").or_erased()?, }; - let parse_ignore = self.ignore_pattern_parser().map_err(gix_error::Error::from)?; + let parse_ignore = self.ignore_pattern_parser()?; Ok(gix_worktree::stack::state::Ignore::new( overrides.unwrap_or_default(), gix_ignore::Search::from_git_dir(git_dir, excludes_file, buf, parse_ignore) diff --git a/gix/src/config/cache/incubate.rs b/gix/src/config/cache/incubate.rs index d87b4b98c2e..eac6e82b4fc 100644 --- a/gix/src/config/cache/incubate.rs +++ b/gix/src/config/cache/incubate.rs @@ -80,8 +80,7 @@ impl StageOne { } let precompose_unicode = Core::PRECOMPOSE_UNICODE .enrich_error(config.boolean(Core::PRECOMPOSE_UNICODE)) - .with_leniency(lenient) - .map_err(gix_error::Error::from)? + .with_leniency(lenient)? .unwrap_or_default(); const IS_WINDOWS: bool = cfg!(windows); @@ -171,8 +170,7 @@ fn load_config( includes: gix_config::file::includes::Options::no_follow(), ..util::base_options(lossy, lenient) }, - ) - .map_err(gix_error::Exn::into_error)?; + )?; Ok(config) } diff --git a/gix/src/config/cache/init.rs b/gix/src/config/cache/init.rs index 2cf5b615422..545d09c08af 100644 --- a/gix/src/config/cache/init.rs +++ b/gix/src/config/cache/init.rs @@ -324,41 +324,36 @@ pub(crate) fn load( includes: gix_config::file::includes::Options::no_follow(), ..options }, - ) - .map_err(gix_error::Exn::into_error)? + )? .unwrap_or_default(); let local_meta = git_dir_config.as_ref().map(gix_config::File::meta_owned); if let Some(git_dir_config) = git_dir_config { globals.append(git_dir_config).or_erased()?; } - globals.resolve_includes(options).map_err(gix_error::Exn::into_error)?; + globals.resolve_includes(options)?; if use_env { globals - .append( - gix_config::File::from_env(options) - .map_err(gix_error::Exn::into_error)? - .unwrap_or_default(), - ) + .append(gix_config::File::from_env(options)?.unwrap_or_default()) .or_erased()?; } if !cli_config_overrides.is_empty() { config::overrides::append(&mut globals, cli_config_overrides, gix_config::Source::Cli, |_| None).map_err( |err| { - gix_error::Error::from(err.and_raise(gix_error::message!( + err.and_raise(gix_error::message!( "{:?} configuration overrides at open or init time could not be applied.", gix_config::Source::Cli - ))) + )) }, )?; } if !api_config_overrides.is_empty() { config::overrides::append(&mut globals, api_config_overrides, gix_config::Source::Api, |_| None).map_err( |err| { - gix_error::Error::from(err.and_raise(gix_error::message!( + err.and_raise(gix_error::message!( "{:?} configuration overrides at open or init time could not be applied.", gix_config::Source::Api - ))) + )) }, )?; } diff --git a/gix/src/config/snapshot/access.rs b/gix/src/config/snapshot/access.rs index 865932063a3..de15ed0f364 100644 --- a/gix/src/config/snapshot/access.rs +++ b/gix/src/config/snapshot/access.rs @@ -128,12 +128,8 @@ impl<'repo> SnapshotMut<'repo> { let current = match section.parent() { Some(parent) => self .config - .set_raw_value_by(parent.name(), section.name(), key.name(), value) - .map_err(gix_error::Exn::into_error)?, - None => self - .config - .set_raw_value_by(section.name(), None, key.name(), value) - .map_err(gix_error::Exn::into_error)?, + .set_raw_value_by(parent.name(), section.name(), key.name(), value)?, + None => self.config.set_raw_value_by(section.name(), None, key.name(), value)?, }; Ok(current) } @@ -161,8 +157,7 @@ impl<'repo> SnapshotMut<'repo> { .expect("statically known keys can always be parsed"); let current = self .config - .set_raw_value_by(key.section_name, key.subsection_name, key.value_name, value) - .map_err(gix_error::Exn::into_error)?; + .set_raw_value_by(key.section_name, key.subsection_name, key.value_name, value)?; Ok(current) } diff --git a/gix/src/config/snapshot/credential_helpers.rs b/gix/src/config/snapshot/credential_helpers.rs index 8f6f42dd1f9..6d3df072e57 100644 --- a/gix/src/config/snapshot/credential_helpers.rs +++ b/gix/src/config/snapshot/credential_helpers.rs @@ -149,10 +149,10 @@ pub(super) mod function { .map(|val| { gix_config::Boolean::try_from(val) .map_err(|err| { - gix_error::Error::from(err.raise(gix_error::ValidationError::new(format!( + err.raise(gix_error::ValidationError::new(format!( "Could not parse 'useHttpPath' key in section {}", section.header().to_bstring() - )))) + ))) }) .map(|b| b.0) }) diff --git a/gix/src/discover.rs b/gix/src/discover.rs index 387099601d5..960123c1e8b 100644 --- a/gix/src/discover.rs +++ b/gix/src/discover.rs @@ -28,15 +28,15 @@ impl ThreadSafeRepository { trust_map: gix_sec::trust::Mapping, ) -> Result { let _span = gix_trace::coarse!("ThreadSafeRepository::discover()"); - let (path, trust) = upwards_opts(directory.as_ref(), options).map_err(gix_error::Exn::into_error)?; + let (path, trust) = upwards_opts(directory.as_ref(), options)?; let (git_dir, worktree_dir) = path.into_repository_and_work_tree_directories(); let mut options = trust_map.into_value_by_level(trust); options.git_dir_trust = trust.into(); // Note that we will adjust the `current_dir` later so it matches the value of `core.precomposeUnicode`. - options.current_dir = Some(gix_fs::current_dir(false).map_err(|err| { - err.and_raise(message("Could not obtain the current working directory")) - .into_error() - })?); + options.current_dir = Some( + gix_fs::current_dir(false) + .map_err(|err| err.and_raise(message("Could not obtain the current working directory")))?, + ); Self::open_from_paths(git_dir, worktree_dir, options, None) } diff --git a/gix/src/filter.rs b/gix/src/filter.rs index 61d2bac061e..fd33b4340d3 100644 --- a/gix/src/filter.rs +++ b/gix/src/filter.rs @@ -182,10 +182,10 @@ impl Pipeline<'_> { }; let (id, kind) = if md.is_symlink() { let target = std::fs::read_link(&path).map_err(|source| { - gix_error::Error::from(source.and_raise(gix_error::message!( + source.and_raise(gix_error::message!( "Failed to perform IO for object creation for '{}'", path.display() - ))) + )) })?; let id = repo.write_blob(gix_path::into_bstr(target).as_ref()).or_erased()?; (id, gix_object::tree::EntryKind::Link) @@ -193,10 +193,10 @@ impl Pipeline<'_> { use gix_filter::pipeline::convert::ToGitOutcome; let file = std::fs::File::open(&path).map_err(|source| { - gix_error::Error::from(source.and_raise(gix_error::message!( + source.and_raise(gix_error::message!( "Failed to perform IO for object creation for '{}'", path.display() - ))) + )) })?; let file_for_git = self.convert_to_git(file, rela_path_as_path.as_ref(), index)?; let id = match file_for_git { @@ -275,9 +275,9 @@ fn extract_drivers(repo: &Repository) -> Result, crate:: if let Some(value) = section.value("required") { driver.required = gix_config::Boolean::try_from(BStr::new(&value)) .map_err(|err| { - gix_error::Error::from(err.raise(gix_error::message!( + err.raise(gix_error::message!( "Could not interpret 'filter.{name}.required' configuration" - ))) + )) })? .into(); } diff --git a/gix/src/init.rs b/gix/src/init.rs index 9188aceafa7..f4c03543c1a 100644 --- a/gix/src/init.rs +++ b/gix/src/init.rs @@ -69,16 +69,16 @@ impl ThreadSafeRepository { let sym_ref: FullName = Category::LocalBranch .to_full_name(configured_branch_name.as_bstr()) .map_err(|err| { - gix_error::Error::from(err.and_raise(gix_error::ValidationError::new_with_input( + err.and_raise(gix_error::ValidationError::new_with_input( "Invalid default branch name", configured_branch_name.clone(), - ))) + )) })?; gix_validate::reference::branch_name(sym_ref.as_bstr()).map_err(|err| { - gix_error::Error::from(err.and_raise(gix_error::ValidationError::new_with_input( + err.and_raise(gix_error::ValidationError::new_with_input( "Invalid default branch name", configured_branch_name, - ))) + )) })?; let mut repo = repo.to_thread_local(); let prev_write_reflog = repo.refs.write_reflog; diff --git a/gix/src/object/tree/editor.rs b/gix/src/object/tree/editor.rs index 5bccd607308..985d408cf50 100644 --- a/gix/src/object/tree/editor.rs +++ b/gix/src/object/tree/editor.rs @@ -265,12 +265,12 @@ fn write_cursor<'repo>(cursor: &mut Cursor<'_, 'repo>) -> Result, crat cursor.validate, ) .map_err(|err| { - gix_error::Error::from(err.and_raise(gix_error::message!( + err.and_raise(gix_error::message!( "The object {} ({}) has an invalid filename: '{}'", entry.oid, kind.as_octal_str(), entry.filename - ))) + )) })?; if !entry.mode.is_commit() && !cursor.repo.has_object(entry.oid) { return Err(gix_error::Error::from_error(gix_error::message!( diff --git a/gix/src/open/repository.rs b/gix/src/open/repository.rs index 17f4e9a0ebf..9016f6a1b87 100644 --- a/gix/src/open/repository.rs +++ b/gix/src/open/repository.rs @@ -208,9 +208,9 @@ impl ThreadSafeRepository { ) .map_err(|err| { use gix_error::ErrorExt; - gix_error::Error::from(err.and_raise(gix_error::CorruptionError::new( + err.and_raise(gix_error::CorruptionError::new( "Repository configuration could not be loaded", - ))) + )) })?; if repo_config.precompose_unicode { @@ -332,11 +332,7 @@ impl ThreadSafeRepository { .boolean_filter(Core::WORKTREE, |section| { is_eligible_worktree_config_section(section, &git_dir, current_dir, &mut filter_config_section) }) - .map_err(|err| { - gix_error::Error::from( - config::key::GenericErrorWithValue::from(&Core::WORKTREE).with_source(err.into_error()), - ) - })? + .map_err(|err| config::key::GenericErrorWithValue::from(&Core::WORKTREE).with_source(err.into_error()))? .is_some() { return Err(gix_error::Error::from(config::key::GenericErrorWithValue::< @@ -440,10 +436,10 @@ impl ThreadSafeRepository { { Some(value) => { gitoxide::Core::INDEX_FILE.validate(value.as_bstr()).map_err(|_| { - gix_error::Error::from(config::key::GenericErrorWithValue::::from_value( + config::key::GenericErrorWithValue::::from_value( &gitoxide::Core::INDEX_FILE, value.clone(), - )) + ) })?; gix_path::from_bstr(value).into_owned() } @@ -481,7 +477,7 @@ impl ThreadSafeRepository { let replacements = match prefix { Some(prefix) => { - let prefix: &RelativePath = prefix.as_bstr().try_into().map_err(gix_error::Exn::into_error)?; + let prefix: &RelativePath = prefix.as_bstr().try_into()?; Some(prefix).and_then(|prefix| { let _span = gix_trace::detail!("find replacement objects"); @@ -609,9 +605,7 @@ fn replacement_objects_refs_prefix( lenient: bool, mut filter_config_section: fn(&gix_config::file::Metadata) -> bool, ) -> Result, crate::Error> { - let is_disabled = config::shared::is_replace_refs_enabled(config, lenient, filter_config_section) - .map_err(gix_error::Error::from)? - .unwrap_or(true); + let is_disabled = config::shared::is_replace_refs_enabled(config, lenient, filter_config_section)?.unwrap_or(true); if is_disabled { return Ok(None); diff --git a/gix/src/pathspec.rs b/gix/src/pathspec.rs index e828a638404..3065de1d413 100644 --- a/gix/src/pathspec.rs +++ b/gix/src/pathspec.rs @@ -35,7 +35,7 @@ impl<'repo> Pathspec<'repo> { let prefix = if patterns.is_empty() && !empty_patterns_match_prefix { None } else { - repo.prefix().map_err(gix_error::Exn::into_error)? + repo.prefix()? }; let search = Search::from_specs( patterns, @@ -44,8 +44,7 @@ impl<'repo> Pathspec<'repo> { repo.workdir().unwrap_or_else(|| repo.git_dir()), repo.options.current_dir_or_empty(), gix_path::realpath::MAX_SYMLINKS, - ) - .map_err(gix_error::Exn::into_error)?, + )?, ) .or_raise(|| { gix_error::message( diff --git a/gix/src/reference/iter.rs b/gix/src/reference/iter.rs index 2e5d1444956..a526fe4ae80 100644 --- a/gix/src/reference/iter.rs +++ b/gix/src/reference/iter.rs @@ -50,7 +50,7 @@ impl<'repo> Platform<'repo> { &self, prefix: impl TryInto<&'a RelativePath, Error = gix_error::Exn>, ) -> Result, crate::Error> { - let prefix = prefix.try_into().map_err(gix_error::Exn::into_error)?; + let prefix = prefix.try_into()?; Ok(Iter::new(self.repo, self.platform.prefixed(prefix).or_erased()?)) } @@ -58,7 +58,7 @@ impl<'repo> Platform<'repo> { /// /// They are all prefixed with `refs/tags`. pub fn tags(&self) -> Result, crate::Error> { - let prefix = b"refs/tags/".try_into().map_err(gix_error::Exn::into_error)?; + let prefix = b"refs/tags/".try_into()?; Ok(Iter::new(self.repo, self.platform.prefixed(prefix).or_erased()?)) } @@ -67,7 +67,7 @@ impl<'repo> Platform<'repo> { /// /// They are all prefixed with `refs/heads`. pub fn local_branches(&self) -> Result, crate::Error> { - let prefix = b"refs/heads/".try_into().map_err(gix_error::Exn::into_error)?; + let prefix = b"refs/heads/".try_into()?; Ok(Iter::new(self.repo, self.platform.prefixed(prefix).or_erased()?)) } @@ -82,7 +82,7 @@ impl<'repo> Platform<'repo> { /// /// They are all prefixed with `refs/remotes`. pub fn remote_branches(&self) -> Result, crate::Error> { - let prefix = b"refs/remotes/".try_into().map_err(gix_error::Exn::into_error)?; + let prefix = b"refs/remotes/".try_into()?; Ok(Iter::new(self.repo, self.platform.prefixed(prefix).or_erased()?)) } } diff --git a/gix/src/remote/connect.rs b/gix/src/remote/connect.rs index 14f6bebc41f..e285f7a8c23 100644 --- a/gix/src/remote/connect.rs +++ b/gix/src/remote/connect.rs @@ -61,8 +61,7 @@ impl<'repo> Remote<'repo> { trace: self.repo.config.trace_packet(), }, ) - .await - .map_err(gix_error::Error::from)?; + .await?; Ok(self.to_connection_with_transport(transport)) } @@ -82,10 +81,10 @@ impl<'repo> Remote<'repo> { gix_discover::is_git(dir.as_ref()) }) .map_err(|err| { - gix_error::Error::from(err.raise(gix_error::message!( + err.raise(gix_error::message!( "Could not verify that {:?} is a valid git directory before attempting to use it", url.to_bstring() - ))) + )) })?; let (git_dir, _work_dir) = gix_discover::repository::Path::from_dot_git_dir( dir.clone().into_owned(), @@ -110,9 +109,9 @@ impl<'repo> Remote<'repo> { let version = crate::config::tree::Protocol::VERSION .try_into_protocol_version(self.repo.config.resolved.integer(Protocol::VERSION)) .map_err(|err| { - gix_error::Error::from(err.and_raise(gix_error::ValidationError::new( + err.and_raise(gix_error::ValidationError::new( "The given protocol version was invalid. Choose between 1 and 2", - ))) + )) })?; let url = self diff --git a/gix/src/remote/connection/fetch/config.rs b/gix/src/remote/connection/fetch/config.rs index cf6793f37ef..f235c79afb8 100644 --- a/gix/src/remote/connection/fetch/config.rs +++ b/gix/src/remote/connection/fetch/config.rs @@ -24,9 +24,9 @@ pub fn pack_index_version(repo: &Repository) -> Result Result { Ok(gix_index::File::from_state( - gix_index::State::from_tree( - tree, - self, - self.config.protect_options().map_err(gix_error::Error::from)?, - ) - .or_raise(|| gix_error::message!("Could not create index from tree at {tree}")) - .map_err(gix_error::Error::from)?, + gix_index::State::from_tree(tree, self, self.config.protect_options()?) + .or_raise(|| gix_error::message!("Could not create index from tree at {tree}"))?, self.index_path(), )) } diff --git a/gix/src/repository/location.rs b/gix/src/repository/location.rs index 09393ae7c25..6cb1bf5e18b 100644 --- a/gix/src/repository/location.rs +++ b/gix/src/repository/location.rs @@ -136,14 +136,12 @@ impl crate::Repository { self.workdir().unwrap_or_else(|| self.git_dir()), self.current_dir(), MAX_SYMLINKS, - ) - .map_err(gix_error::Exn::into_error)?; + )?; let absolute = path.into_owned(); let relative = if let Ok(relative) = absolute.strip_prefix(&root) { relative.to_owned() } else { - gix_path::realpath_opts(&absolute, self.current_dir(), MAX_SYMLINKS) - .map_err(gix_error::Exn::into_error)? + gix_path::realpath_opts(&absolute, self.current_dir(), MAX_SYMLINKS)? .strip_prefix(&root) .map_err(|_| { gix_error::Error::from_error(gix_error::ValidationError::new(format!( @@ -155,11 +153,7 @@ impl crate::Repository { .to_owned() }; Cow::Owned(relative) - } else if let Some(prefix) = self - .prefix() - .map_err(gix_error::Exn::into_error)? - .filter(|prefix| !prefix.as_os_str().is_empty()) - { + } else if let Some(prefix) = self.prefix()?.filter(|prefix| !prefix.as_os_str().is_empty()) { Cow::Owned(prefix.join(path.as_ref())) } else { path diff --git a/gix/src/repository/merge.rs b/gix/src/repository/merge.rs index 9e1a82917cb..d217e4412c0 100644 --- a/gix/src/repository/merge.rs +++ b/gix/src/repository/merge.rs @@ -149,8 +149,7 @@ impl Repository { &mut diff_cache, &mut blob_merge, options.into(), - ) - .map_err(gix_error::Exn::into_error)?; + )?; let validate = self.config.protect_options().or_erased()?; Ok(crate::merge::tree::Outcome { @@ -211,8 +210,7 @@ impl Repository { self, &mut |id| id.to_owned().attach(self).shorten_or_id().to_string(), options.into(), - ) - .map_err(gix_error::Error::from)?; + )?; let validate = self.config.protect_options().or_erased()?; let tree_merge = crate::merge::tree::Outcome { @@ -290,8 +288,7 @@ impl Repository { self, &mut |id| id.to_owned().attach(self).shorten_or_id().to_string(), options.into(), - ) - .map_err(gix_error::Error::from)?; + )?; Ok(crate::merge::virtual_merge_base::Outcome { virtual_merge_bases: virtual_merge_bases.into_iter().map(|id| id.attach(self)).collect(), diff --git a/gix/src/repository/object.rs b/gix/src/repository/object.rs index 37feabee8f9..75610c738c7 100644 --- a/gix/src/repository/object.rs +++ b/gix/src/repository/object.rs @@ -466,18 +466,12 @@ impl crate::Repository { tree: impl Into, parents: impl IntoIterator>, ) -> Result, crate::Error> { - let author = self - .author() - .ok_or_else(|| { - gix_error::Error::from_error(gix_error::ValidationError::new("Author identity is not configured")) - })? - .map_err(gix_error::Error::from)?; - let committer = self - .committer() - .ok_or_else(|| { - gix_error::Error::from_error(gix_error::ValidationError::new("Committer identity is not configured")) - })? - .map_err(gix_error::Error::from)?; + let author = self.author().ok_or_else(|| { + gix_error::Error::from_error(gix_error::ValidationError::new("Author identity is not configured")) + })??; + let committer = self.committer().ok_or_else(|| { + gix_error::Error::from_error(gix_error::ValidationError::new("Committer identity is not configured")) + })??; self.new_commit_as(committer, author, message, tree, parents) } diff --git a/gix/src/repository/pathspec.rs b/gix/src/repository/pathspec.rs index 230c7130b2b..61b09b384c7 100644 --- a/gix/src/repository/pathspec.rs +++ b/gix/src/repository/pathspec.rs @@ -44,16 +44,16 @@ impl Repository { &self, inherit_ignore_case: bool, ) -> Result { - let mut defaults = self.config.pathspec_defaults().map_err(gix_error::Exn::into_error)?; + let mut defaults = self.config.pathspec_defaults()?; if inherit_ignore_case && self .config .fs_capabilities() .with_lenient_default(self.config.lenient_config) .map_err(|err| { - gix_error::Error::from(err.and_raise(gix_error::message( + err.and_raise(gix_error::message( "Filesystem configuration could not be obtained to learn about case sensitivity", - ))) + )) })? .ignore_case { diff --git a/gix/src/repository/reference.rs b/gix/src/repository/reference.rs index fc82c968d71..641614d151f 100644 --- a/gix/src/repository/reference.rs +++ b/gix/src/repository/reference.rs @@ -22,9 +22,7 @@ impl crate::Repository { let id = target.into(); let name = format!("refs/tags/{}", name.as_ref()).try_into().map_err( |err: gix_validate::reference::name::Error| { - gix_error::Error::from( - err.and_raise(gix_error::ValidationError::new("The tag reference name is invalid")), - ) + err.and_raise(gix_error::ValidationError::new("The tag reference name is invalid")) }, )?; let mut edits = self.edit_reference(RefEdit::update(name, id, constraint, ""))?; @@ -90,11 +88,7 @@ impl crate::Repository { self.reference_inner( name.try_into() .map_err(gix_validate::reference::name::Error::from) - .map_err(|err| { - gix_error::Error::from( - err.and_raise(gix_error::ValidationError::new("The reference name is invalid")), - ) - })?, + .map_err(|err| err.and_raise(gix_error::ValidationError::new("The reference name is invalid")))?, target.into(), constraint, log_message.into(), @@ -151,10 +145,10 @@ impl crate::Repository { committer: Option>, ) -> Result, crate::Error> { let (file_lock_fail, packed_refs_lock_fail) = self.config.lock_timeout().map_err(|err| { - gix_error::Error::from(err.and_raise(gix_error::message( + err.and_raise(gix_error::message( "Could not interpret core.filesRefLockTimeout or core.packedRefsTimeout, it must be the number in \ milliseconds to wait for locks or negative to wait forever", - ))) + )) })?; Ok(self .refs diff --git a/gix/src/repository/submodule.rs b/gix/src/repository/submodule.rs index 1aea20ac686..bd5db159efa 100644 --- a/gix/src/repository/submodule.rs +++ b/gix/src/repository/submodule.rs @@ -28,9 +28,11 @@ impl Repository { return Ok(None); } let buf = std::fs::read(&path).or_raise(|| gix_error::message("Could not read '.gitmodules' file"))?; - Ok(Some( - gix_submodule::File::from_bytes(&buf, path, &self.config.resolved).map_err(gix_error::Exn::into_error)?, - )) + Ok(Some(gix_submodule::File::from_bytes( + &buf, + path, + &self.config.resolved, + )?)) } /// Return a shared [`.gitmodules` file](submodule::File) which is updated automatically if the in-memory snapshot @@ -93,8 +95,7 @@ impl Repository { .data, None, &self.config.resolved, - ) - .map_err(gix_error::Exn::into_error)? + )? .into(), ))) } diff --git a/gix/src/repository/worktree.rs b/gix/src/repository/worktree.rs index ad21647ccc2..333f57d10c0 100644 --- a/gix/src/repository/worktree.rs +++ b/gix/src/repository/worktree.rs @@ -104,7 +104,7 @@ impl crate::Repository { ) -> Result<(gix_worktree_stream::Stream, gix_index::File), crate::Error> { use gix_odb::HeaderExt; let id = id.into(); - let header = self.objects.header(id).map_err(gix_error::Exn::into_error)?; + let header = self.objects.header(id)?; if !header.kind().is_tree() { return Err(gix_error::Error::from_error(gix_error::ValidationError::new(format!( "Needed {id} to be a tree to turn into a workspace stream, got {}", diff --git a/gix/src/revision/walk.rs b/gix/src/revision/walk.rs index 7964681efd4..8594dbff5d9 100644 --- a/gix/src/revision/walk.rs +++ b/gix/src/revision/walk.rs @@ -310,8 +310,7 @@ impl<'repo> Platform<'repo> { } } }) - .sorting(sorting.into_simple().expect("for now there is nothing else")) - .map_err(gix_error::Exn::into_error)? + .sorting(sorting.into_simple().expect("for now there is nothing else"))? .parents(parents) .commit_graph( commit_graph.or(use_commit_graph @@ -320,8 +319,7 @@ impl<'repo> Platform<'repo> { .then(|| self.repo.commit_graph().ok()) .flatten()), ) - .hide(hidden) - .map_err(gix_error::Exn::into_error)? + .hide(hidden)? .map(|res| res.map_err(gix_error::Exn::into_error)), ), }) diff --git a/gix/src/status/index_worktree.rs b/gix/src/status/index_worktree.rs index 8a72cabcf58..9d69f1e3d2e 100644 --- a/gix/src/status/index_worktree.rs +++ b/gix/src/status/index_worktree.rs @@ -103,8 +103,7 @@ impl Repository { let pathspec = self.index_worktree_status_pathspec(patterns, index, options.dirwalk_options.as_ref())?; let cwd = self.current_dir(); - let git_dir_realpath = crate::path::realpath_opts(self.git_dir(), cwd, crate::path::realpath::MAX_SYMLINKS) - .map_err(gix_error::Exn::into_error)?; + let git_dir_realpath = crate::path::realpath_opts(self.git_dir(), cwd, crate::path::realpath::MAX_SYMLINKS)?; let fs_caps = self.filesystem_options().or_erased()?; let fscache = config::tree::Core::FS_CACHE .enrich_error(self.config.resolved.boolean(config::tree::Core::FS_CACHE)) @@ -154,8 +153,7 @@ impl Repository { dirwalk: options.dirwalk_options.map(Into::into), rewrites: options.rewrites, }, - ) - .map_err(gix_error::Exn::into_error)?; + )?; Ok(out) } diff --git a/gix/src/status/iter/mod.rs b/gix/src/status/iter/mod.rs index 51e762eb7c3..a97dee3ea6b 100644 --- a/gix/src/status/iter/mod.rs +++ b/gix/src/status/iter/mod.rs @@ -109,9 +109,7 @@ where ) } }) - .map_err(|err| { - gix_error::Error::from(err.and_raise(gix_error::message("Failed to spawn producer thread"))) - })? + .map_err(|err| err.and_raise(gix_error::message("Failed to spawn producer thread")))? .into() } else { None @@ -145,9 +143,7 @@ where }) } }) - .map_err(|err| { - gix_error::Error::from(err.and_raise(gix_error::message("Failed to spawn producer thread"))) - })?; + .map_err(|err| err.and_raise(gix_error::message("Failed to spawn producer thread")))?; Ok(Iter { rx_and_join: Some((rx, join_index_worktree, join_tree_index)), diff --git a/gix/src/status/tree_index.rs b/gix/src/status/tree_index.rs index 7394400587f..573e08f3707 100644 --- a/gix/src/status/tree_index.rs +++ b/gix/src/status/tree_index.rs @@ -121,8 +121,7 @@ impl Repository { ) .is_ok_and(|platform| platform.matching_attributes(out)) }, - ) - .map_err(gix_error::Error::from)?; + )?; Ok(Outcome { rewrite, tree_index }) } diff --git a/gix/src/submodule/mod.rs b/gix/src/submodule/mod.rs index f65ecfc9ee2..89e75b32ede 100644 --- a/gix/src/submodule/mod.rs +++ b/gix/src/submodule/mod.rs @@ -60,13 +60,7 @@ impl<'repo> SharedState<'repo> { if state.is_none() { let platform = self .modules - .is_active_platform( - &self.repo.config.resolved, - self.repo - .config - .pathspec_defaults() - .map_err(gix_error::Exn::into_error)?, - ) + .is_active_platform(&self.repo.config.resolved, self.repo.config.pathspec_defaults()?) .or_erased()?; let index = self.index()?; let attributes = self @@ -140,8 +134,7 @@ impl Submodule<'_> { Ok(match self.state.modules.fetch_recurse(self.name()).or_erased()? { Some(val) => Some(val), None => crate::config::tree::Fetch::RECURSE_SUBMODULES - .try_into_recurse_submodules(self.state.repo.config.resolved.boolean("fetch.recurseSubmodules")) - .map_err(gix_error::Error::from)?, + .try_into_recurse_submodules(self.state.repo.config.resolved.boolean("fetch.recurseSubmodules"))?, }) } @@ -164,18 +157,16 @@ impl Submodule<'_> { /// Please see the [plumbing crate documentation](gix_submodule::IsActivePlatform::is_active()) for details. pub fn is_active(&self) -> Result { let (mut platform, mut attributes) = self.state.active_state_mut()?; - let is_active = platform - .is_active( - &self.state.repo.config.resolved, - self.name.as_ref(), - &mut |relative_path, case, is_dir, out| { - attributes - .set_case(case) - .at_entry(relative_path, Some(is_dir_to_mode(is_dir)), &self.state.repo.objects) - .is_ok_and(|platform| platform.matching_attributes(out)) - }, - ) - .map_err(gix_error::Exn::into_error)?; + let is_active = platform.is_active( + &self.state.repo.config.resolved, + self.name.as_ref(), + &mut |relative_path, case, is_dir, out| { + attributes + .set_case(case) + .at_entry(relative_path, Some(is_dir_to_mode(is_dir)), &self.state.repo.objects) + .is_ok_and(|platform| platform.matching_attributes(out)) + }, + )?; Ok(is_active) } @@ -250,19 +241,19 @@ impl Submodule<'_> { /// Validation may be skipped when callers only need coarse state, like `status_opts()` with /// `Ignore::All`, which returns before opening or inspecting the submodule repository. fn git_dir_try_old_form_inner(&self, validate_gitdir_file_target: bool) -> Result { - let git_dir = self.git_dir().map_err(|err| { - gix_error::Error::from(err.and_raise(gix_error::ValidationError::new("The submodule name is invalid"))) - })?; + let git_dir = self + .git_dir() + .map_err(|err| err.and_raise(gix_error::ValidationError::new("The submodule name is invalid")))?; let worktree_gitdir = self.worktree_gitdir().or_erased()?; let git_dir = if worktree_gitdir.is_dir() { worktree_gitdir } else if worktree_gitdir.is_file() { if validate_gitdir_file_target { let git_dir = gix_discover::path::from_gitdir_file(&worktree_gitdir).map_err(|err| { - gix_error::Error::from(err.raise(gix_error::ValidationError::new(format!( + err.raise(gix_error::ValidationError::new(format!( "The gitdir file at '{}' contains an invalid gitdir target", worktree_gitdir.display() - )))) + ))) })?; if !git_dir.is_dir() { return Err(gix_error::Error::from_error(gix_error::ValidationError::new(format!( diff --git a/gix/src/worktree/mod.rs b/gix/src/worktree/mod.rs index 5d2146e7b2a..7a99ab6da30 100644 --- a/gix/src/worktree/mod.rs +++ b/gix/src/worktree/mod.rs @@ -224,8 +224,7 @@ pub mod pathspec { .with_lenient_default_value( self.parent.config.lenient_config, Some(gitoxide::Pathspec::INHERIT_IGNORE_CASE_DEFAULT), - ) - .map_err(gix_error::Error::from)? + )? .unwrap_or(gitoxide::Pathspec::INHERIT_IGNORE_CASE_DEFAULT); self.parent.pathspec( true, /* empty patterns match prefix */ From 9d0329af119874c7441bafeac950166a1e13d899 Mon Sep 17 00:00:00 2001 From: Codex Date: Tue, 15 Sep 2026 18:12:50 +0100 Subject: [PATCH 33/51] Add error context without preliminary erasure rubberstamp `ResultExt` already accepts exceptions directly. Remove five erasures in pack generation and merge paths that immediately add context, and avoid converting the committer exception to `Error` before adding clone context. Preserve each context message and final erasure where the callback needs it. --- gix-merge/src/blob/pipeline.rs | 1 - gix-merge/src/commit/virtual_merge_base.rs | 2 -- gix-pack/src/data/output/entry/iter_from_counts.rs | 3 +-- gix/src/clone/fetch/util.rs | 1 - 4 files changed, 1 insertion(+), 6 deletions(-) diff --git a/gix-merge/src/blob/pipeline.rs b/gix-merge/src/blob/pipeline.rs index e8c89f0eb4c..957ea5e4fb5 100644 --- a/gix-merge/src/blob/pipeline.rs +++ b/gix-merge/src/blob/pipeline.rs @@ -241,7 +241,6 @@ impl Pipeline { } else { objects .try_find(id, out) - .or_erased() .or_raise_erased(|| message!("Could not find object {id}"))? .ok_or_raise_erased(|| { NotFoundError::new(format!("An object with id {id} could not be found")) diff --git a/gix-merge/src/commit/virtual_merge_base.rs b/gix-merge/src/commit/virtual_merge_base.rs index f8e15c8035d..c8eeebde79a 100644 --- a/gix-merge/src/commit/virtual_merge_base.rs +++ b/gix-merge/src/commit/virtual_merge_base.rs @@ -86,7 +86,6 @@ pub(super) mod function { .tree_merge .tree .write(|tree| objects.write(tree)) - .or_erased() .or_raise(|| message("Failed to write tree for merged merge-base or virtual commit"))?; tree_id = Some(merged_tree_id); @@ -129,7 +128,6 @@ pub(super) mod function { commit.tree = tree_id; objects .write(&commit) - .or_erased() .or_raise(|| message("Failed to write tree for merged merge-base or virtual commit")) } } 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 6449a3462b5..5ff79d4fb30 100644 --- a/gix-pack/src/data/output/entry/iter_from_counts.rs +++ b/gix-pack/src/data/output/entry/iter_from_counts.rs @@ -210,7 +210,7 @@ pub(crate) mod function { entry } None => { - match db.try_find(&count.id, buf).or_erased().or_raise_erased(|| { + match db.try_find(&count.id, buf).or_raise_erased(|| { message("Could not find object while generating pack") })? { Some((obj, _location)) => { @@ -227,7 +227,6 @@ pub(crate) mod function { } None => match db .try_find(&count.id, buf) - .or_erased() .or_raise_erased(|| message("Could not find object while generating pack"))? { Some((obj, _location)) => { diff --git a/gix/src/clone/fetch/util.rs b/gix/src/clone/fetch/util.rs index 77b499e9f1e..1bc95a10d68 100644 --- a/gix/src/clone/fetch/util.rs +++ b/gix/src/clone/fetch/util.rs @@ -224,7 +224,6 @@ pub fn update_head( .commit( repo.committer() .transpose() - .map_err(gix_error::Error::from) .or_raise(|| gix_error::message("Failed to update HEAD with values from remote"))?, ) .or_raise(|| gix_error::message("Failed to update HEAD with values from remote"))?; From 807275e17c36341516157949b2404ee66c2d4acb Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 16 Sep 2026 09:21:42 +0100 Subject: [PATCH 34/51] Use borrowed exception retry policies in workspace tests rubberstamp Inspect retry policies directly on `Exn` in `gix-index`, `gix-transport`, and `gix-worktree-stream` tests. Remove conversions to `gix_error::Error` that were only needed to inspect these exceptions. --- gix-index/tests/index/fuzzed.rs | 1 - gix-transport/tests/client/git.rs | 3 +-- gix-worktree-stream/tests/stream.rs | 3 +-- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/gix-index/tests/index/fuzzed.rs b/gix-index/tests/index/fuzzed.rs index 4f52d1480f4..4704a589612 100644 --- a/gix-index/tests/index/fuzzed.rs +++ b/gix-index/tests/index/fuzzed.rs @@ -150,7 +150,6 @@ fn alloc_limit_constructor_rejects_oversized_allocations() { err.to_string(), "Index data would require more memory than can be reserved" ); - let err = err.into_error(); assert!( err.classify().any(|classification| { classification.class() diff --git a/gix-transport/tests/client/git.rs b/gix-transport/tests/client/git.rs index 489a817f09a..8fe30eb4c4f 100644 --- a/gix-transport/tests/client/git.rs +++ b/gix-transport/tests/client/git.rs @@ -40,8 +40,7 @@ async fn refused_connections_remain_retryable() -> crate::Result { let err = connect(url.as_str(), Default::default()) .await .err() - .expect("the local port has no listener") - .into_error(); + .expect("the local port has no listener"); assert_eq!( err.downcast_any_ref::() .expect("retain the connection failure") diff --git a/gix-worktree-stream/tests/stream.rs b/gix-worktree-stream/tests/stream.rs index 87031c0d13e..890a8036519 100644 --- a/gix-worktree-stream/tests/stream.rs +++ b/gix-worktree-stream/tests/stream.rs @@ -17,8 +17,7 @@ fn impossible_path_allocation_preserves_its_source() { let err = gix_worktree_stream::Stream::from_read(std::io::Cursor::new(input)) .next_entry() - .expect_err("the declared path length cannot be allocated") - .into_error(); + .expect_err("the declared path length cannot be allocated"); let io_err = err .downcast_any_ref::() .expect("the I/O error is retained"); From ac88bd7307b584183815e124b86fa44ac59b3988 Mon Sep 17 00:00:00 2001 From: Byron Date: Thu, 17 Sep 2026 10:20:24 +0100 Subject: [PATCH 35/51] fix(gix-error): preserve classifications in custom I/O payloads `std::io::Error::source()` skips its custom payload, hiding classification markers, custom error types, and branches of a nested `gix_error::Error`. Retain the payload while walking native sources so borrowed retry policies, exception traversal, and both porcelain error modes see the complete cause. Document how custom errors expose their immediate cause and classification markers. Cover every classification, nested branches, and concrete payload downcasts, and update diagnostic snapshots for the retained I/O payload. Assisted-by: GPT 6.0 Co-authored-by: GPT 6.0 --- gix-error/src/concrete/chain.rs | 15 ++- gix-error/src/error.rs | 14 ++- gix-error/src/exn/impls.rs | 6 +- gix-error/src/lib.rs | 7 ++ gix-error/tests/error/classification.rs | 81 +++++++++++++- gix-error/tests/error/exn.rs | 134 ++++++++++++------------ gix-error/tests/error/test.rs | 2 +- gix-worktree-stream/tests/stream.rs | 9 +- 8 files changed, 178 insertions(+), 90 deletions(-) diff --git a/gix-error/src/concrete/chain.rs b/gix-error/src/concrete/chain.rs index 29460364a62..3e213185063 100644 --- a/gix-error/src/concrete/chain.rs +++ b/gix-error/src/concrete/chain.rs @@ -53,7 +53,7 @@ impl std::error::Error for ChainedError { self.source .as_deref() .map(|err| err as &(dyn std::error::Error + 'static)) - .or_else(|| self.err.error().source()) + .or_else(|| crate::error::native_source(self.err.error())) } } @@ -61,16 +61,16 @@ impl std::error::Error for ChainedError { /// /// Keeping the source-chain root in an [`Arc`] makes every source reachable for the lifetime of the flattened chain. /// A handle cannot store both that owner and a reference borrowed from its [`std::error::Error::source()`] chain without -/// becoming self-referential. Instead, `source_depth` records how many `source()` links lead from `owner` to the error -/// represented by this handle: zero represents `owner`, one represents `owner.source()`, and so on. [`Self::error()`] -/// follows that path whenever the borrowed error is needed. +/// becoming self-referential. Instead, `source_depth` records how many native source links, including I/O payloads, +/// lead from `owner` to the error represented by this handle. Zero represents `owner`. [`Self::error()`] follows that +/// path whenever the borrowed error is needed. /// /// Resolving a handle assumes that an error's source chain remains stable while the owning error is alive, as conventional /// [`std::error::Error`] implementations do. pub(crate) struct ErrorHandle { /// The error that owns the complete native source chain. owner: Arc, - /// The number of [`std::error::Error::source()`] links to follow from `owner` to reach this handle's error. + /// The number of native source links, including I/O payloads, from `owner` to this handle's error. source_depth: usize, } @@ -85,15 +85,14 @@ impl ErrorHandle { pub(crate) fn error(&self) -> &(dyn std::error::Error + 'static) { let mut error: &(dyn std::error::Error + 'static) = self.owner.as_ref(); for _ in 0..self.source_depth { - error = error - .source() + error = crate::error::native_source(error) .expect("a captured source path remains stable while its owning error is alive"); } error } pub(crate) fn source(&self) -> Option { - self.error().source()?; + crate::error::native_source(self.error())?; Some(ErrorHandle { owner: Arc::clone(&self.owner), source_depth: self.source_depth + 1, diff --git a/gix-error/src/error.rs b/gix-error/src/error.rs index 0a491620675..82ca13508c7 100644 --- a/gix-error/src/error.rs +++ b/gix-error/src/error.rs @@ -408,7 +408,7 @@ mod _impl { Inner::ExnAsError(frame) | Inner::Exn(frame) => { let error = frame.error(); (!error.is::()) - .then(|| error.source()) + .then(|| super::native_source(error)) .flatten() .or_else(|| frame.children().first().map(|frame| frame.error() as _)) } @@ -672,5 +672,15 @@ pub fn can_retry_lenient(err: &(dyn std::error::Error + 'static)) -> bool { fn error_chain<'a>( err: &'a (dyn std::error::Error + 'static), ) -> impl Iterator { - std::iter::successors(Some(err), |err| err.source()) + std::iter::successors(Some(err), |err| native_source(*err)) +} + +/// Retain I/O payloads, which `std::io::Error::source()` skips even when they carry a classification or an error tree. +pub(crate) fn native_source<'a>( + err: &'a (dyn std::error::Error + 'static), +) -> Option<&'a (dyn std::error::Error + 'static)> { + match err.downcast_ref::() { + Some(err) => err.get_ref().map(|err| err as _), + None => err.source(), + } } diff --git a/gix-error/src/exn/impls.rs b/gix-error/src/exn/impls.rs index 8f824263d07..b24b494a7d7 100644 --- a/gix-error/src/exn/impls.rs +++ b/gix-error/src/exn/impls.rs @@ -442,8 +442,8 @@ impl<'a> ErrorNode<'a> { /// Return this node's immediate logical children in traversal order. /// - /// A direct native [`Error::source()`] is first and inherits this node's formatting location. For a frame, explicitly - /// raised child frames follow it in insertion order. The compatibility `source()` of a nested [`crate::Error`] is + /// A direct native [`Error::source()`] or I/O payload is first and inherits this node's formatting location. + /// For a frame, explicitly raised child frames follow it in insertion order. The compatibility `source()` of a nested [`crate::Error`] is /// skipped because that wrapper retains an internal error graph which its own traversal APIs expand separately; /// following the compatibility source here would expose only one path and duplicate that expansion. pub(crate) fn children(self) -> Vec> { @@ -451,7 +451,7 @@ impl<'a> ErrorNode<'a> { let location = self.location(); let mut children = Vec::new(); if !error.is::() - && let Some(error) = error.source() + && let Some(error) = crate::error::native_source(error) { children.push(ErrorNode::Source { error, location }); } diff --git a/gix-error/src/lib.rs b/gix-error/src/lib.rs index 0765bf0dd06..8d31924db7f 100644 --- a/gix-error/src/lib.rs +++ b/gix-error/src/lib.rs @@ -239,6 +239,13 @@ //! [`Exn::can_retry()`] and [`Error::can_retry()`] additionally recognize certain I/O error kinds. //! Use [`Exn::probable_cause()`] to inspect the likely root cause. //! [`Exn::classify()`] and [`Error::classify()`] expose each known classification together with its original error. +//! Custom payloads of [`std::io::Error`] are inspected too, including any nested [`Error`] trees. +//! +//! Custom error types preserve classifications by exposing their immediate cause as `Some(inner)` from +//! [`std::error::Error::source()`]. Forwarding to `inner.source()` instead can hide a classification carried by +//! `inner` itself. A custom leaf error can expose a borrowed classification error, such as a static +//! [`NotFoundError`], as its source. When storing an [`Exn`] in a custom error, convert it with +//! [`Exn::into_error()`] so the source can expose its complete tree. //! //! To access error-specific metadata (e.g. the `input` field on [`ValidationError`]), //! use [`Exn::downcast_any_ref()`] to find a specific error type within the error tree: diff --git a/gix-error/tests/error/classification.rs b/gix-error/tests/error/classification.rs index 81efd09b710..f5afd3f9c14 100644 --- a/gix-error/tests/error/classification.rs +++ b/gix-error/tests/error/classification.rs @@ -1,6 +1,6 @@ use gix_error::{ - Class, CorruptionError, Error, ErrorExt, ResourceExhaustionError, ResourceExhaustionKind, RetryableError, - ValidationError, can_retry, can_retry_lenient, message, + Class, CorruptionError, Error, ErrorExt, NotFoundError, ResourceExhaustionError, ResourceExhaustionKind, + RetryableError, ValidationError, can_retry, can_retry_lenient, message, }; #[test] @@ -357,3 +357,80 @@ fn exceptions_expose_retry_policies_without_conversion() { let unknown = message("unknown").raise(); assert!(!unknown.can_retry() && !unknown.can_retry_lenient()); } + +#[test] +fn custom_io_payloads_retain_all_classifications() { + let cases: [(Box, Class); 5] = [ + (Box::new(NotFoundError::new("missing object")), Class::NotFound), + (Box::new(ValidationError::new("invalid input")), Class::Validation), + (Box::new(CorruptionError::new("malformed data")), Class::Corruption), + (Box::new(RetryableError::new(message("try again"))), Class::Retryable), + ( + Box::new(ResourceExhaustionError::new( + ResourceExhaustionKind::AllocationLimit, + "limit exceeded", + )), + Class::ResourceExhaustion(ResourceExhaustionKind::AllocationLimit), + ), + ]; + for (payload, class) in cases { + let err = crate::ErrorWithSource("custom backend failed", std::io::Error::other(payload)); + assert_eq!( + can_retry(&err), + class == Class::Retryable, + "borrowed retry inspection reaches the I/O payload" + ); + assert_eq!( + can_retry_lenient(&err), + class == Class::Retryable, + "both retry policies reach the I/O payload" + ); + let err = err.raise(); + assert_eq!( + err.classify().map(|item| item.class()).collect::>(), + [Class::Io(std::io::ErrorKind::Other), class], + "the custom wrapper retains both the I/O error and its classified payload" + ); + let err = err.into_error(); + assert_eq!(err.is_not_found(), class == Class::NotFound); + assert_eq!(err.is_validation(), class == Class::Validation); + assert_eq!(err.is_corrupted(), class == Class::Corruption); + assert_eq!(err.is_retryable(), class == Class::Retryable); + assert_eq!(err.can_retry(), class == Class::Retryable); + assert_eq!(err.can_retry_lenient(), class == Class::Retryable); + assert_eq!( + err.is_resource_exhausted(), + matches!(class, Class::ResourceExhaustion(_)) + ); + } +} + +#[test] +fn io_payloads_retain_custom_errors_and_nested_branches() { + for custom_wrapper in [false, true] { + let nested = message("nested operation failed") + .raise() + .chain(ValidationError::new("invalid input")) + .chain(RetryableError::new(message("try again"))) + .into_error(); + let payload: Box = if custom_wrapper { + Box::new(crate::ErrorWithSource("custom payload", nested)) + } else { + Box::new(nested) + }; + let io = std::io::Error::other(payload); + assert!( + can_retry(&io), + "borrowed inspection reaches every branch in an I/O payload" + ); + let err = Error::from_error(io); + assert!(err.can_retry(), "conversion retains retryable branches"); + assert!(err.is_validation(), "conversion retains other classifications too"); + if custom_wrapper { + assert!( + err.downcast_any_ref::>().is_some(), + "the custom payload remains available for downcasting" + ); + } + } +} diff --git a/gix-error/tests/error/exn.rs b/gix-error/tests/error/exn.rs index e5fd1b68be7..1466f1bc5c1 100644 --- a/gix-error/tests/error/exn.rs +++ b/gix-error/tests/error/exn.rs @@ -121,10 +121,12 @@ fn raise_chain() { fn and_raise() { let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found"); let exn = io_err.and_raise(message("could not read config")); - insta::assert_debug_snapshot!(exn, "and_raise links context above its source", @r" + insta::assert_debug_snapshot!(exn, "and_raise retains context, the I/O error, and its payload", @r" could not read config | └─ file not found + | + └─ file not found "); let io_err2 = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found"); @@ -173,35 +175,35 @@ fn raise_all() { └─ E4-3 "); insta::assert_compact_debug_snapshot!(&e, "raise_all retains every caller location", @" - Top, at gix-error/tests/error/exn.rs:141 + Top, at gix-error/tests/error/exn.rs:143 | - └─ E1, at gix-error/tests/error/exn.rs:142 + └─ E1, at gix-error/tests/error/exn.rs:144 | | - | └─ E1-0, at gix-error/tests/error/exn.rs:142 + | └─ E1-0, at gix-error/tests/error/exn.rs:144 | - └─ E2, at gix-error/tests/error/exn.rs:142 + └─ E2, at gix-error/tests/error/exn.rs:144 | | - | └─ E2-0, at gix-error/tests/error/exn.rs:142 + | └─ E2-0, at gix-error/tests/error/exn.rs:144 | | - | └─ E2-1, at gix-error/tests/error/exn.rs:142 + | └─ E2-1, at gix-error/tests/error/exn.rs:144 | - └─ E3, at gix-error/tests/error/exn.rs:142 + └─ E3, at gix-error/tests/error/exn.rs:144 | | - | └─ E3-0, at gix-error/tests/error/exn.rs:142 + | └─ E3-0, at gix-error/tests/error/exn.rs:144 | | - | └─ E3-1, at gix-error/tests/error/exn.rs:142 + | └─ E3-1, at gix-error/tests/error/exn.rs:144 | | - | └─ E3-2, at gix-error/tests/error/exn.rs:142 + | └─ E3-2, at gix-error/tests/error/exn.rs:144 | - └─ E4, at gix-error/tests/error/exn.rs:142 + └─ E4, at gix-error/tests/error/exn.rs:144 | - └─ E4-0, at gix-error/tests/error/exn.rs:142 + └─ E4-0, at gix-error/tests/error/exn.rs:144 | - └─ E4-1, at gix-error/tests/error/exn.rs:142 + └─ E4-1, at gix-error/tests/error/exn.rs:144 | - └─ E4-2, at gix-error/tests/error/exn.rs:142 + └─ E4-2, at gix-error/tests/error/exn.rs:144 | - └─ E4-3, at gix-error/tests/error/exn.rs:142 + └─ E4-3, at gix-error/tests/error/exn.rs:144 "); let e = e.chain_all((1..3).map(|idx| message!("SE{}", idx))); @@ -304,15 +306,15 @@ fn inverse_error_call_chain() { └─ E5 "); insta::assert_compact_debug_snapshot!(&e5, "chain retains caller locations in call order", @" - E1, at gix-error/tests/error/exn.rs:290 + E1, at gix-error/tests/error/exn.rs:292 | - └─ E2, at gix-error/tests/error/exn.rs:291 + └─ E2, at gix-error/tests/error/exn.rs:293 | - └─ E3, at gix-error/tests/error/exn.rs:292 + └─ E3, at gix-error/tests/error/exn.rs:294 | - └─ E4, at gix-error/tests/error/exn.rs:293 + └─ E4, at gix-error/tests/error/exn.rs:295 | - └─ E5, at gix-error/tests/error/exn.rs:294 + └─ E5, at gix-error/tests/error/exn.rs:296 "); insta::assert_snapshot!(format!("{e5:#}"), "alternate display follows chained order", @r#" @@ -434,9 +436,9 @@ fn result_ext() { let result: Result<(), Message> = Err(message("An error")); let result = result.or_raise(|| message("Another error")); insta::assert_compact_debug_snapshot!(result.unwrap_err(), "or_raise records context and source at the call site", @" - Another error, at gix-error/tests/error/exn.rs:435 + Another error, at gix-error/tests/error/exn.rs:437 | - └─ An error, at gix-error/tests/error/exn.rs:435 + └─ An error, at gix-error/tests/error/exn.rs:437 "); } @@ -444,7 +446,7 @@ fn result_ext() { fn option_ext() { let result: Option<()> = None; let result = result.ok_or_raise(|| message("An error")); - insta::assert_compact_debug_snapshot!(result.unwrap_err(), "ok_or_raise records the failure call site", @"An error, at gix-error/tests/error/exn.rs:446"); + insta::assert_compact_debug_snapshot!(result.unwrap_err(), "ok_or_raise records the failure call site", @"An error, at gix-error/tests/error/exn.rs:448"); } #[test] @@ -455,7 +457,7 @@ fn from_message() { } let result = foo(); - insta::assert_compact_debug_snapshot!(result.unwrap_err(), "question-mark conversion records the propagation site", @"An error, at gix-error/tests/error/exn.rs:453"); + insta::assert_compact_debug_snapshot!(result.unwrap_err(), "question-mark conversion records the propagation site", @"An error, at gix-error/tests/error/exn.rs:455"); } #[test] @@ -475,7 +477,7 @@ fn bail() { } let result = foo(); - insta::assert_compact_debug_snapshot!(result.unwrap_err(), "bail records the invocation site", @"An error, at gix-error/tests/error/exn.rs:474"); + insta::assert_compact_debug_snapshot!(result.unwrap_err(), "bail records the invocation site", @"An error, at gix-error/tests/error/exn.rs:476"); } #[test] @@ -496,7 +498,7 @@ fn ensure_fail() { } let result = foo(); - insta::assert_compact_debug_snapshot!(result.unwrap_err(), "ensure failure records the invocation site", @"An error, at gix-error/tests/error/exn.rs:494"); + insta::assert_compact_debug_snapshot!(result.unwrap_err(), "ensure failure records the invocation site", @"An error, at gix-error/tests/error/exn.rs:496"); } #[test] @@ -554,17 +556,17 @@ fn raise_chain_anyhow() { "#); insta::assert_snapshot!(remove_stackstrace(format!("{:?}", anyhow::Error::from(root))), "anyhow traverses every error frame in order", @" - root, at gix-error/tests/error/exn.rs:533 + root, at gix-error/tests/error/exn.rs:535 Caused by: - 0: E2, at gix-error/tests/error/exn.rs:532 - 1: E1, at gix-error/tests/error/exn.rs:529 - 2: E1-2, at gix-error/tests/error/exn.rs:530 - 3: E1-3, at gix-error/tests/error/exn.rs:531 - 4: E1c1-1, at gix-error/tests/error/exn.rs:530 - 5: E1c1-2, at gix-error/tests/error/exn.rs:530 - 6: E1c2-1, at gix-error/tests/error/exn.rs:531 - 7: E1c2-2, at gix-error/tests/error/exn.rs:531 + 0: E2, at gix-error/tests/error/exn.rs:534 + 1: E1, at gix-error/tests/error/exn.rs:531 + 2: E1-2, at gix-error/tests/error/exn.rs:532 + 3: E1-3, at gix-error/tests/error/exn.rs:533 + 4: E1c1-1, at gix-error/tests/error/exn.rs:532 + 5: E1c1-2, at gix-error/tests/error/exn.rs:532 + 6: E1c2-1, at gix-error/tests/error/exn.rs:533 + 7: E1c2-2, at gix-error/tests/error/exn.rs:533 "); } @@ -589,13 +591,13 @@ fn inverse_error_call_chain_anyhow() { "); insta::assert_snapshot!(remove_stackstrace(format!("{:?}", anyhow::Error::from(e5))), "anyhow preserves inverse chain source order", @" - E1, at gix-error/tests/error/exn.rs:574 + E1, at gix-error/tests/error/exn.rs:576 Caused by: - 0: E2, at gix-error/tests/error/exn.rs:575 - 1: E3, at gix-error/tests/error/exn.rs:576 - 2: E4, at gix-error/tests/error/exn.rs:577 - 3: E5, at gix-error/tests/error/exn.rs:578 + 0: E2, at gix-error/tests/error/exn.rs:577 + 1: E3, at gix-error/tests/error/exn.rs:578 + 2: E4, at gix-error/tests/error/exn.rs:579 + 3: E5, at gix-error/tests/error/exn.rs:580 "); } @@ -637,15 +639,15 @@ fn into_chain() { // By default, there is paths displayed, just like everywhere. insta::assert_debug_snapshot!(causes_display(&root, Style::Normal), "into_chain exposes locations for every source", @r#" [ - "root, at gix-error/tests/error/exn.rs:613", - "E2, at gix-error/tests/error/exn.rs:612", - "E1, at gix-error/tests/error/exn.rs:609", - "E1-2, at gix-error/tests/error/exn.rs:610", - "E1-3, at gix-error/tests/error/exn.rs:611", - "E1c1-1, at gix-error/tests/error/exn.rs:610", - "E1c1-2, at gix-error/tests/error/exn.rs:610", - "E1c2-1, at gix-error/tests/error/exn.rs:611", - "E1c2-2, at gix-error/tests/error/exn.rs:611", + "root, at gix-error/tests/error/exn.rs:615", + "E2, at gix-error/tests/error/exn.rs:614", + "E1, at gix-error/tests/error/exn.rs:611", + "E1-2, at gix-error/tests/error/exn.rs:612", + "E1-3, at gix-error/tests/error/exn.rs:613", + "E1c1-1, at gix-error/tests/error/exn.rs:612", + "E1c1-2, at gix-error/tests/error/exn.rs:612", + "E1c2-1, at gix-error/tests/error/exn.rs:613", + "E1c2-2, at gix-error/tests/error/exn.rs:613", ] "#); @@ -667,17 +669,17 @@ fn into_chain() { // This should look similar. #[cfg(feature = "anyhow")] insta::assert_snapshot!(remove_stackstrace(format!("{:?}", anyhow::Error::from(root))), "into_chain matches anyhow source traversal", @" - root, at gix-error/tests/error/exn.rs:613 + root, at gix-error/tests/error/exn.rs:615 Caused by: - 0: E2, at gix-error/tests/error/exn.rs:612 - 1: E1, at gix-error/tests/error/exn.rs:609 - 2: E1-2, at gix-error/tests/error/exn.rs:610 - 3: E1-3, at gix-error/tests/error/exn.rs:611 - 4: E1c1-1, at gix-error/tests/error/exn.rs:610 - 5: E1c1-2, at gix-error/tests/error/exn.rs:610 - 6: E1c2-1, at gix-error/tests/error/exn.rs:611 - 7: E1c2-2, at gix-error/tests/error/exn.rs:611 + 0: E2, at gix-error/tests/error/exn.rs:614 + 1: E1, at gix-error/tests/error/exn.rs:611 + 2: E1-2, at gix-error/tests/error/exn.rs:612 + 3: E1-3, at gix-error/tests/error/exn.rs:613 + 4: E1c1-1, at gix-error/tests/error/exn.rs:612 + 5: E1c1-2, at gix-error/tests/error/exn.rs:612 + 6: E1c2-1, at gix-error/tests/error/exn.rs:613 + 7: E1c2-2, at gix-error/tests/error/exn.rs:613 "); } @@ -1067,19 +1069,19 @@ fn nested_error_formatting_prints_each_cause_once() { fixup_paths(format!("{err:?}")), "compact Debug expands nested error boundaries once and retains caller locations", @r" - outer-root, at gix-error/tests/error/exn.rs:1057 + outer-root, at gix-error/tests/error/exn.rs:1059 | - └─ native-wrapper, at gix-error/tests/error/exn.rs:1058 + └─ native-wrapper, at gix-error/tests/error/exn.rs:1060 | | - | └─ inner-root, at gix-error/tests/error/exn.rs:1058 + | └─ inner-root, at gix-error/tests/error/exn.rs:1060 | | - | └─ extra-child, at gix-error/tests/error/exn.rs:1055 + | └─ extra-child, at gix-error/tests/error/exn.rs:1057 | | - | └─ boundary-child, at gix-error/tests/error/exn.rs:1054 + | └─ boundary-child, at gix-error/tests/error/exn.rs:1056 | | - | └─ inner-child, at gix-error/tests/error/exn.rs:1053 + | └─ inner-child, at gix-error/tests/error/exn.rs:1055 | - └─ outer-sibling, at gix-error/tests/error/exn.rs:1059 + └─ outer-sibling, at gix-error/tests/error/exn.rs:1061 " ); insta::assert_snapshot!( diff --git a/gix-error/tests/error/test.rs b/gix-error/tests/error/test.rs index b9c3a88094f..20ddff36eb2 100644 --- a/gix-error/tests/error/test.rs +++ b/gix-error/tests/error/test.rs @@ -15,7 +15,7 @@ fn debug_output_and_propagation_into_porcelain_errors() { let string = test_failure("message").unwrap_err(); let plumbing = test_failure(message("plumbing").raise()).unwrap_err(); let porcelain_input = test_failure(gix_error::Error::from(message("porcelain input").raise())).unwrap_err(); - let boxed: Box = Box::new(std::io::Error::other("boxed")); + let boxed: Box = Box::new(message("boxed")); let boxed = test_failure(boxed).unwrap_err(); let porcelain = porcelain().unwrap_err(); let output = format!( diff --git a/gix-worktree-stream/tests/stream.rs b/gix-worktree-stream/tests/stream.rs index 890a8036519..8305a16cb19 100644 --- a/gix-worktree-stream/tests/stream.rs +++ b/gix-worktree-stream/tests/stream.rs @@ -29,14 +29,7 @@ fn impossible_path_allocation_preserves_its_source() { .is::(), "the allocation error is retained" ); - assert_eq!( - err.classify() - .map(|classification| classification.class()) - .collect::>(), - [gix_error::Class::ResourceExhaustion( - gix_error::ResourceExhaustionKind::AllocationFailure - )] - ); + assert!(err.is_resource_exhausted(), "the allocation failure remains classified"); assert!(!err.is_corrupted()); assert!(!err.can_retry()); } From 348af0bfe67b94ee1de1c41162af955f1778c1c3 Mon Sep 17 00:00:00 2001 From: Byron Date: Thu, 17 Sep 2026 10:24:49 +0100 Subject: [PATCH 36/51] fix(gix-validate): preserve validation classifications after error conversion Expose the shared `ValidationError` marker from reference, tag, submodule, and path-component errors without changing their variants or input details. This lets callers distinguish invalid names from absent references through `gix::Error`, including optional reference lookups. Assisted-by: GPT 6.0 Co-authored-by: GPT 6.0 --- Cargo.lock | 1 + gix-index/tests/index/init.rs | 4 +++- gix-validate/Cargo.toml | 2 ++ gix-validate/src/lib.rs | 7 +++++++ gix-validate/src/path.rs | 6 +++++- gix-validate/src/reference.rs | 6 +++++- gix-validate/src/submodule.rs | 6 +++++- gix-validate/src/tag.rs | 6 +++++- gix-validate/tests/validate/main.rs | 25 +++++++++++++++++++++++++ gix/tests/gix/reference/mod.rs | 21 +++++++++++++++++++++ 10 files changed, 79 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4f77cac6c20..51926cf02ac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2671,6 +2671,7 @@ name = "gix-validate" version = "0.11.4" dependencies = [ "bstr", + "gix-error", "gix-testtools", ] diff --git a/gix-index/tests/index/init.rs b/gix-index/tests/index/init.rs index b7939d41307..38ce6a3cdac 100644 --- a/gix-index/tests/index/init.rs +++ b/gix-index/tests/index/init.rs @@ -50,7 +50,9 @@ fn from_tree_validation() -> crate::Result { .expect_err("tree entries with path separators must fail validation"); assert!(err.is_validation(), "invalid path components are validation errors"); assert_eq!( - err.probable_cause().to_string(), + err.downcast_any_ref::() + .expect("the invalid path component remains available") + .to_string(), r"Path separators like / or \ are not allowed", r"Note that this effectively tests what would happen on Windows, where \ also isn't allowed" ); diff --git a/gix-validate/Cargo.toml b/gix-validate/Cargo.toml index d841a29de7e..3cf51933558 100644 --- a/gix-validate/Cargo.toml +++ b/gix-validate/Cargo.toml @@ -16,6 +16,8 @@ doctest = true test = true [dependencies] +gix-error = { version = "^0.3.0", path = "../gix-error" } + bstr = { version = "1.12.0", default-features = false, features = ["std"] } [dev-dependencies] diff --git a/gix-validate/src/lib.rs b/gix-validate/src/lib.rs index e5bd6a1a31e..52d623d95b1 100644 --- a/gix-validate/src/lib.rs +++ b/gix-validate/src/lib.rs @@ -1,5 +1,7 @@ //! Validation for various kinds of git related items. //! +//! Errors expose a [`gix_error::ValidationError`] source so their classification survives type erasure. +//! //! ## Examples //! //! ``` @@ -15,6 +17,11 @@ #![deny(missing_docs)] #![forbid(unsafe_code)] +static INVALID_NAME: gix_error::ValidationError = gix_error::ValidationError { + message: std::borrow::Cow::Borrowed("Invalid name"), + input: None, +}; + /// pub mod reference; diff --git a/gix-validate/src/path.rs b/gix-validate/src/path.rs index 1f5541d6c5f..88472da6d59 100644 --- a/gix-validate/src/path.rs +++ b/gix-validate/src/path.rs @@ -37,7 +37,11 @@ pub mod component { } } - impl std::error::Error for Error {} + impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + Some(&crate::INVALID_NAME) + } + } /// Further specify what to check for in [`component()`](super::component()) /// diff --git a/gix-validate/src/reference.rs b/gix-validate/src/reference.rs index 29c6e1a2ead..a4d54bd99af 100644 --- a/gix-validate/src/reference.rs +++ b/gix-validate/src/reference.rs @@ -45,7 +45,11 @@ pub mod name { } } - impl std::error::Error for Error {} + impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + Some(&crate::INVALID_NAME) + } + } impl From for Error { fn from(err: crate::tag::name::Error) -> Self { diff --git a/gix-validate/src/submodule.rs b/gix-validate/src/submodule.rs index 70053d59631..add177e1a68 100644 --- a/gix-validate/src/submodule.rs +++ b/gix-validate/src/submodule.rs @@ -20,7 +20,11 @@ pub mod name { } } - impl std::error::Error for Error {} + impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + Some(&crate::INVALID_NAME) + } + } } /// Return the original `name` if it is valid, or the respective error indicating what was wrong with it. diff --git a/gix-validate/src/tag.rs b/gix-validate/src/tag.rs index 9c78fa52b8d..f494a425cf7 100644 --- a/gix-validate/src/tag.rs +++ b/gix-validate/src/tag.rs @@ -46,7 +46,11 @@ pub mod name { } } - impl std::error::Error for Error {} + impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + Some(&crate::INVALID_NAME) + } + } } /// Assure the given `input` resemble a valid git tag name, which is returned unchanged on success. diff --git a/gix-validate/tests/validate/main.rs b/gix-validate/tests/validate/main.rs index d1951c3d159..0c4ccf4acc4 100644 --- a/gix-validate/tests/validate/main.rs +++ b/gix-validate/tests/validate/main.rs @@ -2,3 +2,28 @@ mod path; mod reference; mod submodule; mod tag; + +#[test] +fn invalid_names_retain_their_classification() { + use bstr::ByteSlice; + use gix_error::ErrorExt; + + fn check(err: E) { + let err = err.raise(); + assert!(err.is_validation(), "invalid names classify as validation failures"); + let err = err.into_error(); + assert!(err.is_validation(), "conversion preserves the classification"); + assert!( + err.downcast_any_ref::().is_some(), + "the concrete validation error remains available" + ); + } + + check(gix_validate::reference::name(b"refs//heads/main".as_bstr()).expect_err("repeated slashes are invalid")); + check(gix_validate::tag::name(b"v1..0".as_bstr()).expect_err("repeated dots are invalid")); + check(gix_validate::submodule::name(b"../module".as_bstr()).expect_err("parent components are invalid")); + check( + gix_validate::path::component(b".git".as_bstr(), None, Default::default()) + .expect_err("the Git directory cannot be a worktree entry"), + ); +} diff --git a/gix/tests/gix/reference/mod.rs b/gix/tests/gix/reference/mod.rs index 1964eb58015..3c3927ed5a1 100644 --- a/gix/tests/gix/reference/mod.rs +++ b/gix/tests/gix/reference/mod.rs @@ -101,6 +101,27 @@ mod find { Ok(()) } + #[test] + fn invalid_reference_names_are_classified() -> crate::Result { + let repo = repo()?; + for err in [ + repo.find_reference("refs//heads/main") + .expect_err("repeated slashes are invalid"), + repo.try_find_reference("refs//heads/main") + .expect_err("optional lookup still validates the name"), + ] { + assert!(err.is_validation(), "porcelain errors retain name validation failures"); + assert!(!err.is_not_found(), "an invalid name is not a missing reference"); + assert!(!err.can_retry(), "retrying cannot fix an invalid name"); + assert!( + err.downcast_any_ref::() + .is_some(), + "the original name error remains available" + ); + } + Ok(()) + } + #[test] fn and_peel() -> crate::Result { let repo = repo()?; From 045519ded0a128df295d189f2de3c4950f65fa4d Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 17 Sep 2026 10:27:08 +0100 Subject: [PATCH 37/51] fix(gix-command): classify malformed command lines rubberstamp, checked diff Expose a `ValidationError` source from command-line parser errors so their classification survives raising and conversion to `gix_error::Error`. Keep the original parser variant available for callers that need details. Regression cases cover missing quotes, dangling escapes, and assignment-only input. All command tests and doctests pass in both error modes; focused Clippy also passes. --- Cargo.lock | 1 + gix-command/Cargo.toml | 1 + gix-command/src/parse.rs | 12 +++++++++++- gix-command/tests/command/command_line.rs | 18 ++++++++++++++++++ 4 files changed, 31 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 51926cf02ac..1d874383a89 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1708,6 +1708,7 @@ name = "gix-command" version = "0.10.1" dependencies = [ "bstr", + "gix-error", "gix-path", "gix-quote", "gix-testtools", diff --git a/gix-command/Cargo.toml b/gix-command/Cargo.toml index a7511fb29fe..95d16d9a7af 100644 --- a/gix-command/Cargo.toml +++ b/gix-command/Cargo.toml @@ -15,6 +15,7 @@ include = ["/src/*.rs", "/LICENSE-*"] doctest = true [dependencies] +gix-error = { version = "^0.3.0", path = "../gix-error" } gix-trace = { version = "^0.1.20", path = "../gix-trace" } gix-path = { version = "^0.12.6", path = "../gix-path" } gix-quote = { version = "^0.8.0", path = "../gix-quote" } diff --git a/gix-command/src/parse.rs b/gix-command/src/parse.rs index 5c4b40dda95..f1b420b18b5 100644 --- a/gix-command/src/parse.rs +++ b/gix-command/src/parse.rs @@ -14,6 +14,8 @@ pub struct Outcome { } /// The error returned when a command line cannot be parsed into a command. +/// +/// Its [`gix_error::ValidationError`] source preserves the classification after type erasure. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Error { /// A quote was opened but never closed. @@ -39,7 +41,15 @@ impl std::fmt::Display for Error { } } -impl std::error::Error for Error {} +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + static INVALID_COMMAND: gix_error::ValidationError = gix_error::ValidationError { + message: std::borrow::Cow::Borrowed("Invalid command line"), + input: None, + }; + Some(&INVALID_COMMAND) + } +} #[derive(Clone, Copy, PartialEq, Eq)] enum Quote { diff --git a/gix-command/tests/command/command_line.rs b/gix-command/tests/command/command_line.rs index b588322cbca..db673c8909b 100644 --- a/gix-command/tests/command/command_line.rs +++ b/gix-command/tests/command/command_line.rs @@ -81,6 +81,24 @@ fn a_command_is_required() { } } +#[test] +fn parse_errors_retain_their_classification() { + use gix_error::ErrorExt; + + for input in ["cmd '", "cmd arg\\", "FOO=one"] { + let cause = command_line(input).expect_err("the command line is invalid"); + let err = cause.raise(); + assert!(err.is_validation(), "invalid commands classify as validation failures"); + let err = err.into_error(); + assert!(err.is_validation(), "conversion preserves the classification"); + assert_eq!( + err.downcast_any_ref::(), + Some(&cause), + "the original parser error remains available" + ); + } +} + #[test] #[cfg(unix)] fn non_utf8_input_is_preserved() -> gix_testtools::Result { From 0cec25600b0f771437fd87f78021a907227ee832 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 17 Sep 2026 10:27:24 +0100 Subject: [PATCH 38/51] fix(gix-tempfile): expose persistence I/O errors as sources stamp of approval, after looking at the diff and description. Forwarding to the inner I/O error's `source()` hides the I/O error itself, preventing callers from inspecting its kind through a custom persistence error. Return the immediate cause so generic error classification can find it, while preserving the handle needed to recover from failed persistence. Extend both writable-file and marker recovery tests to check the source. All tempfile tests and doctests pass. --- gix-tempfile/src/handle.rs | 2 +- gix-tempfile/tests/tempfile/handle.rs | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/gix-tempfile/src/handle.rs b/gix-tempfile/src/handle.rs index 6bde455eddb..71b6a3ef73f 100644 --- a/gix-tempfile/src/handle.rs +++ b/gix-tempfile/src/handle.rs @@ -280,7 +280,7 @@ pub mod persist { impl std::error::Error for Error { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - self.error.source() + Some(&self.error) } } } diff --git a/gix-tempfile/tests/tempfile/handle.rs b/gix-tempfile/tests/tempfile/handle.rs index 45245b0937d..4b098bcce53 100644 --- a/gix-tempfile/tests/tempfile/handle.rs +++ b/gix-tempfile/tests/tempfile/handle.rs @@ -18,6 +18,10 @@ mod mark_path { let err = handle .persist(&new_filename) .expect_err("cannot persist onto directory"); + assert!( + std::error::Error::source(&err).is_some_and(::is::), + "the persistence error exposes its I/O cause for classification" + ); let handle = err.handle; std::fs::remove_dir(&new_filename)?; @@ -107,6 +111,10 @@ mod at_path { let err = handle .persist(&new_filename) .expect_err("cannot persist onto directory"); + assert!( + std::error::Error::source(&err).is_some_and(::is::), + "the persistence error exposes its I/O cause for classification" + ); let handle = err.handle; std::fs::remove_dir(&new_filename)?; From 82776d93ab7671729b80ad89e420236c93cd1651 Mon Sep 17 00:00:00 2001 From: Byron Date: Thu, 17 Sep 2026 11:04:44 +0100 Subject: [PATCH 39/51] feat(gix-error): inspect borrowed error classifications lazily Admittedly, I was mostly checking the public API, thinking that it's most certainly useful and overally, having lazy iterators for everything is a step in the right direction. This wasn't a crazily detailed review in the interest of time. Error inspection rebuilt the entire error graph before returning its first item, so even a root match visited unrelated sources and allocated storage. Use a shared iterator that expands each node only when another item is needed, preserving breadth-first order, concrete types, and caller locations in both error representations. Expose `classify(&error)` for custom borrowed errors, and share classification predicates with `Error` and `Exn`. The shared traversal and predicate definitions remove more code than the borrowed API adds. Keep `probable_cause()` unchanged. Assisted-by: GPT 6.0 Co-authored-by: GPT 6.0 --- gix-error/src/error.rs | 592 +++++++++++------------- gix-error/src/exn/impls.rs | 24 +- gix-error/src/exn/mod.rs | 1 - gix-error/src/lib.rs | 2 +- gix-error/tests/error/classification.rs | 12 +- gix-error/tests/error/exn.rs | 40 +- 6 files changed, 317 insertions(+), 354 deletions(-) diff --git a/gix-error/src/error.rs b/gix-error/src/error.rs index 82ca13508c7..849d4dc6d21 100644 --- a/gix-error/src/error.rs +++ b/gix-error/src/error.rs @@ -1,3 +1,62 @@ +// Keep inherent methods on Error and Exn while sharing their implementation and documentation. +macro_rules! classification_predicates { + () => { + /// Return `true` if any stored error or native source is explicitly marked with [`crate::RetryableError`]. + /// + /// Nested [`crate::Error`] values are inspected recursively. Unlike [`Self::can_retry()`], this does not infer + /// retryability from I/O error kinds. + pub fn is_retryable(&self) -> bool { + self.classify().is_retryable() + } + + /// Return `true` if any stored error or native source reports resource exhaustion. + /// + /// This recognizes [`crate::ResourceExhaustionError`] of any kind, [`std::collections::TryReserveError`], and + /// [`std::io::ErrorKind::OutOfMemory`], including within nested [`crate::Error`] values. + pub fn is_resource_exhausted(&self) -> bool { + self.classify().is_resource_exhausted() + } + + /// Return `true` if any stored error, or an error in its [`source()`](std::error::Error::source) chain, is: + /// + /// * explicitly marked with [`RetryableError`](crate::RetryableError), or + /// * an [`std::io::Error`] with kind `Interrupted` or `TimedOut`. + /// + /// Nested [`crate::Error`] values are inspected recursively. `false` only means that no known retryable error was + /// found; it does not guarantee that retrying cannot succeed. + pub fn can_retry(&self) -> bool { + self.classify().can_retry() + } + + /// Return `true` if any stored error, or an error in its [`source()`](std::error::Error::source) chain, is: + /// + /// * explicitly marked with [`RetryableError`](crate::RetryableError), or + /// * an [`std::io::Error`] with kind `Interrupted`, `UnexpectedEof`, `OutOfMemory`, `TimedOut`, `BrokenPipe`, + /// `AddrInUse`, `ConnectionAborted`, `ConnectionReset`, or `ConnectionRefused`. + /// + /// This applies a more lenient policy than [`Self::can_retry`]. Nested [`crate::Error`] values are inspected recursively. + /// `false` only means that no known retryable error was found; it does not guarantee that retrying cannot succeed. + pub fn can_retry_lenient(&self) -> bool { + self.classify().can_retry_lenient() + } + + /// Return `true` if malformed or internally inconsistent data caused the failure. + pub fn is_corrupted(&self) -> bool { + self.classify().is_corrupted() + } + + /// Return `true` if a requested resource was not found. + pub fn is_not_found(&self) -> bool { + self.classify().is_not_found() + } + + /// Return `true` if invalid input caused the failure. + pub fn is_validation(&self) -> bool { + self.classify().is_validation() + } + }; +} + /// A borrowed error together with its optional caller location, intended for diagnostic display. /// /// Errors owned by a [`crate::Frame`] have the location captured when that frame was created. Native @@ -40,6 +99,20 @@ impl std::fmt::Display for DisplaySource<'_> { } impl crate::Error { + /// Lazily visit stored errors and native sources in logical breadth-first order, expanding nested [`crate::Error`] values. + /// + /// The stored error is first. A frame's native source precedes its explicitly raised children. Concrete error types + /// remain available for downcasting. Traversal stops when the iterator is dropped. + pub fn iter_errors(&self) -> impl Iterator + '_ { + self.iter_errors_with_locations().map(|source| source.error) + } + + /// Visit the same errors as [`Self::iter_errors()`], with caller locations for explicitly raised frames. + /// Native sources have no caller location of their own. [`DisplaySource`] can render either representation. + pub fn iter_errors_with_locations(&self) -> impl Iterator> + '_ { + Errors::new(self.iter_root()) + } + /// Find the first stored error or native source that downcasts to `T` in logical breadth-first order. pub fn downcast_any_ref(&self) -> Option<&T> { self.iter_errors().find_map(|error| error.downcast_ref()) @@ -49,69 +122,11 @@ impl crate::Error { /// /// Unknown errors are omitted. Classifications aren't deduplicated because distinct errors may independently have /// the same meaning. Each item retains the classified error for downcasting and origin inspection. - pub fn classify(&self) -> impl Iterator> + '_ { - self.iter_errors().filter_map(classify_one) + pub fn classify(&self) -> Classifications<'_> { + classify(self) } - /// Return `true` if any stored error or native source is explicitly marked with [`crate::RetryableError`]. - /// - /// Nested [`crate::Error`] values are inspected recursively. Unlike [`Self::can_retry()`], this does not infer - /// retryability from I/O error kinds. - pub fn is_retryable(&self) -> bool { - self.classify() - .any(|classification| classification.class() == Class::Retryable) - } - - /// Return `true` if any stored error or native source reports resource exhaustion. - /// - /// This recognizes [`crate::ResourceExhaustionError`] of any kind, [`std::collections::TryReserveError`], and - /// [`std::io::ErrorKind::OutOfMemory`], including within nested [`crate::Error`] values. - pub fn is_resource_exhausted(&self) -> bool { - self.classify() - .any(|classification| matches!(classification.class(), Class::ResourceExhaustion(_))) - } - - /// Return `true` if any stored error, or an error in its [`source()`](std::error::Error::source) chain, is: - /// - /// * explicitly marked with [`RetryableError`](crate::RetryableError), or - /// * an [`std::io::Error`] with kind `Interrupted` or `TimedOut`. - /// - /// Nested [`crate::Error`] values are inspected recursively. `false` only means that no known retryable error was - /// found; it does not guarantee that retrying cannot succeed. - pub fn can_retry(&self) -> bool { - self.classify() - .any(|classification| class_can_retry(classification.class())) - } - - /// Return `true` if any stored error, or an error in its [`source()`](std::error::Error::source) chain, is: - /// - /// * explicitly marked with [`RetryableError`](crate::RetryableError), or - /// * an [`std::io::Error`] with kind `Interrupted`, `UnexpectedEof`, `OutOfMemory`, `TimedOut`, `BrokenPipe`, - /// `AddrInUse`, `ConnectionAborted`, `ConnectionReset`, or `ConnectionRefused`. - /// - /// This applies a more lenient policy than [`Self::can_retry`]. Nested [`crate::Error`] values are inspected recursively. - /// `false` only means that no known retryable error was found; it does not guarantee that retrying cannot succeed. - pub fn can_retry_lenient(&self) -> bool { - self.classify().any(classification_can_retry_lenient) - } - - /// Return `true` if malformed or internally inconsistent data caused the failure. - pub fn is_corrupted(&self) -> bool { - self.classify() - .any(|classification| classification.class() == Class::Corruption) - } - - /// Return `true` if a requested resource was not found. - pub fn is_not_found(&self) -> bool { - self.classify() - .any(|classification| classification.class() == Class::NotFound) - } - - /// Return `true` if invalid input caused the failure. - pub fn is_validation(&self) -> bool { - self.classify() - .any(|classification| classification.class() == Class::Validation) - } + classification_predicates!(); } /// Classification helpers for inspecting an exception without consuming it or losing its typed outer error. @@ -126,73 +141,11 @@ impl crate::Exn { /// /// As with [`crate::Error::classify()`], unknown errors are omitted, classifications aren't deduplicated, and each /// item retains the classified error for downcasting and origin inspection. - pub fn classify(&self) -> impl Iterator> + '_ { - self.frame() - .collect_errors_with_locations() - .into_iter() - .filter_map(|source| classify_one(source.error())) + pub fn classify(&self) -> Classifications<'_> { + Classifications(Errors::new(Node::Frame(self.frame()))) } - /// Apply the conservative retry policy of [`crate::Error::can_retry()`] without consuming this exception. - /// - /// This inspects stored errors, native sources, and nested [`crate::Error`] values for explicit retry markers or - /// I/O errors with kind `Interrupted` or `TimedOut`. `false` does not guarantee that retrying cannot succeed. - pub fn can_retry(&self) -> bool { - self.classify() - .any(|classification| class_can_retry(classification.class())) - } - - /// Apply the broader I/O retry policy of [`crate::Error::can_retry_lenient()`] without consuming this exception. - /// - /// This inspects stored errors, native sources, and nested [`crate::Error`] values. `false` does not guarantee that - /// retrying cannot succeed. - pub fn can_retry_lenient(&self) -> bool { - self.classify().any(classification_can_retry_lenient) - } - - /// Return `true` if any stored error or native source is explicitly marked with [`crate::RetryableError`]. - /// - /// Nested [`crate::Error`] values are inspected recursively. Unlike [`Self::can_retry()`], this does not - /// infer retryability from I/O error kinds. - pub fn is_retryable(&self) -> bool { - self.any_class(|class| class == Class::Retryable) - } - - /// Return `true` if any stored error or native source reports a missing resource. - /// - /// This recognizes [`crate::NotFoundError`] and [`std::io::ErrorKind::NotFound`], including within nested - /// [`crate::Error`] values. It does not require the outermost error to have this classification. - pub fn is_not_found(&self) -> bool { - self.any_class(|class| class == Class::NotFound) - } - - /// Return `true` if any stored error or native source is a [`crate::ValidationError`]. - /// - /// Nested [`crate::Error`] values are inspected recursively. It does not require the outermost error to have this - /// classification. - pub fn is_validation(&self) -> bool { - self.any_class(|class| class == Class::Validation) - } - - /// Return `true` if any stored error or native source is a [`crate::CorruptionError`]. - /// - /// Nested [`crate::Error`] values are inspected recursively. It does not require the outermost error to have this - /// classification. - pub fn is_corrupted(&self) -> bool { - self.any_class(|class| class == Class::Corruption) - } - - /// Return `true` if any stored error or native source reports resource exhaustion. - /// - /// This recognizes [`crate::ResourceExhaustionError`] of any kind, [`std::collections::TryReserveError`], and - /// [`std::io::ErrorKind::OutOfMemory`], including within nested [`crate::Error`] values. - pub fn is_resource_exhausted(&self) -> bool { - self.any_class(|class| matches!(class, Class::ResourceExhaustion(_))) - } - - fn any_class(&self, predicate: impl Fn(Class) -> bool) -> bool { - self.classify().any(|classification| predicate(classification.class())) - } + classification_predicates!(); } /// The semantic class of an error. @@ -220,6 +173,72 @@ pub struct Classification<'a> { error: &'a (dyn std::error::Error + 'static), } +/// Lazily inspect the classifications of any borrowed error, including its native sources, I/O payloads and nested +/// [`crate::Error`] values. Unknown errors are omitted and distinct causes may yield the same classification. +/// +/// ``` +/// let error = std::io::Error::other(gix_error::NotFoundError::new("missing object")); +/// assert!(gix_error::classify(&error).is_not_found()); +/// ``` +pub fn classify<'a>(err: &'a (dyn std::error::Error + 'static)) -> Classifications<'a> { + Classifications(Errors::new( + err.downcast_ref::() + .map_or(Node::Source(err), crate::Error::iter_root), + )) +} + +/// A lazy iterator over classified causes. Its predicates consume the remaining iterator and stop at the first match. +pub struct Classifications<'a>(Errors<'a>); + +impl<'a> Iterator for Classifications<'a> { + type Item = Classification<'a>; + + fn next(&mut self) -> Option { + self.0.find_map(|source| classify_one(source.error)) + } +} + +impl Classifications<'_> { + /// Return whether any remaining cause is explicitly marked as retryable. + pub fn is_retryable(self) -> bool { + self.has(Class::Retryable) + } + + /// Apply the conservative retry policy of [`crate::Error::can_retry()`] to the remaining causes. + pub fn can_retry(mut self) -> bool { + self.any(|classification| class_can_retry(classification.class())) + } + + /// Apply the broader I/O policy of [`crate::Error::can_retry_lenient()`] to the remaining causes. + pub fn can_retry_lenient(mut self) -> bool { + self.any(classification_can_retry_lenient) + } + + /// Return whether any remaining cause reports a missing resource. + pub fn is_not_found(self) -> bool { + self.has(Class::NotFound) + } + + /// Return whether any remaining cause reports invalid input. + pub fn is_validation(self) -> bool { + self.has(Class::Validation) + } + + /// Return whether any remaining cause reports malformed or inconsistent data. + pub fn is_corrupted(self) -> bool { + self.has(Class::Corruption) + } + + /// Return whether any remaining cause reports resource exhaustion. + pub fn is_resource_exhausted(mut self) -> bool { + self.any(|classification| matches!(classification.class(), Class::ResourceExhaustion(_))) + } + + fn has(mut self, class: Class) -> bool { + self.any(|classification| classification.class() == class) + } +} + impl<'a> Classification<'a> { /// Return the semantic class. pub fn class(&self) -> Class { @@ -287,9 +306,123 @@ fn classification_can_retry_lenient(classification: Classification<'_>) -> bool }) } +#[derive(Clone, Copy)] +enum Node<'a> { + Frame(&'a crate::Frame), + Source(&'a (dyn std::error::Error + 'static)), + #[cfg(all(feature = "auto-chain-error", not(feature = "tree-error")))] + Chain { + node: &'a crate::ChainedError, + index: usize, + cursor: Option, + }, +} + +impl<'a> Node<'a> { + fn display(self) -> DisplaySource<'a> { + let (error, location) = match self { + Node::Frame(frame) => ( + frame.error() as &(dyn std::error::Error + 'static), + Some(frame.location()), + ), + Node::Source(error) => (error, None), + #[cfg(all(feature = "auto-chain-error", not(feature = "tree-error")))] + Node::Chain { node, .. } => ( + node.err.error(), + (!node.err.is_native_source()).then_some(node.location), + ), + }; + DisplaySource { error, location } + } +} + +struct Errors<'a> { + root: Option>, + previous: Option>, + pending: std::collections::VecDeque>, + #[cfg(all(feature = "auto-chain-error", not(feature = "tree-error")))] + chains: Vec<(usize, Option<&'a crate::ChainedError>)>, +} + +impl<'a> Errors<'a> { + fn new(root: Node<'a>) -> Self { + Errors { + root: Some(root), + previous: None, + pending: Default::default(), + #[cfg(all(feature = "auto-chain-error", not(feature = "tree-error")))] + chains: Vec::new(), + } + } + + fn source(&mut self, error: &'a (dyn std::error::Error + 'static)) { + if let Some(error) = error.downcast_ref::() { + self.pending.push_back(error.iter_root()); + } else if let Some(source) = native_source(error) { + self.pending.push_back(Node::Source(source)); + } + } + + fn children(&mut self, node: Node<'a>) { + match node { + Node::Frame(frame) => { + self.source(frame.error()); + self.pending.extend(frame.children().iter().map(Node::Frame)); + } + Node::Source(error) => self.source(error), + #[cfg(all(feature = "auto-chain-error", not(feature = "tree-error")))] + Node::Chain { node, index, cursor } => { + if let Some(error) = node.err.error().downcast_ref::() { + self.pending.push_back(error.iter_root()); + } + let cursor = match cursor { + Some(cursor) => cursor, + None if node.source.is_none() => return, + None => { + self.chains.push((1, node.source.as_deref())); + self.chains.len() - 1 + } + }; + // Flattened parents occur in increasing order. One cursor per boundary streams each child once, + // even when other error trees are interleaved at their logical breadth-first positions. + let (child_index, next) = &mut self.chains[cursor]; + while let Some(child) = next.filter(|child| child.logical_parent == Some(index)) { + self.pending.push_back(Node::Chain { + node: child, + index: *child_index, + cursor: Some(cursor), + }); + *child_index += 1; + *next = child.source.as_deref(); + } + } + } + } +} + +impl<'a> Iterator for Errors<'a> { + type Item = DisplaySource<'a>; + + fn next(&mut self) -> Option { + // Defer expansion until the caller asks for another error, so a match need not inspect any of its causes. + if let Some(previous) = self.previous.take() { + self.children(previous); + } + let node = self.root.take().or_else(|| self.pending.pop_front())?; + self.previous = Some(node); + Some(node.display()) + } +} + +impl crate::Frame { + pub(crate) fn iter_errors_with_locations(&self) -> impl Iterator> + '_ { + Errors::new(Node::Frame(self)) + } +} + #[cfg(any(feature = "tree-error", not(feature = "auto-chain-error")))] mod _impl { - use crate::{DisplaySource, Error, Exn}; + use crate::{Error, Exn}; use std::fmt::Formatter; /// Utilities @@ -310,47 +443,8 @@ mod _impl { cause.downcast_ref::().map_or(cause, Error::probable_cause) } - /// Return the stored error and all frame errors and native sources reachable from it, recursively expanding nested - /// [`Error`] values. - /// - /// The first item is the error stored inside this [`Error`], not this `Error` wrapper. Remaining errors are ordered - /// logically breadth-first; a frame's direct native source precedes its explicitly raised child frames. - /// These are references to the underlying errors, so their concrete types remain available for *downcasting* and - /// their own [`Display`](std::fmt::Display) implementations can be used. Use - /// [`Self::iter_errors_with_locations()`] to access caller locations. - pub fn iter_errors(&self) -> impl Iterator + '_ { - self.collect_errors_with_locations() - .into_iter() - .map(|source| source.error) - } - - /// Return the same errors as [`Self::iter_errors()`], paired with their captured caller locations where available - /// and in the same order. - pub fn iter_errors_with_locations(&self) -> impl Iterator> + '_ { - self.collect_errors_with_locations().into_iter() - } - - fn collect_errors_with_locations(&self) -> Vec> { - self.inner.frame().collect_errors_with_locations() - } - } - - impl crate::Frame { - pub(crate) fn collect_errors_with_locations(&self) -> Vec> { - let mut queue = std::collections::VecDeque::from([crate::exn::ErrorNode::Frame(self)]); - let mut out = Vec::new(); - while let Some(node) = queue.pop_front() { - let error = node.error(); - out.push(DisplaySource { - error, - location: node.captured_location(), - }); - if let Some(error) = error.downcast_ref::() { - queue.push_back(crate::exn::ErrorNode::Frame(error.inner.frame())); - } - queue.extend(node.children()); - } - out + pub(super) fn iter_root(&self) -> super::Node<'_> { + super::Node::Frame(self.inner.frame()) } } @@ -432,79 +526,9 @@ pub(super) use _impl::Inner; #[cfg(all(feature = "auto-chain-error", not(feature = "tree-error")))] mod _impl { - use crate::{DisplaySource, Error, Exn}; + use crate::{Error, Exn}; use std::fmt::Formatter; - /// A temporary adjacency-list node used to recover the logical error graph from flattened [`crate::ChainedError`] - /// values. - /// - /// Auto-chain storage is a linked list for compatibility with [`std::error::Error::source()`]. The iteration APIs - /// rebuild its retained parent relationships in a `Vec` so nested [`Error`] values can join the same - /// breadth-first traversal without changing that source-chain representation. - struct ErrorGraphNode<'a> { - /// The borrowed error and optional frame location yielded for this node. - source: DisplaySource<'a>, - /// Indices of logical children in the temporary node vector, in traversal order. - children: Vec, - } - - /// Expand nested [`Error`] values, then collect the graph's errors and locations breadth-first from `root`. - /// - /// `nested` pairs each wrapper's index in `graph` with the error whose stored chain still needs to be added. - /// Each nested chain's root becomes the wrapper's first child, before its existing children, matching tree-backed - /// traversal. Newly discovered nested errors are expanded in turn before collecting the output. - /// - /// Expanding the graph first lets nested errors participate at their logical depth: simply splicing their flattened - /// chains into the output would visit their descendants before shallower errors elsewhere in the graph. - fn collect_graph_errors<'a>( - mut graph: Vec>, - root: usize, - nested: Vec<(usize, &'a Error)>, - ) -> Vec> { - let mut pending = std::collections::VecDeque::from(nested); - while let Some((parent, error)) = pending.pop_front() { - let (nested_root, more_nested) = error.append_error_chain(&mut graph); - graph[parent].children.insert(0, nested_root); - pending.extend(more_nested); - } - - let mut queue = std::collections::VecDeque::from([root]); - let mut out = Vec::new(); - while let Some(index) = queue.pop_front() { - let node = &graph[index]; - out.push(node.source); - queue.extend(node.children.iter().copied()); - } - out - } - - impl crate::Frame { - pub(crate) fn collect_errors_with_locations(&self) -> Vec> { - let mut graph: Vec> = Vec::new(); - let mut nested = Vec::new(); - let mut queue = std::collections::VecDeque::from([(crate::exn::ErrorNode::Frame(self), None::)]); - while let Some((node, parent)) = queue.pop_front() { - let index = graph.len(); - let error = node.error(); - graph.push(ErrorGraphNode { - source: DisplaySource { - error, - location: node.captured_location(), - }, - children: Vec::new(), - }); - if let Some(parent) = parent { - graph[parent].children.push(index); - } - if let Some(error) = error.downcast_ref::() { - nested.push((index, error)); - } - queue.extend(node.children().into_iter().map(|child| (child, Some(index)))); - } - collect_graph_errors(graph, 0, nested) - } - } - /// Utilities impl Error { /// Return the error stored at this error boundary. @@ -524,72 +548,12 @@ mod _impl { cause.downcast_ref::().map_or(cause, Error::probable_cause) } - /// Return the stored error and all frame errors and native sources reachable from it, recursively expanding nested - /// [`Error`] values. - /// - /// The first item is the error stored inside this [`Error`], not this `Error` wrapper. Remaining errors retain the - /// logical breadth-first order captured while flattening; a frame's direct native source precedes its - /// explicitly raised child frames. These are references to the underlying errors, so their concrete types remain - /// available for downcasting and their own [`Display`](std::fmt::Display) implementations can be used. Use - /// [`Self::iter_errors_with_locations()`] to access caller locations. - pub fn iter_errors(&self) -> impl Iterator + '_ { - self.collect_errors_with_locations() - .into_iter() - .map(|source| source.error) - } - - /// Return the same errors as [`Self::iter_errors()`], paired with their captured caller locations where available - /// and in the same order. - /// - /// The stored error, rather than this `Error` wrapper, is the first item. Frame errors have the location - /// captured when their frame was created; native sources have no caller location of their own. This enables opt-in - /// caller traces without making locations part of the underlying errors or their - /// [`Display`](std::fmt::Display) implementations. Each [`DisplaySource`] exposes both values for custom rendering, - /// and its own [`Display`](std::fmt::Display) implementation appends an available location by default. - pub fn iter_errors_with_locations(&self) -> impl Iterator> + '_ { - self.collect_errors_with_locations().into_iter() - } - - fn collect_errors_with_locations(&self) -> Vec> { - let mut graph = Vec::new(); - let (root, nested) = self.append_error_chain(&mut graph); - collect_graph_errors(graph, root, nested) - } - - /// Append this error boundary's flattened chain to `graph` and reconstruct its local parent-child relationships. - /// - /// The first tuple value is the graph index of the stored error. Each entry in the second value contains the graph - /// index of a node whose error is another [`Error`], paired with that nested boundary. The caller appends those - /// nested chains iteratively and connects each returned root as the wrapper node's first child, avoiding recursive - /// graph construction before the completed graph is traversed breadth-first. - fn append_error_chain<'a>(&'a self, graph: &mut Vec>) -> (usize, Vec<(usize, &'a Error)>) { - let chain = std::iter::successors(Some(&self.inner), |err| err.source.as_deref()).collect::>(); - let root = graph.len(); - graph.extend(chain.iter().map(|chained| ErrorGraphNode { - source: DisplaySource { - error: chained.err.error(), - location: (!chained.err.is_native_source()).then_some(chained.location), - }, - children: Vec::new(), - })); - - for (index, chained) in chain.iter().enumerate() { - if let Some(parent) = chained.logical_parent { - graph[root + parent].children.push(root + index); - } + pub(super) fn iter_root(&self) -> super::Node<'_> { + super::Node::Chain { + node: &self.inner, + index: 0, + cursor: None, } - let nested = chain - .into_iter() - .enumerate() - .filter_map(|(index, chained)| { - chained - .err - .error() - .downcast_ref::() - .map(|error| (root + index, error)) - }) - .collect(); - (root, nested) } } @@ -646,12 +610,7 @@ mod _impl { /// Nested [`crate::Error`] values are inspected recursively. `false` only means that no known retryable error was found; it /// does not guarantee that retrying cannot succeed. pub fn can_retry(err: &(dyn std::error::Error + 'static)) -> bool { - error_chain(err).any(|err| { - if let Some(err) = err.downcast_ref::() { - return err.can_retry(); - } - classify_one(err).is_some_and(|classification| class_can_retry(classification.class())) - }) + classify(err).can_retry() } /// Return `true` if `err` or any error in its [`source()`](std::error::Error::source) chain is explicitly marked with @@ -661,18 +620,7 @@ pub fn can_retry(err: &(dyn std::error::Error + 'static)) -> bool { /// This applies a more lenient policy than [`can_retry`]. Nested [`crate::Error`] values are inspected recursively. /// `false` only means that no known retryable error was found; it does not guarantee that retrying cannot succeed. pub fn can_retry_lenient(err: &(dyn std::error::Error + 'static)) -> bool { - error_chain(err).any(|err| { - if let Some(err) = err.downcast_ref::() { - return err.can_retry_lenient(); - } - classify_one(err).is_some_and(classification_can_retry_lenient) - }) -} - -fn error_chain<'a>( - err: &'a (dyn std::error::Error + 'static), -) -> impl Iterator { - std::iter::successors(Some(err), |err| native_source(*err)) + classify(err).can_retry_lenient() } /// Retain I/O payloads, which `std::io::Error::source()` skips even when they carry a classification or an error tree. diff --git a/gix-error/src/exn/impls.rs b/gix-error/src/exn/impls.rs index b24b494a7d7..146cf0667f6 100644 --- a/gix-error/src/exn/impls.rs +++ b/gix-error/src/exn/impls.rs @@ -197,8 +197,7 @@ impl Exn { /// Nested [`crate::Error`] values are inspected recursively, matching [`crate::Error::downcast_any_ref()`]. pub fn downcast_any_ref(&self) -> Option<&T> { self.frame - .collect_errors_with_locations() - .into_iter() + .iter_errors_with_locations() .find_map(|source| source.error().downcast_ref()) } } @@ -377,12 +376,7 @@ impl Frame { } /// Return the source code location where this exception frame was created. - /// Return the frame location used when formatting this node. - /// - /// A frame returns its own captured location. A native source inherits the location of the frame whose error owns its - /// source chain, providing formatting context even though no location was captured for the source itself. In contrast, - /// `captured_location()` reports only locations belonging to the node itself. - pub fn location(self) -> &'static Location<'static> { + pub fn location(&self) -> &'static Location<'static> { self.location } @@ -420,8 +414,7 @@ impl<'a> ErrorNode<'a> { /// Return the frame location used when formatting this node. /// /// A frame returns its own captured location. A native source inherits the location of the frame whose error owns its - /// source chain, providing formatting context even though no location was captured for the source itself. In contrast, - /// `captured_location()` reports only locations belonging to the node itself. + /// source chain, providing formatting context even though no location was captured for the source itself. pub(crate) fn location(self) -> &'static Location<'static> { match self { ErrorNode::Frame(frame) => frame.location, @@ -429,17 +422,6 @@ impl<'a> ErrorNode<'a> { } } - /// Return the location captured for this node itself. - /// - /// This is `Some` for an explicitly created frame and `None` for a native source. Unlike [`Self::location()`], it does - /// not return the owning frame's location as inherited formatting context for a source. - pub(crate) fn captured_location(self) -> Option<&'static Location<'static>> { - match self { - ErrorNode::Frame(frame) => Some(frame.location), - ErrorNode::Source { .. } => None, - } - } - /// Return this node's immediate logical children in traversal order. /// /// A direct native [`Error::source()`] or I/O payload is first and inherits this node's formatting location. diff --git a/gix-error/src/exn/mod.rs b/gix-error/src/exn/mod.rs index 28d9c6103db..f398541860e 100644 --- a/gix-error/src/exn/mod.rs +++ b/gix-error/src/exn/mod.rs @@ -20,7 +20,6 @@ mod ext; pub use ext::{BoxedResultExt, ErrorExt, OptionExt, ResultExt}; mod impls; -pub(crate) use impls::ErrorNode; pub use impls::{Frame, Something, Untyped}; mod macros; diff --git a/gix-error/src/lib.rs b/gix-error/src/lib.rs index 8d31924db7f..a9d1420c8ab 100644 --- a/gix-error/src/lib.rs +++ b/gix-error/src/lib.rs @@ -391,7 +391,7 @@ mod test; pub use test::{TestError, TestResult}; mod error; -pub use error::{Class, Classification, DisplaySource, can_retry, can_retry_lenient}; +pub use error::{Class, Classification, Classifications, DisplaySource, can_retry, can_retry_lenient, classify}; /// Various kinds of concrete errors that implement [`std::error::Error`]. mod concrete; diff --git a/gix-error/tests/error/classification.rs b/gix-error/tests/error/classification.rs index f5afd3f9c14..d0548ee3465 100644 --- a/gix-error/tests/error/classification.rs +++ b/gix-error/tests/error/classification.rs @@ -1,6 +1,6 @@ use gix_error::{ Class, CorruptionError, Error, ErrorExt, NotFoundError, ResourceExhaustionError, ResourceExhaustionKind, - RetryableError, ValidationError, can_retry, can_retry_lenient, message, + RetryableError, ValidationError, can_retry, can_retry_lenient, classify, message, }; #[test] @@ -375,6 +375,15 @@ fn custom_io_payloads_retain_all_classifications() { ]; for (payload, class) in cases { let err = crate::ErrorWithSource("custom backend failed", std::io::Error::other(payload)); + assert_eq!(classify(&err).is_not_found(), class == Class::NotFound); + assert_eq!(classify(&err).is_validation(), class == Class::Validation); + assert_eq!(classify(&err).is_corrupted(), class == Class::Corruption); + assert_eq!(classify(&err).is_retryable(), class == Class::Retryable); + assert_eq!( + classify(&err).is_resource_exhausted(), + matches!(class, Class::ResourceExhaustion(_)), + "all borrowed predicates reach custom errors through their sources" + ); assert_eq!( can_retry(&err), class == Class::Retryable, @@ -423,6 +432,7 @@ fn io_payloads_retain_custom_errors_and_nested_branches() { can_retry(&io), "borrowed inspection reaches every branch in an I/O payload" ); + assert!(classify(&io).is_validation()); let err = Error::from_error(io); assert!(err.can_retry(), "conversion retains retryable branches"); assert!(err.is_validation(), "conversion retains other classifications too"); diff --git a/gix-error/tests/error/exn.rs b/gix-error/tests/error/exn.rs index 1466f1bc5c1..69d11c58584 100644 --- a/gix-error/tests/error/exn.rs +++ b/gix-error/tests/error/exn.rs @@ -785,7 +785,7 @@ fn native_sources_are_retained_and_traversed_lazily() { } #[test] -fn new_does_not_inspect_native_sources() { +fn inspection_visits_native_sources_on_demand() { #[derive(Debug)] struct CountedSource { source_calls: std::sync::Arc, @@ -815,6 +815,12 @@ fn new_does_not_inspect_native_sources() { 0, "constructing an Exn neither traverses nor snapshots native sources" ); + assert!(e.downcast_any_ref::().is_some()); + assert_eq!( + source_calls.load(std::sync::atomic::Ordering::Relaxed), + 0, + "finding the outer error does not visit its sources" + ); assert!( e.downcast_any_ref::().is_some(), @@ -824,6 +830,24 @@ fn new_does_not_inspect_native_sources() { source_calls.load(std::sync::atomic::Ordering::Relaxed) > 0, "source-aware operations traverse native sources on demand" ); + + let e = e.raise(gix_error::RetryableError::new(message("retry"))); + source_calls.store(0, std::sync::atomic::Ordering::Relaxed); + assert!(e.can_retry()); + assert_eq!( + source_calls.load(std::sync::atomic::Ordering::Relaxed), + 0, + "classification stops at the first match" + ); + let e = e.into_error(); + source_calls.store(0, std::sync::atomic::Ordering::Relaxed); + assert!(e.can_retry()); + assert!(e.iter_errors_with_locations().next().is_some()); + assert_eq!( + source_calls.load(std::sync::atomic::Ordering::Relaxed), + 0, + "porcelain iteration and predicates do not resolve unused sources" + ); } #[test] @@ -1069,19 +1093,19 @@ fn nested_error_formatting_prints_each_cause_once() { fixup_paths(format!("{err:?}")), "compact Debug expands nested error boundaries once and retains caller locations", @r" - outer-root, at gix-error/tests/error/exn.rs:1059 + outer-root, at gix-error/tests/error/exn.rs:1083 | - └─ native-wrapper, at gix-error/tests/error/exn.rs:1060 + └─ native-wrapper, at gix-error/tests/error/exn.rs:1084 | | - | └─ inner-root, at gix-error/tests/error/exn.rs:1060 + | └─ inner-root, at gix-error/tests/error/exn.rs:1084 | | - | └─ extra-child, at gix-error/tests/error/exn.rs:1057 + | └─ extra-child, at gix-error/tests/error/exn.rs:1081 | | - | └─ boundary-child, at gix-error/tests/error/exn.rs:1056 + | └─ boundary-child, at gix-error/tests/error/exn.rs:1080 | | - | └─ inner-child, at gix-error/tests/error/exn.rs:1055 + | └─ inner-child, at gix-error/tests/error/exn.rs:1079 | - └─ outer-sibling, at gix-error/tests/error/exn.rs:1061 + └─ outer-sibling, at gix-error/tests/error/exn.rs:1085 " ); insta::assert_snapshot!( From b692589cc1417f3b8ff49e3e872ce067b02d262f Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 17 Sep 2026 11:09:00 +0100 Subject: [PATCH 40/51] fix(gix-revision)!: preserve merge-base lookup failures rubber stamp, but checked the diff and asked for fixes to use gix_error::TestResult instead, which for the sake of simplicity is going to happen in a future commit. Merge-base traversal replaced object-store and decoding errors with a static message. Missing objects, retryable I/O failures, and custom backend errors therefore lost their causes and classifications at the revision boundary. Return `Exn` and raise graph insertion context around the original failure. Remove the forwarding `Error` alias and `Simple` type, and adapt the porcelain API to return `gix::Error`. The regression reproduces the lost backend cause before the fix and verifies its concrete I/O kind, missing-resource classification, and retry policy after conversion. All 113 revision tests and the porcelain revision tests pass; `gix` and `gix-merge` compile with the changed signature. --- gix-revision/src/merge_base/function.rs | 18 +++---- gix-revision/src/merge_base/mod.rs | 20 ++------ gix-revision/tests/revision/merge_base.rs | 57 +++++++++++++++++++---- gix/src/repository/revision.rs | 2 +- 4 files changed, 62 insertions(+), 35 deletions(-) diff --git a/gix-revision/src/merge_base/function.rs b/gix-revision/src/merge_base/function.rs index f05d30f441b..d537c8deb3c 100644 --- a/gix-revision/src/merge_base/function.rs +++ b/gix-revision/src/merge_base/function.rs @@ -1,9 +1,9 @@ use std::cmp::Ordering; +use gix_error::{Exn, Message, ResultExt, message}; use gix_hash::ObjectId; use gix_revwalk::graph; -use super::{Error, Simple}; use crate::{Graph, PriorityQueue, merge_base::Flags}; /// Given a commit at `first` id, traverse the commit `graph` and return all possible merge-base between it and `others`, @@ -28,7 +28,7 @@ pub fn merge_base( first: ObjectId, others: &[ObjectId], graph: &mut Graph<'_, '_, graph::Commit>, -) -> Result>, Error> { +) -> Result>, Exn> { let _span = gix_trace::coarse!("gix_revision::merge_base()", ?first, ?others); if others.is_empty() || others.contains(&first) { return Ok(Some(nonempty::NonEmpty::new(first))); @@ -46,7 +46,7 @@ pub fn merge_base( fn remove_redundant( commits: &[(ObjectId, GenThenTime)], graph: &mut Graph<'_, '_, graph::Commit>, -) -> Result, Error> { +) -> Result, Exn> { if commits.is_empty() { return Ok(Vec::new()); } @@ -73,7 +73,7 @@ fn remove_redundant( walk_start.push((parent_id, GenThenTime::from(&*parent))); } }) - .map_err(|_| Simple("could not insert parent commit into graph"))?; + .or_raise(|| message("could not insert parent commit into graph"))?; } } walk_start.sort_by_key(|a| a.0); @@ -126,7 +126,7 @@ fn remove_redundant( stack.push((*parent_id, GenThenTime::from(&*parent))); } }) - .map_err(|_| Simple("could not insert parent commit into graph"))? + .or_raise(|| message("could not insert parent commit into graph"))? .is_some() { break; @@ -207,7 +207,7 @@ fn paint_down_to_common( first: ObjectId, others: &[ObjectId], graph: &mut Graph<'_, '_, graph::Commit>, -) -> Result, Error> { +) -> Result, Exn> { let mut queue = PaintQueue { queue: PriorityQueue::new(), non_stale: [0; 2], @@ -216,14 +216,14 @@ fn paint_down_to_common( .get_or_insert_full_commit(first, |commit| { queue.insert(first, commit, Flags::COMMIT1); }) - .map_err(|_| Simple("could not insert commit into graph"))?; + .or_raise(|| message("could not insert commit into graph"))?; for other in others { graph .get_or_insert_full_commit(*other, |commit| { queue.insert(*other, commit, Flags::COMMIT2); }) - .map_err(|_| Simple("could not insert commit into graph"))?; + .or_raise(|| message("could not insert commit into graph"))?; } let mut out = Vec::new(); @@ -245,7 +245,7 @@ fn paint_down_to_common( queue.insert(parent_id, parent, flags_without_result); } }) - .map_err(|_| Simple("could not insert parent commit into graph"))?; + .or_raise(|| message("could not insert parent commit into graph"))?; } } diff --git a/gix-revision/src/merge_base/mod.rs b/gix-revision/src/merge_base/mod.rs index 3599d27c66e..8f13c50c90d 100644 --- a/gix-revision/src/merge_base/mod.rs +++ b/gix-revision/src/merge_base/mod.rs @@ -16,28 +16,14 @@ bitflags::bitflags! { } } -/// The error returned by the [`merge_base()`][function::merge_base()] function. -pub type Error = Simple; - -/// A simple error type for merge base operations. -#[derive(Debug)] -pub struct Simple(pub &'static str); - -impl std::fmt::Display for Simple { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(self.0) - } -} - -impl std::error::Error for Simple {} - pub(crate) mod function; mod octopus { + use gix_error::{Exn, Message}; use gix_hash::ObjectId; use gix_revwalk::{Graph, graph}; - use crate::merge_base::{Error, Flags}; + use crate::merge_base::Flags; /// Given a commit at `first` id, traverse the commit `graph` and return *the best common ancestor* between it and `others`, /// sorted from best to worst. Returns `None` if there is no common merge-base as `first` and `others` don't *all* share history. @@ -51,7 +37,7 @@ mod octopus { mut first: ObjectId, others: &[ObjectId], graph: &mut Graph<'_, '_, graph::Commit>, - ) -> Result, Error> { + ) -> Result, Exn> { for other in others { if let Some(next) = crate::merge_base(first, std::slice::from_ref(other), graph)?.map(|bases| *bases.first()) diff --git a/gix-revision/tests/revision/merge_base.rs b/gix-revision/tests/revision/merge_base.rs index 61881e1dd8e..3616827518e 100644 --- a/gix-revision/tests/revision/merge_base.rs +++ b/gix-revision/tests/revision/merge_base.rs @@ -2,6 +2,39 @@ use gix_revision::merge_base; use crate::odb_at; +#[test] +fn lookup_failures_retain_their_causes() { + use gix_error::{ErrorExt, ResultExt}; + + struct FailingLookup(std::io::ErrorKind); + + impl gix_object::Find for FailingLookup { + fn try_find<'a>( + &self, + _id: &gix_hash::oid, + _buffer: &'a mut Vec, + ) -> Result>, gix_error::Exn> { + Err(std::io::Error::from(self.0).raise_erased()) + } + } + + for kind in [std::io::ErrorKind::NotFound, std::io::ErrorKind::TimedOut] { + let mut graph = gix_revision::Graph::new(FailingLookup(kind), None); + let hash = gix_testtools::object_hash(); + let err = merge_base(hash.null(), &[hash.empty_blob()], &mut graph) + .or_erased() + .expect_err("the custom object store always fails") + .into_error(); + assert_eq!( + err.downcast_any_ref::().map(std::io::Error::kind), + Some(kind), + "merge-base context preserves the concrete lookup failure" + ); + assert_eq!(err.is_not_found(), kind == std::io::ErrorKind::NotFound); + assert_eq!(err.can_retry(), kind == std::io::ErrorKind::TimedOut); + } +} + #[test] fn validate() -> crate::Result { let root = gix_testtools::scripted_fixture_read_only("make_merge_base_repos.sh")?; @@ -14,7 +47,8 @@ fn validate() -> crate::Result { .then(|| gix_commitgraph::Graph::from_info_dir(&odb.store_ref().path().join("info")).unwrap()); for expected in baseline::parse_expectations(&baseline_path)? { let mut graph = gix_revision::Graph::new(&odb, cache.as_ref()); - let actual = merge_base(expected.first, &expected.others, &mut graph)?; + let actual = + merge_base(expected.first, &expected.others, &mut graph).map_err(gix_error::Exn::into_error)?; assert_eq!( actual, expected.bases, @@ -25,7 +59,8 @@ fn validate() -> crate::Result { } let mut graph = gix_revision::Graph::new(&odb, cache.as_ref()); for expected in baseline::parse_expectations(&baseline_path)? { - let actual = merge_base(expected.first, &expected.others, &mut graph)?; + let actual = + merge_base(expected.first, &expected.others, &mut graph).map_err(gix_error::Exn::into_error)?; assert_eq!( actual, expected.bases, @@ -130,8 +165,10 @@ mod octopus { ]; let mut heap = permutohedron::Heap::new(&mut heads); while let Some(heads) = heap.next_permutation() { - let actual = gix_revision::merge_base::octopus(*heads.first().unwrap(), &heads[1..], &mut graph)? - .expect("a merge base"); + let actual = + gix_revision::merge_base::octopus(*heads.first().expect("three heads"), &heads[1..], &mut graph) + .map_err(gix_error::Exn::into_error)? + .expect("a merge base"); assert_eq!(actual, first_commit); } Ok(()) @@ -149,8 +186,10 @@ mod octopus { ]; let mut heap = permutohedron::Heap::new(&mut heads); while let Some(heads) = heap.next_permutation() { - let actual = gix_revision::merge_base::octopus(*heads.first().unwrap(), &heads[1..], &mut graph)? - .expect("a merge base"); + let actual = + gix_revision::merge_base::octopus(*heads.first().expect("three heads"), &heads[1..], &mut graph) + .map_err(gix_error::Exn::into_error)? + .expect("a merge base"); assert_eq!(actual, base); } Ok(()) @@ -168,8 +207,10 @@ mod octopus { ]; let mut heap = permutohedron::Heap::new(&mut heads); while let Some(heads) = heap.next_permutation() { - let actual = gix_revision::merge_base::octopus(*heads.first().unwrap(), &heads[1..], &mut graph)? - .expect("a merge base"); + let actual = + gix_revision::merge_base::octopus(*heads.first().expect("three heads"), &heads[1..], &mut graph) + .map_err(gix_error::Exn::into_error)? + .expect("a merge base"); assert_eq!(actual, base); } Ok(()) diff --git a/gix/src/repository/revision.rs b/gix/src/repository/revision.rs index 8fc2c7a52e6..cc3fac42494 100644 --- a/gix/src/repository/revision.rs +++ b/gix/src/repository/revision.rs @@ -115,7 +115,7 @@ impl crate::Repository { one: impl Into, others: &[gix_hash::ObjectId], graph: &mut gix_revwalk::Graph<'_, '_, gix_revwalk::graph::Commit>, - ) -> Result>, gix_revision::merge_base::Error> { + ) -> Result>, crate::Error> { use crate::prelude::ObjectIdExt; let one = one.into(); Ok(match gix_revision::merge_base(one, others, graph)? { From 1068667fa3d97b595e9ee3f3a41cc0ba31066025 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 17 Sep 2026 11:40:13 +0100 Subject: [PATCH 41/51] fix(gix-transport): preserve HTTP callback and port parsing failures rubber stamp, checked diff Curl replaces upload and download callback failures with generic transfer errors. Keep the original I/O error alongside curl's diagnostic so custom retry policies and other classifications remain inspectable after conversion to `gix::Error`. Clear the saved callback failure between transfers. Also retain the integer parser's cause when a virtual-host port is invalid. Both conversions previously discarded their sources in `map_err()`. --- .../client/blocking_io/http/curl/remote.rs | 83 ++++++++++++++++--- gix-transport/src/client/git/blocking_io.rs | 6 +- 2 files changed, 75 insertions(+), 14 deletions(-) diff --git a/gix-transport/src/client/blocking_io/http/curl/remote.rs b/gix-transport/src/client/blocking_io/http/curl/remote.rs index f7b475c8e27..84e1b1221d1 100644 --- a/gix-transport/src/client/blocking_io/http/curl/remote.rs +++ b/gix-transport/src/client/blocking_io/http/curl/remote.rs @@ -59,6 +59,8 @@ struct Handler { send_data: Option, /// Provides the optional upload body to curl, either streamed from the caller or buffered for known-size uploads. receive_body: Option, + /// The I/O failure that made a callback abort the active transfer. + io_error: Option, /// `true` once the status line of the current response header block was parsed. checked_status: bool, /// Status code of the current response header block, used to associate following headers with redirects. @@ -83,6 +85,7 @@ struct Handler { impl Handler { fn reset(&mut self) { + self.io_error = None; self.checked_status = false; self.current_status = None; self.authentication.www_authenticate.clear(); @@ -92,6 +95,20 @@ impl Handler { self.redirect_action = RedirectAction::Stop; } + fn transfer_error(&mut self, err: curl::Error) -> io::Error { + match self.io_error.take() { + Some(source) => io::Error::new(source.kind(), source.and_raise(err).into_error()), + None => io::Error::new( + if curl_is_retryable(&err) { + io::ErrorKind::ConnectionReset + } else { + io::ErrorKind::Other + }, + err, + ), + } + } + fn track_redirects( &mut self, request_url: String, @@ -275,16 +292,23 @@ impl curl::easy::Handler for Handler { fn write(&mut self, data: &[u8]) -> Result { drop(self.send_header.take()); // signal header readers to stop trying match self.send_data.as_mut() { - Some(writer) => writer.write_all(data).map(|_| data.len()).or(Ok(0)), + Some(writer) => writer.write_all(data).map(|_| data.len()).or_else(|err| { + self.io_error = Some(err); + Ok(0) + }), None => Ok(0), // nothing more to receive, reader is done } } fn read(&mut self, data: &mut [u8]) -> Result { match self.receive_body.as_mut() { - Some(StreamOrBuffer::Stream(reader)) => reader.read(data).map_err(|_err| curl::easy::ReadError::Abort), - Some(StreamOrBuffer::Buffer(cursor)) => cursor.read(data).map_err(|_err| curl::easy::ReadError::Abort), + Some(StreamOrBuffer::Stream(reader)) => reader.read(data), + Some(StreamOrBuffer::Buffer(cursor)) => cursor.read(data), None => Ok(0), // nothing more to read/writer depleted } + .map_err(|err| { + self.io_error = Some(err); + curl::easy::ReadError::Abort + }) } fn header(&mut self, data: &[u8]) -> bool { @@ -579,19 +603,12 @@ pub fn new() -> Worker { if let Err(err) = handle.perform() { let handler = handle.get_mut(); + let err = Err(handler.transfer_error(err)); handler.reset(); if let Some((action, authenticate)) = proxy_auth_action { authenticate.lock().expect("no panics in other threads")(action.erase()).ok(); } - let err = Err(io::Error::new( - if curl_is_retryable(&err) { - std::io::ErrorKind::ConnectionReset - } else { - std::io::ErrorKind::Other - }, - err, - )); handler.receive_body.take(); match (handler.send_header.take(), handler.send_data.take()) { (Some(header), mut data) => { @@ -845,3 +862,47 @@ mod resolve_location_path_tests { ); } } + +#[cfg(test)] +mod tests { + use curl::easy::Handler as _; + + use super::{Handler, StreamOrBuffer, io, pipe}; + + #[test] + fn aborted_uploads_preserve_the_callback_error() { + let (writer, reader) = pipe::unidirectional(1); + writer + .channel + .send(Err(io::Error::other(gix_error::RetryableError::new( + gix_error::NotFoundError::new("custom upload source is unavailable"), + )))) + .expect("the upload reader is alive"); + let mut handler = Handler { + receive_body: Some(StreamOrBuffer::Stream(reader)), + ..Default::default() + }; + assert!(handler.read(&mut [0]).is_err(), "the callback aborts the transfer"); + // CURLE_ABORTED_BY_CALLBACK is the error curl returns after ReadError::Abort. + let err = gix_error::Error::from_error(handler.transfer_error(curl::Error::new(42))); + assert!(err.can_retry(), "the custom upload source retains its retry policy"); + assert!(err.is_not_found(), "other callback classifications survive too"); + assert!(err.downcast_any_ref::().is_some()); + assert!(handler.io_error.is_none(), "a later request cannot reuse this failure"); + } + + #[test] + fn failed_download_writes_preserve_the_pipe_error() { + let (writer, reader) = pipe::unidirectional(1); + drop(reader); + let mut handler = Handler { + send_data: Some(writer), + ..Default::default() + }; + assert_eq!(handler.write(b"response").expect("short writes abort curl"), 0); + // CURLE_WRITE_ERROR is the error curl returns after a short callback write. + let err = handler.transfer_error(curl::Error::new(23)); + assert_eq!(err.kind(), io::ErrorKind::BrokenPipe); + assert!(gix_error::can_retry_lenient(&err)); + } +} diff --git a/gix-transport/src/client/git/blocking_io.rs b/gix-transport/src/client/git/blocking_io.rs index 78fb0934ea8..0859353842d 100644 --- a/gix-transport/src/client/git/blocking_io.rs +++ b/gix-transport/src/client/git/blocking_io.rs @@ -174,7 +174,7 @@ pub mod connect { use std::net::{TcpStream, ToSocketAddrs}; use bstr::BString; - use gix_error::{ErrorExt, ResultExt, message}; + use gix_error::{ResultExt, message}; use super::Connection; use crate::client::git; @@ -185,8 +185,8 @@ pub mod connect { (Some(host), None) => (host.to_owned(), None), (Some(host), Some(port)) => ( host.to_owned(), - Some(port.parse().map_err(|_| { - gix_error::message!("Could not parse {input:?} as virtual host with format [:port]").raise() + Some(port.parse::().or_raise(|| { + gix_error::message!("Could not parse {input:?} as virtual host with format [:port]") })?), ), _ => unreachable!("we expect at least one token, the original string"), From e79baac854f818b717d9500373e30c3525cfc137 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 17 Sep 2026 11:41:30 +0100 Subject: [PATCH 42/51] fix(gix-pack): reject delta instructions that exceed the result size rubber stamp, looked at diff Writing to a byte slice may succeed with a short write. Delta application ignored that byte count, so oversized copy and insert instructions silently truncated their output instead of reporting corrupt data. Split off an output slice of exactly the required size before copying. This also removes two mappings of I/O failures that slice writes cannot produce. A regression covers both copy and insert instructions; all `gix-pack` tests pass. --- gix-pack/src/data/delta.rs | 42 +++++++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/gix-pack/src/data/delta.rs b/gix-pack/src/data/delta.rs index 04782406be4..bea24961ab8 100644 --- a/gix-pack/src/data/delta.rs +++ b/gix-pack/src/data/delta.rs @@ -34,7 +34,7 @@ pub(crate) fn apply(base: &[u8], mut target: &mut [u8], data: &[u8]) -> Result<( let mut i = 0; while let Some(cmd) = data.get(i) { i += 1; - match cmd { + let bytes = match cmd { cmd if cmd & 0b1000_0000 != 0 => { let (mut ofs, mut size): (u32, u32) = (0, 0); if cmd & 0b0000_0001 != 0 { @@ -65,12 +65,8 @@ pub(crate) fn apply(base: &[u8], mut target: &mut [u8], data: &[u8]) -> Result<( let end = ofs .checked_add(size as usize) .ok_or_else(|| corrupt("delta copy range overflows"))?; - std::io::Write::write( - &mut target, - base.get(ofs..end) - .ok_or_else(|| corrupt("delta copy range exceeds base object size"))?, - ) - .map_err(|_| gix_error::CorruptionError::new("Delta copy from base: byte slices must match"))?; + base.get(ofs..end) + .ok_or_else(|| corrupt("delta copy range exceeds base object size"))? } 0 => { return Err(corrupt("delta command 0 is reserved and invalid")); @@ -79,15 +75,18 @@ pub(crate) fn apply(base: &[u8], mut target: &mut [u8], data: &[u8]) -> Result<( let end = i .checked_add(*size as usize) .ok_or_else(|| corrupt("delta insert range overflows"))?; - std::io::Write::write( - &mut target, - data.get(i..end) - .ok_or_else(|| corrupt("delta insert data is truncated"))?, - ) - .map_err(|_| gix_error::CorruptionError::new("Delta copy data: byte slices must match"))?; + let bytes = data + .get(i..end) + .ok_or_else(|| corrupt("delta insert data is truncated"))?; i = end; + bytes } - } + }; + let (out, rest) = target + .split_at_mut_checked(bytes.len()) + .ok_or_else(|| corrupt("delta instructions produced more bytes than promised"))?; + out.copy_from_slice(bytes); + target = rest; } debug_assert_eq!( i, @@ -100,3 +99,18 @@ pub(crate) fn apply(base: &[u8], mut target: &mut [u8], data: &[u8]) -> Result<( Ok(()) } + +#[cfg(test)] +mod tests { + #[test] + fn instructions_cannot_exceed_the_declared_result_size() { + for instructions in [b"\x90\x02".as_slice(), b"\x02ab".as_slice()] { + let err = super::apply(b"ab", &mut [0], instructions) + .expect_err("neither copying nor inserting may truncate the result"); + assert_eq!( + err.to_string(), + "Corrupt delta data: delta instructions produced more bytes than promised" + ); + } + } +} From 0b2a5c40b25db63f57837b8455c537f759a887ed Mon Sep 17 00:00:00 2001 From: Byron Date: Thu, 17 Sep 2026 11:44:33 +0100 Subject: [PATCH 43/51] fix(gix): propagate failed fetch ancestry checks Looked at this in detail to understand how error handling improvements were made. It all makes sense, and teaches me to... not ignore or skip over errors, ever, it's basically a bug unless there is a test that proves it's not a bug. Fetch ref updates discarded commit decoding and traversal setup errors, treating any such failure as permission to force the update. A malformed local or remote commit could therefore overwrite a ref without a force refspec. Traversal errors were also ignored when looking for the ancestor. Propagate those failures with their original causes and context. Check object kinds explicitly to retain the existing behavior for non-commit targets without mistaking corruption for an object-kind mismatch. Assisted-by: GPT 6.0 Co-authored-by: GPT 6.0 --- .../connection/fetch/update_refs/mod.rs | 285 ++++++++++-------- .../connection/fetch/update_refs/tests.rs | 63 ++++ .../connection/fetch/update_refs/update.rs | 6 +- 3 files changed, 219 insertions(+), 135 deletions(-) diff --git a/gix/src/remote/connection/fetch/update_refs/mod.rs b/gix/src/remote/connection/fetch/update_refs/mod.rs index 8305a70b798..93de2c61c63 100644 --- a/gix/src/remote/connection/fetch/update_refs/mod.rs +++ b/gix/src/remote/connection/fetch/update_refs/mod.rs @@ -113,152 +113,169 @@ pub(crate) fn update( } let (mode, edit_index, type_change) = match local { Some(name) => { - let (mode, reflog_message, name, previous_value) = - match repo.try_find_reference(name)? { - Some(existing) => { - if let Some(wt_dirs) = checked_out_branches.get_mut(existing.name()) { - wt_dirs.sort(); - wt_dirs.dedup(); - let mode = Mode::RejectedCurrentlyCheckedOut { - worktree_dirs: wt_dirs.to_owned(), - }; - updates.push(mode.into()); - continue; - } + let (mode, reflog_message, name, previous_value) = match repo.try_find_reference(name)? { + Some(existing) => { + if let Some(wt_dirs) = checked_out_branches.get_mut(existing.name()) { + wt_dirs.sort(); + wt_dirs.dedup(); + let mode = Mode::RejectedCurrentlyCheckedOut { + worktree_dirs: wt_dirs.to_owned(), + }; + updates.push(mode.into()); + continue; + } - match existing - .try_id() - .map_or_else(|| existing.clone().peel_to_id(), Ok) - .map_err(|err| { - gix_error::Error::from(err.and_raise(gix_error::message( + match existing + .try_id() + .map_or_else(|| existing.clone().peel_to_id(), Ok) + .map_err(|err| { + gix_error::Error::from( + err.and_raise(gix_error::message( "Could not peel symbolic local reference to its ID", - ))) - }) - .map(crate::Id::detach) - { - Ok(local_id) => { - let remote_id = match remote_id { - Some(id) => id, - None => { - // we don't allow to go back to unborn state if there is a local reference already present. - // Note that we will be changing it to a symbolic reference just fine. - updates.push(Mode::RejectedToReplaceWithUnborn.into()); - continue; - } - }; - let (mode, reflog_message) = if local_id == remote_id { - (Mode::NoChangeNeeded, "no update will be performed") - } else if let Some(gix_ref::Category::Tag) = existing.name().category() { - if spec.allow_non_fast_forward() { - (Mode::Forced, "updating tag") - } else { - updates.push(Mode::RejectedTagUpdate.into()); - continue; - } + )), + ) + }) + .map(crate::Id::detach) + { + Ok(local_id) => { + let remote_id = match remote_id { + Some(id) => id, + None => { + // we don't allow to go back to unborn state if there is a local reference already present. + // Note that we will be changing it to a symbolic reference just fine. + updates.push(Mode::RejectedToReplaceWithUnborn.into()); + continue; + } + }; + let (mode, reflog_message) = if local_id == remote_id { + (Mode::NoChangeNeeded, "no update will be performed") + } else if let Some(gix_ref::Category::Tag) = existing.name().category() { + if spec.allow_non_fast_forward() { + (Mode::Forced, "updating tag") } else { - let mut force = spec.allow_non_fast_forward(); - let is_fast_forward = match dry_run { - fetch::DryRun::No => { - let ancestors = repo - .find_object(local_id) - .or_raise(|| { - gix_error::message( - "Could not find local commit for fast-forward ancestor check", + updates.push(Mode::RejectedTagUpdate.into()); + continue; + } + } else { + let mut force = spec.allow_non_fast_forward(); + let is_fast_forward = match dry_run { + fetch::DryRun::No => { + let local = repo.find_object(local_id).or_raise(|| { + gix_error::message( + "Could not find local commit for fast-forward ancestor check", + ) + })?; + if local.kind == gix_object::Kind::Commit + && repo.find_header(remote_id)?.kind() == gix_object::Kind::Commit + { + let local_commit_time = local + .into_commit() + .committer() + .or_raise(|| { + gix_error::message( + "Could not read local commit time for fast-forward ancestor check", + ) + })? + .seconds(); + let mut ancestors = remote_id + .to_owned() + .ancestors(&repo.objects) + .sorting( + gix_traverse::commit::simple::Sorting::ByCommitTimeCutoff { + order: Default::default(), + seconds: local_commit_time, + }, ) - })? - .try_into_commit() - .map_err(|_| ()) - .and_then(|c| c.committer().map(|a| a.seconds()).map_err(|_| ())) - .and_then(|local_commit_time| { - remote_id - .to_owned() - .ancestors(&repo.objects) - .sorting( - gix_traverse::commit::simple::Sorting::ByCommitTimeCutoff { - order: Default::default(), - seconds: local_commit_time, - }, + .or_raise(|| { + gix_error::message( + "Could not start fast-forward ancestor check", + ) + })?; + // Stop at either a matching ancestor or the first traversal error. + ancestors + .find(|entry| { + entry.as_ref().map_or(true, |entry| entry.id == local_id) + }) + .transpose() + .or_raise(|| { + gix_error::message( + "Could not traverse commits for fast-forward ancestor check", ) - .map_err(|_| ()) - }); - match ancestors { - Ok(mut ancestors) => { - ancestors.any(|cid| cid.is_ok_and(|c| c.id == local_id)) - } - Err(_) => { - force = true; - false - } - } + })? + .is_some() + } else { + force = true; + false } - fetch::DryRun::Yes => true, - }; - if is_fast_forward { - ( - Mode::FastForward, - matches!(dry_run, fetch::DryRun::Yes) - .then(|| "fast-forward (guessed in dry-run)") - .unwrap_or("fast-forward"), - ) - } else if force { - (Mode::Forced, "forced-update") - } else { - updates.push(Mode::RejectedNonFastForward.into()); - continue; } + fetch::DryRun::Yes => true, }; - ( - mode, - reflog_message, - existing.name().to_owned(), - PreviousValue::MustExistAndMatch(existing.target().into_owned()), - ) - } - Err(err) - if matches!( - err.downcast_any_ref::(), - Some(gix_ref::peel::to_id::Error::FollowToObject( - gix_ref::peel::to_object::Error::Follow(_) - )) - ) => - { - // An unborn reference, always allow it to be changed to whatever the remote wants. - ( - if existing.target().try_name().map(gix_ref::FullNameRef::as_bstr) - == remote.as_target() - { - Mode::NoChangeNeeded - } else { - Mode::Forced - }, - "change unborn ref", - existing.name().to_owned(), - PreviousValue::MustExistAndMatch(existing.target().into_owned()), - ) - } - Err(err) => return Err(err), + if is_fast_forward { + ( + Mode::FastForward, + matches!(dry_run, fetch::DryRun::Yes) + .then(|| "fast-forward (guessed in dry-run)") + .unwrap_or("fast-forward"), + ) + } else if force { + (Mode::Forced, "forced-update") + } else { + updates.push(Mode::RejectedNonFastForward.into()); + continue; + } + }; + ( + mode, + reflog_message, + existing.name().to_owned(), + PreviousValue::MustExistAndMatch(existing.target().into_owned()), + ) } + Err(err) + if matches!( + err.downcast_any_ref::(), + Some(gix_ref::peel::to_id::Error::FollowToObject( + gix_ref::peel::to_object::Error::Follow(_) + )) + ) => + { + // An unborn reference, always allow it to be changed to whatever the remote wants. + ( + if existing.target().try_name().map(gix_ref::FullNameRef::as_bstr) + == remote.as_target() + { + Mode::NoChangeNeeded + } else { + Mode::Forced + }, + "change unborn ref", + existing.name().to_owned(), + PreviousValue::MustExistAndMatch(existing.target().into_owned()), + ) + } + Err(err) => return Err(err), } - None => { - let name = gix_ref::FullName::try_from(name).or_raise(|| { - gix_error::message( - "A remote reference had a name that wasn't considered valid. \ + } + None => { + let name = gix_ref::FullName::try_from(name).or_raise(|| { + gix_error::message( + "A remote reference had a name that wasn't considered valid. \ Corrupt remote repo or insufficient checks on remote?", - ) - })?; - let reflog_msg = match name.category() { - Some(gix_ref::Category::Tag) => "storing tag", - Some(gix_ref::Category::LocalBranch) => "storing head", - _ => "storing ref", - }; - ( - Mode::New, - reflog_msg, - name, - PreviousValue::ExistingMustMatch(new_value_by_remote(remote)?), ) - } - }; + })?; + let reflog_msg = match name.category() { + Some(gix_ref::Category::Tag) => "storing tag", + Some(gix_ref::Category::LocalBranch) => "storing head", + _ => "storing ref", + }; + ( + Mode::New, + reflog_msg, + name, + PreviousValue::ExistingMustMatch(new_value_by_remote(remote)?), + ) + } + }; let new = new_value_by_remote(remote)?; let type_change = match (&previous_value, &new) { diff --git a/gix/src/remote/connection/fetch/update_refs/tests.rs b/gix/src/remote/connection/fetch/update_refs/tests.rs index 8c239dc0312..f95df10c0c6 100644 --- a/gix/src/remote/connection/fetch/update_refs/tests.rs +++ b/gix/src/remote/connection/fetch/update_refs/tests.rs @@ -952,6 +952,69 @@ mod update { } } + #[test] + fn malformed_commits_cannot_force_reference_updates() -> Result { + use gix_object::Write; + + let (repo, _tmp) = repo_rw("two-origins"); + let malformed_commit_id = repo + .objects + .write_buf(gix_object::Kind::Commit, b"malformed commit") + .map_err(gix_error::Exn::into_error)?; + let commit_id = repo.head_id()?; + let name = "refs/remotes/origin/broken"; + for (local_id, remote_id) in [ + (malformed_commit_id, commit_id.into()), + (commit_id.into(), malformed_commit_id), + ] { + repo.reference(name, local_id, PreviousValue::Any, "install local target")?; + let (mappings, specs) = mapping_from_spec(&format!("{remote_id}:{name}"), &repo); + let err = fetch::refs::update( + &repo, + prefixed("fetch"), + &mappings, + &specs, + &[], + fetch::Tags::None, + fetch::DryRun::No, + fetch::WritePackedRefs::Never, + ) + .expect_err("a failed ancestry check must not authorize a forced update"); + assert!(err.is_validation(), "the commit parser's cause survives"); + assert_eq!( + repo.find_reference(name)?.id(), + local_id, + "the failed check cannot change the ref" + ); + } + Ok(()) + } + + #[test] + fn non_commit_targets_can_still_be_updated() -> Result { + let (repo, _tmp) = repo_rw("two-origins"); + let blob_id = repo.write_blob(b"valid blob")?; + let commit_id = repo.head_id()?; + let name = "refs/remotes/origin/non-commit"; + for (local_id, remote_id) in [(blob_id, commit_id), (commit_id, blob_id)] { + repo.reference(name, local_id, PreviousValue::Any, "install local target")?; + let (mappings, specs) = mapping_from_spec(&format!("{remote_id}:{name}"), &repo); + let out = fetch::refs::update( + &repo, + prefixed("fetch"), + &mappings, + &specs, + &[], + fetch::Tags::None, + fetch::DryRun::No, + fetch::WritePackedRefs::Never, + )?; + assert_eq!(out.updates[0].mode, fetch::refs::update::Mode::Forced); + assert_eq!(repo.find_reference(name)?.id(), remote_id); + } + Ok(()) + } + #[test] fn fast_forwards_are_called_out_even_if_force_is_given() { let (repo, _tmp) = repo_rw("two-origins"); diff --git a/gix/src/remote/connection/fetch/update_refs/update.rs b/gix/src/remote/connection/fetch/update_refs/update.rs index d5d74fd09b1..51354c62a68 100644 --- a/gix/src/remote/connection/fetch/update_refs/update.rs +++ b/gix/src/remote/connection/fetch/update_refs/update.rs @@ -24,7 +24,11 @@ pub enum Mode { NoChangeNeeded, /// The old ref's commit was an ancestor of the new one, allowing for a fast-forward without a merge. FastForward, - /// The ref was set to point to the new commit from the remote without taking into consideration its ancestry. + /// The ref was set to the new target from the remote without requiring a fast-forward. + /// + /// An explicitly forced ref-spec (with a leading `+`) permits non-fast-forward updates and replacing existing tags. + /// This mode can also occur without `+` when a non-tag ref's old or new target is not a commit (e.g. a tree or blob), + /// or when the local ref is unborn: in these cases, there is no commit ancestry to compare. Forced, /// A new ref has been created as there was none before. New, From dcf08a412b4bad19250fab104896c8aa648aebe9 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 17 Sep 2026 11:45:41 +0100 Subject: [PATCH 44/51] fix(gix)!: preserve causes across fallible conversions Rubber stamp, looked at diff. This is a cleanup commit. There is going to be considerable cleanup done later as well. Parsers and adapters discarded encoding, integer, date, signature, and object-access failures when replacing them with context. Preserve their concrete causes so classification and downcasting keep working after conversion to `gix::Error` or an I/O error. Return `Exn` from fallible path, command-line, gitdir, and pack-entry conversions where necessary, and adapt their consumers in the same change. Packed-ref and reflog errors retain their parser sources and input details; reflog recovery reports the actual recovery failure. Loose-object verification now propagates lookup and enumeration failures instead of treating every lookup error as retryable or silently skipping failed enumeration. Remove unnecessary UTF-8 conversions for ASCII suffixes and check span bounds before narrowing. Parsers that only return `()` explicitly destructure it. No production `map_err()` closure still discards a wildcard-bound error. Also preserve causes in formatting-only CLI and commit-graph adapters, where stringification previously lost checksum corruption classifications. --- gitoxide-core/src/repository/merge/commit.rs | 4 +- gitoxide-core/src/repository/merge/file.rs | 2 +- gitoxide-core/src/repository/merge/tree.rs | 6 +-- gix-bitmap/src/ewah.rs | 17 +++---- gix-command/src/parse.rs | 11 ++-- gix-command/tests/command/command_line.rs | 38 ++++++++------ gix-commitgraph/src/file/verify.rs | 10 ++-- gix-commitgraph/tests/commitgraph.rs | 19 +++++++ gix-config-value/src/integer.rs | 16 +++--- gix-config/src/parse/from_bytes/mod.rs | 4 +- gix-config/src/parse/mod.rs | 7 ++- gix-discover/src/parse.rs | 12 ++--- gix-discover/tests/discover/parse.rs | 22 ++++++-- gix-fs/src/lib.rs | 3 +- gix-fs/src/stack.rs | 50 +++++++++---------- gix-fs/tests/fs/stack.rs | 15 +++++- gix-index/src/extension/tree/verify.rs | 4 +- gix-index/src/extension/tree/write.rs | 4 +- gix-odb/src/alternate/parse.rs | 15 ++++-- gix-odb/src/lib.rs | 4 -- gix-odb/src/store_impls/dynamic/find.rs | 8 +-- gix-odb/src/store_impls/loose/find.rs | 14 +++--- gix-odb/src/store_impls/loose/verify.rs | 14 +++++- gix-odb/tests/odb/error.rs | 9 +++- gix-odb/tests/odb/store/loose.rs | 12 +++++ gix-pack/src/cache/delta/traverse/resolve.rs | 2 +- gix-pack/src/data/entry/decode.rs | 27 +++++----- gix-pack/src/data/file/decode/entry.rs | 11 ++-- gix-pack/src/data/output/entry/mod.rs | 2 +- gix-pack/src/index/traverse/mod.rs | 2 +- gix-pack/src/multi_index/chunk.rs | 3 +- gix-path/src/convert.rs | 44 ++++++++-------- gix-path/tests/path/convert/mod.rs | 21 ++++++++ gix-ref/src/name.rs | 6 ++- gix-ref/src/store/file/find.rs | 22 ++++---- gix-ref/src/store/file/log/line.rs | 35 +++++++------ .../src/store/file/loose/reference/decode.rs | 2 +- gix-ref/src/store/file/loose/reflog.rs | 4 +- gix-ref/src/store/file/overlay_iter.rs | 6 ++- gix-ref/src/store/packed/buffer.rs | 2 +- gix-ref/src/store/packed/decode/mod.rs | 21 ++++---- gix-ref/src/store/packed/find.rs | 23 ++++++--- gix-ref/src/store/packed/iter.rs | 21 ++++++-- gix-ref/src/store/packed/transaction.rs | 9 ++-- gix-ref/tests/refs/error.rs | 41 +++++++++++++-- gix-revision/src/spec/parse/function.rs | 4 +- gix-tix/spec.md | 2 + gix-tix/src/command.rs | 3 ++ gix-tix/src/command/new.rs | 1 + gix-tix/src/command/rebase.rs | 5 +- gix-tix/src/command/reword.rs | 21 ++++++++ gix-tix/src/edit/mod.rs | 2 +- gix-tix/src/edit/rebase.rs | 3 +- gix-tix/src/history.rs | 5 +- gix-tix/src/lib.rs | 1 + gix-worktree/Cargo.toml | 2 +- gix-worktree/src/stack/delegate.rs | 11 ++-- gix/src/config/tree/keys.rs | 35 +++++++------ gix/src/config/tree/sections/core.rs | 3 +- gix/src/open/repository.rs | 3 +- gix/src/repository/location.rs | 7 +-- gix/tests/gix/config/tree.rs | 5 +- gix/tests/gix/id.rs | 4 +- gix/tests/repository.rs | 8 ++- tests/it/src/commands/blame_copy_royal.rs | 2 +- 65 files changed, 487 insertions(+), 264 deletions(-) diff --git a/gitoxide-core/src/repository/merge/commit.rs b/gitoxide-core/src/repository/merge/commit.rs index 84541a33c9b..65a048b102f 100644 --- a/gitoxide-core/src/repository/merge/commit.rs +++ b/gitoxide-core/src/repository/merge/commit.rs @@ -1,4 +1,4 @@ -use anyhow::{Context, anyhow, bail}; +use anyhow::{Context, bail}; use gix::{ bstr::{BString, ByteSlice}, merge::tree::TreatAsUnresolved, @@ -66,7 +66,7 @@ pub fn commit( written += 1; repo.write(tree) }) - .map_err(|err| anyhow!("{err}"))?; + .map_err(gix::Exn::into_error)?; writeln!(out, "{tree_id} (wrote {written} trees)")?; } diff --git a/gitoxide-core/src/repository/merge/file.rs b/gitoxide-core/src/repository/merge/file.rs index 4e6b70f1753..34c085e46f7 100644 --- a/gitoxide-core/src/repository/merge/file.rs +++ b/gitoxide-core/src/repository/merge/file.rs @@ -79,7 +79,7 @@ pub fn file( let (pick, resolution) = platform.merge(&mut buf, labels, &repo.command_context()?)?; let buf = platform .buffer_by_pick(pick) - .map_err(|_| anyhow!("Participating object was too large"))? + .map_err(|()| anyhow!("Participating object was too large"))? .unwrap_or(&buf); out.write_all(buf)?; diff --git a/gitoxide-core/src/repository/merge/tree.rs b/gitoxide-core/src/repository/merge/tree.rs index 78e6e1166ae..d91fab9da8b 100644 --- a/gitoxide-core/src/repository/merge/tree.rs +++ b/gitoxide-core/src/repository/merge/tree.rs @@ -14,7 +14,7 @@ pub(super) mod function { use std::collections::BTreeSet; - use anyhow::{Context, anyhow, bail}; + use anyhow::{Context, bail}; use gix::{ bstr::{BString, ByteSlice}, merge::tree::TreatAsUnresolved, @@ -100,7 +100,7 @@ pub(super) mod function { written += 1; repo.write(tree) }) - .map_err(|err| anyhow!("{err}"))?; + .map_err(gix::Exn::into_error)?; writeln!(out, "{tree_id} (wrote {written} trees)")?; tree_id }; @@ -139,7 +139,7 @@ pub(super) mod function { fn persist_in_memory_objects(repo: &mut gix::Repository) -> anyhow::Result<()> { let objects = repo.objects.take_object_memory().expect("always write in memory first"); for (_id, (kind, data)) in objects.iter() { - repo.write_buf(*kind, data).map_err(|err| anyhow!("{err}"))?; + repo.write_buf(*kind, data).map_err(gix::Exn::into_error)?; } Ok(()) } diff --git a/gix-bitmap/src/ewah.rs b/gix-bitmap/src/ewah.rs index 090b83eb3d4..079f8797a78 100644 --- a/gix-bitmap/src/ewah.rs +++ b/gix-bitmap/src/ewah.rs @@ -36,6 +36,8 @@ pub fn decode(data: &[u8]) -> Result<(Vec, &[u8]), gix_error::Exn std::io::Result<()> { - let len: u32 = self.bits.len().try_into().map_err(|_| { - std::io::Error::new(std::io::ErrorKind::InvalidInput, "bit word count exceeds u32::MAX") - })?; - let rlw: u32 = self.rlw.try_into().map_err(|_| { - std::io::Error::new( - std::io::ErrorKind::InvalidInput, - "run length word offset exceeds u32::MAX", - ) - })?; + let len = u32::try_from(self.bits.len()) + .or_raise(|| ValidationError::new("bit word count exceeds u32::MAX")) + .map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidInput, err.into_error()))?; + let rlw = u32::try_from(self.rlw) + .or_raise(|| ValidationError::new("run length word offset exceeds u32::MAX")) + .map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidInput, err.into_error()))?; out.write_all(&self.num_bits.to_be_bytes())?; out.write_all(&len.to_be_bytes())?; diff --git a/gix-command/src/parse.rs b/gix-command/src/parse.rs index f1b420b18b5..9fd0de63076 100644 --- a/gix-command/src/parse.rs +++ b/gix-command/src/parse.rs @@ -1,6 +1,7 @@ use std::ffi::OsString; use bstr::{BStr, BString}; +use gix_error::{Exn, ResultExt}; /// The result of [`command_line()`]. #[derive(Debug, Clone, PartialEq, Eq)] @@ -71,7 +72,7 @@ struct Word { /// shell identifier. Assignment-only input is rejected because it contains no command to execute. Environment /// assignment names are strings, while their values, the command, and arguments are converted losslessly to OS /// strings or rejected if the platform cannot represent them. -pub fn command_line(input: &BStr) -> Result { +pub fn command_line(input: &BStr) -> Result> { let mut words = parse_words(input)?; let assignment_count = words .iter() @@ -89,7 +90,7 @@ pub fn command_line(input: &BStr) -> Result { into_os_string(word.value[separator + 1..].to_owned().into())?, )) }) - .collect::>()?; + .collect::>>()?; Ok(Outcome { env, command, @@ -97,7 +98,7 @@ pub fn command_line(input: &BStr) -> Result { }) } -pub(crate) fn arguments(input: &BStr) -> Result, Error> { +pub(crate) fn arguments(input: &BStr) -> Result, Exn> { parse_words(input)? .into_iter() .map(|word| into_os_string(word.value)) @@ -197,8 +198,8 @@ fn push_unquoted( value.push(byte); } -fn into_os_string(value: BString) -> Result { +fn into_os_string(value: BString) -> Result> { gix_path::try_from_bstring(value) .map(std::path::PathBuf::into_os_string) - .map_err(|_| Error::UnrepresentableOsString) + .or_raise(|| Error::UnrepresentableOsString) } diff --git a/gix-command/tests/command/command_line.rs b/gix-command/tests/command/command_line.rs index db673c8909b..950e74c4c41 100644 --- a/gix-command/tests/command/command_line.rs +++ b/gix-command/tests/command/command_line.rs @@ -8,8 +8,8 @@ fn words_are_split_without_expansion() -> gix_testtools::Result { command_line( r#"cmd 'single quoted' "double \"quoted\"" escaped\ word "kept\q" "" # ignored next"#, - ), - Ok(Outcome { + )?, + Outcome { env: Vec::new(), command: "cmd".into(), args: args(&[ @@ -20,7 +20,7 @@ next"#, "", "next" ]), - }) + } ); assert_eq!( command_line("cmd one\\\ntwo")?.args, @@ -64,30 +64,38 @@ fn invalid_assignment_names_are_arguments() -> gix_testtools::Result { #[test] fn unterminated_quotes_are_rejected() { - assert_eq!(command_line("cmd '"), Err(parse::Error::MissingClosingQuote)); - assert_eq!(command_line("cmd \""), Err(parse::Error::MissingClosingQuote)); - assert_eq!(command_line("cmd \"\\"), Err(parse::Error::MissingClosingQuote)); + for input in ["cmd '", "cmd \"", "cmd \"\\"] { + assert_eq!( + *parse::command_line(input.into()).expect_err("unterminated quote"), + parse::Error::MissingClosingQuote + ); + } } #[test] fn dangling_unquoted_escape_is_rejected() { - assert_eq!(command_line("cmd arg\\"), Err(parse::Error::MissingEscapedByte)); + assert_eq!( + *parse::command_line("cmd arg\\".into()).expect_err("dangling escape"), + parse::Error::MissingEscapedByte + ); } #[test] fn a_command_is_required() { for input in ["", " ", "\t\n", "# comment", "\\\n", "tool=name", "FOO=one BAR=two"] { - assert_eq!(command_line(input), Err(parse::Error::MissingCommand), "{input:?}"); + assert_eq!( + *parse::command_line(input.into()).expect_err("no command"), + parse::Error::MissingCommand, + "{input:?}" + ); } } #[test] fn parse_errors_retain_their_classification() { - use gix_error::ErrorExt; - for input in ["cmd '", "cmd arg\\", "FOO=one"] { - let cause = command_line(input).expect_err("the command line is invalid"); - let err = cause.raise(); + let err = parse::command_line(input.into()).expect_err("the command line is invalid"); + let cause = *err; assert!(err.is_validation(), "invalid commands classify as validation failures"); let err = err.into_error(); assert!(err.is_validation(), "conversion preserves the classification"); @@ -106,7 +114,7 @@ fn non_utf8_input_is_preserved() -> gix_testtools::Result { use std::os::unix::ffi::OsStringExt; assert_eq!( - parse::command_line(b"FOO=\xff cmd \xfe".as_bstr())?, + parse::command_line(b"FOO=\xff cmd \xfe".as_bstr()).map_err(gix_error::Exn::into_error)?, Outcome { env: vec![("FOO".into(), OsString::from_vec(vec![0xff]))], command: "cmd".into(), @@ -116,8 +124,8 @@ fn non_utf8_input_is_preserved() -> gix_testtools::Result { Ok(()) } -fn command_line(input: &str) -> Result { - parse::command_line(input.into()) +fn command_line(input: &str) -> Result { + parse::command_line(input.into()).map_err(gix_error::Exn::into_error) } fn args(input: &[&str]) -> Vec { diff --git a/gix-commitgraph/src/file/verify.rs b/gix-commitgraph/src/file/verify.rs index 73571946370..19ff767a84a 100644 --- a/gix-commitgraph/src/file/verify.rs +++ b/gix-commitgraph/src/file/verify.rs @@ -104,17 +104,15 @@ impl File { /// /// Return the actual checksum on success or [`Exn`] if there is a mismatch. pub fn verify_checksum(&self) -> Result> { - // Even though we could use gix_hash::bytes_of_file(…), this would require extending our - // Error type to support io::Error. As we only gain progress, there probably isn't much value - // as these files are usually small enough to process them in less than a second, even for the large ones. - // But it's possible, once a progress instance is passed. let data_len_without_trailer = self.data.len() - self.hash_len; let mut hasher = gix_hash::hasher(self.object_hash()); hasher.update(&self.data[..data_len_without_trailer]); let actual = hasher .try_finalize() - .map_err(|e| message!("failed to hash commit graph file: {e}").raise())?; - actual.verify(self.checksum()).map_err(|e| message!("{e}").raise())?; + .or_raise(|| message("failed to hash commit graph file"))?; + actual + .verify(self.checksum()) + .or_raise(|| message("commit-graph checksum does not match"))?; Ok(actual) } } diff --git a/gix-commitgraph/tests/commitgraph.rs b/gix-commitgraph/tests/commitgraph.rs index 5d9142a9d19..fb54c578b3f 100644 --- a/gix-commitgraph/tests/commitgraph.rs +++ b/gix-commitgraph/tests/commitgraph.rs @@ -23,6 +23,25 @@ fn missing_path_is_not_found() -> gix_testtools::Result { Ok(()) } +#[test] +fn checksum_mismatches_retain_their_classification() -> gix_testtools::Result { + let repo = gix_testtools::scripted_fixture_writable("single_commit.sh")?; + let mut data = std::fs::read(repo.path().join(".git/objects/info/commit-graph"))?; + *data.last_mut().expect("the graph has a checksum trailer") ^= 1; + // Git can make its graph read-only; corrupt a separate file. + let path = repo.path().join("corrupt-commit-graph"); + std::fs::write(&path, data)?; + + let graph = gix_commitgraph::File::at(path).map_err(gix_error::Exn::into_error)?; + let err = graph.verify_checksum().expect_err("the checksum no longer matches"); + assert!(err.is_corrupted(), "a checksum mismatch is corruption"); + assert!( + err.into_error().is_corrupted(), + "conversion preserves the checksum failure" + ); + Ok(()) +} + pub fn check_common(cg: &Graph, expected: &HashMap) { cg.verify_integrity(|_| Ok::<_, gix_error::Message>(())) .expect("graph is valid"); diff --git a/gix-config-value/src/integer.rs b/gix-config-value/src/integer.rs index 82346391e37..a1b33b7f45a 100644 --- a/gix-config-value/src/integer.rs +++ b/gix-config-value/src/integer.rs @@ -1,6 +1,6 @@ use std::{borrow::Cow, fmt::Display, str::FromStr}; -use bstr::{BStr, BString}; +use bstr::{BStr, BString, ByteSlice}; use gix_error::{ErrorExt, ResultExt, ValidationError}; use crate::Integer; @@ -192,12 +192,7 @@ impl FromStr for Suffix { type Err = (); fn from_str(s: &str) -> Result { - match s { - "k" | "K" => Ok(Self::Kibi), - "m" | "M" => Ok(Self::Mebi), - "g" | "G" => Ok(Self::Gibi), - _ => Err(()), - } + Self::try_from(BStr::new(s)) } } @@ -205,6 +200,11 @@ impl TryFrom<&BStr> for Suffix { type Error = (); fn try_from(s: &BStr) -> Result { - Self::from_str(std::str::from_utf8(s).map_err(|_| ())?) + match s.as_bytes() { + b"k" | b"K" => Ok(Self::Kibi), + b"m" | b"M" => Ok(Self::Mebi), + b"g" | b"G" => Ok(Self::Gibi), + _ => Err(()), + } } } diff --git a/gix-config/src/parse/from_bytes/mod.rs b/gix-config/src/parse/from_bytes/mod.rs index a8585bdbe9f..c51e8eb1a2a 100644 --- a/gix-config/src/parse/from_bytes/mod.rs +++ b/gix-config/src/parse/from_bytes/mod.rs @@ -39,7 +39,7 @@ pub(crate) fn from_bytes(mut input: &[u8], dispatch: &mut dyn FnMut(Event)) -> R } else if !input.starts_with(b"[") { let mut node = ParseNode::SectionHeader; key_value_pair(backing, &mut input, &mut node, dispatch) - .map_err(|_| Error::parse(newlines_from(backing, input), node, input.as_bstr().into()))?; + .map_err(|()| Error::parse(newlines_from(backing, input), node, input.as_bstr().into()))?; } if input.len() == before.len() { break; @@ -53,7 +53,7 @@ pub(crate) fn from_bytes(mut input: &[u8], dispatch: &mut dyn FnMut(Event)) -> R let mut node = ParseNode::SectionHeader; while !input.is_empty() { section(backing, &mut input, &mut node, dispatch) - .map_err(|_| Error::parse(newlines_from(backing, input), node, input.as_bstr().into()))?; + .map_err(|()| Error::parse(newlines_from(backing, input), node, input.as_bstr().into()))?; } Ok(()) } diff --git a/gix-config/src/parse/mod.rs b/gix-config/src/parse/mod.rs index 898cc1d12c1..b3e4bb9228f 100644 --- a/gix-config/src/parse/mod.rs +++ b/gix-config/src/parse/mod.rs @@ -103,9 +103,12 @@ impl Span { } pub(crate) fn range(start: usize, len: usize) -> Result { + if start > u32::MAX as usize || len > u32::MAX as usize { + return Err(span::error()); + } Ok(Span { - start: start.try_into().map_err(|_| span::error())?, - len: len.try_into().map_err(|_| span::error())?, + start: start as u32, + len: len as u32, }) } diff --git a/gix-discover/src/parse.rs b/gix-discover/src/parse.rs index bb8b007206d..f0fdbf189b0 100644 --- a/gix-discover/src/parse.rs +++ b/gix-discover/src/parse.rs @@ -1,21 +1,21 @@ use std::path::PathBuf; use bstr::ByteSlice; +use gix_error::{ErrorExt, ResultExt}; /// Parse typical `gitdir` files as seen in worktrees and submodules. -pub fn gitdir(input: &[u8]) -> Result { +pub fn gitdir(input: &[u8]) -> Result> { let path = input .strip_prefix(b"gitdir: ") .ok_or_else(|| gix_error::ValidationError::new_with_input("Format should be 'gitdir: ', but got", input))? .as_bstr(); let path = path.trim_end().as_bstr(); if path.is_empty() { - return Err(gix_error::ValidationError::new_with_input( - "Format should be 'gitdir: ', but got", - input, - )); + return Err( + gix_error::ValidationError::new_with_input("Format should be 'gitdir: ', but got", input).raise(), + ); } Ok(gix_path::try_from_bstr(path) - .map_err(|_| gix_error::ValidationError::new_with_input("Couldn't decode input as UTF8", input))? + .or_raise(|| gix_error::ValidationError::new_with_input("Couldn't decode input as UTF8", input))? .into_owned()) } diff --git a/gix-discover/tests/discover/parse.rs b/gix-discover/tests/discover/parse.rs index b8d5fb9ef79..0c7f4041f8e 100644 --- a/gix-discover/tests/discover/parse.rs +++ b/gix-discover/tests/discover/parse.rs @@ -4,12 +4,24 @@ use gix_discover::parse; #[test] fn valid() -> crate::Result { - assert_eq!(parse::gitdir(b"gitdir: a")?, Path::new("a")); - assert_eq!(parse::gitdir(b"gitdir: relative/path")?, Path::new("relative/path")); - assert_eq!(parse::gitdir(b"gitdir: ./relative/path")?, Path::new("./relative/path")); - assert_eq!(parse::gitdir(b"gitdir: /absolute/path\n")?, Path::new("/absolute/path")); assert_eq!( - parse::gitdir(b"gitdir: C:/hello/there\r\n")?, + parse::gitdir(b"gitdir: a").map_err(gix_error::Exn::into_error)?, + Path::new("a") + ); + assert_eq!( + parse::gitdir(b"gitdir: relative/path").map_err(gix_error::Exn::into_error)?, + Path::new("relative/path") + ); + assert_eq!( + parse::gitdir(b"gitdir: ./relative/path").map_err(gix_error::Exn::into_error)?, + Path::new("./relative/path") + ); + assert_eq!( + parse::gitdir(b"gitdir: /absolute/path\n").map_err(gix_error::Exn::into_error)?, + Path::new("/absolute/path") + ); + assert_eq!( + parse::gitdir(b"gitdir: C:/hello/there\r\n").map_err(gix_error::Exn::into_error)?, Path::new("C:/hello/there") ); diff --git a/gix-fs/src/lib.rs b/gix-fs/src/lib.rs index fb03491dd72..cf166784088 100644 --- a/gix-fs/src/lib.rs +++ b/gix-fs/src/lib.rs @@ -13,7 +13,8 @@ //! //! let components = "src/lib.rs" //! .to_normal_path_components() -//! .collect::, _>>()?; +//! .collect::, _>>() +//! .map_err(|err| err.into_error())?; //! assert_eq!( //! components //! .into_iter() diff --git a/gix-fs/src/stack.rs b/gix-fs/src/stack.rs index 80984feaee8..4d3b9168a61 100644 --- a/gix-fs/src/stack.rs +++ b/gix-fs/src/stack.rs @@ -4,65 +4,63 @@ use std::{ }; use bstr::{BStr, BString, ByteSlice}; +use gix_error::{ErrorExt, Exn, ValidationError}; use crate::Stack; /// Obtain an iterator over `OsStr`-components which are normal, none-relative and not absolute. pub trait ToNormalPathComponents { /// Return an iterator over the normal components of a path, without the separator. - fn to_normal_path_components(&self) -> impl Iterator>; + fn to_normal_path_components(&self) -> impl Iterator>>; } impl ToNormalPathComponents for &Path { - fn to_normal_path_components(&self) -> impl Iterator> { - self.components().map(|c| component_to_os_str(c, self)) + fn to_normal_path_components(&self) -> impl Iterator>> { + self.components().map(|c| component_to_os_str(c, self.display())) } } impl ToNormalPathComponents for PathBuf { - fn to_normal_path_components(&self) -> impl Iterator> { - self.components().map(|c| component_to_os_str(c, self)) + fn to_normal_path_components(&self) -> impl Iterator>> { + self.components().map(|c| component_to_os_str(c, self.display())) } } -fn component_to_os_str<'a>( - component: Component<'a>, - path_with_component: &Path, -) -> Result<&'a OsStr, gix_error::ValidationError> { +fn component_to_os_str( + component: Component<'_>, + path_with_component: impl std::fmt::Display, +) -> Result<&OsStr, Exn> { match component { Component::Normal(os_str) => Ok(os_str), - _ => Err(gix_error::ValidationError::new(format!( - "Input path \"{}\" contains relative or absolute components", - path_with_component.display() - ))), + _ => Err(ValidationError::new(format!( + "Input path \"{path_with_component}\" contains relative or absolute components" + )) + .raise()), } } impl ToNormalPathComponents for &BStr { - fn to_normal_path_components(&self) -> impl Iterator> { + fn to_normal_path_components(&self) -> impl Iterator>> { self.split(|b| *b == b'/') .filter_map(|c| bytes_component_to_os_str(c, self)) } } impl ToNormalPathComponents for &str { - fn to_normal_path_components(&self) -> impl Iterator> { + fn to_normal_path_components(&self) -> impl Iterator>> { self.split('/') .filter_map(|c| bytes_component_to_os_str(c.as_bytes(), (*self).into())) } } impl ToNormalPathComponents for &BString { - fn to_normal_path_components(&self) -> impl Iterator> { + fn to_normal_path_components(&self) -> impl Iterator>> { self.split(|b| *b == b'/') .filter_map(|c| bytes_component_to_os_str(c, self.as_bstr())) } } -fn bytes_component_to_os_str<'a>( - component: &'a [u8], - path: &BStr, -) -> Option> { +fn bytes_component_to_os_str<'a>(component: &'a [u8], path: &BStr) -> Option>> { if component.is_empty() { return None; } @@ -71,10 +69,7 @@ fn bytes_component_to_os_str<'a>( Err(err) => return Some(Err(err)), }; let component = component.components().next()?; - Some(component_to_os_str( - component, - gix_path::try_from_byte_slice(path.as_ref()).ok()?, - )) + Some(component_to_os_str(component, path)) } /// Access @@ -165,7 +160,10 @@ impl Stack { break; } } - Err(err) => return Err(std::io::Error::other(format!("{err}"))), + Err(_) => { + let err = components.next().expect("just peeked").expect_err("peeked an error"); + return Err(std::io::Error::other(err.into_error())); + } } } @@ -199,7 +197,7 @@ impl Stack { } while let Some(comp) = components.next() { - let comp = comp.map_err(std::io::Error::other)?; + let comp = comp.map_err(|err| std::io::Error::other(err.into_error()))?; let is_last_component = components.peek().is_none(); let parent_is_directory = self.current_is_directory; self.current_is_directory = !is_last_component; diff --git a/gix-fs/tests/fs/stack.rs b/gix-fs/tests/fs/stack.rs index fc1c042749b..eb6b31a06af 100644 --- a/gix-fs/tests/fs/stack.rs +++ b/gix-fs/tests/fs/stack.rs @@ -229,7 +229,13 @@ fn relative_components_are_invalid() { let mut s = Stack::new(root.clone()); let mut r = Record::default(); - let err = s.make_relative_path_current(p("a/.."), &mut r).unwrap_err(); + let err = s + .make_relative_path_current(p("a/.."), &mut r) + .expect_err("parent components are forbidden"); + assert!( + gix_error::classify(&err).is_validation(), + "the I/O wrapper retains the cause" + ); assert_eq!( err.to_string(), format!( @@ -265,6 +271,13 @@ fn relative_components_are_invalid() { }, "the terminal component is validated again, but its parent stays cached" ); + let err = s + .make_relative_path_current(p("a/.."), &mut r) + .expect_err("parent components are also forbidden when reusing a path prefix"); + assert!( + gix_error::classify(&err).is_validation(), + "peeked errors retain their cause too" + ); } #[test] diff --git a/gix-index/src/extension/tree/verify.rs b/gix-index/src/extension/tree/verify.rs index 6ee52e7fd3a..83af46bec61 100644 --- a/gix-index/src/extension/tree/verify.rs +++ b/gix-index/src/extension/tree/verify.rs @@ -55,9 +55,9 @@ impl Tree { } children .binary_search_by(|e| e.name.as_bstr().cmp(entry.filename)) - .map_err(|_| { + .map_err(|position| { gix_error::CorruptionError::new(format!( - "The entry {} at path '{}' in parent tree {parent_id} wasn't found in the nodes children, making it incomplete", + "The entry {} at path '{}' in parent tree {parent_id} wasn't found at child position {position}, making it incomplete", entry.oid, entry.filename )) .raise() diff --git a/gix-index/src/extension/tree/write.rs b/gix-index/src/extension/tree/write.rs index 9341cb7e752..a818053dd92 100644 --- a/gix-index/src/extension/tree/write.rs +++ b/gix-index/src/extension/tree/write.rs @@ -1,4 +1,5 @@ use crate::extension::{Tree, tree}; +use gix_error::{CorruptionError, ResultExt}; impl Tree { /// Serialize this instance to `out`. @@ -40,7 +41,8 @@ impl Tree { out.write_all(&signature)?; out.write_all( &u32::try_from(entries.len()) - .map_err(|_| std::io::Error::new(std::io::ErrorKind::InvalidData, "tree extension exceeds 4GB"))? + .or_raise(|| CorruptionError::new("tree extension exceeds 4GB")) + .map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidData, err.into_error()))? .to_be_bytes(), )?; out.write_all(&entries)?; diff --git a/gix-odb/src/alternate/parse.rs b/gix-odb/src/alternate/parse.rs index 3c83cc119f2..3940be55b5d 100644 --- a/gix-odb/src/alternate/parse.rs +++ b/gix-odb/src/alternate/parse.rs @@ -2,16 +2,16 @@ #[derive(Debug)] #[allow(missing_docs)] pub enum Error { - PathConversion(Vec), + PathConversion { path: Vec, source: gix_error::Error }, } impl std::fmt::Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Error::PathConversion(bytes) => write!( + Error::PathConversion { path, .. } => write!( f, "Could not obtain an object path for the alternate directory '{}'", - String::from_utf8_lossy(bytes) + String::from_utf8_lossy(path) ), } } @@ -19,7 +19,9 @@ impl std::fmt::Display for Error { impl std::error::Error for Error { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - Some(&crate::INVALID_INPUT) + match self { + Error::PathConversion { source, .. } => Some(source), + } } } @@ -59,7 +61,10 @@ pub(super) mod function { }; out.push( gix_path::try_from_bstr(path) - .map_err(|_| Error::PathConversion(original.to_vec()))? + .map_err(|err| Error::PathConversion { + path: original.to_vec(), + source: err.into_error(), + })? .into_owned(), ); } diff --git a/gix-odb/src/lib.rs b/gix-odb/src/lib.rs index a58649d93c7..051f0ce6375 100644 --- a/gix-odb/src/lib.rs +++ b/gix-odb/src/lib.rs @@ -69,10 +69,6 @@ static NOT_FOUND: gix_error::NotFoundError = gix_error::NotFoundError { static CORRUPTION: gix_error::CorruptionError = gix_error::CorruptionError { message: std::borrow::Cow::Borrowed("Object database is malformed or inconsistent"), }; -static INVALID_INPUT: gix_error::ValidationError = gix_error::ValidationError { - message: std::borrow::Cow::Borrowed("Invalid object database input"), - input: None, -}; // Global sources must be synchronized even when the `parallel` feature is disabled. static RETRYABLE: std::sync::LazyLock = std::sync::LazyLock::new(|| { gix_error::RetryableError::new(gix_error::message("Object database operation can be retried")) diff --git a/gix-odb/src/store_impls/dynamic/find.rs b/gix-odb/src/store_impls/dynamic/find.rs index 516a9047df3..0f7543f1ca1 100644 --- a/gix-odb/src/store_impls/dynamic/find.rs +++ b/gix-odb/src/store_impls/dynamic/find.rs @@ -17,7 +17,7 @@ pub(crate) mod error { Pack(gix_error::Error), LoadIndex(crate::store::load_index::Error), LoadPack(std::io::Error), - EntryType(gix_error::CorruptionError), + EntryType(gix_error::Error), DeltaBaseRecursionLimit { /// the maximum recursion depth we encountered. max_depth: usize, @@ -110,9 +110,9 @@ pub(crate) mod error { } } - impl From for Error { - fn from(err: gix_error::CorruptionError) -> Self { - Error::EntryType(err) + impl From> for Error { + fn from(err: gix_error::Exn) -> Self { + Error::EntryType(err.into_error()) } } diff --git a/gix-odb/src/store_impls/loose/find.rs b/gix-odb/src/store_impls/loose/find.rs index 3a1cec490b7..a053e7f4030 100644 --- a/gix-odb/src/store_impls/loose/find.rs +++ b/gix-odb/src/store_impls/loose/find.rs @@ -1,6 +1,6 @@ use std::{cmp::Ordering, collections::HashSet, io, path::PathBuf}; -use gix_error::{CorruptionError, ResourceExhaustionError, ResourceExhaustionKind}; +use gix_error::{CorruptionError, ErrorExt, ResourceExhaustionError, ResourceExhaustionKind}; use crate::store_impls::loose::{HEADER_MAX_SIZE, Store, hash_path}; @@ -205,12 +205,14 @@ impl Store { let (kind, size, header_size) = gix_object::decode::loose_header(&header[..consumed_out])?; self.ensure_in_alloc_limit(size)?; - let size_usize = usize::try_from(size).map_err(|_| Error::OutOfMemory { + let size_usize = usize::try_from(size).map_err(|err| Error::OutOfMemory { size, - source: gix_error::Error::from_error(ResourceExhaustionError::new( - ResourceExhaustionKind::AllocationFailure, - "The object size cannot be represented in memory", - )), + source: err + .and_raise(ResourceExhaustionError::new( + ResourceExhaustionKind::AllocationFailure, + "The object size cannot be represented in memory", + )) + .into_error(), })?; let decompressed_body_prefix_len = consumed_out.checked_sub(header_size).ok_or(Error::SizeMismatch { actual: consumed_out as u64, diff --git a/gix-odb/src/store_impls/loose/verify.rs b/gix-odb/src/store_impls/loose/verify.rs index b98c34ed272..04a190d08c5 100644 --- a/gix-odb/src/store_impls/loose/verify.rs +++ b/gix-odb/src/store_impls/loose/verify.rs @@ -13,6 +13,11 @@ pub mod integrity { #[derive(Debug)] #[allow(missing_docs)] pub enum Error { + Iteration(crate::loose::iter::Error), + ObjectLookup { + source: crate::loose::find::Error, + id: gix_hash::ObjectId, + }, ObjectDecode { source: gix_error::ValidationError, kind: gix_object::Kind, @@ -34,6 +39,8 @@ pub mod integrity { impl std::fmt::Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { + Error::Iteration(_) => f.write_str("Could not enumerate loose objects"), + Error::ObjectLookup { id, .. } => write!(f, "Could not read loose object {id}"), Error::ObjectDecode { kind, id, .. } => write!(f, "{kind} object {id} could not be decoded"), Error::ObjectHasher { kind, expected, .. } => { write!(f, "{kind} object {expected} could not be hashed") @@ -50,6 +57,8 @@ pub mod integrity { impl std::error::Error for Error { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { match self { + Error::Iteration(err) => Some(err), + Error::ObjectLookup { source, .. } => Some(source), Error::ObjectDecode { source, .. } => Some(source), Error::ObjectHasher { source, .. } => Some(source), Error::ObjectEncodeMismatch { source, .. } => Some(source), @@ -97,10 +106,11 @@ impl Store { let start = Instant::now(); let mut progress = progress.add_child_with_id("Validating".into(), integrity::ProgressId::LooseObjects.into()); progress.init(None, gix_features::progress::count("loose objects")); - for id in self.iter().filter_map(Result::ok) { + for id in self.iter() { + let id = id.map_err(integrity::Error::Iteration)?; let object = self .try_find(&id, &mut buf) - .map_err(|_| integrity::Error::Retry)? + .map_err(|source| integrity::Error::ObjectLookup { source, id })? .ok_or(integrity::Error::Retry)?; gix_object::compute_hash(self.object_hash, object.kind, object.data) .map_err(|source| integrity::Error::ObjectHasher { diff --git a/gix-odb/tests/odb/error.rs b/gix-odb/tests/odb/error.rs index c3578a9c2f0..56306774bf7 100644 --- a/gix-odb/tests/odb/error.rs +++ b/gix-odb/tests/odb/error.rs @@ -67,7 +67,8 @@ fn custom_wrappers_retain_classified_errors_and_branches() { let corrupt = || Error::from_error(CorruptionError::new("malformed object")); for err in [ loose::find::Error::Decode(corrupt()).raise_erased(), - store::find::Error::EntryType(CorruptionError::new("invalid pack entry type")).raise_erased(), + store::find::Error::EntryType(Error::from_error(CorruptionError::new("invalid pack entry type"))) + .raise_erased(), store::verify::integrity::Error::MultiIndexIntegrity(corrupt()).raise_erased(), store::verify::integrity::Error::IndexIntegrity(corrupt()).raise_erased(), store::verify::integrity::Error::IndexOpen(corrupt()).raise_erased(), @@ -118,7 +119,11 @@ fn custom_leaf_errors_expose_their_classification() { ); assert!(err.into_error().is_corrupted()); } - let err = alternate::Error::Parse(alternate::parse::Error::PathConversion(vec![0xff])).raise(); + let err = alternate::Error::Parse(alternate::parse::Error::PathConversion { + path: vec![0xff], + source: Error::from_error(gix_error::ValidationError::new("invalid path encoding")), + }) + .raise(); assert!(err.is_validation(), "unrepresentable alternate paths are invalid input"); assert!(err.into_error().is_validation()); diff --git a/gix-odb/tests/odb/store/loose.rs b/gix-odb/tests/odb/store/loose.rs index 0802c4b8875..2075deb961b 100644 --- a/gix-odb/tests/odb/store/loose.rs +++ b/gix-odb/tests/odb/store/loose.rs @@ -321,6 +321,18 @@ mod find { ); assert!(db.try_find(&id, &mut buf).is_err(), "it must not panic"); assert!(db.try_header(&id).is_err(), "it must not panic"); + let err = gix_error::Error::from_error( + db.verify_integrity( + &mut gix_features::progress::Discard, + &std::sync::atomic::AtomicBool::new(false), + ) + .expect_err("verification must report the invalid object"), + ); + assert!(!err.can_retry(), "corrupt objects do not become valid when retried"); + assert!( + err.downcast_any_ref::().is_some(), + "verification preserves the original lookup error" + ); Ok(()) } diff --git a/gix-pack/src/cache/delta/traverse/resolve.rs b/gix-pack/src/cache/delta/traverse/resolve.rs index bc3c53d1ace..eb60a016002 100644 --- a/gix-pack/src/cache/delta/traverse/resolve.rs +++ b/gix-pack/src/cache/delta/traverse/resolve.rs @@ -648,7 +648,7 @@ fn decompress_all_at_once_with( fn decoded_size_limited(size: u64, alloc_limit_bytes: Option) -> Result { let size: usize = size .try_into() - .map_err(|_| allocation_error(ResourceExhaustionKind::AllocationFailure))?; + .map_err(|err| allocation_error(ResourceExhaustionKind::AllocationFailure).chain(err))?; if alloc_limit_bytes.is_some_and(|limit| size > limit) { return Err(allocation_error(ResourceExhaustionKind::AllocationLimit)); } diff --git a/gix-pack/src/data/entry/decode.rs b/gix-pack/src/data/entry/decode.rs index bc2b5459d7f..51823979193 100644 --- a/gix-pack/src/data/entry/decode.rs +++ b/gix-pack/src/data/entry/decode.rs @@ -1,3 +1,4 @@ +use gix_error::{ErrorExt, Exn, ResultExt}; use std::io; use gix_features::decode::leb64_from_read; @@ -16,7 +17,7 @@ impl data::Entry { d: &[u8], pack_offset: data::Offset, object_hash: gix_hash::Kind, - ) -> Result { + ) -> Result> { let (type_id, size, mut consumed) = parse_header_info(d)?; let hash_len = object_hash.len_in_bytes(); @@ -37,7 +38,7 @@ impl data::Entry { .ok_or_else(|| corrupt("ref-delta base object id"))?; let delta = RefDelta { base_id: gix_hash::ObjectId::try_from(hash) - .map_err(|_| corrupt("unsupported object hash length"))?, + .or_raise(|| corrupt("unsupported object hash length"))?, }; consumed += hash_len; delta @@ -47,9 +48,7 @@ impl data::Entry { COMMIT => Commit, TAG => Tag, other => { - return Err(gix_error::CorruptionError::new(format!( - "Object type {other} is unsupported" - ))); + return Err(gix_error::CorruptionError::new(format!("Object type {other} is unsupported")).raise()); } }; Ok(data::Entry { @@ -96,15 +95,13 @@ impl data::Entry { decompressed_size: size, data_offset: pack_offset + consumed as u64, encoded_header_size: encoded_header_size(consumed) - .map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))?, + .map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err.into_error()))?, }) } } -fn encoded_header_size(consumed: usize) -> Result { - consumed - .try_into() - .map_err(|_| corrupt("entry header size does not fit into u16")) +fn encoded_header_size(consumed: usize) -> Result> { + u16::try_from(consumed).or_raise(|| corrupt("entry header size does not fit into u16")) } #[inline] @@ -242,10 +239,14 @@ mod tests { #[test] fn oversized_encoded_header_size_is_rejected() { + let err = encoded_header_size(usize::from(u16::MAX) + 1).expect_err("the encoded size exceeds u16"); + assert!(err.is_corrupted()); + assert!( + err.downcast_any_ref::().is_some(), + "the integer conversion failure remains available" + ); assert_eq!( - encoded_header_size(usize::from(u16::MAX) + 1) - .expect_err("the encoded size exceeds u16") - .to_string(), + err.to_string(), "Pack entry is truncated: entry header size does not fit into u16", "entry header lengths that cannot be stored in the Entry metadata must be rejected" ); diff --git a/gix-pack/src/data/file/decode/entry.rs b/gix-pack/src/data/file/decode/entry.rs index 1577047adc5..e8e0de688d2 100644 --- a/gix-pack/src/data/file/decode/entry.rs +++ b/gix-pack/src/data/file/decode/entry.rs @@ -98,7 +98,7 @@ where let size: usize = entry .decompressed_size .try_into() - .map_err(|_| allocation_error(ResourceExhaustionKind::AllocationFailure))?; + .map_err(|err| allocation_error(ResourceExhaustionKind::AllocationFailure).chain(err))?; if out.len() < size { return Err(ValidationError::new("Output buffer is too small for the decompressed entry").raise_erased()); } @@ -108,12 +108,13 @@ where /// Obtain the [`Entry`][crate::data::Entry] at the given `offset` into the pack. /// /// The `offset` is typically obtained from the pack index file. - pub fn entry(&self, offset: data::Offset) -> Result { + pub fn entry(&self, offset: data::Offset) -> Result> { let pack_offset: usize = offset.try_into().expect("offset representable by machine"); if pack_offset > self.data.len() { return Err(gix_error::CorruptionError::new( "Pack entry is truncated: an entry offset pointing beyond pack data", - )); + ) + .raise()); } let object_data = &self.data[pack_offset..]; @@ -320,7 +321,7 @@ where // so that we can find the biggest result size. let total_delta_data_size: usize = total_delta_data_size .try_into() - .map_err(|_| allocation_error(ResourceExhaustionKind::AllocationFailure))?; + .map_err(|err| allocation_error(ResourceExhaustionKind::AllocationFailure).chain(err))?; let chain_len = chain.len(); let actual_base_size = match base_buffer_size { @@ -494,7 +495,7 @@ where fn decoded_object_size(size: u64, alloc_limit_bytes: Option) -> Result { let size: usize = size .try_into() - .map_err(|_| allocation_error(ResourceExhaustionKind::AllocationFailure))?; + .map_err(|err| allocation_error(ResourceExhaustionKind::AllocationFailure).chain(err))?; if alloc_limit_bytes.is_some_and(|limit| size > limit) { return Err(allocation_error(ResourceExhaustionKind::AllocationLimit)); } diff --git a/gix-pack/src/data/output/entry/mod.rs b/gix-pack/src/data/output/entry/mod.rs index 17e8f21f6ec..3a1b4551ea9 100644 --- a/gix-pack/src/data/output/entry/mod.rs +++ b/gix-pack/src/data/output/entry/mod.rs @@ -69,7 +69,7 @@ impl output::Entry { let pack_offset_must_be_zero = 0; let pack_entry = match data::Entry::from_bytes(&entry.data, pack_offset_must_be_zero, count.id.kind()) { Ok(e) => e, - Err(err) => return Some(Err(err.raise_erased())), + Err(err) => return Some(Err(err.erased())), }; use crate::data::entry::Header::*; diff --git a/gix-pack/src/index/traverse/mod.rs b/gix-pack/src/index/traverse/mod.rs index 8bb7270c0b1..df9a18bea33 100644 --- a/gix-pack/src/index/traverse/mod.rs +++ b/gix-pack/src/index/traverse/mod.rs @@ -168,7 +168,7 @@ where C: crate::cache::DecodeEntry, D: crate::FileData + Send + Sync, { - let pack_entry = pack.entry(index_entry.pack_offset).map_err(ErrorExt::raise_erased)?; + let pack_entry = pack.entry(index_entry.pack_offset).or_erased()?; let pack_entry_data_offset = pack_entry.data_offset; let entry_stats = match pack.decode_entry( pack_entry, diff --git a/gix-pack/src/multi_index/chunk.rs b/gix-pack/src/multi_index/chunk.rs index 3c3ac8f1416..499fbd197b5 100644 --- a/gix-pack/src/multi_index/chunk.rs +++ b/gix-pack/src/multi_index/chunk.rs @@ -30,12 +30,11 @@ pub mod index_names { }; let mut out = Vec::new(); - let num_packs = usize::try_from(num_packs).map_err(|_| { + let num_packs = usize::try_from(num_packs).or_raise_erased(|| { ResourceExhaustionError::new( ResourceExhaustionKind::AllocationFailure, "Pack count does not fit into memory", ) - .raise_erased() })?; if num_packs > chunk.len() { return Err(CorruptionError::new("Pack count exceeds the available pack-name data").raise_erased()); diff --git a/gix-path/src/convert.rs b/gix-path/src/convert.rs index 703168f474e..c313ca20993 100644 --- a/gix-path/src/convert.rs +++ b/gix-path/src/convert.rs @@ -5,6 +5,9 @@ use std::{ }; use bstr::{BStr, BString}; +#[cfg(not(unix))] +use gix_error::ResultExt; +use gix_error::{Exn, ValidationError}; #[cfg(not(unix))] fn utf8_error() -> gix_error::ValidationError { @@ -12,7 +15,7 @@ fn utf8_error() -> gix_error::ValidationError { } /// Like [`into_bstr()`], but takes `OsStr` as input for a lossless, but fallible, conversion. -pub fn os_str_into_bstr(path: &OsStr) -> Result<&BStr, gix_error::ValidationError> { +pub fn os_str_into_bstr(path: &OsStr) -> Result<&BStr, Exn> { let path = try_into_bstr(Cow::Borrowed(path.as_ref()))?; match path { Cow::Borrowed(path) => Ok(path), @@ -21,7 +24,7 @@ pub fn os_str_into_bstr(path: &OsStr) -> Result<&BStr, gix_error::ValidationErro } /// Like [`into_bstr()`], but takes `OsString` as input for a lossless, but fallible, conversion. -pub fn os_string_into_bstring(path: OsString) -> Result { +pub fn os_string_into_bstring(path: OsString) -> Result> { let path = try_into_bstr(Cow::Owned(path.into()))?; match path { Cow::Borrowed(_path) => unreachable!("borrowed cows stay borrowed"), @@ -30,7 +33,7 @@ pub fn os_string_into_bstring(path: OsString) -> Result` as input for a lossless, but fallible, conversion. -pub fn try_os_str_into_bstr(path: Cow<'_, OsStr>) -> Result, gix_error::ValidationError> { +pub fn try_os_str_into_bstr(path: Cow<'_, OsStr>) -> Result, Exn> { match path { Cow::Borrowed(path) => os_str_into_bstr(path).map(Cow::Borrowed), Cow::Owned(path) => os_string_into_bstring(path).map(Cow::Owned), @@ -39,9 +42,9 @@ pub fn try_os_str_into_bstr(path: Cow<'_, OsStr>) -> Result, gix_e /// Convert the given path either into its raw bytes on Unix or its UTF-8 encoded counterpart on non-Unix platforms. /// -/// On non-Unix platforms, if the source `Path`` contains ill-formed, lone surrogates, the UTF-8 conversion will fail -/// causing `gix_error::ValidationError` to be returned. -pub fn try_into_bstr<'a>(path: impl Into>) -> Result, gix_error::ValidationError> { +/// On non-Unix platforms, if the source `Path` contains ill-formed, lone surrogates, the UTF-8 conversion will fail +/// causing an [`Exn`](Exn) with the encoding failure as its source to be returned. +pub fn try_into_bstr<'a>(path: impl Into>) -> Result, Exn> { let path = path.into(); let path_str = match path { Cow::Owned(path) => Cow::Owned({ @@ -51,7 +54,9 @@ pub fn try_into_bstr<'a>(path: impl Into>) -> Result path.into_os_string().into_vec().into() }; #[cfg(not(unix))] - let p: BString = path.into_os_string().into_string().map_err(|_| utf8_error())?.into(); + let p: BString = String::from_utf8(path.into_os_string().into_encoded_bytes()) + .or_raise(utf8_error)? + .into(); p }), Cow::Borrowed(path) => Cow::Borrowed({ @@ -61,7 +66,10 @@ pub fn try_into_bstr<'a>(path: impl Into>) -> Result path.as_os_str().as_bytes().into() }; #[cfg(not(unix))] - let p: &BStr = path.to_str().ok_or_else(utf8_error)?.as_bytes().into(); + let p: &BStr = std::str::from_utf8(path.as_os_str().as_encoded_bytes()) + .or_raise(utf8_error)? + .as_bytes() + .into(); p }), }; @@ -88,19 +96,19 @@ pub fn join_bstr_unix_pathsep<'a, 'b>(base: impl Into>, path: impl /// On non-Unix platforms, 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 installations which produce malformed UTF-16 /// if certain emojis are in the path. It's as rare as it sounds, but possible. -pub fn try_from_byte_slice(input: &[u8]) -> Result<&Path, gix_error::ValidationError> { +pub fn try_from_byte_slice(input: &[u8]) -> Result<&Path, Exn> { #[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(|_| utf8_error())?); + let p = Path::new(std::str::from_utf8(input).or_raise(utf8_error)?); Ok(p) } /// Similar to [`from_byte_slice()`], but takes either borrowed or owned `input`. -pub fn try_from_bstr<'a>(input: impl Into>) -> Result, gix_error::ValidationError> { +pub fn try_from_bstr<'a>(input: impl Into>) -> Result, Exn> { let input = input.into(); match input { Cow::Borrowed(input) => try_from_byte_slice(input).map(Cow::Borrowed), @@ -114,7 +122,7 @@ pub fn from_bstr<'a>(input: impl Into>) -> Cow<'a, Path> { } /// Similar to [`try_from_bstr()`], but takes and produces owned data. -pub fn try_from_bstring(input: impl Into) -> Result { +pub fn try_from_bstring(input: impl Into) -> Result> { let input = input.into(); #[cfg(unix)] let p = { @@ -122,17 +130,7 @@ pub fn try_from_bstring(input: impl Into) -> Result = input.into(); - v - } - .into_string() - .map_err(|_| utf8_error())?, - ) - }; + let p = PathBuf::from(String::from_utf8(input.into()).or_raise(utf8_error)?); Ok(p) } diff --git a/gix-path/tests/path/convert/mod.rs b/gix-path/tests/path/convert/mod.rs index 2fe7952a971..ebf706d660b 100644 --- a/gix-path/tests/path/convert/mod.rs +++ b/gix-path/tests/path/convert/mod.rs @@ -20,6 +20,27 @@ fn assure_windows_separators() { mod normalize; +#[cfg(windows)] +#[test] +fn invalid_encodings_retain_their_sources() { + use std::{ffi::OsString, os::windows::ffi::OsStringExt}; + + let path = OsString::from_wide(&[0xd800]); + for err in [ + gix_path::os_str_into_bstr(&path).expect_err("a lone surrogate is not UTF-8"), + gix_path::os_string_into_bstring(path).expect_err("owned paths reject lone surrogates too"), + gix_path::try_from_byte_slice(b"\xff").expect_err("Windows paths require UTF-8"), + gix_path::try_from_bstring(b"\xff".as_bstr()).expect_err("owned Windows paths require UTF-8"), + ] { + assert!(err.is_validation(), "invalid encodings remain validation errors"); + assert!( + err.downcast_any_ref::().is_some() + || err.downcast_any_ref::().is_some(), + "both borrowed and owned conversions retain the concrete encoding failure" + ); + } +} + mod normalize_and_clean { use std::{borrow::Cow, path::Path}; diff --git a/gix-ref/src/name.rs b/gix-ref/src/name.rs index 70a36f2190a..04aa7233c38 100644 --- a/gix-ref/src/name.rs +++ b/gix-ref/src/name.rs @@ -132,10 +132,12 @@ impl<'a> From<&'a FullNameRef> for &'a PartialNameRef { } impl<'a> convert::TryFrom<&'a OsStr> for &'a PartialNameRef { - type Error = Error; + type Error = gix_error::Exn; fn try_from(v: &'a OsStr) -> Result { - let v = gix_path::os_str_into_bstr(v).map_err(|_| Error::InvalidByte { + use gix_error::ResultExt; + + let v = gix_path::os_str_into_bstr(v).or_raise(|| Error::InvalidByte { byte: "".into(), })?; Ok(PartialNameRef::new_unchecked(gix_validate::reference::name_partial( diff --git a/gix-ref/src/store/file/find.rs b/gix-ref/src/store/file/find.rs index ff1b6822298..1ac734488e2 100644 --- a/gix-ref/src/store/file/find.rs +++ b/gix-ref/src/store/file/find.rs @@ -367,7 +367,7 @@ pub mod existing { pub fn find<'a, Name, E>(&self, partial: Name) -> Result where Name: TryInto<&'a PartialNameRef, Error = E>, - crate::name::Error: From, + find::Error: From, { let packed = self.assure_packed_refs_uptodate().map_err(find::Error::PackedOpen)?; self.find_existing_inner(partial, packed.as_ref().map(|b| &***b)) @@ -381,7 +381,7 @@ pub mod existing { ) -> Result where Name: TryInto<&'a PartialNameRef, Error = E>, - crate::name::Error: From, + find::Error: From, { self.find_existing_inner(partial, packed) } @@ -390,7 +390,7 @@ pub mod existing { pub fn find_loose<'a, Name, E>(&self, partial: Name) -> Result where Name: TryInto<&'a PartialNameRef, Error = E>, - crate::name::Error: From, + find::Error: From, { self.find_existing_inner(partial, None).map(Into::into) } @@ -403,11 +403,9 @@ pub mod existing { ) -> Result where Name: TryInto<&'a PartialNameRef, Error = E>, - crate::name::Error: From, + find::Error: From, { - let path = partial - .try_into() - .map_err(|err| Error::Find(find::Error::RefnameValidation(err.into())))?; + let path = partial.try_into().map_err(|err| Error::Find(err.into()))?; match self.find_one_with_verified_input(path, packed) { Ok(Some(r)) => Ok(r), Ok(None) => Err(Error::NotFound { @@ -468,7 +466,7 @@ mod error { #[derive(Debug)] #[expect(missing_docs)] pub enum Error { - RefnameValidation(crate::name::Error), + RefnameValidation(gix_error::Error), ReadFileContents { source: io::Error, path: PathBuf, @@ -519,7 +517,13 @@ mod error { impl From for Error { fn from(err: crate::name::Error) -> Self { - Error::RefnameValidation(err) + Error::RefnameValidation(gix_error::Error::from_error(err)) + } + } + + impl From> for Error { + fn from(err: gix_error::Exn) -> Self { + Error::RefnameValidation(err.into_error()) } } diff --git a/gix-ref/src/store/file/log/line.rs b/gix-ref/src/store/file/log/line.rs index 8c32123da12..6111738b2a5 100644 --- a/gix-ref/src/store/file/log/line.rs +++ b/gix-ref/src/store/file/log/line.rs @@ -85,18 +85,20 @@ impl<'a> From> for Line { /// pub mod decode { + use gix_error::{CorruptionError, ErrorExt, Exn, ResultExt}; use gix_object::bstr::{BStr, ByteSlice}; use crate::{file::log::LineRef, parse::hex_hash_any}; /// mod error { - use gix_object::bstr::{BString, ByteSlice}; + use gix_object::bstr::BString; /// The error returned by [`from_bytes(…)`][super::Line::from_bytes()] #[derive(Debug)] pub struct Error { pub input: BString, + pub(super) source: gix_error::Error, } impl std::fmt::Display for Error { @@ -111,15 +113,7 @@ pub mod decode { impl std::error::Error for Error { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - Some(&crate::CORRUPTION) - } - } - - impl Error { - pub(crate) fn new(input: &[u8]) -> Self { - Error { - input: input.as_bstr().to_owned(), - } + Some(&self.source) } } } @@ -133,7 +127,10 @@ pub mod decode { /// /// `0123456789012345678901234567890123456789 89abcdef89abcdef89abcdef89abcdef89abcdef Name 1700000000 +0000\tmessage` pub fn from_bytes(input: &'a [u8]) -> Result, Error> { - decode(input).map_err(|_| Error::new(first_line(input))) + decode(input).map_err(|err| Error { + input: first_line(input).into(), + source: err.into_error(), + }) } } @@ -152,20 +149,22 @@ pub mod decode { /// /// Return an error if the first line does not match the reflog line /// format. - fn decode(bytes: &[u8]) -> Result, ()> { + fn decode(bytes: &[u8]) -> Result, Exn> { + let invalid = || CorruptionError::new("Malformed reflog line"); let line = first_line(bytes); let (mut head, message) = match line.find_byte(b'\t') { Some(tab) => (&line[..tab], line[tab + 1..].as_bstr()), None => (line, BStr::new(b"")), }; - let old = hex_hash_any(&mut head)?; - head = head.strip_prefix(b" ").ok_or(())?; - let new = hex_hash_any(&mut head)?; - head = head.strip_prefix(b" ").ok_or(())?; - let signature = gix_actor::signature::decode(&mut head).map_err(|_| ())?; + let old = hex_hash_any(&mut head).map_err(|()| invalid())?; + head = head.strip_prefix(b" ").ok_or_else(invalid)?; + let new = hex_hash_any(&mut head).map_err(|()| invalid())?; + head = head.strip_prefix(b" ").ok_or_else(invalid)?; + let signature = + gix_actor::signature::decode(&mut head).or_raise(|| CorruptionError::new("Invalid reflog signature"))?; if !head.is_empty() { - return Err(()); + return Err(invalid().raise()); } Ok(LineRef { previous_oid: old, diff --git a/gix-ref/src/store/file/loose/reference/decode.rs b/gix-ref/src/store/file/loose/reference/decode.rs index f499d29c5fd..a778268dfbf 100644 --- a/gix-ref/src/store/file/loose/reference/decode.rs +++ b/gix-ref/src/store/file/loose/reference/decode.rs @@ -79,7 +79,7 @@ impl Reference { Ok(Reference { name, target: parse(path_contents, object_hash) - .map_err(|_| Error::Parse { + .map_err(|()| Error::Parse { content: path_contents.into(), })? .try_into()?, diff --git a/gix-ref/src/store/file/loose/reflog.rs b/gix-ref/src/store/file/loose/reflog.rs index 9bab96a756c..366377940ce 100644 --- a/gix-ref/src/store/file/loose/reflog.rs +++ b/gix-ref/src/store/file/loose/reflog.rs @@ -136,8 +136,8 @@ pub mod create_or_update { gix_tempfile::remove_dir::empty_depth_first(log_path.clone()) .and_then(|_| options.open(&log_path)) .map(Some) - .map_err(|_| Error::Append { - source: err, + .map_err(|source| Error::Append { + source, reflog_path: self.reflog_path(name), })? } else { diff --git a/gix-ref/src/store/file/overlay_iter.rs b/gix-ref/src/store/file/overlay_iter.rs index 67c583c5c18..aa28dc41168 100644 --- a/gix-ref/src/store/file/overlay_iter.rs +++ b/gix-ref/src/store/file/overlay_iter.rs @@ -75,9 +75,11 @@ impl<'p> LooseThenPacked<'p, '_> { packed::iter::Error::Reference { invalid_line, line_number, + source, } => Error::PackedReference { invalid_line, line_number, + source, }, packed::iter::Error::Header { .. } => unreachable!("this one only happens on iteration creation"), }) @@ -481,6 +483,7 @@ mod error { PackedReference { invalid_line: BString, line_number: usize, + source: gix_error::Error, }, } @@ -503,6 +506,7 @@ mod error { Error::PackedReference { invalid_line, line_number, + .. } => write!(f, "Invalid reference in line {line_number}: {invalid_line:?}"), } } @@ -514,7 +518,7 @@ mod error { Error::Traversal(err) => Some(err), Error::ReadFileContents { source, .. } => Some(source), Error::ReferenceCreation { source, .. } => Some(source), - Error::PackedReference { .. } => Some(&crate::CORRUPTION), + Error::PackedReference { source, .. } => Some(source), } } } diff --git a/gix-ref/src/store/packed/buffer.rs b/gix-ref/src/store/packed/buffer.rs index 45252401ff6..4d0795a1721 100644 --- a/gix-ref/src/store/packed/buffer.rs +++ b/gix-ref/src/store/packed/buffer.rs @@ -32,7 +32,7 @@ pub mod open { let (offset, sorted) = { let mut input = backing.as_ref(); if *input.first().unwrap_or(&b' ') == b'#' { - let header = packed::decode::header(&mut input).map_err(|_| Error::HeaderParsing)?; + let header = packed::decode::header(&mut input).map_err(|()| Error::HeaderParsing)?; let offset = backing.as_ref().len() - input.len(); (offset, header.sorted) } else { diff --git a/gix-ref/src/store/packed/decode/mod.rs b/gix-ref/src/store/packed/decode/mod.rs index c967ffa757d..6f0ba2936cb 100644 --- a/gix-ref/src/store/packed/decode/mod.rs +++ b/gix-ref/src/store/packed/decode/mod.rs @@ -1,3 +1,4 @@ +use gix_error::{CorruptionError, Exn, ResultExt}; use gix_object::bstr::{BStr, ByteSlice}; use crate::{parse, store_impl::packed}; @@ -76,18 +77,20 @@ pub fn header(input: &mut &[u8]) -> Result { /// /// On success, `input` is advanced past the reference line and, if present, the /// peeled object line. -pub fn reference<'a>(input: &mut &'a [u8], object_hash: gix_hash::Kind) -> Result, ()> { - let target = parse::hex_hash(input, object_hash)?; - let Some(rest) = input.strip_prefix(b" ") else { - return Err(()); - }; - *input = rest; - let name = until_line_end_without_separator(input)?.try_into().map_err(|_| ())?; +pub fn reference<'a>( + input: &mut &'a [u8], + object_hash: gix_hash::Kind, +) -> Result, Exn> { + let invalid = || CorruptionError::new("Malformed packed reference"); + let target = parse::hex_hash(input, object_hash).map_err(|()| invalid())?; + *input = input.strip_prefix(b" ").ok_or_else(invalid)?; + let name = <&crate::FullNameRef>::try_from(until_line_end_without_separator(input).map_err(|()| invalid())?) + .or_raise(invalid)?; let object = if let Some(rest) = input.strip_prefix(b"^") { *input = rest; - let object = parse::hex_hash(input, object_hash)?; - parse::newline(input)?; + let object = parse::hex_hash(input, object_hash).map_err(|()| invalid())?; + parse::newline(input).map_err(|()| invalid())?; Some(object) } else { None diff --git a/gix-ref/src/store/packed/find.rs b/gix-ref/src/store/packed/find.rs index 0a5f0c5f3c4..67f45e2de1a 100644 --- a/gix-ref/src/store/packed/find.rs +++ b/gix-ref/src/store/packed/find.rs @@ -41,12 +41,15 @@ impl packed::Buffer { Ok(line_start) => { let mut input = &self.as_ref()[line_start..]; Ok(Some( - packed::decode::reference(&mut input, self.object_hash).map_err(|_| Error::Parse)?, + packed::decode::reference(&mut input, self.object_hash) + .map_err(|err| Error::Parse(err.into_error()))?, )) } Err((parse_failure, _)) => { if parse_failure { - Err(Error::Parse) + Err(Error::Parse(gix_error::Error::from_error( + gix_error::CorruptionError::new("Malformed packed reference record"), + ))) } else { Ok(None) } @@ -104,15 +107,15 @@ mod error { #[derive(Debug)] #[expect(missing_docs)] pub enum Error { - RefnameValidation(crate::name::Error), - Parse, + RefnameValidation(gix_error::Error), + Parse(gix_error::Error), } impl std::fmt::Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Error::RefnameValidation(_) => f.write_str("The ref name or path is not a valid ref name"), - Error::Parse => f.write_str("The reference could not be parsed"), + Error::Parse(_) => f.write_str("The reference could not be parsed"), } } } @@ -121,14 +124,20 @@ mod error { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { match self { Error::RefnameValidation(err) => Some(err), - Error::Parse => Some(&crate::CORRUPTION), + Error::Parse(err) => Some(err), } } } impl From for Error { fn from(err: crate::name::Error) -> Self { - Error::RefnameValidation(err) + Error::RefnameValidation(gix_error::Error::from_error(err)) + } + } + + impl From> for Error { + fn from(err: gix_error::Exn) -> Self { + Error::RefnameValidation(err.into_error()) } } diff --git a/gix-ref/src/store/packed/iter.rs b/gix-ref/src/store/packed/iter.rs index 0ac9642d031..9a55a6bd4f5 100644 --- a/gix-ref/src/store/packed/iter.rs +++ b/gix-ref/src/store/packed/iter.rs @@ -44,7 +44,7 @@ impl<'a> Iterator for packed::Iter<'a> { } Some(Ok(reference)) } - Err(_) => { + Err(err) => { self.cursor = start; let (failed_line, next_cursor) = self .cursor @@ -55,6 +55,7 @@ impl<'a> Iterator for packed::Iter<'a> { self.current_line += 1; Some(Err(Error::Reference { + source: err.into_error(), invalid_line: failed_line .get(..failed_line.len().saturating_sub(1)) .unwrap_or(failed_line) @@ -90,7 +91,7 @@ impl<'a> packed::Iter<'a> { }) } else if packed[0] == b'#' { let mut input = packed; - decode::header(&mut input).map_err(|_| Error::Header { + decode::header(&mut input).map_err(|()| Error::Header { invalid_first_line: packed.lines().next().unwrap_or(packed).into(), })?; let refs = input; @@ -118,8 +119,14 @@ mod error { #[derive(Debug)] #[expect(missing_docs)] pub enum Error { - Header { invalid_first_line: BString }, - Reference { invalid_line: BString, line_number: usize }, + Header { + invalid_first_line: BString, + }, + Reference { + invalid_line: BString, + line_number: usize, + source: gix_error::Error, + }, } impl std::fmt::Display for Error { @@ -131,6 +138,7 @@ mod error { Error::Reference { invalid_line, line_number, + .. } => write!(f, "Invalid reference in line {line_number}: {invalid_line:?}"), } } @@ -138,7 +146,10 @@ mod error { impl std::error::Error for Error { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - Some(&crate::CORRUPTION) + match self { + Error::Header { .. } => Some(&crate::CORRUPTION), + Error::Reference { source, .. } => Some(source), + } } } } diff --git a/gix-ref/src/store/packed/transaction.rs b/gix-ref/src/store/packed/transaction.rs index 4a34d9e1e36..390a2993ae4 100644 --- a/gix-ref/src/store/packed/transaction.rs +++ b/gix-ref/src/store/packed/transaction.rs @@ -1,5 +1,7 @@ use std::{borrow::Cow, fmt::Formatter, io::Write}; +use gix_error::ResultExt; + use crate::{ Namespace, Target, file, store_impl::{packed, packed::Edit}, @@ -117,11 +119,8 @@ impl packed::Transaction { }) => { next_id = gix_object::TagRefIter::from_bytes(data, hash_kind) .target_id() - .map_err(|_| { - prepare::Error::Resolve( - format!("Couldn't get target object id from tag {next_id}").into(), - ) - })?; + .or_raise(|| gix_error::message!("Couldn't get target object id from tag {next_id}")) + .map_err(|err| prepare::Error::Resolve(err.into()))?; } Some(_) => { break if next_id == new { None } else { Some(next_id) }; diff --git a/gix-ref/tests/refs/error.rs b/gix-ref/tests/refs/error.rs index 7c6050139a5..21c210cbfec 100644 --- a/gix-ref/tests/refs/error.rs +++ b/gix-ref/tests/refs/error.rs @@ -5,15 +5,16 @@ use gix_ref::{file::ReferenceExt, packed, transaction::PreviousValue}; fn missing_references_remain_classified_after_erasure() -> crate::Result { let store = crate::file::store_with_packed_refs()?; let packed = store.open_packed_buffer()?.expect("the fixture has packed refs"); + let missing = std::ffi::OsStr::new("missing"); for err in [ - Error::from_error(store.find("missing").expect_err("the reference is absent")), - Error::from_error(store.find_loose("missing").expect_err("the reference is absent")), + Error::from_error(store.find(missing).expect_err("the reference is absent")), + Error::from_error(store.find_loose(missing).expect_err("the reference is absent")), Error::from_error( store - .find_packed("missing", Some(&packed)) + .find_packed(missing, Some(&packed)) .expect_err("the reference is absent"), ), - Error::from_error(packed.find("missing").expect_err("the reference is absent")), + Error::from_error(packed.find(missing).expect_err("the reference is absent")), ] { assert!( err.is_not_found(), @@ -228,3 +229,35 @@ fn existing_sources_and_retry_classifications_are_preserved() { assert!(!err.is_not_found(), "a failed lookup does not imply absence"); } } + +#[test] +fn malformed_packed_names_and_reflog_signatures_retain_parser_errors() -> crate::Result { + let hash = crate::fixture_hash_kind(); + let packed = packed::Buffer::from_bytes( + format!("# pack-refs with: sorted\n{} refs/heads/bad..name\n", hash.null()).as_bytes(), + hash, + )?; + let err = Error::from_error( + packed + .iter()? + .next() + .expect("one packed ref") + .expect_err("the name is invalid"), + ); + assert!(err.is_corrupted()); + assert!( + err.downcast_any_ref::().is_some(), + "packed iteration retains the name validator's error" + ); + + let line = format!("{0} {0} invalid signature\tmessage", hash.null()); + let err = Error::from_error( + gix_ref::file::log::LineRef::from_bytes(line.as_bytes()).expect_err("the signature is invalid"), + ); + assert!(err.is_corrupted()); + assert!( + err.downcast_any_ref::().is_some(), + "reflog decoding retains the signature validator's error" + ); + Ok(()) +} diff --git a/gix-revision/src/spec/parse/function.rs b/gix-revision/src/spec/parse/function.rs index 03d4cc7421c..5ee038877eb 100644 --- a/gix-revision/src/spec/parse/function.rs +++ b/gix-revision/src/spec/parse/function.rs @@ -582,11 +582,11 @@ where } else if has_ref_or_implied_name { let time = nav .to_str() - .map_err(|_| { + .or_raise(|| { gix_error::ValidationError::new_with_input("could not parse time for reflog lookup", nav) }) .and_then(|date| { - gix_date::parse(date, Some(gix_date::Zoned::now())).map_err(|_| { + gix_date::parse(date, Some(gix_date::Zoned::now())).or_raise(|| { gix_error::ValidationError::new_with_input("could not parse time for reflog lookup", nav) }) })?; diff --git a/gix-tix/spec.md b/gix-tix/spec.md index d2b44e9d9a3..72f877fa31f 100644 --- a/gix-tix/spec.md +++ b/gix-tix/spec.md @@ -100,6 +100,8 @@ without trading responsiveness for metadata that is not visible. - `-x/--hide REVSPEC` excludes the revision and its reachable ancestry. The option may be repeated. - `-h/--help` prints Clap's standard help for `tix` and every subcommand. +- Diagnostics retain underlying causes when adding command or argument context, + including encoding failures when OS-string conversions fail. - `--quit-on-finish[=INPUTS]` exits after traversal, lane computation, and one completed frame, for measurement and non-interactive inspection. Optional characters are replayed as read-only keyboard input before the retained final diff --git a/gix-tix/src/command.rs b/gix-tix/src/command.rs index 046552332b2..e96ea5f5f66 100644 --- a/gix-tix/src/command.rs +++ b/gix-tix/src/command.rs @@ -484,6 +484,7 @@ fn resolve_commit( description: &str, ) -> Result<(gix::ObjectId, Option)> { let revision = gix::path::os_str_into_bstr(revision) + .map_err(gix::Exn::into_error) .with_context(|| format!("revision {} is not valid UTF-8", revision.to_string_lossy()))?; match crate::history::resolve_revision(repository, revision) { Ok((id, _reference)) => Ok((id, None)), @@ -572,6 +573,7 @@ fn create_pins(repository: &gix::Repository, revisions: &[OsString]) -> Result Resu for path in paths { let display = path.to_string_lossy(); let path = gix::path::os_str_into_bstr(path) + .map_err(gix::Exn::into_error) .with_context(|| format!("path {display:?} could not be converted to a Git path"))?; let path = repository .normalize_path(path) diff --git a/gix-tix/src/command/new.rs b/gix-tix/src/command/new.rs index 6851159fd65..5376b1809ba 100644 --- a/gix-tix/src/command/new.rs +++ b/gix-tix/src/command/new.rs @@ -43,6 +43,7 @@ pub(super) fn run(repository: gix::Repository, args: Args) -> Result<()> { .as_deref() .map(gix::path::os_str_into_bstr) .transpose() + .map_err(gix::Exn::into_error) .context("author is not valid UTF-8")?; let repository_path = repository.git_dir().to_owned(); let bare = repository.is_bare(); diff --git a/gix-tix/src/command/rebase.rs b/gix-tix/src/command/rebase.rs index bb679a3ba5b..495e8b1c9eb 100644 --- a/gix-tix/src/command/rebase.rs +++ b/gix-tix/src/command/rebase.rs @@ -174,8 +174,9 @@ fn prepare(repo: &gix::Repository, args: &Todo) -> Result { } fn resolve_commit(repo: &gix::Repository, revision: &OsStr, description: &str) -> Result { - let revision = - gix::path::os_str_into_bstr(revision).with_context(|| format!("{description} is not valid UTF-8"))?; + let revision = gix::path::os_str_into_bstr(revision) + .map_err(gix::Exn::into_error) + .with_context(|| format!("{description} is not valid UTF-8"))?; crate::history::resolve_revision(repo, revision) .with_context(|| format!("could not resolve {description}")) .map(|(id, _reference)| id) diff --git a/gix-tix/src/command/reword.rs b/gix-tix/src/command/reword.rs index 52da6721c16..8efed53c9c6 100644 --- a/gix-tix/src/command/reword.rs +++ b/gix-tix/src/command/reword.rs @@ -53,6 +53,7 @@ pub(super) fn run(repository: gix::Repository, args: Args) -> Result<()> { .as_deref() .map(gix::path::os_str_into_bstr) .transpose() + .map_err(gix::Exn::into_error) .context("author is not valid UTF-8")?; if let Some(message) = explicit_message(&args.edit, std::io::stdin())? { @@ -125,6 +126,7 @@ pub(super) fn explicit_message(args: &MessageArgs, mut stdin: impl Read) -> Resu } out.extend_from_slice( gix::path::os_str_into_bstr(message) + .map_err(gix::Exn::into_error) .with_context(|| format!("message {} is not valid UTF-8", index + 1))?, ); } @@ -237,6 +239,25 @@ mod tests { Ok(()) } + #[test] + #[cfg(windows)] + fn invalid_message_encoding_retains_its_cause() { + use std::{ffi::OsString, os::windows::ffi::OsStringExt}; + + let mut message_args = args("HEAD"); + message_args.edit.message = vec![OsString::from_wide(&[0xd800])]; + let err = explicit_message(&message_args.edit, &b""[..]).expect_err("lone surrogates are not UTF-8"); + assert_eq!(err.to_string(), "message 1 is not valid UTF-8"); + let cause = err + .downcast_ref::() + .expect("the path error remains available"); + assert!(cause.is_validation()); + assert!( + cause.downcast_any_ref::().is_some(), + "adding command context preserves the concrete encoding error" + ); + } + #[test] fn message_bypasses_the_editor_and_rewords_head() -> gix_testtools::Result { let fixture = gix_testtools::scripted_fixture_writable("rebase_edit.sh")?; diff --git a/gix-tix/src/edit/mod.rs b/gix-tix/src/edit/mod.rs index bf5c9cae4d8..3773223b649 100644 --- a/gix-tix/src/edit/mod.rs +++ b/gix-tix/src/edit/mod.rs @@ -8,7 +8,7 @@ pub(super) fn loaded_graph(repo: &gix::Repository) -> Result repository diff --git a/gix-worktree/Cargo.toml b/gix-worktree/Cargo.toml index 4879cceef63..07f17d310d5 100644 --- a/gix-worktree/Cargo.toml +++ b/gix-worktree/Cargo.toml @@ -28,6 +28,7 @@ serde = ["dep:serde", "bstr/serde", "gix-index/serde", "gix-hash/serde", "gix-ob parallel = ["gix-features/parallel", "gix-attributes?/parallel"] [dependencies] +gix-error = { version = "^0.3.2", path = "../gix-error" } gix-index = { version = "^0.55.0", path = "../gix-index" } gix-fs = { version = "^0.22.1", path = "../gix-fs" } gix-hash = { version = "^0.26.1", path = "../gix-hash" } @@ -45,7 +46,6 @@ bstr = { version = "1.12.0", default-features = false } document-features = { version = "0.2.0", optional = true } [dev-dependencies] -gix-error = { version = "^0.3.2", path = "../gix-error" } gix-hash = { path = "../gix-hash", features = ["sha1", "sha256"] } gix-testtools = { path = "../tests/tools", default-features = false, features = ["sha1", "sha256"] } gix-odb = { path = "../gix-odb", features = ["sha1", "sha256"] } diff --git a/gix-worktree/src/stack/delegate.rs b/gix-worktree/src/stack/delegate.rs index 83377255577..98fa7bea2c9 100644 --- a/gix-worktree/src/stack/delegate.rs +++ b/gix-worktree/src/stack/delegate.rs @@ -134,16 +134,19 @@ fn validate_last_component( mode: Option, opts: gix_validate::path::component::Options, ) -> std::io::Result<()> { + use gix_error::ResultExt; + let Some(last_component) = stack.current_relative().components().next_back() else { return Ok(()); }; let last_component = gix_path::try_into_bstr(std::borrow::Cow::Borrowed(last_component.as_os_str().as_ref())) - .map_err(|_err| { - std::io::Error::other(format!( + .or_raise(|| { + gix_error::message!( "Path component {last_component:?} of path \"{}\" contained invalid UTF-8 and could not be validated", stack.current_relative().display() - )) - })?; + ) + }) + .map_err(|err| std::io::Error::other(err.into_error()))?; if let Err(err) = gix_validate::path::component( last_component.as_ref(), diff --git a/gix/src/config/tree/keys.rs b/gix/src/config/tree/keys.rs index 013d50ba0bc..e550dcf39a7 100644 --- a/gix/src/config/tree/keys.rs +++ b/gix/src/config/tree/keys.rs @@ -439,9 +439,10 @@ mod workers { .map_err(|err| crate::config::unsigned_integer::Error::from(self).with_source(err.into_error()))?; value .map(|value| { - value - .try_into() - .map_err(|_| crate::config::unsigned_integer::Error::from(self)) + value.try_into().map_err(|err| { + crate::config::unsigned_integer::Error::from(self) + .with_source(gix_error::Error::from_error(err)) + }) }) .transpose() } @@ -455,9 +456,10 @@ mod workers { .map_err(|err| crate::config::unsigned_integer::Error::from(self).with_source(err.into_error()))?; value .map(|value| { - value - .try_into() - .map_err(|_| crate::config::unsigned_integer::Error::from(self)) + value.try_into().map_err(|err| { + crate::config::unsigned_integer::Error::from(self) + .with_source(gix_error::Error::from_error(err)) + }) }) .transpose() } @@ -471,9 +473,10 @@ mod workers { .map_err(|err| crate::config::unsigned_integer::Error::from(self).with_source(err.into_error()))?; value .map(|value| { - value - .try_into() - .map_err(|_| crate::config::unsigned_integer::Error::from(self)) + value.try_into().map_err(|err| { + crate::config::unsigned_integer::Error::from(self) + .with_source(gix_error::Error::from_error(err)) + }) }) .transpose() } @@ -488,7 +491,7 @@ mod time { keys::{Time, validate}, }, }; - use gix_error::Exn; + use gix_error::{Exn, ResultExt}; impl Time { /// Create a new instance. @@ -507,7 +510,7 @@ mod time { value .as_bstr() .to_str() - .map_err(|_| gix_error::ValidationError::new_with_input("UTF8 conversion failed", value))?, + .or_raise(|| gix_error::ValidationError::new_with_input("UTF8 conversion failed", value))?, now, ) } @@ -616,7 +619,9 @@ pub mod validate { .to_decimal() .ok_or_else(|| message!("integer {value} cannot be represented as `usize`").raise_erased())?, ) - .map_err(|_| message("cannot use sign for unsigned integer").raise_erased())?; + .or_raise_erased(|| { + gix_error::ValidationError::new_with_input("unsigned integer is out of range", value) + })?; Ok(()) } } @@ -664,8 +669,10 @@ pub mod validate { pub struct RemoteName; impl Validate for RemoteName { fn validate(&self, value: &BStr) -> Result<(), gix_error::Exn> { - remote::Name::try_from(Cow::Borrowed(value)) - .map_err(|_| message!("Illformed UTF-8 in remote name: \"{}\"", value.to_str_lossy()).raise_erased())?; + remote::Name::try_from(Cow::Borrowed(value)).map_err(|invalid| { + gix_error::ValidationError::new_with_input("Illformed UTF-8 in remote name", invalid.into_owned()) + .raise_erased() + })?; Ok(()) } } diff --git a/gix/src/config/tree/sections/core.rs b/gix/src/config/tree/sections/core.rs index b102ec95eca..011c2a5fdd5 100644 --- a/gix/src/config/tree/sections/core.rs +++ b/gix/src/config/tree/sections/core.rs @@ -440,6 +440,7 @@ mod check_stat { } mod abbrev { + use gix_error::ResultExt; use crate::{bstr::ByteSlice, config::tree::core::Abbrev}; @@ -469,7 +470,7 @@ mod abbrev { Ok(object_hash.len_in_hex().into()) } else { let value = gix_config::Integer::try_from(value_bytes) - .map_err(|_| invalid())? + .or_raise(invalid)? .to_decimal() .ok_or_else(&invalid)?; if value < 4 || value as usize > object_hash.len_in_hex() { diff --git a/gix/src/open/repository.rs b/gix/src/open/repository.rs index 9016f6a1b87..43096c2ea63 100644 --- a/gix/src/open/repository.rs +++ b/gix/src/open/repository.rs @@ -435,11 +435,12 @@ impl ThreadSafeRepository { .string_filter(gitoxide::Core::INDEX_FILE, &mut filter_config_section) { Some(value) => { - gitoxide::Core::INDEX_FILE.validate(value.as_bstr()).map_err(|_| { + gitoxide::Core::INDEX_FILE.validate(value.as_bstr()).map_err(|err| { config::key::GenericErrorWithValue::::from_value( &gitoxide::Core::INDEX_FILE, value.clone(), ) + .with_source(err) })?; gix_path::from_bstr(value).into_owned() } diff --git a/gix/src/repository/location.rs b/gix/src/repository/location.rs index 6cb1bf5e18b..fac59a94245 100644 --- a/gix/src/repository/location.rs +++ b/gix/src/repository/location.rs @@ -1,3 +1,4 @@ +use gix_error::ResultExt; use gix_path::realpath::MAX_SYMLINKS; use std::{ borrow::Cow, @@ -143,12 +144,12 @@ impl crate::Repository { } else { gix_path::realpath_opts(&absolute, self.current_dir(), MAX_SYMLINKS)? .strip_prefix(&root) - .map_err(|_| { - gix_error::Error::from_error(gix_error::ValidationError::new(format!( + .or_raise(|| { + gix_error::ValidationError::new(format!( "The absolute path '{}' is not inside the repository at '{}'", absolute.display(), root.display() - ))) + )) })? .to_owned() }; diff --git a/gix/tests/gix/config/tree.rs b/gix/tests/gix/config/tree.rs index 81d99c7f75e..b8cf490e5cd 100644 --- a/gix/tests/gix/config/tree.rs +++ b/gix/tests/gix/config/tree.rs @@ -110,7 +110,10 @@ mod keys { let err = gix::config::tree::Core::DELTA_BASE_CACHE_LIMIT .validate(invalid.to_string().as_str().into()) .expect_err("negative values are not unsigned"); - assert_eq!(err.probable_cause().to_string(), "cannot use sign for unsigned integer"); + assert!( + err.downcast_any_ref::().is_some(), + "the signed-to-unsigned conversion failure remains available" + ); assert!(err.is_validation()); } diff --git a/gix/tests/gix/id.rs b/gix/tests/gix/id.rs index 50577fb2a2e..fb5b8307ef0 100644 --- a/gix/tests/gix/id.rs +++ b/gix/tests/gix/id.rs @@ -47,7 +47,9 @@ fn prefix() -> crate::Result { "an empty core.abbrev fails the open operation in strict config mode, emulating git behaviour" ); assert_eq!( - err.probable_cause().to_string(), + err.downcast_any_ref::() + .expect("invalid abbreviations retain their validation diagnostic") + .to_string(), format!( "Invalid value for 'core.abbrev' = 'invalid'. It must be between 4 and {}", repo.object_hash().len_in_hex() diff --git a/gix/tests/repository.rs b/gix/tests/repository.rs index ae0a90ff179..c988f440630 100644 --- a/gix/tests/repository.rs +++ b/gix/tests/repository.rs @@ -266,8 +266,14 @@ fn absolute_paths_outside_the_repository_are_rejected() -> gix_testtools::Result .normalize_path(&outside_as_bstr) .expect_err("an absolute path outside the repository must fail"); assert!(err.is_validation(), "an outside path is a validation error"); + assert!( + err.downcast_any_ref::().is_some(), + "the path comparison failure remains available" + ); assert_eq!( - err.probable_cause().to_string(), + err.downcast_any_ref::() + .expect("the path validation diagnostic is preserved") + .to_string(), format!( "The absolute path '{}' is not inside the repository at '{}'", outside.display(), diff --git a/tests/it/src/commands/blame_copy_royal.rs b/tests/it/src/commands/blame_copy_royal.rs index 5e49eb1de3c..96f20a431d9 100644 --- a/tests/it/src/commands/blame_copy_royal.rs +++ b/tests/it/src/commands/blame_copy_royal.rs @@ -47,7 +47,7 @@ pub(super) mod function { // The following block, including the `TODO` comment, comes from // `gitoxide_core::repository::blame`. - let file = gix::path::os_str_into_bstr(file)?; + let file = gix::path::os_str_into_bstr(file).map_err(gix::Exn::into_error)?; let specs = repo.pathspec( false, [file], From f042b3bc682a008aabc84a116d1009983880bc11 Mon Sep 17 00:00:00 2001 From: Byron Date: Wed, 23 Sep 2026 09:47:09 +0200 Subject: [PATCH 45/51] Add tasks for Zed to run crate tests with many features This is just for convenience, as I can never remember. Assisted-by: GPT 6.0 Co-authored-by: GPT 6.0 --- .gitignore | 5 ++ .zed/tasks.json | 233 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 238 insertions(+) create mode 100644 .zed/tasks.json diff --git a/.gitignore b/.gitignore index 78cd84318a8..682ceb272a3 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,8 @@ $**/fuzz/Cargo.lock # Instead of adding more environment-specific ignores here, like for the IDE in use, prefer Git's user-global # `core.excludesFile` mechanism, see https://git-scm.com/docs/git-config#Documentation/git-config.txt-coreexcludesFile. + +# Share Zed tasks even when editor files are ignored globally; keep other Zed settings local. +!/.zed/ +/.zed/* +!/.zed/tasks.json diff --git a/.zed/tasks.json b/.zed/tasks.json new file mode 100644 index 00000000000..a46d249b6d3 --- /dev/null +++ b/.zed/tasks.json @@ -0,0 +1,233 @@ +// Feature-specific test configurations, with both hashes enabled where applicable. +// gix-transport has no hash toggles, so enable them on its gix-hash dev-dependency. +// gix-error tests enable anyhow through a dev-dependency; tree-error overrides auto-chain-error. +[ + { + "label": "test gix-error (default tree)", + "command": "cargo", + "args": [ + "nextest", + "run", + "-p", + "gix-error", + "--no-fail-fast" + ], + "cwd": "$ZED_WORKTREE_ROOT", + "env": { "GIX_TEST_IGNORE_ARCHIVES": "1" } + }, + { + "label": "test gix-error (auto-chain-error)", + "command": "cargo", + "args": [ + "nextest", + "run", + "-p", + "gix-error", + "--features", + "auto-chain-error", + "--no-fail-fast" + ], + "cwd": "$ZED_WORKTREE_ROOT", + "env": { "GIX_TEST_IGNORE_ARCHIVES": "1" } + }, + { + "label": "test gix-error (auto-chain-error, tree-error)", + "command": "cargo", + "args": [ + "nextest", + "run", + "-p", + "gix-error", + "--features", + "auto-chain-error,tree-error", + "--no-fail-fast" + ], + "cwd": "$ZED_WORKTREE_ROOT", + "env": { "GIX_TEST_IGNORE_ARCHIVES": "1" } + }, + { + "label": "test gix-protocol (blocking-client)", + "command": "cargo", + "args": [ + "nextest", + "run", + "-p", + "gix-protocol", + "--features", + "blocking-client,sha1,sha256", + "--no-fail-fast" + ], + "cwd": "$ZED_WORKTREE_ROOT", + "env": { "GIX_TEST_IGNORE_ARCHIVES": "1" } + }, + { + "label": "test gix-protocol (async-client)", + "command": "cargo", + "args": [ + "nextest", + "run", + "-p", + "gix-protocol", + "--features", + "async-client,sha1,sha256", + "--no-fail-fast" + ], + "cwd": "$ZED_WORKTREE_ROOT", + "env": { "GIX_TEST_IGNORE_ARCHIVES": "1" } + }, + { + "label": "test gix-transport (curl)", + "command": "cargo", + "args": [ + "nextest", + "run", + "-p", + "gix-transport", + "--features", + "http-client-curl,gix-hash/sha1,gix-hash/sha256", + "--no-fail-fast" + ], + "cwd": "$ZED_WORKTREE_ROOT", + "env": { "GIX_TEST_IGNORE_ARCHIVES": "1" } + }, + { + "label": "test gix-transport (curl, insecure HTTP credentials)", + "command": "cargo", + "args": [ + "nextest", + "run", + "-p", + "gix-transport", + "--features", + "http-client-curl,http-client-insecure-credentials,gix-hash/sha1,gix-hash/sha256", + "--test", + "blocking-transport-http-only", + "--no-fail-fast" + ], + "cwd": "$ZED_WORKTREE_ROOT", + "env": { "GIX_TEST_IGNORE_ARCHIVES": "1" } + }, + { + "label": "test gix-transport (reqwest)", + "command": "cargo", + "args": [ + "nextest", + "run", + "-p", + "gix-transport", + "--features", + "http-client-reqwest,gix-hash/sha1,gix-hash/sha256", + "--no-fail-fast" + ], + "cwd": "$ZED_WORKTREE_ROOT", + "env": { "GIX_TEST_IGNORE_ARCHIVES": "1" } + }, + { + "label": "test gix-transport (reqwest, insecure HTTP credentials)", + "command": "cargo", + "args": [ + "nextest", + "run", + "-p", + "gix-transport", + "--no-default-features", + "--features", + "blocking-client,http-client-reqwest,http-client-insecure-credentials,gix-hash/sha1,gix-hash/sha256", + "--test", + "blocking-transport", + "--no-fail-fast" + ], + "cwd": "$ZED_WORKTREE_ROOT", + "env": { "GIX_TEST_IGNORE_ARCHIVES": "1" } + }, + { + "label": "test gix-transport (async-client)", + "command": "cargo", + "args": [ + "nextest", + "run", + "-p", + "gix-transport", + "--features", + "async-client,gix-hash/sha1,gix-hash/sha256", + "--no-fail-fast" + ], + "cwd": "$ZED_WORKTREE_ROOT", + "env": { "GIX_TEST_IGNORE_ARCHIVES": "1" } + }, + { + "label": "test gix (basic, comfort, max-performance-safe)", + "command": "cargo", + "args": [ + "nextest", + "run", + "-p", + "gix", + "--no-default-features", + "--features", + "basic,comfort,max-performance-safe,sha1,sha256", + "--no-fail-fast" + ], + "cwd": "$ZED_WORKTREE_ROOT", + "env": { "GIX_TEST_IGNORE_ARCHIVES": "1" } + }, + { + "label": "test gix (basic, extras, comfort)", + "command": "cargo", + "args": [ + "nextest", + "run", + "-p", + "gix", + "--no-default-features", + "--features", + "basic,extras,comfort,sha1,sha256", + "--no-fail-fast" + ], + "cwd": "$ZED_WORKTREE_ROOT", + "env": { "GIX_TEST_IGNORE_ARCHIVES": "1" } + }, + { + "label": "test gix (async-network-client)", + "command": "cargo", + "args": [ + "nextest", + "run", + "-p", + "gix", + "--features", + "async-network-client,sha1,sha256", + "--no-fail-fast" + ], + "cwd": "$ZED_WORKTREE_ROOT", + "env": { "GIX_TEST_IGNORE_ARCHIVES": "1" } + }, + { + "label": "test gix (blocking-network-client)", + "command": "cargo", + "args": [ + "nextest", + "run", + "-p", + "gix", + "--features", + "blocking-network-client,sha1,sha256", + "--no-fail-fast" + ], + "cwd": "$ZED_WORKTREE_ROOT", + "env": { "GIX_TEST_IGNORE_ARCHIVES": "1" } + }, + { + "label": "test gix (async-std, flaky)", + "command": "cargo", + "args": [ + "test", + "-p", + "gix", + "--features", + "async-network-client-async-std,sha1,sha256" + ], + "cwd": "$ZED_WORKTREE_ROOT", + "env": { "GIX_TEST_IGNORE_ARCHIVES": "1" } + } +] From dcdddd9429e3faf2083be340f9ec2bfaab232c2f Mon Sep 17 00:00:00 2001 From: Byron Date: Thu, 17 Sep 2026 14:02:26 +0100 Subject: [PATCH 46/51] feat(gix-error): attach scalar metadata to error contexts Callers can enrich errors with named values they already possess without introducing a custom payload type. `Metadata` keeps a message and an ordered dictionary of typed scalar values, including lossless bytes and native paths. `Error::metadata()` and `Exn::metadata()` iterate separate contexts through existing error traversal, preserving original causes and classifications. Recovery continues to use classifications and concrete domain errors. Document metadata keys on each function that directly returns them. Assisted-by: GPT 6.0 Co-authored-by: GPT 6.0 --- gix-error/src/concrete/metadata.rs | 108 ++++++++++++++++++++++++ gix-error/src/concrete/mod.rs | 1 + gix-error/src/error.rs | 6 ++ gix-error/src/exn/impls.rs | 16 +++- gix-error/src/lib.rs | 21 +++++ gix-error/tests/error/main.rs | 1 + gix-error/tests/error/metadata.rs | 131 +++++++++++++++++++++++++++++ 7 files changed, 281 insertions(+), 3 deletions(-) create mode 100644 gix-error/src/concrete/metadata.rs create mode 100644 gix-error/tests/error/metadata.rs diff --git a/gix-error/src/concrete/metadata.rs b/gix-error/src/concrete/metadata.rs new file mode 100644 index 00000000000..c60b0a1116e --- /dev/null +++ b/gix-error/src/concrete/metadata.rs @@ -0,0 +1,108 @@ +use std::{borrow::Cow, collections::BTreeMap, fmt, path::PathBuf}; + +use bstr::BString; + +/// A context message with named diagnostic values. +/// +/// Attach it with [`ResultExt::or_raise()`](crate::ResultExt::or_raise) or [`Exn::raise()`](crate::Exn::raise), +/// keeping the original error as its cause. Use concrete error types and classifications for recovery; +/// metadata describes a particular context and dictionaries from separate contexts aren't merged. +#[derive(Debug)] +pub struct Metadata { + /// The operation or situation described by these values. + pub message: Cow<'static, str>, + /// Diagnostic values, ordered by key. Functions returning metadata document their keys. + pub values: BTreeMap, Value>, +} + +impl Metadata { + /// Create a context with `message` and no values. + pub fn new(message: impl Into>) -> Self { + Self { + message: message.into(), + values: BTreeMap::new(), + } + } + + /// Add `value` under `key`, replacing any previous value in this context. + pub fn with(mut self, key: impl Into>, value: impl Into) -> Self { + self.values.insert(key.into(), value.into()); + self + } +} + +impl fmt::Display for Metadata { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&self.message)?; + for (key, value) in &self.values { + write!(f, ", {key:?}={value}")?; + } + Ok(()) + } +} + +impl std::error::Error for Metadata {} + +/// An owned scalar diagnostic value. Bytes and native paths retain their original representation. +#[derive(Clone, Debug, PartialEq)] +#[non_exhaustive] +pub enum Value { + /// A boolean. + Bool(bool), + /// A signed integer. + I64(i64), + /// An unsigned integer. + U64(u64), + /// A floating-point number. + F64(f64), + /// UTF-8 text. + String(String), + /// An arbitrary byte string. + Bytes(BString), + /// A native filesystem path. + Path(PathBuf), +} + +impl fmt::Display for Value { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Value::Bool(value) => fmt::Display::fmt(value, f), + Value::I64(value) => fmt::Display::fmt(value, f), + Value::U64(value) => fmt::Display::fmt(value, f), + Value::F64(value) => fmt::Display::fmt(value, f), + Value::String(value) => fmt::Debug::fmt(value, f), + Value::Bytes(value) => fmt::Debug::fmt(value, f), + Value::Path(value) => fmt::Debug::fmt(value, f), + } + } +} + +macro_rules! from { + ($variant:ident: $($ty:ty),+ $(,)?) => { + $(impl From<$ty> for Value { + fn from(value: $ty) -> Self { + Self::$variant(value.into()) + } + })+ + }; +} + +from!(Bool: bool); +from!(I64: i8, i16, i32, i64); +from!(U64: u8, u16, u32, u64); +from!(F64: f32, f64); +from!(String: String, &str); +from!(Bytes: BString, &bstr::BStr, Vec, &[u8]); +from!(Path: PathBuf, &std::path::Path); + +impl From for Value { + fn from(value: usize) -> Self { + Self::U64(value as u64) + } +} + +impl From for Value { + fn from(value: isize) -> Self { + Self::I64(value as i64) + } +} diff --git a/gix-error/src/concrete/mod.rs b/gix-error/src/concrete/mod.rs index 9546d99898d..4318e10b48c 100644 --- a/gix-error/src/concrete/mod.rs +++ b/gix-error/src/concrete/mod.rs @@ -1,4 +1,5 @@ pub(super) mod chain; pub(super) mod classify; pub(super) mod message; +pub(super) mod metadata; pub(super) mod validate; diff --git a/gix-error/src/error.rs b/gix-error/src/error.rs index 849d4dc6d21..949504bcc3e 100644 --- a/gix-error/src/error.rs +++ b/gix-error/src/error.rs @@ -118,6 +118,12 @@ impl crate::Error { self.iter_errors().find_map(|error| error.downcast_ref()) } + /// Visit metadata contexts in error traversal order, keeping their dictionaries separate. + /// Functions that directly return metadata document the keys available in each context. + pub fn metadata(&self) -> impl Iterator + '_ { + self.iter_errors().filter_map(|error| error.downcast_ref()) + } + /// Return all known classifications in the same logical breadth-first order as [`Self::iter_errors()`]. /// /// Unknown errors are omitted. Classifications aren't deduplicated because distinct errors may independently have diff --git a/gix-error/src/exn/impls.rs b/gix-error/src/exn/impls.rs index 146cf0667f6..8514e9d72e1 100644 --- a/gix-error/src/exn/impls.rs +++ b/gix-error/src/exn/impls.rs @@ -181,6 +181,18 @@ impl Exn { self.frame().iter_frames() } + /// Lazily visit stored errors and native sources in logical breadth-first order, expanding nested [`crate::Error`] values. + /// Concrete error types remain available for downcasting, as with [`crate::Error::iter_errors()`]. + pub fn iter_errors(&self) -> impl Iterator + '_ { + self.frame.iter_errors_with_locations().map(|source| source.error()) + } + + /// Visit metadata contexts in error traversal order, keeping their dictionaries separate. + /// Functions that directly return metadata document the keys available in each context. + pub fn metadata(&self) -> impl Iterator + '_ { + self.iter_errors().filter_map(|error| error.downcast_ref()) + } + /// Return the error that is most likely the root cause, based on [`Frame::probable_cause()`]. /// /// If there is no source or child, return the stored error. A selected nested [`crate::Error`] is inspected @@ -196,9 +208,7 @@ impl Exn { /// /// Nested [`crate::Error`] values are inspected recursively, matching [`crate::Error::downcast_any_ref()`]. pub fn downcast_any_ref(&self) -> Option<&T> { - self.frame - .iter_errors_with_locations() - .find_map(|source| source.error().downcast_ref()) + self.iter_errors().find_map(|error| error.downcast_ref()) } } diff --git a/gix-error/src/lib.rs b/gix-error/src/lib.rs index a9d1420c8ab..793f55d822b 100644 --- a/gix-error/src/lib.rs +++ b/gix-error/src/lib.rs @@ -40,6 +40,26 @@ //! ``` //! - Use [`ValidationError::new_with_input()`] when you need to preserve the offending input. //! +//! ## [`Metadata`] +//! +//! Callers should add context using information they already possess. Use [`Metadata`] when that context +//! includes named scalar values, and document its keys on the function that returns it. Preserve concrete +//! error types for recovery signals and complex results discovered by the callee, such as partial outcomes. +//! +//! ``` +//! use gix_error::{Metadata, ResultExt, Value}; +//! +//! let error = Err::<(), _>(std::io::Error::from(std::io::ErrorKind::NotFound)) +//! .or_raise(|| Metadata::new("Could not read reference").with("path", std::path::Path::new("HEAD"))) +//! .expect_err("the lookup failed"); +//! assert!(error.is_not_found()); +//! let context = error.metadata().next().expect("lookup context"); +//! assert_eq!(context.values["path"], Value::Path("HEAD".into())); +//! ``` +//! +//! [`Exn::metadata()`] and [`Error::metadata()`] visit each dictionary separately in error traversal order. +//! Keys are local to their context; dictionaries from independent causes are never combined. +//! //! # [`Exn`](Exn) and [`Exn`] //! //! The [`Exn`] type does not implement [`Error`](std::error::Error) itself, but is able to store causing errors @@ -400,6 +420,7 @@ pub use concrete::classify::{ CorruptionError, NotFoundError, ResourceExhaustionError, ResourceExhaustionKind, RetryableError, }; pub use concrete::message::{Message, message}; +pub use concrete::metadata::{Metadata, Value}; pub use concrete::validate::ValidationError; pub(crate) fn write_location(f: &mut std::fmt::Formatter<'_>, location: &std::panic::Location) -> std::fmt::Result { diff --git a/gix-error/tests/error/main.rs b/gix-error/tests/error/main.rs index 7b824b6c3f3..358ec4dc648 100644 --- a/gix-error/tests/error/main.rs +++ b/gix-error/tests/error/main.rs @@ -51,4 +51,5 @@ mod utils { } pub use utils::*; +mod metadata; mod test; diff --git a/gix-error/tests/error/metadata.rs b/gix-error/tests/error/metadata.rs new file mode 100644 index 00000000000..ccc9af81e99 --- /dev/null +++ b/gix-error/tests/error/metadata.rs @@ -0,0 +1,131 @@ +use std::path::Path; + +use gix_error::{Error, ErrorExt, Metadata, NotFoundError, ResultExt, Value}; + +#[test] +fn scalar_values_are_lossless_and_keys_are_local_to_a_context() { + let metadata = Metadata::new("details") + .with("signed", i64::MIN) + .with("unsigned", u64::MAX) + .with("float", 1.5) + .with("bytes", b"ref\xff".as_slice()) + .with("path", Path::new("objects")) + .with("text", "line\nbreak") + .with("flag", false) + .with("flag", true); + assert_eq!(metadata.values["signed"], Value::I64(i64::MIN)); + assert_eq!(metadata.values["unsigned"], Value::U64(u64::MAX)); + assert_eq!(metadata.values["float"], Value::F64(1.5)); + assert_eq!(metadata.values["bytes"], Value::Bytes(b"ref\xff".as_slice().into())); + assert_eq!(metadata.values["path"], Value::Path(Path::new("objects").into())); + assert_eq!( + metadata.values["flag"], + Value::Bool(true), + "the last value replaces its predecessor" + ); + assert_eq!( + Metadata::new("details") + .with("z", "line\nbreak") + .with("a", 2) + .to_string(), + "details, \"a\"=2, \"z\"=\"line\\nbreak\"", + "keys are ordered and text is escaped" + ); + #[cfg(unix)] + { + use std::os::unix::ffi::OsStringExt; + let path = std::path::PathBuf::from(std::ffi::OsString::from_vec(b"ref\xff".to_vec())); + assert_eq!( + Value::from(path.as_path()), + Value::Path(path), + "paths retain native bytes" + ); + } + #[cfg(windows)] + { + use std::os::windows::ffi::OsStringExt; + let path = std::path::PathBuf::from(std::ffi::OsString::from_wide(&[0xd800])); + assert_eq!( + Value::from(path.as_path()), + Value::Path(path), + "paths retain native code units" + ); + } + + insta::assert_snapshot!(metadata, "validate Display", @r#"details, "bytes"="ref\xff", "flag"=true, "float"=1.5, "path"="objects", "signed"=-9223372036854775808, "text"="line\nbreak", "unsigned"=18446744073709551615"#); + insta::assert_debug_snapshot!(metadata, "validate Display", @r#" + Metadata { + message: "details", + values: { + "bytes": Bytes( + "ref\xff", + ), + "flag": Bool( + true, + ), + "float": F64( + 1.5, + ), + "path": Path( + "objects", + ), + "signed": I64( + -9223372036854775808, + ), + "text": String( + "line\nbreak", + ), + "unsigned": U64( + 18446744073709551615, + ), + }, + } + "#); +} + +#[test] +fn metadata_contexts_preserve_causes_and_remain_separate_through_conversion() { + let missing = NotFoundError::new("missing").and_raise(Metadata::new("lookup").with("path", "first")); + let retry = + std::io::Error::from(std::io::ErrorKind::TimedOut).and_raise(Metadata::new("read").with("path", "second")); + let err = Error::from_error(super::ErrorWithSource("custom", missing.into_error())) + .raise() + .chain(retry); + let paths = |metadata: &Metadata| metadata.values["path"].clone(); + assert_eq!( + err.metadata().map(paths).collect::>(), + [Value::from("second"), Value::from("first")], + "native sources and explicit child contexts share the existing traversal order" + ); + assert!(err.is_not_found() && err.can_retry()); + + insta::assert_snapshot!(err, @"custom"); + insta::assert_debug_snapshot!(err, @r#" + custom + | + └─ lookup, "path"="first" + | + └─ missing + | + └─ read, "path"="second" + | + └─ timed out + "#); + + let err = err.into_error(); + assert!( + err.is_not_found() && err.can_retry(), + "context leaves classifications intact" + ); + assert_eq!( + err.metadata().map(paths).collect::>(), + [Value::from("second"), Value::from("first")] + ); + assert!( + err.downcast_any_ref::().is_some(), + "the concrete cause remains accessible" + ); + + let ok: Result<(), std::io::Error> = Ok(()); + assert!(ok.or_raise(|| -> Metadata { panic!("context must be lazy") }).is_ok()); +} From fb36616cba282af08e1104f33fb52f526a804b30 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 17 Sep 2026 14:34:55 +0100 Subject: [PATCH 47/51] change(gix-ref)!: replace forwarding errors with metadata and recovery signals Took a closer look over about 40% of the commit, skimmed/skipped the rest it. So not a complete rubber stamp. Later-on, a lot of these explicit downcasts should be turned into quick classification checks. Return canonical `Exn` errors from reference operations and `gix::Error` at porcelain boundaries. Preserve native parser, filesystem, lock and custom name-conversion sources instead of rewrapping them in operation-specific enums. Use documented `Metadata` dictionaries for diagnostic paths, reference names, input bytes and positions. Keep concrete signals for absent references, malformed loose references, stale expected values, existing references and missing committer identity. These support GitButler-style recovery without string matching; stale reference values still require reconciliation before retrying. Fetch only treats an absent referent as unborn, propagating malformed referents and read failures. Remove empty error namespaces and duplicate conversions along with their workspace callers. Preserve unterminated packed input and count peeled lines when reporting iterator positions. --- gitoxide-core/src/repository/diff.rs | 8 +- gix-discover/src/is.rs | 11 +- gix-protocol/src/fetch/negotiate.rs | 4 +- gix-ref/fuzz/fuzz_targets/fuzz_log.rs | 3 +- .../fuzz/fuzz_targets/fuzz_packed_buffer.rs | 3 +- gix-ref/src/lib.rs | 14 +- gix-ref/src/peel.rs | 102 ------ gix-ref/src/store/file/find.rs | 305 ++++++---------- gix-ref/src/store/file/log/iter.rs | 140 ++------ gix-ref/src/store/file/log/line.rs | 81 +---- gix-ref/src/store/file/log/mod.rs | 2 +- .../src/store/file/loose/reference/decode.rs | 72 +--- .../src/store/file/loose/reference/logiter.rs | 16 +- gix-ref/src/store/file/loose/reflog.rs | 175 ++++----- .../loose/reflog/create_or_update/tests.rs | 2 +- gix-ref/src/store/file/mod.rs | 5 - gix-ref/src/store/file/overlay_iter.rs | 109 +----- gix-ref/src/store/file/packed.rs | 74 +--- gix-ref/src/store/file/raw_ext.rs | 109 +++--- gix-ref/src/store/file/transaction/commit.rs | 113 ++---- gix-ref/src/store/file/transaction/prepare.rs | 336 ++++++------------ gix-ref/src/store/general/handle/find.rs | 127 +------ gix-ref/src/store/general/init.rs | 43 +-- gix-ref/src/store/packed/buffer.rs | 215 +++++------ gix-ref/src/store/packed/find.rs | 126 ++----- gix-ref/src/store/packed/iter.rs | 83 ++--- gix-ref/src/store/packed/transaction.rs | 156 +++----- gix-ref/tests/refs/error.rs | 157 +++++--- gix-ref/tests/refs/file/log.rs | 15 +- gix-ref/tests/refs/file/reference.rs | 54 ++- gix-ref/tests/refs/file/store/find.rs | 10 +- gix-ref/tests/refs/file/store/iter.rs | 4 +- .../create_or_update/collisions.rs | 26 +- .../create_or_update/mod.rs | 98 ++--- .../transaction/prepare_and_commit/delete.rs | 57 +-- gix-ref/tests/refs/file/worktree.rs | 6 +- gix-ref/tests/refs/packed/find.rs | 8 +- gix-ref/tests/refs/packed/iter.rs | 39 +- gix-ref/tests/refs/packed/open.rs | 13 +- gix-tix/src/history.rs | 11 +- gix/src/reference/iter.rs | 28 +- gix/src/reference/mod.rs | 3 +- .../connection/fetch/update_refs/mod.rs | 9 +- .../connection/fetch/update_refs/tests.rs | 42 ++- gix/src/repository/reference.rs | 30 +- gix/src/revision/spec/parse/delegate/mod.rs | 13 +- .../revision/spec/parse/delegate/revision.rs | 16 +- gix/tests/gix/reference/mod.rs | 6 +- gix/tests/gix/remote/connect.rs | 3 +- gix/tests/gix/repository/object.rs | 9 +- .../gix/revision/spec/from_bytes/ambiguous.rs | 36 ++ gix/tests/gix/revision/spec/from_bytes/mod.rs | 125 ++++++- gix/tests/gix/submodule.rs | 7 +- tests/tools/src/repository/gix.rs | 6 +- tests/tools/tests/env.rs | 9 +- tests/tools/tests/tools/repository.rs | 2 +- 56 files changed, 1159 insertions(+), 2117 deletions(-) delete mode 100644 gix-ref/src/peel.rs diff --git a/gitoxide-core/src/repository/diff.rs b/gitoxide-core/src/repository/diff.rs index 35ab1773335..94ada38e02b 100644 --- a/gitoxide-core/src/repository/diff.rs +++ b/gitoxide-core/src/repository/diff.rs @@ -124,10 +124,10 @@ fn resolve_revspec( match result { Err(err) => { - // When the revspec is just a name, the delegate tries to resolve a reference which fails. - // We extract the error from the tree to learn the name, and treat it as file. - let not_found = err.downcast_any_ref::(); - if let Some(gix::refs::file::find::existing::Error::NotFound { name }) = not_found { + // `is_not_found()` also matches missing objects, which must not become filesystem paths. + // Extract the missing reference's name, which may differ from the revspec after following symbolic refs. + let not_found = err.downcast_any_ref::(); + if let Some(gix::refs::file::find::NotFound { name }) = not_found { let root = repo.workdir().map(ToOwned::to_owned); let name = gix::path::os_string_into_bstring(name.into())?; diff --git a/gix-discover/src/is.rs b/gix-discover/src/is.rs index cc3da0c6afa..1c70fb1ca99 100644 --- a/gix-discover/src/is.rs +++ b/gix-discover/src/is.rs @@ -79,14 +79,15 @@ pub(crate) fn git_with_metadata( .raise_erased()); } } - Err(gix_ref::file::find::existing::Error::Find(gix_ref::file::find::Error::ReferenceCreation { - source: _, - relative_path, - })) if relative_path == Path::new("HEAD") => { + Err(err) + if err + .downcast_any_ref::() + .is_some_and(|err| err.relative_path == Path::new("HEAD")) => + { // It's fine as long as the reference is found is `HEAD`. } Err(err) => { - return Err(err.and_raise(message("Could not find a valid HEAD reference")).erased()); + return Err(err.raise(message("Could not find a valid HEAD reference")).erased()); } } } diff --git a/gix-protocol/src/fetch/negotiate.rs b/gix-protocol/src/fetch/negotiate.rs index 6f1a4199d6a..b5fe49f288a 100644 --- a/gix-protocol/src/fetch/negotiate.rs +++ b/gix-protocol/src/fetch/negotiate.rs @@ -371,9 +371,7 @@ fn mark_all_refs_in_repo( .or_raise(|| message("Could not open packed refs"))?; let id = match local_ref.peel_to_id_packed(store, objects, packed.as_ref().map(|b| &***b)) { Ok(id) => id, - Err(gix_ref::peel::to_id::Error::FollowToObject(gix_ref::peel::to_object::Error::Follow( - gix_ref::file::find::existing::Error::NotFound { .. }, - ))) => continue, + Err(err) if err.downcast_any_ref::().is_some() => continue, Err(err) => return Err(err).or_raise(|| message("Could not peel reference to ID")), }; let mut is_complete = false; diff --git a/gix-ref/fuzz/fuzz_targets/fuzz_log.rs b/gix-ref/fuzz/fuzz_targets/fuzz_log.rs index 204ab6e7ba2..b69fa96162a 100644 --- a/gix-ref/fuzz/fuzz_targets/fuzz_log.rs +++ b/gix-ref/fuzz/fuzz_targets/fuzz_log.rs @@ -1,6 +1,5 @@ #![no_main] -use anyhow::Result; use arbitrary::Arbitrary; use gix_ref::file::log; use libfuzzer_sys::fuzz_target; @@ -13,7 +12,7 @@ struct Ctx<'a> { multi_line_forward: &'a [u8], } -fn fuzz(ctx: Ctx) -> Result<()> { +fn fuzz(ctx: Ctx) -> Result<(), Box> { let line = log::LineRef::from_bytes(ctx.line_ref)?; _ = black_box(line.previous_oid()); _ = black_box(line.new_oid()); diff --git a/gix-ref/fuzz/fuzz_targets/fuzz_packed_buffer.rs b/gix-ref/fuzz/fuzz_targets/fuzz_packed_buffer.rs index 241c8cc1628..02e2ea80700 100644 --- a/gix-ref/fuzz/fuzz_targets/fuzz_packed_buffer.rs +++ b/gix-ref/fuzz/fuzz_targets/fuzz_packed_buffer.rs @@ -1,6 +1,5 @@ #![no_main] -use anyhow::Result; use arbitrary::Arbitrary; use bstr::BStr; use gix_ref::{packed::Buffer, FullNameRef}; @@ -13,7 +12,7 @@ struct Ctx<'a> { name: &'a [u8], } -fn fuzz(ctx: Ctx) -> Result<()> { +fn fuzz(ctx: Ctx) -> Result<(), Box> { let buffer = Buffer::from_bytes(ctx.packed_file_contents, gix_hash::Kind::Sha1)?; _ = black_box(buffer.iter()?.count()); diff --git a/gix-ref/src/lib.rs b/gix-ref/src/lib.rs index 9b6ddd99e2f..144f110107d 100644 --- a/gix-ref/src/lib.rs +++ b/gix-ref/src/lib.rs @@ -18,6 +18,12 @@ //! Missing references and objects expose [`gix_error::NotFoundError`] through their error sources, so //! [`gix_error::Error::is_not_found()`] works after conversion. Malformed reference data and invalid reflog //! input similarly expose corruption and validation classifications, while retaining their concrete errors. +//! Operations return [`gix_error::Exn`], with diagnostic [`gix_error::Metadata`] keys documented where they are added. +//! Inspect these with [`metadata()`](gix_error::Exn::metadata), and add context available at the call site. +//! For recovery, downcast to [`file::find::NotFound`] to distinguish an absent reference from an absent object, +//! [`file::find::ReferenceDecode`] for loose reference contents that could not be decoded, +//! [`file::transaction::prepare::ReferenceOutOfDate`] or [`file::transaction::prepare::MustNotExist`] for a failed +//! update constraint, and [`file::log::create_or_update::MissingCommitter`] for a missing reflog identity. //! //! ## Feature Flags #![cfg_attr( @@ -31,13 +37,10 @@ use gix_hash::{ObjectId, oid}; pub use gix_object::bstr; use gix_object::bstr::{BStr, BString}; -// Static causes classify leaf errors without changing their public variants or allocating a new error. +// Static causes classify concrete recovery signals without allocating a new error. static NOT_FOUND: gix_error::NotFoundError = gix_error::NotFoundError { message: std::borrow::Cow::Borrowed("Reference or object not found"), }; -static CORRUPTION: gix_error::CorruptionError = gix_error::CorruptionError { - message: std::borrow::Cow::Borrowed("Reference data is malformed or inconsistent"), -}; static INVALID_REFLOG: gix_error::ValidationError = gix_error::ValidationError { message: std::borrow::Cow::Borrowed("Invalid reflog input"), input: None, @@ -66,9 +69,6 @@ mod target; /// pub mod log; -/// -pub mod peel; - /// pub mod store { /// diff --git a/gix-ref/src/peel.rs b/gix-ref/src/peel.rs deleted file mode 100644 index 3010cbef504..00000000000 --- a/gix-ref/src/peel.rs +++ /dev/null @@ -1,102 +0,0 @@ -/// -pub mod to_id { - use gix_object::bstr::BString; - - /// The error returned by [`crate::file::ReferenceExt::peel_to_id()`]. - // TODO(review): this implementation hand-preserves `#[error(transparent)]` semantics for - // `FollowToObject`: `Display` passes the formatter through and `source()` - // forwards to the inner error's source, exactly like the `thiserror`-generated - // code did. - #[derive(Debug)] - #[expect(missing_docs)] - pub enum Error { - FollowToObject(super::to_object::Error), - Find(gix_error::Error), - NotFound { oid: gix_hash::ObjectId, name: BString }, - } - - impl std::fmt::Display for Error { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Error::FollowToObject(err) => std::fmt::Display::fmt(err, f), - Error::Find(_) => { - f.write_str("An error occurred when trying to resolve an object a reference points to") - } - Error::NotFound { oid, name } => { - write!(f, "Object {oid} as referred to by {name:?} could not be found") - } - } - } - } - - impl std::error::Error for Error { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - Error::FollowToObject(err) => err.source(), - Error::Find(err) => Some(err), - Error::NotFound { .. } => Some(&crate::NOT_FOUND), - } - } - } - - impl From for Error { - fn from(err: super::to_object::Error) -> Self { - Error::FollowToObject(err) - } - } - - impl From for Error { - fn from(err: gix_error::Exn) -> Self { - Error::Find(err.into_error()) - } - } -} - -/// -pub mod to_object { - use std::path::PathBuf; - - use crate::file; - - /// The error returned by [`file::ReferenceExt::follow_to_object_packed()`]. - #[derive(Debug)] - #[expect(missing_docs)] - pub enum Error { - Follow(file::find::existing::Error), - Cycle { start_absolute: PathBuf }, - DepthLimitExceeded { max_depth: usize }, - } - - impl std::fmt::Display for Error { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Error::Follow(_) => f.write_str("Could not follow a single level of a symbolic reference"), - #[allow(clippy::unnecessary_debug_formatting)] - // `{:?}` of a `Path` is what `thiserror` generated; keep the rendered text identical. - Error::Cycle { start_absolute } => write!( - f, - "Aborting due to reference cycle with first seen path being {start_absolute:?}" - ), - Error::DepthLimitExceeded { max_depth } => { - write!(f, "Refusing to follow more than {max_depth} levels of indirection") - } - } - } - } - - impl std::error::Error for Error { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - Error::Follow(err) => Some(err), - Error::Cycle { .. } => Some(&crate::CORRUPTION), - Error::DepthLimitExceeded { .. } => None, - } - } - } - - impl From for Error { - fn from(err: file::find::existing::Error) -> Self { - Error::Follow(err) - } - } -} diff --git a/gix-ref/src/store/file/find.rs b/gix-ref/src/store/file/find.rs index 1ac734488e2..b1c186b5b16 100644 --- a/gix-ref/src/store/file/find.rs +++ b/gix-ref/src/store/file/find.rs @@ -1,11 +1,11 @@ +use gix_error::{ErrorExt, Exn, Metadata, ResultExt, message}; + use std::{ borrow::Cow, io::{self, Read}, path::{Path, PathBuf}, }; -pub use error::Error; - use crate::{ BStr, BString, FullNameRef, PartialName, PartialNameRef, Reference, file, name::is_pseudo_ref, @@ -34,27 +34,37 @@ impl file::Store { /// for a version with more control. /// /// [git-lookup-docs]: https://github.com/git/git/blob/5d5b1473453400224ebb126bf3947e0a3276bdf5/Documentation/revisions.txt#L34-L46 - pub fn try_find<'a, Name, E>(&self, partial: Name) -> Result, Error> + pub fn try_find<'a, Name, E>(&self, partial: Name) -> Result, Exn> where Name: TryInto<&'a PartialNameRef, Error = E>, - Error: From, + Result<&'a PartialNameRef, E>: ResultExt, { let packed = self.assure_packed_refs_uptodate()?; - self.find_one_with_verified_input(partial.try_into()?, packed.as_ref().map(|b| &***b)) + self.find_one_with_verified_input( + partial + .try_into() + .or_raise_erased(|| message("The ref name or path is not a valid ref name"))?, + packed.as_ref().map(|b| &***b), + ) } - /// Similar to [`file::Store::find()`] but a non-existing ref is treated as error. + /// Like [`file::Store::try_find()`], returning `None` for a non-existing reference. /// /// Find only loose references, that is references that aren't in the packed-refs buffer. /// All symbolic references are loose references. /// `HEAD` is always a loose reference. - pub fn try_find_loose<'a, Name, E>(&self, partial: Name) -> Result, Error> + pub fn try_find_loose<'a, Name, E>(&self, partial: Name) -> Result, Exn> where Name: TryInto<&'a PartialNameRef, Error = E>, - Error: From, + Result<&'a PartialNameRef, E>: ResultExt, { - self.find_one_with_verified_input(partial.try_into()?, None) - .map(|r| r.map(Into::into)) + self.find_one_with_verified_input( + partial + .try_into() + .or_raise_erased(|| message("The ref name or path is not a valid ref name"))?, + None, + ) + .map(|r| r.map(Into::into)) } /// Similar to [`file::Store::find()`], but allows to pass a snapshotted packed buffer instead. @@ -62,19 +72,24 @@ impl file::Store { &self, partial: Name, packed: Option<&packed::Buffer>, - ) -> Result, Error> + ) -> Result, Exn> where Name: TryInto<&'a PartialNameRef, Error = E>, - Error: From, + Result<&'a PartialNameRef, E>: ResultExt, { - self.find_one_with_verified_input(partial.try_into()?, packed) + self.find_one_with_verified_input( + partial + .try_into() + .or_raise_erased(|| message("The ref name or path is not a valid ref name"))?, + packed, + ) } pub(crate) fn find_one_with_verified_input( &self, partial_name: &PartialNameRef, packed: Option<&packed::Buffer>, - ) -> Result, Error> { + ) -> Result, Exn> { fn decompose_if(mut r: Reference, input_changed_to_precomposed: bool) -> Reference { if input_changed_to_precomposed { use gix_object::bstr::ByteSlice; @@ -152,6 +167,7 @@ impl file::Store { } } + /// Resolve and read a candidate. Read failures include metadata `path` (native path), the file that failed. fn find_inner( &self, inbetween: &str, @@ -160,7 +176,7 @@ impl file::Store { packed: Option<&packed::Buffer>, path_buf: &mut BString, consider_pseudo_ref: bool, - ) -> Result, Error> { + ) -> Result, Exn> { let full_name = precomposed_partial_name .unwrap_or(partial_name) .construct_full_name_ref(inbetween, path_buf, consider_pseudo_ref); @@ -168,10 +184,9 @@ impl file::Store { Ok(content_buf) => content_buf, Err(err) if err.kind() == io::ErrorKind::NotADirectory => return Ok(None), Err(err) => { - return Err(Error::ReadFileContents { - source: err, - path: self.reference_path(full_name), - }); + return Err(err + .and_raise(Metadata::new("Could not read reference").with("path", self.reference_path(full_name))) + .erased()); } }; @@ -207,8 +222,7 @@ impl file::Store { } r }) - .map_err(|err| Error::ReferenceCreation { - source: err, + .or_raise_erased(|| ReferenceDecode { relative_path: full_name.to_path().to_owned(), })?, )), @@ -349,199 +363,92 @@ fn path_has_file_prefix(base: &Path, relative_path: &Path) -> bool { false } -/// -pub mod existing { - pub use error::Error; - - use crate::{ - PartialNameRef, Reference, - file::{self}, - store_impl::{ - file::{find, loose}, - packed, - }, - }; - - impl file::Store { - /// Similar to [`file::Store::try_find()`] but a non-existing ref is treated as error. - pub fn find<'a, Name, E>(&self, partial: Name) -> Result - where - Name: TryInto<&'a PartialNameRef, Error = E>, - find::Error: From, - { - let packed = self.assure_packed_refs_uptodate().map_err(find::Error::PackedOpen)?; - self.find_existing_inner(partial, packed.as_ref().map(|b| &***b)) - } - - /// Similar to [`file::Store::find()`], but supports a stable packed buffer. - pub fn find_packed<'a, Name, E>( - &self, - partial: Name, - packed: Option<&packed::Buffer>, - ) -> Result - where - Name: TryInto<&'a PartialNameRef, Error = E>, - find::Error: From, - { - self.find_existing_inner(partial, packed) - } - - /// Similar to [`file::Store::find()`] won't handle packed-refs. - pub fn find_loose<'a, Name, E>(&self, partial: Name) -> Result - where - Name: TryInto<&'a PartialNameRef, Error = E>, - find::Error: From, - { - self.find_existing_inner(partial, None).map(Into::into) - } - - /// Similar to [`file::Store::find()`] but a non-existing ref is treated as error. - pub(crate) fn find_existing_inner<'a, Name, E>( - &self, - partial: Name, - packed: Option<&packed::Buffer>, - ) -> Result - where - Name: TryInto<&'a PartialNameRef, Error = E>, - find::Error: From, - { - let path = partial.try_into().map_err(|err| Error::Find(err.into()))?; - match self.find_one_with_verified_input(path, packed) { - Ok(Some(r)) => Ok(r), - Ok(None) => Err(Error::NotFound { - name: path.to_partial_path().to_owned(), - }), - Err(err) => Err(err.into()), - } - } - } - - mod error { - use std::path::PathBuf; - - use crate::store_impl::file::find; - - /// The error returned by [file::Store::find_existing()][crate::file::Store::find()]. - #[derive(Debug)] - #[expect(missing_docs)] - pub enum Error { - Find(find::Error), - NotFound { name: PathBuf }, - } - - impl std::fmt::Display for Error { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Error::Find(_) => f.write_str("An error occurred while trying to find a reference"), - #[allow(clippy::unnecessary_debug_formatting)] - // `{:?}` of a `Path` is what `thiserror` generated; keep the rendered text identical. - Error::NotFound { name } => write!(f, "The ref partially named {name:?} could not be found"), - } - } - } - - impl std::error::Error for Error { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - Error::Find(err) => Some(err), - Error::NotFound { .. } => Some(&crate::NOT_FOUND), - } - } - } - - impl From for Error { - fn from(err: find::Error) -> Self { - Error::Find(err) - } - } +impl file::Store { + /// Similar to [`file::Store::try_find()`] but a non-existing ref is treated as error. + pub fn find<'a, Name, E>(&self, partial: Name) -> Result + where + Name: TryInto<&'a PartialNameRef, Error = E>, + Result<&'a PartialNameRef, E>: ResultExt, + { + let packed = self.assure_packed_refs_uptodate()?; + self.find_existing_inner(partial, packed.as_ref().map(|b| &***b)) } -} -mod error { - use std::{convert::Infallible, io, path::PathBuf}; - - use crate::{file, store_impl::packed}; - - /// The error returned by [file::Store::find()]. - #[derive(Debug)] - #[expect(missing_docs)] - pub enum Error { - RefnameValidation(gix_error::Error), - ReadFileContents { - source: io::Error, - path: PathBuf, - }, - ReferenceCreation { - source: file::loose::reference::decode::Error, - relative_path: PathBuf, - }, - PackedRef(packed::find::Error), - PackedOpen(packed::buffer::open::Error), + /// Similar to [`file::Store::find()`], but supports a stable packed buffer. + pub fn find_packed<'a, Name, E>(&self, partial: Name, packed: Option<&packed::Buffer>) -> Result + where + Name: TryInto<&'a PartialNameRef, Error = E>, + Result<&'a PartialNameRef, E>: ResultExt, + { + self.find_existing_inner(partial, packed) } - impl std::fmt::Display for Error { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Error::RefnameValidation(_) => f.write_str("The ref name or path is not a valid ref name"), - #[allow(clippy::unnecessary_debug_formatting)] - // `{:?}` of a `Path` is what `thiserror` generated; keep the rendered text identical. - Error::ReadFileContents { path, .. } => { - write!(f, "The ref file {path:?} could not be read in full") - } - Error::ReferenceCreation { relative_path, .. } => { - write!( - f, - "The reference at \"{}\" could not be instantiated", - relative_path.display() - ) - } - Error::PackedRef(_) => f.write_str("A packed ref lookup failed"), - Error::PackedOpen(_) => { - f.write_str("Could not open the packed refs buffer when trying to find references.") - } - } - } + /// Similar to [`file::Store::find()`] won't handle packed-refs. + pub fn find_loose<'a, Name, E>(&self, partial: Name) -> Result + where + Name: TryInto<&'a PartialNameRef, Error = E>, + Result<&'a PartialNameRef, E>: ResultExt, + { + self.find_existing_inner(partial, None).map(Into::into) } - impl std::error::Error for Error { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - Error::RefnameValidation(err) => Some(err), - Error::ReadFileContents { source, .. } => Some(source), - Error::ReferenceCreation { source, .. } => Some(source), - Error::PackedRef(err) => Some(err), - Error::PackedOpen(err) => Some(err), + /// Similar to [`file::Store::find()`] but a non-existing ref is treated as error. + pub(crate) fn find_existing_inner<'a, Name, E>( + &self, + partial: Name, + packed: Option<&packed::Buffer>, + ) -> Result + where + Name: TryInto<&'a PartialNameRef, Error = E>, + Result<&'a PartialNameRef, E>: ResultExt, + { + let path = partial + .try_into() + .or_raise_erased(|| message("The ref name or path is not a valid ref name"))?; + match self.find_one_with_verified_input(path, packed) { + Ok(Some(r)) => Ok(r), + Ok(None) => Err(NotFound { + name: path.to_partial_path().to_owned(), } + .raise_erased()), + Err(err) => Err(err), } } +} - impl From for Error { - fn from(err: crate::name::Error) -> Self { - Error::RefnameValidation(gix_error::Error::from_error(err)) - } - } +/// A reference lookup found no matching name, including a missing symbolic referent. +#[derive(Debug)] +pub struct NotFound { + /// The name whose lookup failed. It may have been discovered while following symbolic references. + pub name: PathBuf, +} - impl From> for Error { - fn from(err: gix_error::Exn) -> Self { - Error::RefnameValidation(err.into_error()) - } +impl std::fmt::Display for NotFound { + #[allow(clippy::unnecessary_debug_formatting)] + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "The ref partially named {:?} could not be found", self.name) } +} - impl From for Error { - fn from(err: packed::find::Error) -> Self { - Error::PackedRef(err) - } +impl std::error::Error for NotFound { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + Some(&crate::NOT_FOUND) } +} - impl From for Error { - fn from(err: packed::buffer::open::Error) -> Self { - Error::PackedOpen(err) - } - } +/// A loose reference was found at this path, but its contents could not be decoded. +/// The decoding error is retained as a cause in the exception. +#[derive(Debug)] +pub struct ReferenceDecode { + /// The resolved reference path, relative to the Git directory. + pub relative_path: PathBuf, +} - impl From for Error { - fn from(_: Infallible) -> Self { - unreachable!("this impl is needed to allow passing a known valid partial path as parameter") - } +impl std::fmt::Display for ReferenceDecode { + #[allow(clippy::unnecessary_debug_formatting)] + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "The reference at {:?} could not be decoded", self.relative_path) } } + +impl std::error::Error for ReferenceDecode {} diff --git a/gix-ref/src/store/file/log/iter.rs b/gix-ref/src/store/file/log/iter.rs index 5d40f0ccf67..ea0f1e01174 100644 --- a/gix-ref/src/store/file/log/iter.rs +++ b/gix-ref/src/store/file/log/iter.rs @@ -1,56 +1,8 @@ -use gix_object::bstr::ByteSlice; - -use crate::{ - FullNameRef, file, - file::loose::reference::logiter::must_be_io_err, - store_impl::file::{log, log::iter::decode::LineNumber}, -}; - -/// -pub mod decode { - use crate::store_impl::file::log; - - /// The error returned by items in the [forward][super::forward()] and [reverse][super::reverse()] iterators - #[derive(Debug)] - pub struct Error { - inner: log::line::decode::Error, - line: LineNumber, - } +use gix_error::{ErrorExt, Exn, Metadata, ResultExt}; - impl std::fmt::Display for Error { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "In line {}: {}", self.line, self.inner) - } - } - - impl std::error::Error for Error { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - Some(&self.inner) - } - } - - impl Error { - pub(crate) fn new(err: log::line::decode::Error, line: LineNumber) -> Self { - Error { line, inner: err } - } - } - - #[derive(Debug)] - pub(crate) enum LineNumber { - FromStart(usize), - FromEnd(usize), - } +use gix_object::bstr::ByteSlice; - impl std::fmt::Display for LineNumber { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let (line, suffix) = match self { - LineNumber::FromStart(line) => (line, ""), - LineNumber::FromEnd(line) => (line, " from the end"), - }; - write!(f, "{}{}", line + 1, suffix) - } - } -} +use crate::{FullNameRef, file, store_impl::file::log}; /// Returns a forward iterator over the given `lines`, starting from the first line in the file and ending at the last. /// @@ -71,12 +23,13 @@ pub struct Forward<'a> { } impl<'a> Iterator for Forward<'a> { - type Item = Result, decode::Error>; + type Item = Result, Exn>; + /// Decode failures include metadata `line` (one-based position) and `from_end` (whether counting from the end). fn next(&mut self) -> Option { - self.inner.next().map(|(ln, line)| { - log::LineRef::from_bytes(line).map_err(|err| decode::Error::new(err, decode::LineNumber::FromStart(ln))) - }) + self.inner + .next() + .map(|(ln, line)| log::LineRef::from_bytes(line).or_raise(|| invalid_reflog_entry(ln + 1, false))) } } @@ -96,15 +49,13 @@ impl Platform<'_, '_> { pub fn rev(&mut self) -> std::io::Result>> { self.buf.clear(); self.buf.resize(1024 * 4, 0); - self.store - .reflog_iter_rev(self.name, &mut self.buf) - .map_err(must_be_io_err) + self.store.reflog_iter_rev_inner(self.name, &mut self.buf) } /// Return a forward iterator over all log-lines, oldest to most recent. pub fn all(&mut self) -> std::io::Result>> { self.buf.clear(); - self.store.reflog_iter(self.name, &mut self.buf).map_err(must_be_io_err) + self.store.reflog_iter_inner(self.name, &mut self.buf) } } @@ -144,63 +95,20 @@ where }) } -/// -pub mod reverse { - - use super::decode; - - /// The error returned by the [`Reverse`][super::Reverse] iterator - #[derive(Debug)] - #[expect(missing_docs)] - pub enum Error { - Io(std::io::Error), - Decode(decode::Error), - } - - impl std::fmt::Display for Error { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Error::Io(_) => f.write_str("The buffer could not be filled to make more lines available"), - Error::Decode(_) => f.write_str("Could not decode log line"), - } - } - } - - impl std::error::Error for Error { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - Error::Io(err) => Some(err), - Error::Decode(err) => Some(err), - } - } - } - - impl From for Error { - fn from(err: std::io::Error) -> Self { - Error::Io(err) - } - } - - impl From for Error { - fn from(err: decode::Error) -> Self { - Error::Decode(err) - } - } -} - impl Iterator for Reverse<'_, F> where F: std::io::Read + std::io::Seek, { - type Item = Result; + type Item = Result; + /// Decode failures include metadata `line` (one-based position) and `from_end` (whether counting from the end). fn next(&mut self) -> Option { match (self.last_nl_pos.take(), self.read_and_pos.take()) { // Initial state - load first data block (None, Some((mut read, pos))) => { let npos = pos.saturating_sub(self.buf.len() as u64); if let Err(err) = read.seek(std::io::SeekFrom::Start(npos)) { - return Some(Err(err.into())); + return Some(Err(err.raise_erased())); } let n = (pos - npos) as usize; @@ -209,7 +117,7 @@ where } let buf = &mut self.buf[..n]; if let Err(err) = read.read_exact(buf) { - return Some(Err(err.into())); + return Some(Err(err.raise_erased())); } let last_byte = *buf.last().expect("we have read non-zero bytes before"); @@ -225,9 +133,7 @@ where let buf = &self.buf[start + 1..end]; let res = Some( log::LineRef::from_bytes(buf) - .map_err(|err| { - reverse::Error::Decode(decode::Error::new(err, LineNumber::FromEnd(self.count))) - }) + .or_raise_erased(|| invalid_reflog_entry(self.count + 1, true)) .map(Into::into), ); self.count += 1; @@ -239,9 +145,7 @@ where let buf = &self.buf[..end]; Some( log::LineRef::from_bytes(buf) - .map_err(|err| { - reverse::Error::Decode(decode::Error::new(err, LineNumber::FromEnd(self.count))) - }) + .or_raise_erased(|| invalid_reflog_entry(self.count + 1, true)) .map(Into::into), ) } else { @@ -251,15 +155,15 @@ where "buffer too small for line size, got until {:?}", self.buf.as_bstr() )) - .into())); + .raise_erased())); } let n = (last_read_pos - npos) as usize; self.buf.copy_within(0..end, n); if let Err(err) = read.seek(std::io::SeekFrom::Start(npos)) { - return Some(Err(err.into())); + return Some(Err(err.raise_erased())); } if let Err(err) = read.read_exact(&mut self.buf[..n]) { - return Some(Err(err.into())); + return Some(Err(err.raise_erased())); } self.read_and_pos = Some((read, npos)); self.last_nl_pos = Some(n + end); @@ -273,3 +177,9 @@ where } } } + +fn invalid_reflog_entry(line: usize, from_end: bool) -> Metadata { + Metadata::new("Invalid reflog entry") + .with("line", line) + .with("from_end", from_end) +} diff --git a/gix-ref/src/store/file/log/line.rs b/gix-ref/src/store/file/log/line.rs index 6111738b2a5..df679b1cf63 100644 --- a/gix-ref/src/store/file/log/line.rs +++ b/gix-ref/src/store/file/log/line.rs @@ -16,45 +16,25 @@ mod write { use crate::log::Line; - /// The Error produced by [`Line::write_to()`] (but wrapped in an io error). - #[derive(Debug)] - enum Error { - IllegalCharacter, - } - - impl std::fmt::Display for Error { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Error::IllegalCharacter => f.write_str(r"Messages must not contain newlines (\n)"), - } - } - } - - impl std::error::Error for Error { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - Some(&crate::INVALID_REFLOG) - } - } - - impl From for io::Error { - fn from(err: Error) -> Self { - io::Error::other(err) - } - } - /// Output impl Line { /// Serialize this instance to `out` in the git serialization format for ref log lines. pub fn write_to(&self, out: &mut dyn io::Write) -> io::Result<()> { write!(out, "{} {} ", self.previous_oid, self.new_oid)?; self.signature.write_to(out)?; - writeln!(out, "\t{}", check_newlines(self.message.as_ref())?) + writeln!( + out, + "\t{}", + check_newlines(self.message.as_ref()).map_err(io::Error::other)? + ) } } - fn check_newlines(input: &BStr) -> Result<&BStr, Error> { + fn check_newlines(input: &BStr) -> Result<&BStr, gix_error::ValidationError> { if input.find_byte(b'\n').is_some() { - return Err(Error::IllegalCharacter); + return Err(gix_error::ValidationError::new( + r"Messages must not contain newlines (\n)", + )); } Ok(input) } @@ -83,42 +63,12 @@ impl<'a> From> for Line { } } -/// -pub mod decode { - use gix_error::{CorruptionError, ErrorExt, Exn, ResultExt}; +mod decode { + use gix_error::{CorruptionError, ErrorExt, Exn, Metadata, ResultExt}; use gix_object::bstr::{BStr, ByteSlice}; use crate::{file::log::LineRef, parse::hex_hash_any}; - /// - mod error { - use gix_object::bstr::BString; - - /// The error returned by [`from_bytes(…)`][super::Line::from_bytes()] - #[derive(Debug)] - pub struct Error { - pub input: BString, - pub(super) source: gix_error::Error, - } - - impl std::fmt::Display for Error { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - r"{:?} did not match ' <> \t'", - self.input - ) - } - } - - impl std::error::Error for Error { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - Some(&self.source) - } - } - } - pub use error::Error; - impl<'a> LineRef<'a> { /// Decode a reflog line from the given bytes. /// @@ -126,11 +76,10 @@ pub mod decode { /// signature, and an optional tab-separated message, for example: /// /// `0123456789012345678901234567890123456789 89abcdef89abcdef89abcdef89abcdef89abcdef Name 1700000000 +0000\tmessage` - pub fn from_bytes(input: &'a [u8]) -> Result, Error> { - decode(input).map_err(|err| Error { - input: first_line(input).into(), - source: err.into_error(), - }) + /// + /// Errors include metadata `input` (bytes), the first input line without its trailing newline. + pub fn from_bytes(input: &'a [u8]) -> Result, Exn> { + decode(input).or_raise(|| Metadata::new("Could not decode reflog line").with("input", first_line(input))) } } diff --git a/gix-ref/src/store/file/log/mod.rs b/gix-ref/src/store/file/log/mod.rs index 3ba2582308c..da583da3805 100644 --- a/gix-ref/src/store/file/log/mod.rs +++ b/gix-ref/src/store/file/log/mod.rs @@ -1,6 +1,6 @@ use gix_object::bstr::BStr; -pub use super::loose::reflog::{Error, create_or_update}; +pub use super::loose::reflog::create_or_update; /// pub mod iter; diff --git a/gix-ref/src/store/file/loose/reference/decode.rs b/gix-ref/src/store/file/loose/reference/decode.rs index a778268dfbf..ecf3d886cf7 100644 --- a/gix-ref/src/store/file/loose/reference/decode.rs +++ b/gix-ref/src/store/file/loose/reference/decode.rs @@ -1,3 +1,5 @@ +use gix_error::{CorruptionError, ErrorExt, Exn, Metadata, ResultExt, message}; + use gix_hash::ObjectId; use gix_object::bstr::BString; @@ -8,63 +10,26 @@ enum MaybeUnsafeState { UnvalidatedPath(BString), } -/// The error returned by [`Reference::try_from_path()`]. -#[derive(Debug)] -#[expect(missing_docs)] -pub enum Error { - /// Git's placeholder for an unsupported backend, such as reftable, was encountered. - UnsupportedStorage, - Parse { - content: BString, - }, - RefnameValidation { - source: gix_validate::reference::name::Error, - path: BString, - }, -} - -impl std::fmt::Display for Error { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Error::UnsupportedStorage => { - f.write_str("This reference uses an unsupported storage backend, such as reftable") - } - Error::Parse { content } => write!(f, "{content:?} could not be parsed"), - Error::RefnameValidation { path, .. } => { - write!( - f, - "The path {path:?} to a symbolic reference within a ref file is invalid" - ) - } - } - } -} - -impl std::error::Error for Error { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - Error::UnsupportedStorage => None, - Error::Parse { .. } => Some(&crate::CORRUPTION), - Error::RefnameValidation { source, .. } => Some(source), - } - } -} - impl TryFrom for Target { - type Error = Error; + type Error = Exn; + /// Invalid symbolic targets include metadata `target` (bytes), as read from the reference contents. fn try_from(v: MaybeUnsafeState) -> Result { Ok(match v { MaybeUnsafeState::Id(id) => Target::Object(id), MaybeUnsafeState::UnvalidatedPath(name) => { Target::Symbolic(match gix_validate::reference::name(name.as_ref()) { Ok(_) => FullName(name), - Err(_) if name == "refs/heads/.invalid" => return Err(Error::UnsupportedStorage), + Err(_) if name == "refs/heads/.invalid" => { + return Err( + message("This reference uses an unsupported storage backend, such as reftable") + .raise_erased(), + ); + } Err(err) => { - return Err(Error::RefnameValidation { - source: err, - path: name, - }); + return Err(err + .and_raise(Metadata::new("Invalid symbolic reference target").with("target", name)) + .erased()); } }) } @@ -75,14 +40,15 @@ impl TryFrom for Target { impl Reference { /// Create a new reference named `name` from the loose reference file contents in `path_contents`, /// parsing object ids as `object_hash`. - pub fn try_from_path(name: FullName, path_contents: &[u8], object_hash: gix_hash::Kind) -> Result { + /// + /// Errors include metadata `input` (bytes), the supplied reference contents. + pub fn try_from_path(name: FullName, path_contents: &[u8], object_hash: gix_hash::Kind) -> Result { Ok(Reference { name, target: parse(path_contents, object_hash) - .map_err(|()| Error::Parse { - content: path_contents.into(), - })? - .try_into()?, + .map_err(|()| CorruptionError::new("Reference content could not be parsed").raise_erased()) + .and_then(Target::try_from) + .or_raise_erased(|| Metadata::new("Could not decode reference").with("input", path_contents))?, }) } } diff --git a/gix-ref/src/store/file/loose/reference/logiter.rs b/gix-ref/src/store/file/loose/reference/logiter.rs index 0bc81f22de9..1fe5f43e560 100644 --- a/gix-ref/src/store/file/loose/reference/logiter.rs +++ b/gix-ref/src/store/file/loose/reference/logiter.rs @@ -1,15 +1,8 @@ use crate::store_impl::{ file, - file::{log, loose, loose::Reference}, + file::{log, loose::Reference}, }; -pub(crate) fn must_be_io_err(err: loose::reflog::Error) -> std::io::Error { - match err { - loose::reflog::Error::Io(err) => err, - loose::reflog::Error::RefnameValidation(_) => unreachable!("we are called from a valid ref"), - } -} - impl Reference { /// Returns true if a reflog exists in the given `store`. /// @@ -30,7 +23,7 @@ impl Reference { store: &file::Store, buf: &'b mut [u8], ) -> std::io::Result>> { - store.reflog_iter_rev(self.name.as_ref(), buf).map_err(must_be_io_err) + store.reflog_iter_rev_inner(self.name.as_ref(), buf) } /// Return a reflog forward iterator for this ref and write its file contents into `buf`, in the given `store`. @@ -41,7 +34,8 @@ impl Reference { &'a self, store: &file::Store, buf: &'b mut Vec, - ) -> std::io::Result, log::iter::decode::Error>> + 'a>> { - store.reflog_iter(self.name.as_ref(), buf).map_err(must_be_io_err) + ) -> std::io::Result, gix_error::Exn>> + 'a>> + { + store.reflog_iter_inner(self.name.as_ref(), buf) } } diff --git a/gix-ref/src/store/file/loose/reflog.rs b/gix-ref/src/store/file/loose/reflog.rs index 366377940ce..c2d697902e2 100644 --- a/gix-ref/src/store/file/loose/reflog.rs +++ b/gix-ref/src/store/file/loose/reflog.rs @@ -1,3 +1,5 @@ +use gix_error::{Exn, Metadata, ResultExt, message}; + use std::{io::Read, path::PathBuf}; use crate::{ @@ -14,7 +16,6 @@ impl file::Store { pub fn reflog_exists<'a, Name, E>(&self, name: Name) -> Result where Name: TryInto<&'a FullNameRef, Error = E>, - crate::name::Error: From, { Ok(self.reflog_path(name.try_into()?).is_file()) } @@ -23,16 +24,29 @@ impl file::Store { /// /// The iterator will traverse log entries from most recent to oldest, reading the underlying file in chunks from the back. /// Return `Ok(None)` if no reflog exists. + /// + /// Read failures include metadata `path` (native path), the resolved reflog path. pub fn reflog_iter_rev<'a, 'b, Name, E>( &self, name: Name, buf: &'b mut [u8], - ) -> Result>, Error> + ) -> Result>, Exn> where Name: TryInto<&'a FullNameRef, Error = E>, - crate::name::Error: From, + Result<&'a FullNameRef, E>: ResultExt, { - let name: &FullNameRef = name.try_into().map_err(|err| Error::RefnameValidation(err.into()))?; + let name = name + .try_into() + .or_raise_erased(|| message("The reflog name or path is not a valid ref name"))?; + self.reflog_iter_rev_inner(name, buf) + .or_raise_erased(|| Metadata::new("Could not read reflog").with("path", self.reflog_path(name))) + } + + pub(crate) fn reflog_iter_rev_inner<'b>( + &self, + name: &FullNameRef, + buf: &'b mut [u8], + ) -> std::io::Result>> { let path = self.reflog_path(name); if path.is_dir() { return Ok(None); @@ -40,7 +54,7 @@ impl file::Store { match std::fs::File::open(&path) { Ok(file) => Ok(Some(log::iter::reverse(file, buf)?)), Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None), - Err(err) => Err(err.into()), + Err(err) => Err(err), } } @@ -48,29 +62,42 @@ impl file::Store { /// /// The iterator will traverse log entries from oldest to newest. /// Return `Ok(None)` if no reflog exists. + /// + /// Read failures include metadata `path` (native path), the resolved reflog path. pub fn reflog_iter<'a, 'b, Name, E>( &self, name: Name, buf: &'b mut Vec, - ) -> Result>, Error> + ) -> Result>, Exn> where Name: TryInto<&'a FullNameRef, Error = E>, - crate::name::Error: From, + Result<&'a FullNameRef, E>: ResultExt, { - let name: &FullNameRef = name.try_into().map_err(|err| Error::RefnameValidation(err.into()))?; + let name = name + .try_into() + .or_raise_erased(|| message("The reflog name or path is not a valid ref name"))?; + self.reflog_iter_inner(name, buf) + .or_raise_erased(|| Metadata::new("Could not read reflog").with("path", self.reflog_path(name))) + } + + pub(crate) fn reflog_iter_inner<'b>( + &self, + name: &FullNameRef, + buf: &'b mut Vec, + ) -> std::io::Result>> { let path = self.reflog_path(name); match std::fs::File::open(&path) { Ok(mut file) => { buf.clear(); if let Err(err) = file.read_to_end(buf) { - return if path.is_dir() { Ok(None) } else { Err(err.into()) }; + return if path.is_dir() { Ok(None) } else { Err(err) }; } Ok(Some(log::iter::forward(buf))) } Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None), #[cfg(windows)] Err(err) if err.kind() == std::io::ErrorKind::PermissionDenied => Ok(None), - Err(err) => Err(err.into()), + Err(err) => Err(err), } } } @@ -91,12 +118,15 @@ pub mod create_or_update { path::{Path, PathBuf}, }; + use gix_error::{ErrorExt, Exn, Metadata, ResultExt}; use gix_hash::{ObjectId, oid}; use gix_object::bstr::BStr; use crate::store_impl::{file, file::WriteReflog}; impl file::Store { + /// Append a reflog entry. Filesystem failures include metadata `path` (native path), the affected file or directory. + /// A missing identity is reported as [`MissingCommitter`] only when a log entry must actually be written. pub(crate) fn reflog_create_or_append( &self, name: &FullNameRef, @@ -105,7 +135,7 @@ pub mod create_or_update { committer: Option>, message: &BStr, mut force_create_reflog: bool, - ) -> Result<(), Error> { + ) -> Result<(), Exn> { let (reflog_base, full_name) = self.reflog_base_and_relative_path(name); match self.write_reflog { WriteReflog::Normal | WriteReflog::Always => { @@ -118,11 +148,8 @@ pub mod create_or_update { if force_create_reflog || self.should_autocreate_reflog(&full_name) { let parent_dir = log_path.parent().expect("always with parent directory"); - gix_tempfile::create_dir::all(parent_dir, Default::default()).map_err(|err| { - Error::CreateLeadingDirectories { - source: err, - reflog_directory: parent_dir.to_owned(), - } + gix_tempfile::create_dir::all(parent_dir, Default::default()).or_raise_erased(|| { + Metadata::new("Could not create reflog directory").with("path", parent_dir) })?; options.create(true); } @@ -136,21 +163,22 @@ pub mod create_or_update { gix_tempfile::remove_dir::empty_depth_first(log_path.clone()) .and_then(|_| options.open(&log_path)) .map(Some) - .map_err(|source| Error::Append { - source, - reflog_path: self.reflog_path(name), + .or_raise_erased(|| { + Metadata::new("Could not open reflog for appending") + .with("path", log_path.as_path()) })? } else { - return Err(Error::Append { - source: err, - reflog_path: log_path, - }); + return Err(err + .and_raise( + Metadata::new("Could not open reflog for appending").with("path", log_path), + ) + .erased()); } } }; if let Some(mut file) = file_for_appending { - let committer = committer.ok_or(Error::MissingCommitter)?; + let committer = committer.ok_or_else(|| MissingCommitter.raise_erased())?; write!(file, "{} {} ", previous_oid.unwrap_or_else(|| new.kind().null()), new) .and_then(|_| committer.trim().write_to(&mut file)) .and_then(|_| { @@ -160,9 +188,8 @@ pub mod create_or_update { writeln!(file) } }) - .map_err(|err| Error::Append { - source: err, - reflog_path: self.reflog_path(name), + .or_raise_erased(|| { + Metadata::new("Could not append reflog entry").with("path", log_path.as_path()) })?; } Ok(()) @@ -198,100 +225,24 @@ pub mod create_or_update { } } - #[cfg(test)] - mod tests; - - mod error { - use std::path::PathBuf; - - /// The error returned when creating or appending to a reflog - #[derive(Debug)] - #[expect(missing_docs)] - pub enum Error { - CreateLeadingDirectories { - source: std::io::Error, - reflog_directory: PathBuf, - }, - Append { - source: std::io::Error, - reflog_path: PathBuf, - }, - MessageWithNewlines, - MissingCommitter, - } - - impl std::fmt::Display for Error { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - #[allow(clippy::unnecessary_debug_formatting)] - // `{:?}` of a `Path` is what `thiserror` generated; keep the rendered text identical. - Error::CreateLeadingDirectories { reflog_directory, .. } => write!( - f, - "Could create one or more directories in {reflog_directory:?} to contain reflog file" - ), - #[allow(clippy::unnecessary_debug_formatting)] - // `{:?}` of a `Path` is what `thiserror` generated; keep the rendered text identical. - Error::Append { reflog_path, .. } => { - write!(f, "Could not open reflog file at {reflog_path:?} for appending") - } - Error::MessageWithNewlines => f.write_str("reflog message must not contain newlines"), - Error::MissingCommitter => f.write_str("reflog messages need a committer which isn't set"), - } - } - } - - impl std::error::Error for Error { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - Error::CreateLeadingDirectories { source, .. } => Some(source), - Error::Append { source, .. } => Some(source), - Error::MessageWithNewlines | Error::MissingCommitter => Some(&crate::INVALID_REFLOG), - } - } - } - } - pub use error::Error; - - use crate::FullNameRef; -} - -mod error { - /// The error returned by [`crate::file::Store::reflog_iter()`]. + /// A reflog entry requires a committer identity which wasn't provided. #[derive(Debug)] - #[expect(missing_docs)] - pub enum Error { - RefnameValidation(crate::name::Error), - Io(std::io::Error), - } + pub struct MissingCommitter; - impl std::fmt::Display for Error { + impl std::fmt::Display for MissingCommitter { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Error::RefnameValidation(_) => f.write_str("The reflog name or path is not a valid ref name"), - Error::Io(_) => f.write_str("The reflog file could not read"), - } + f.write_str("reflog messages need a committer which isn't set") } } - impl std::error::Error for Error { + impl std::error::Error for MissingCommitter { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - Error::RefnameValidation(err) => Some(err), - Error::Io(err) => Some(err), - } + Some(&crate::INVALID_REFLOG) } } - impl From for Error { - fn from(err: crate::name::Error) -> Self { - Error::RefnameValidation(err) - } - } + #[cfg(test)] + mod tests; - impl From for Error { - fn from(err: std::io::Error) -> Self { - Error::Io(err) - } - } + use crate::FullNameRef; } -pub use error::Error; diff --git a/gix-ref/src/store/file/loose/reflog/create_or_update/tests.rs b/gix-ref/src/store/file/loose/reflog/create_or_update/tests.rs index 4a18fa445aa..8b2fd14e36c 100644 --- a/gix-ref/src/store/file/loose/reflog/create_or_update/tests.rs +++ b/gix-ref/src/store/file/loose/reflog/create_or_update/tests.rs @@ -5,7 +5,7 @@ use gix_testtools::tempfile::TempDir; use super::*; -type Result = std::result::Result>; +use gix_testtools::Result; static SHA1_TO_SHA256_HASHES: std::sync::LazyLock> = std::sync::LazyLock::new(|| { diff --git a/gix-ref/src/store/file/mod.rs b/gix-ref/src/store/file/mod.rs index 022ad604e90..97e49c1088c 100644 --- a/gix-ref/src/store/file/mod.rs +++ b/gix-ref/src/store/file/mod.rs @@ -125,11 +125,6 @@ mod overlay_iter; /// pub mod iter { pub use super::overlay_iter::{LooseThenPacked, Platform}; - - /// - pub mod loose_then_packed { - pub use super::super::overlay_iter::Error; - } } /// diff --git a/gix-ref/src/store/file/overlay_iter.rs b/gix-ref/src/store/file/overlay_iter.rs index aa28dc41168..9f06edf7fbd 100644 --- a/gix-ref/src/store/file/overlay_iter.rs +++ b/gix-ref/src/store/file/overlay_iter.rs @@ -1,3 +1,5 @@ +use gix_error::{Exn, Metadata, ResultExt, message}; + use gix_object::bstr::ByteSlice; use gix_path::RelativePath; use std::{ @@ -64,50 +66,30 @@ impl<'p> LooseThenPacked<'p, '_> { } } - fn convert_packed( - &mut self, - packed: Result, packed::iter::Error>, - ) -> Result { - packed - .map(Into::into) - .map(|r| self.strip_namespace(r)) - .map_err(|err| match err { - packed::iter::Error::Reference { - invalid_line, - line_number, - source, - } => Error::PackedReference { - invalid_line, - line_number, - source, - }, - packed::iter::Error::Header { .. } => unreachable!("this one only happens on iteration creation"), - }) + fn convert_packed(&mut self, packed: Result, Exn>) -> Result { + packed.map(Into::into).map(|r| self.strip_namespace(r)).or_erased() } - fn convert_loose(&mut self, res: std::io::Result<(PathBuf, FullName)>) -> Result { + /// Read failures include metadata `path` (native path), the loose reference being visited. + fn convert_loose(&mut self, res: std::io::Result<(PathBuf, FullName)>) -> Result { let buf = &mut self.buf; let git_dir = self.git_dir; let common_dir = self.common_dir; - let (refpath, name) = res.map_err(Error::Traversal)?; + let (refpath, name) = res.or_raise_erased(|| message("Could not traverse reference directory"))?; std::fs::File::open(&refpath) .and_then(|mut f| { buf.clear(); f.read_to_end(buf) }) - .map_err(|err| Error::ReadFileContents { - source: err, - path: refpath.to_owned(), - })?; + .or_raise_erased(|| Metadata::new("Could not read reference").with("path", refpath.as_path()))?; loose::Reference::try_from_path(name, buf, self.object_hash) - .map_err(|err| { + .or_raise_erased(|| { let relative_path = refpath .strip_prefix(git_dir) .ok() .or_else(|| common_dir.and_then(|common_dir| refpath.strip_prefix(common_dir).ok())) .expect("one of our bases contains the path"); - Error::ReferenceCreation { - source: err, + file::find::ReferenceDecode { relative_path: relative_path.into(), } }) @@ -117,7 +99,7 @@ impl<'p> LooseThenPacked<'p, '_> { } impl Iterator for LooseThenPacked<'_, '_> { - type Item = Result; + type Item = Result; fn next(&mut self) -> Option { fn advance_to_non_private(iter: &mut Peekable) { @@ -226,7 +208,7 @@ impl file::Store { /// /// Note that since packed-refs are storing refs as precomposed unicode if [`Self::precompose_unicode`] is true, for consistency /// we also return loose references as precomposed unicode. - pub fn iter(&self) -> Result, packed::buffer::open::Error> { + pub fn iter(&self) -> Result, Exn> { Ok(Platform { store: self, packed: self.assure_packed_refs_uptodate()?, @@ -447,7 +429,7 @@ impl file::Store { Some(prefix) => packed.iter_prefixed(prefix.into_owned()), None => packed.iter(), } - .map_err(std::io::Error::other)? + .map_err(|err| std::io::Error::other(err.into_error()))? .peekable(), ), None => None, @@ -459,68 +441,3 @@ impl file::Store { }) } } - -mod error { - use std::{io, path::PathBuf}; - - use gix_object::bstr::BString; - - use crate::store_impl::file; - - /// The error returned by the [`LooseThenPacked`][super::LooseThenPacked] iterator. - #[derive(Debug)] - #[expect(missing_docs)] - pub enum Error { - Traversal(io::Error), - ReadFileContents { - source: io::Error, - path: PathBuf, - }, - ReferenceCreation { - source: file::loose::reference::decode::Error, - relative_path: PathBuf, - }, - PackedReference { - invalid_line: BString, - line_number: usize, - source: gix_error::Error, - }, - } - - impl std::fmt::Display for Error { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Error::Traversal(_) => f.write_str("The file system could not be traversed"), - #[allow(clippy::unnecessary_debug_formatting)] - // `{:?}` of a `Path` is what `thiserror` generated; keep the rendered text identical. - Error::ReadFileContents { path, .. } => { - write!(f, "The ref file {path:?} could not be read in full") - } - Error::ReferenceCreation { relative_path, .. } => { - write!( - f, - "The reference at \"{}\" could not be instantiated", - relative_path.display() - ) - } - Error::PackedReference { - invalid_line, - line_number, - .. - } => write!(f, "Invalid reference in line {line_number}: {invalid_line:?}"), - } - } - } - - impl std::error::Error for Error { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - Error::Traversal(err) => Some(err), - Error::ReadFileContents { source, .. } => Some(source), - Error::ReferenceCreation { source, .. } => Some(source), - Error::PackedReference { source, .. } => Some(source), - } - } - } -} -pub use error::Error; diff --git a/gix-ref/src/store/file/packed.rs b/gix-ref/src/store/file/packed.rs index 5192236ceb4..0cbc038b600 100644 --- a/gix-ref/src/store/file/packed.rs +++ b/gix-ref/src/store/file/packed.rs @@ -1,3 +1,5 @@ +use gix_error::{Exn, ResultExt, message}; + use std::path::PathBuf; use crate::store_impl::{file, packed}; @@ -5,12 +7,9 @@ use crate::store_impl::{file, packed}; impl file::Store { /// Return a packed transaction ready to receive updates. Use this to create or update `packed-refs`. /// Note that if you already have a [`packed::Buffer`] then use its [`packed::Buffer::into_transaction()`] method instead. - pub(crate) fn packed_transaction( - &self, - lock_mode: gix_lock::acquire::Fail, - ) -> Result { + pub(crate) fn packed_transaction(&self, lock_mode: gix_lock::acquire::Fail) -> Result { let lock = gix_lock::File::acquire_to_update_resource(self.packed_refs_path(), lock_mode, None) - .map_err(|err| transaction::Error::TransactionLock(std::io::Error::other(err.into_error())))?; + .or_raise_erased(|| message("Could not lock packed refs"))?; // We 'steal' the possibly existing packed buffer which may safe time if it's already there and fresh. // If nothing else is happening, nobody will get to see the soon stale buffer either, but if so, they will pay // for reloading it. That seems preferred over always loading up a new one. @@ -26,14 +25,14 @@ impl file::Store { /// /// Note that it will automatically be memory mapped if it exceeds the default threshold of 32KB. /// Change the threshold with [file::Store::set_packed_buffer_mmap_threshold()]. - pub fn open_packed_buffer(&self) -> Result, packed::buffer::open::Error> { + pub fn open_packed_buffer(&self) -> Result, Exn> { match packed::Buffer::open( self.packed_refs_path(), self.packed_buffer_mmap_threshold, self.object_hash, ) { Ok(buf) => Ok(Some(buf)), - Err(packed::buffer::open::Error::Io(err)) if err.kind() == std::io::ErrorKind::NotFound => Ok(None), + Err(err) if err.is_not_found() => Ok(None), Err(err) => Err(err), } } @@ -43,9 +42,7 @@ impl file::Store { /// /// Use this to make successive calls to [`file::Store::try_find_packed()`] /// or obtain iterators using [`file::Store::iter_packed()`] in a way that assures the packed-refs content won't change. - pub fn cached_packed_buffer( - &self, - ) -> Result, packed::buffer::open::Error> { + pub fn cached_packed_buffer(&self) -> Result, Exn> { self.assure_packed_refs_uptodate() } @@ -61,46 +58,6 @@ impl file::Store { } } -/// -pub mod transaction { - - use crate::store_impl::packed; - - /// The error returned by [`file::Transaction::prepare()`][crate::file::Transaction::prepare()]. - #[derive(Debug)] - #[expect(missing_docs)] - pub enum Error { - BufferOpen(packed::buffer::open::Error), - TransactionLock(std::io::Error), - } - - impl std::fmt::Display for Error { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Error::BufferOpen(_) => { - f.write_str("An existing pack couldn't be opened or read when preparing a transaction") - } - Error::TransactionLock(_) => f.write_str("The lock for a packed transaction could not be obtained"), - } - } - } - - impl std::error::Error for Error { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - Error::BufferOpen(err) => Some(err), - Error::TransactionLock(err) => Some(err), - } - } - } - - impl From for Error { - fn from(err: packed::buffer::open::Error) -> Self { - Error::BufferOpen(err) - } - } -} - /// An up-to-date snapshot of the packed refs buffer. pub type SharedBufferSnapshot = gix_fs::SharedFileSnapshot; @@ -108,6 +65,7 @@ pub(crate) mod modifiable { use gix_features::threading::OwnShared; use crate::{file, packed}; + use gix_error::{Exn, Metadata, ResultExt}; pub(crate) type MutableSharedBuffer = OwnShared>; @@ -119,15 +77,21 @@ pub(crate) mod modifiable { /// /// As some filesystems don't have nanosecond granularity, changes are likely to be missed /// if they happen within one second otherwise. - pub fn force_refresh_packed_buffer(&self) -> Result<(), packed::buffer::open::Error> { + /// + /// Metadata `path` (native path) identifies a packed-refs file whose modification time could not be read. + pub fn force_refresh_packed_buffer(&self) -> Result<(), Exn> { self.packed.force_refresh(|| { - let modified = self.packed_refs_path().metadata()?.modified()?; + let path = self.packed_refs_path(); + let modified = path + .metadata() + .and_then(|metadata| metadata.modified()) + .or_raise_erased(|| { + Metadata::new("Could not read packed refs modification time").with("path", path) + })?; self.open_packed_buffer().map(|packed| Some(modified).zip(packed)) }) } - pub(crate) fn assure_packed_refs_uptodate( - &self, - ) -> Result, packed::buffer::open::Error> { + pub(crate) fn assure_packed_refs_uptodate(&self) -> Result, Exn> { self.packed.recent_snapshot( || self.packed_refs_path().metadata().and_then(|m| m.modified()).ok(), || self.open_packed_buffer(), diff --git a/gix-ref/src/store/file/raw_ext.rs b/gix-ref/src/store/file/raw_ext.rs index d492f6cafe8..0e3f942f82c 100644 --- a/gix-ref/src/store/file/raw_ext.rs +++ b/gix-ref/src/store/file/raw_ext.rs @@ -1,10 +1,10 @@ use std::collections::BTreeSet; -use gix_error::{CorruptionError, ResultExt, message}; +use gix_error::{CorruptionError, ErrorExt, Exn, Metadata, NotFoundError, ResultExt, message}; use gix_hash::ObjectId; use crate::{ - Target, packed, peel, + Target, packed, raw::Reference, store_impl::{file, file::log}, }; @@ -27,11 +27,7 @@ pub trait ReferenceExt: Sealed { /// This is useful to learn where this reference is ultimately pointing to after following all symbolic /// refs and all annotated tags to the first non-tag object. #[deprecated = "Use `peel_to_id()` instead"] - fn peel_to_id_in_place( - &mut self, - store: &file::Store, - objects: &dyn gix_object::Find, - ) -> Result; + fn peel_to_id_in_place(&mut self, store: &file::Store, objects: &dyn gix_object::Find) -> Result; /// Follow all symbolic targets this reference might point to and peel the underlying object /// to the end of the tag-chain, returning the first non-tag object the annotated tag points to, @@ -42,11 +38,7 @@ pub trait ReferenceExt: Sealed { /// /// Note that this method mutates `self` in place if it does not already point to a /// non-symbolic object. - fn peel_to_id( - &mut self, - store: &file::Store, - objects: &dyn gix_object::Find, - ) -> Result; + fn peel_to_id(&mut self, store: &file::Store, objects: &dyn gix_object::Find) -> Result; /// Like [`ReferenceExt::peel_to_id_in_place()`], but with support for a known stable `packed` buffer /// to use for resolving symbolic links. @@ -56,16 +48,17 @@ pub trait ReferenceExt: Sealed { store: &file::Store, objects: &dyn gix_object::Find, packed: Option<&packed::Buffer>, - ) -> Result; + ) -> Result; /// Like [`ReferenceExt::peel_to_id()`], but with support for a known stable `packed` buffer to /// use for resolving symbolic links. + /// Object lookup failures include metadata `object_id` (hex text) and `reference` (name bytes). fn peel_to_id_packed( &mut self, store: &file::Store, objects: &dyn gix_object::Find, packed: Option<&packed::Buffer>, - ) -> Result; + ) -> Result; /// Like [`ReferenceExt::follow()`], but follows all symbolic references while gracefully handling loops, /// altering this instance in place. @@ -74,30 +67,27 @@ pub trait ReferenceExt: Sealed { &mut self, store: &file::Store, packed: Option<&packed::Buffer>, - ) -> Result; + ) -> Result; /// Like [`ReferenceExt::follow()`], but follows all symbolic references while gracefully handling loops, /// altering this instance in place. + /// Cycle failures include metadata `path` (native path); depth-limit failures include `max_depth` (unsigned integer). fn follow_to_object_packed( &mut self, store: &file::Store, packed: Option<&packed::Buffer>, - ) -> Result; + ) -> Result; /// Follow this symbolic reference one level and return the ref it refers to. /// /// Returns `None` if this is not a symbolic reference, hence the leaf of the chain. - fn follow(&self, store: &file::Store) -> Option>; + fn follow(&self, store: &file::Store) -> Option>; /// Follow this symbolic reference one level and return the ref it refers to, /// possibly providing access to `packed` references for lookup if it contains the referent. /// /// Returns `None` if this is not a symbolic reference, hence the leaf of the chain. - fn follow_packed( - &self, - store: &file::Store, - packed: Option<&packed::Buffer>, - ) -> Option>; + fn follow_packed(&self, store: &file::Store, packed: Option<&packed::Buffer>) -> Option>; } impl ReferenceExt for Reference { @@ -115,24 +105,12 @@ impl ReferenceExt for Reference { .expect("infallible name conversion") } - fn peel_to_id_in_place( - &mut self, - store: &file::Store, - objects: &dyn gix_object::Find, - ) -> Result { + fn peel_to_id_in_place(&mut self, store: &file::Store, objects: &dyn gix_object::Find) -> Result { self.peel_to_id(store, objects) } - fn peel_to_id( - &mut self, - store: &file::Store, - objects: &dyn gix_object::Find, - ) -> Result { - let packed = store.assure_packed_refs_uptodate().map_err(|err| { - peel::to_id::Error::FollowToObject(peel::to_object::Error::Follow(file::find::existing::Error::Find( - file::find::Error::PackedOpen(err), - ))) - })?; + fn peel_to_id(&mut self, store: &file::Store, objects: &dyn gix_object::Find) -> Result { + let packed = store.assure_packed_refs_uptodate()?; self.peel_to_id_packed(store, objects, packed.as_ref().map(|b| &***b)) } @@ -141,16 +119,17 @@ impl ReferenceExt for Reference { store: &file::Store, objects: &dyn gix_object::Find, packed: Option<&packed::Buffer>, - ) -> Result { + ) -> Result { self.peel_to_id_packed(store, objects, packed) } + /// Object lookup failures include metadata `object_id` (hex text) and `reference` (name bytes). fn peel_to_id_packed( &mut self, store: &file::Store, objects: &dyn gix_object::Find, packed: Option<&packed::Buffer>, - ) -> Result { + ) -> Result { match self.peeled { Some(peeled) => { self.target = Target::Object(peeled.to_owned()); @@ -165,10 +144,14 @@ impl ReferenceExt for Reference { data, object_hash: hash_kind, } = objects - .try_find(&object_id, &mut buf)? - .ok_or_else(|| peel::to_id::Error::NotFound { - oid: object_id, - name: self.name.0.clone(), + .try_find(&object_id, &mut buf) + .and_then(|object| { + object.ok_or_else(|| NotFoundError::new("Object could not be found").raise_erased()) + }) + .or_raise_erased(|| { + Metadata::new("Could not peel reference to an object") + .with("object_id", object_id.to_string()) + .with("reference", self.name.as_bstr()) })?; match kind { gix_object::Kind::Tag => { @@ -196,15 +179,16 @@ impl ReferenceExt for Reference { &mut self, store: &file::Store, packed: Option<&packed::Buffer>, - ) -> Result { + ) -> Result { self.follow_to_object_packed(store, packed) } + /// Cycle failures include metadata `path` (native path); depth-limit failures include `max_depth` (unsigned integer). fn follow_to_object_packed( &mut self, store: &file::Store, packed: Option<&packed::Buffer>, - ) -> Result { + ) -> Result { match self.target { Target::Object(id) => Ok(id), Target::Symbolic(_) => { @@ -213,17 +197,20 @@ impl ReferenceExt for Reference { while let Some(next) = cursor.follow_packed(store, packed) { let next = next?; if seen.contains(&next.name) { - return Err(peel::to_object::Error::Cycle { - start_absolute: store.reference_path(cursor.name.as_ref()), - }); + return Err(CorruptionError::new("Symbolic references form a cycle") + .and_raise( + Metadata::new("Aborting reference cycle") + .with("path", store.reference_path(cursor.name.as_ref())), + ) + .erased()); } *cursor = next; seen.insert(cursor.name.clone()); const MAX_REF_DEPTH: usize = 5; if seen.len() == MAX_REF_DEPTH { - return Err(peel::to_object::Error::DepthLimitExceeded { - max_depth: MAX_REF_DEPTH, - }); + return Err(Metadata::new("Symbolic reference depth limit exceeded") + .with("max_depth", MAX_REF_DEPTH) + .raise_erased()); } } let oid = self.target.try_id().expect("peeled ref").to_owned(); @@ -232,30 +219,24 @@ impl ReferenceExt for Reference { } } - fn follow(&self, store: &file::Store) -> Option> { - let packed = match store - .assure_packed_refs_uptodate() - .map_err(|err| file::find::existing::Error::Find(file::find::Error::PackedOpen(err))) - { + fn follow(&self, store: &file::Store) -> Option> { + let packed = match store.assure_packed_refs_uptodate() { Ok(packed) => packed, Err(err) => return Some(Err(err)), }; self.follow_packed(store, packed.as_ref().map(|b| &***b)) } - fn follow_packed( - &self, - store: &file::Store, - packed: Option<&packed::Buffer>, - ) -> Option> { + fn follow_packed(&self, store: &file::Store, packed: Option<&packed::Buffer>) -> Option> { match &self.target { Target::Object(_) => None, Target::Symbolic(full_name) => match store.try_find_packed(full_name.as_ref(), packed) { Ok(Some(next)) => Some(Ok(next)), - Ok(None) => Some(Err(file::find::existing::Error::NotFound { + Ok(None) => Some(Err(file::find::NotFound { name: full_name.to_path().to_owned(), - })), - Err(err) => Some(Err(file::find::existing::Error::Find(err))), + } + .raise_erased())), + Err(err) => Some(Err(err)), }, } } diff --git a/gix-ref/src/store/file/transaction/commit.rs b/gix-ref/src/store/file/transaction/commit.rs index 53c586eb0fa..78bd221071c 100644 --- a/gix-ref/src/store/file/transaction/commit.rs +++ b/gix-ref/src/store/file/transaction/commit.rs @@ -1,3 +1,5 @@ +use gix_error::{ErrorExt, Exn, Metadata, ResultExt, message}; + use crate::{ Target, store_impl::file::{Transaction, transaction::PackedRefs}, @@ -25,11 +27,14 @@ impl Transaction<'_, '_> { /// along with empty parent directories /// /// Note that transactions will be prepared automatically as needed. - pub fn commit<'a>(self, committer: impl Into>>) -> Result, Error> { + /// Per-reference failures include metadata `reference` (bytes), the affected name. + /// A missing reflog identity is identifiable as [`file::log::create_or_update::MissingCommitter`](crate::file::log::create_or_update::MissingCommitter). + pub fn commit<'a>(self, committer: impl Into>>) -> Result, Exn> { self.commit_inner(committer.into()) } - fn commit_inner(self, committer: Option>) -> Result, Error> { + /// Per-reference failures include metadata `reference` (bytes), the affected name. + fn commit_inner(self, committer: Option>) -> Result, Exn> { let mut updates = self.updates.expect("BUG: must call prepare before commit"); let delete_loose_refs = matches!( self.packed_refs, @@ -74,14 +79,19 @@ impl Transaction<'_, '_> { if let Some((previous, new_oid)) = log_update { let do_update = previous.as_ref() != Some(new_oid); if do_update { - self.store.reflog_create_or_append( - change.update.name.as_ref(), - previous, - new_oid, - committer, - log.message.as_ref(), - log.force_create_reflog, - )?; + self.store + .reflog_create_or_append( + change.update.name.as_ref(), + previous, + new_oid, + committer, + log.message.as_ref(), + log.force_create_reflog, + ) + .or_raise_erased(|| { + Metadata::new("Could not update reflog") + .with("reference", change.update.name.as_bstr()) + })?; } } } @@ -104,10 +114,9 @@ impl Transaction<'_, '_> { }; if let Some(err) = err { - return Err(Error::LockCommit { - source: err, - full_name: change.name(), - }); + return Err(err + .and_raise(Metadata::new("Could not commit reference").with("reference", change.name())) + .erased()); } } } @@ -125,10 +134,9 @@ impl Transaction<'_, '_> { let reflog_path = reflog_root.join(relative_name); if let Err(err) = std::fs::remove_file(&reflog_path) { if err.kind() != std::io::ErrorKind::NotFound { - return Err(Error::DeleteReflog { - source: err, - full_name: change.name(), - }); + return Err(err + .and_raise(Metadata::new("Could not delete reflog").with("reference", change.name())) + .erased()); } } else { gix_tempfile::remove_dir::empty_upward_until_boundary( @@ -142,7 +150,8 @@ impl Transaction<'_, '_> { } if let Some(t) = self.packed_transaction { - t.commit().map_err(Error::PackedTransactionCommit)?; + t.commit() + .or_raise_erased(|| message("Could not commit packed-ref transaction"))?; // Always refresh ourselves right away to avoid races. We ignore errors as there may be many reasons this fails, and it's not // critical to be done here. In other words, the pack may be refreshed at a later time and then it might work. self.store.force_refresh_packed_buffer().ok(); @@ -163,10 +172,9 @@ impl Transaction<'_, '_> { if let Err(err) = std::fs::remove_file(reference_path) && err.kind() != std::io::ErrorKind::NotFound { - return Err(Error::DeleteReference { - err, - full_name: change.name(), - }); + return Err(err + .and_raise(Metadata::new("Could not delete reference").with("reference", change.name())) + .erased()); } drop(lock); } @@ -174,64 +182,5 @@ impl Transaction<'_, '_> { Ok(updates.into_iter().map(|edit| edit.update).collect()) } } -mod error { - use gix_object::bstr::BString; - - use crate::store_impl::{file, packed}; - - /// The error returned by various [`Transaction`][super::Transaction] methods. - // TODO(review): `DeleteReference` stores its io error in a field named `err`, which `thiserror` - // did NOT treat as a source — `source()` returning `None` for it is preserved - // behavior, not an omission. - #[derive(Debug)] - #[expect(missing_docs)] - pub enum Error { - PackedTransactionCommit(packed::transaction::commit::Error), - PreprocessingFailed { source: std::io::Error }, - LockCommit { source: std::io::Error, full_name: BString }, - DeleteReference { full_name: BString, err: std::io::Error }, - DeleteReflog { full_name: BString, source: std::io::Error }, - CreateOrUpdateRefLog(file::log::create_or_update::Error), - } - - impl std::fmt::Display for Error { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Error::PackedTransactionCommit(_) => f.write_str("The packed-ref transaction could not be committed"), - Error::PreprocessingFailed { .. } => f.write_str("Edit preprocessing failed with error"), - Error::LockCommit { full_name, .. } => { - write!(f, "The change for reference {full_name:?} could not be committed") - } - Error::DeleteReference { full_name, .. } => { - write!(f, "The reference {full_name} could not be deleted") - } - Error::DeleteReflog { full_name, .. } => { - write!(f, "The reflog of reference {full_name:?} could not be deleted") - } - Error::CreateOrUpdateRefLog(_) => f.write_str("The reflog could not be created or updated"), - } - } - } - - impl std::error::Error for Error { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - Error::PackedTransactionCommit(err) => Some(err), - Error::PreprocessingFailed { source } => Some(source), - Error::LockCommit { source, .. } => Some(source), - Error::DeleteReference { .. } => None, - Error::DeleteReflog { source, .. } => Some(source), - Error::CreateOrUpdateRefLog(err) => Some(err), - } - } - } - - impl From for Error { - fn from(err: file::log::create_or_update::Error) -> Self { - Error::CreateOrUpdateRefLog(err) - } - } -} -pub use error::Error; use crate::transaction::PreviousValue; diff --git a/gix-ref/src/store/file/transaction/prepare.rs b/gix-ref/src/store/file/transaction/prepare.rs index c5edaa30300..cb61650fcb4 100644 --- a/gix-ref/src/store/file/transaction/prepare.rs +++ b/gix-ref/src/store/file/transaction/prepare.rs @@ -1,3 +1,5 @@ +use gix_error::{ErrorExt, Exn, Metadata, NotFoundError, ResultExt, message}; + use crate::{ FullName, FullNameRef, Reference, Target, packed, packed::transaction::buffer_into_transaction, @@ -20,47 +22,16 @@ impl Transaction<'_, '_> { store: &file::Store, name: &FullNameRef, packed: Option<&packed::Buffer>, - ) -> Result, Error> { - store + ) -> Result, Exn> { + let loose = store .ref_contents(name) - .map_err(Error::from) - .and_then(|maybe_loose| { - maybe_loose - .map(|buf| { - loose::Reference::try_from_path(name.to_owned(), &buf, store.object_hash) - .map(Reference::from) - .map_err(Error::from) - }) - .transpose() - }) - .or_else(|err| match err { - Error::ReferenceDecode(_) => Ok(None), - other => Err(other), - }) - .and_then(|maybe_loose| match (maybe_loose, packed) { - (None, Some(packed)) => packed - .try_find(name) - .map(|opt| opt.map(Into::into)) - .map_err(Error::from), - (None, None) => Ok(None), - (maybe_loose, _) => Ok(maybe_loose), - }) - } - - /// Map a lock-acquisition failure to our error type, surfacing genuine I/O errors - /// (such as a path collision reported as `NotADirectory`) as [`Error::Io`] rather than - /// burying them in [`Error::LockAcquire`], which is reserved for actual contention. - // This happens for path collisions where `a` is a ref file, and `a/b` is the lock to be created. - fn lock_acquire_error(err: gix_error::Exn, full_name: &str) -> Error { - match ( - err.is_retryable(), - err.downcast_any_ref::().map(std::io::Error::kind), - ) { - (false, Some(kind)) => Error::Io(std::io::Error::new(kind, err.into_error())), - _ => Error::LockAcquire { - source: std::io::Error::other(err.into_error()), - full_name: full_name.into(), - }, + .or_raise_erased(|| message("Could not read existing reference"))? + // Git permits replacing malformed loose references, but I/O errors must propagate. + .and_then(|buf| loose::Reference::try_from_path(name.to_owned(), &buf, store.object_hash).ok()) + .map(Reference::from); + match (loose, packed) { + (None, Some(packed)) => packed.try_find(name).map(|reference| reference.map(Into::into)), + (reference, _) => Ok(reference), } } @@ -70,7 +41,7 @@ impl Transaction<'_, '_> { packed: Option<&packed::Buffer>, change: &mut Edit, direct_to_packed_refs: bool, - ) -> Result<(), Error> { + ) -> Result<(), Exn> { use std::io::Write; assert!( change.lock.is_none(), @@ -81,7 +52,9 @@ impl Transaction<'_, '_> { // The lock file itself (e.g. `CON.lock`) is also a device name, // so acquiring it would fail or open the device instead of // returning the configured validation error. - store.check_windows_device_name(change.update.name.as_ref())?; + store + .check_windows_device_name(change.update.name.as_ref()) + .or_raise_erased(|| message("Invalid reference filename"))?; let lock = match &mut change.update.change { Change::Delete { expected, .. } => { @@ -90,8 +63,7 @@ impl Transaction<'_, '_> { base.join(relative_path.as_ref()), lock_fail_mode, Some(base.clone().into_owned()), - ) - .map_err(|err| Self::lock_acquire_error(err, "borrowcheck won't allow change.name()"))?; + )?; let existing_ref = Self::read_existing_ref(store, change.update.name.as_ref(), packed)?; @@ -102,9 +74,7 @@ impl Transaction<'_, '_> { (PreviousValue::ExistingMustMatch(_) | PreviousValue::Any, None) | (PreviousValue::MustExist | PreviousValue::Any, Some(_)) => {} (PreviousValue::MustExist | PreviousValue::MustExistAndMatch(_), None) => { - return Err(Error::DeleteReferenceMustExist { - full_name: change.name(), - }); + return Err(NotFoundError::new("The reference to delete must exist").raise_erased()); } ( PreviousValue::MustExistAndMatch(previous) | PreviousValue::ExistingMustMatch(previous), @@ -112,12 +82,13 @@ impl Transaction<'_, '_> { ) => { let actual = existing.target.clone(); if *previous != actual { - let expected = previous.clone(); - return Err(Error::ReferenceOutOfDate { + let context = message!("Expected reference content {previous}"); + return Err(ReferenceOutOfDate { full_name: change.name(), - expected, actual, - }); + } + .and_raise(context) + .erased()); } } } @@ -137,12 +108,6 @@ impl Transaction<'_, '_> { lock_fail_mode, Some(base.clone().into_owned()), ) - .map_err(|err| { - Self::lock_acquire_error( - err, - "borrowcheck won't allow change.name() and this will be corrected by caller", - ) - }) }; let mut lock = obtain_lock()?; @@ -153,18 +118,17 @@ impl Transaction<'_, '_> { | (PreviousValue::MustExist, Some(_)) | (PreviousValue::MustNotExist | PreviousValue::ExistingMustMatch(_), None) => {} (PreviousValue::MustExist, None) => { - let expected = Target::Object(store.object_hash.null()); - let full_name = change.name(); - return Err(Error::MustExist { full_name, expected }); + return Err(NotFoundError::new("The reference to update must exist").raise_erased()); } (PreviousValue::MustNotExist, Some(existing)) => { if existing.target != *new { - let new = new.clone(); - return Err(Error::MustNotExist { + let context = message!("Expected the reference not to exist when writing {new}"); + return Err(MustNotExist { full_name: change.name(), actual: existing.target.clone(), - new, - }); + } + .and_raise(context) + .erased()); } } ( @@ -173,20 +137,21 @@ impl Transaction<'_, '_> { ) => { if *previous != existing.target { let actual = existing.target.clone(); - let expected = previous.to_owned(); - let full_name = change.name(); - return Err(Error::ReferenceOutOfDate { - full_name, + let context = message!("Expected reference content {previous}"); + return Err(ReferenceOutOfDate { + full_name: change.name(), actual, - expected, - }); + } + .and_raise(context) + .erased()); } } (PreviousValue::MustExistAndMatch(previous), None) => { - let expected = previous.to_owned(); - let full_name = change.name(); - return Err(Error::MustExist { full_name, expected }); + return Err( + NotFoundError::new(format!("The reference must exist with content {previous}")) + .raise_erased(), + ); } } @@ -212,10 +177,17 @@ impl Transaction<'_, '_> { lock.with_mut(|file| match new { Target::Object(oid) => writeln!(file, "{oid}"), Target::Symbolic(name) => writeln!(file, "ref: {}", name.0), - })?; - Some(lock.close()?) + }) + .or_raise_erased(|| message("Could not write loose reference"))?; + Some( + lock.close() + .or_raise_erased(|| message("Could not close reference lock"))?, + ) } else if keep_lock_for_loose_source_delete { - Some(lock.close()?) + Some( + lock.close() + .or_raise_erased(|| message("Could not close reference lock"))?, + ) } else { None } @@ -232,12 +204,15 @@ impl Transaction<'_, '_> { /// If the operation succeeds, the transaction can be committed or dropped to cause a rollback automatically. /// Rollbacks happen automatically on failure and they tend to be perfect. /// This method is idempotent. + /// + /// Failed edits identify the requested and resolved names in metadata `reference` and `referent` (bytes). + /// [`ReferenceOutOfDate`] and [`MustNotExist`] retain the actual target observed while holding the lock. pub fn prepare( self, edits: impl IntoIterator, ref_files_lock_fail_mode: gix_lock::acquire::Fail, packed_refs_lock_fail_mode: gix_lock::acquire::Fail, - ) -> Result { + ) -> Result { self.prepare_inner( &mut edits.into_iter(), ref_files_lock_fail_mode, @@ -245,12 +220,13 @@ impl Transaction<'_, '_> { ) } + /// Failed edits include metadata `reference` (requested name bytes) and `referent` (resolved name bytes). fn prepare_inner( mut self, edits: &mut dyn Iterator, ref_files_lock_fail_mode: gix_lock::acquire::Fail, packed_refs_lock_fail_mode: gix_lock::acquire::Fail, - ) -> Result { + ) -> Result { assert!(self.updates.is_none(), "BUG: Must not call prepare(…) multiple times"); let store = self.store; let mut updates: Vec<_> = edits @@ -277,7 +253,7 @@ impl Transaction<'_, '_> { leaf_referent_previous_oid: None, }, ) - .map_err(Error::PreprocessingFailed)?; + .or_raise_erased(|| message("Could not preprocess reference edits"))?; let mut maybe_updates_for_packed_refs = match self.packed_refs { PackedRefs::DeletionsAndNonSymbolicUpdates(_) @@ -341,15 +317,7 @@ impl Transaction<'_, '_> { let packed_transaction: Option<_> = if maybe_updates_for_packed_refs.unwrap_or(0) > 0 || self.store.packed_refs_lock_path().is_file() { // We have to create a packed-ref even if it doesn't exist - self.store - .packed_transaction(packed_refs_lock_fail_mode) - .map_err(|err| match err { - file::packed::transaction::Error::BufferOpen(err) => Error::from(err), - file::packed::transaction::Error::TransactionLock(err) => { - Error::PackedTransactionAcquire(err) - } - })? - .into() + self.store.packed_transaction(packed_refs_lock_fail_mode)?.into() } else { // A packed transaction is optional - we only have deletions that can't be made if // no packed-ref file exists anyway @@ -362,7 +330,6 @@ impl Transaction<'_, '_> { self.store.precompose_unicode, self.store.namespace.clone(), ) - .map_err(|err| Error::PackedTransactionAcquire(std::io::Error::other(err.into_error()))) }) .transpose()? }; @@ -391,26 +358,21 @@ impl Transaction<'_, '_> { PackedRefs::DeletionsAndNonSymbolicUpdatesRemoveLooseSourceReference(_) ), ) { - let err = match err { - Error::LockAcquire { - source, - full_name: _bogus, - } => Error::LockAcquire { - source, - full_name: { - let mut cursor = change.parent_index; - let mut ref_name = change.name(); - while let Some(parent_idx) = cursor { - let parent = &updates[parent_idx]; - ref_name = parent.name(); - cursor = parent.parent_index; - } - ref_name - }, - }, - other => other, - }; - return Err(err); + let referent = change.name(); + let mut ref_name = referent.clone(); + let mut cursor = change.parent_index; + while let Some(parent_idx) = cursor { + let parent = &updates[parent_idx]; + ref_name = parent.name(); + cursor = parent.parent_index; + } + return Err(err + .raise( + Metadata::new("Could not prepare reference edit") + .with("reference", ref_name) + .with("referent", referent), + ) + .erased()); } // traverse parent chain from leaf/peeled ref and set the leaf previous oid accordingly @@ -464,137 +426,41 @@ fn possibly_adjust_name_for_prefixes(name: &FullNameRef) -> Option { } } -mod error { - use gix_object::bstr::BString; - - use crate::{ - Target, - store_impl::{file, packed}, - }; - - /// The error returned by various [`Transaction`][super::Transaction] methods. - #[derive(Debug)] - #[expect(missing_docs)] - pub enum Error { - Packed(packed::buffer::open::Error), - PackedTransactionAcquire(std::io::Error), - PackedTransactionPrepare(packed::transaction::prepare::Error), - PackedFind(packed::find::Error), - PreprocessingFailed(std::io::Error), - LockAcquire { - source: std::io::Error, - full_name: BString, - }, - Io(std::io::Error), - DeleteReferenceMustExist { - full_name: BString, - }, - MustNotExist { - full_name: BString, - actual: Target, - new: Target, - }, - MustExist { - full_name: BString, - expected: Target, - }, - ReferenceOutOfDate { - full_name: BString, - expected: Target, - actual: Target, - }, - ReferenceDecode(file::loose::reference::decode::Error), - } - - impl std::fmt::Display for Error { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Error::Packed(_) => f.write_str("The packed ref buffer could not be loaded"), - Error::PackedTransactionAcquire(_) => { - f.write_str("The lock for the packed-ref file could not be obtained") - } - Error::PackedTransactionPrepare(_) => f.write_str("The packed transaction could not be prepared"), - Error::PackedFind(_) => f.write_str("The packed ref file could not be parsed"), - Error::PreprocessingFailed(_) => f.write_str("Edit preprocessing failed with an error"), - Error::LockAcquire { full_name, .. } => { - write!(f, "A lock could not be obtained for reference {full_name:?}") - } - Error::Io(_) => f.write_str("An IO error occurred while applying an edit"), - Error::DeleteReferenceMustExist { full_name } => { - write!( - f, - "The reference {full_name:?} for deletion did not exist or could not be parsed" - ) - } - Error::MustNotExist { full_name, actual, new } => write!( - f, - "Reference {full_name:?} was not supposed to exist when writing it with value {new:?}, but actual content was {actual:?}" - ), - Error::MustExist { full_name, expected } => { - write!( - f, - "Reference {full_name:?} was supposed to exist with value {expected}, but didn't." - ) - } - Error::ReferenceOutOfDate { - full_name, - expected, - actual, - } => write!( - f, - "The reference {full_name:?} should have content {expected}, actual content was {actual}" - ), - Error::ReferenceDecode(_) => f.write_str("Could not read reference"), - } - } - } - - impl std::error::Error for Error { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - Error::Packed(err) => Some(err), - Error::PackedTransactionAcquire(err) => Some(err), - Error::PackedTransactionPrepare(err) => Some(err), - Error::PackedFind(err) => Some(err), - Error::PreprocessingFailed(err) => Some(err), - Error::LockAcquire { source, .. } => Some(source), - Error::Io(err) => Some(err), - Error::DeleteReferenceMustExist { .. } | Error::MustExist { .. } => Some(&crate::NOT_FOUND), - Error::MustNotExist { .. } | Error::ReferenceOutOfDate { .. } => None, - Error::ReferenceDecode(err) => Some(err), - } - } - } +/// A reference changed since the caller obtained its expected value. +/// Retrying requires reading and reconciling the new value first. +#[derive(Debug)] +pub struct ReferenceOutOfDate { + /// The reference whose target did not match the expected value. + pub full_name: crate::bstr::BString, + /// The target observed while holding the reference lock. + pub actual: Target, +} - impl From for Error { - fn from(err: packed::buffer::open::Error) -> Self { - Error::Packed(err) - } +impl std::fmt::Display for ReferenceOutOfDate { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "The reference {:?} changed to {}", self.full_name, self.actual) } +} - impl From for Error { - fn from(err: packed::transaction::prepare::Error) -> Self { - Error::PackedTransactionPrepare(err) - } - } +impl std::error::Error for ReferenceOutOfDate {} - impl From for Error { - fn from(err: packed::find::Error) -> Self { - Error::PackedFind(err) - } - } - - impl From for Error { - fn from(err: std::io::Error) -> Self { - Error::Io(err) - } - } +/// A reference exists with a different target although the edit required its absence. +#[derive(Debug)] +pub struct MustNotExist { + /// The reference which unexpectedly exists. + pub full_name: crate::bstr::BString, + /// The target observed while holding the reference lock. + pub actual: Target, +} - impl From for Error { - fn from(err: file::loose::reference::decode::Error) -> Self { - Error::ReferenceDecode(err) - } +impl std::fmt::Display for MustNotExist { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "The reference {:?} already exists with content {}", + self.full_name, self.actual + ) } } -pub use error::Error; +impl std::error::Error for MustNotExist {} diff --git a/gix-ref/src/store/general/handle/find.rs b/gix-ref/src/store/general/handle/find.rs index 560592dcbc4..4dbfc5a58d1 100644 --- a/gix-ref/src/store/general/handle/find.rs +++ b/gix-ref/src/store/general/handle/find.rs @@ -1,67 +1,19 @@ -use crate::{PartialNameRef, Reference, store}; - -mod error { - use std::convert::Infallible; - - /// The error returned by [`crate::file::Store::find_loose()`]. - #[derive(Debug)] - #[expect(missing_docs)] - pub enum Error { - Loose(crate::file::find::Error), - RefnameValidation(crate::name::Error), - } - - impl std::fmt::Display for Error { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Error::Loose(_) => { - f.write_str("An error occurred while finding a reference in the loose file database") - } - Error::RefnameValidation(_) => f.write_str("The ref name or path is not a valid ref name"), - } - } - } - - impl std::error::Error for Error { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - Error::Loose(err) => Some(err), - Error::RefnameValidation(err) => Some(err), - } - } - } - - impl From for Error { - fn from(err: crate::file::find::Error) -> Self { - Error::Loose(err) - } - } - - impl From for Error { - fn from(err: crate::name::Error) -> Self { - Error::RefnameValidation(err) - } - } - - impl From for Error { - fn from(_: Infallible) -> Self { - unreachable!("this impl is needed to allow passing a known valid partial path as parameter") - } - } -} +use gix_error::{Exn, ResultExt, message}; -pub use error::Error; +use crate::{PartialNameRef, Reference, store}; use crate::store::handle; impl store::Handle { /// TODO: actually implement this with handling of the packed buffer. - pub fn try_find<'a, Name, E>(&self, partial: Name) -> Result, Error> + pub fn try_find<'a, Name, E>(&self, partial: Name) -> Result, Exn> where Name: TryInto<&'a PartialNameRef, Error = E>, - Error: From, + Result<&'a PartialNameRef, E>: ResultExt, { - let _name = partial.try_into()?; + let _name = partial + .try_into() + .or_raise_erased(|| message("Invalid reference name"))?; match &self.state { handle::State::Loose { .. } => { todo!() @@ -70,62 +22,13 @@ impl store::Handle { } } -mod existing { - mod error { - use std::path::PathBuf; - - /// The error returned by [file::Store::find_existing()][crate::file::Store::find_existing()]. - #[derive(Debug)] - pub enum Error { - Find(crate::store::find::Error), - NotFound { name: PathBuf }, - } - - impl std::fmt::Display for Error { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Error::Find(_) => f.write_str("An error occurred while finding a reference in the database"), - #[allow(clippy::unnecessary_debug_formatting)] - // `{:?}` of a `Path` is what `thiserror` generated; keep the rendered text identical. - Error::NotFound { name } => write!(f, "The ref partially named {name:?} could not be found"), - } - } - } - - impl std::error::Error for Error { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - Error::Find(err) => Some(err), - Error::NotFound { .. } => Some(&crate::NOT_FOUND), - } - } - } - - impl From for Error { - fn from(err: crate::store::find::Error) -> Self { - Error::Find(err) - } - } - } - - pub use error::Error; - - use crate::{PartialNameRef, Reference, store}; - - impl store::Handle { - /// Similar to [`crate::file::Store::find()`] but a non-existing ref is treated as error. - pub fn find<'a, Name, E>(&self, _partial: Name) -> Result - where - Name: TryInto<&'a PartialNameRef, Error = E>, - crate::name::Error: From, - { - todo!() - // match self.try_find(partial) {} - // match self.find_one_with_verified_input(path.to_partial_path().as_ref(), packed) { - // Ok(Some(r)) => Ok(r), - // Ok(None) => Err(Error::NotFound(path.to_partial_path().into_owned())), - // Err(err) => Err(err.into()), - // } - } +impl store::Handle { + /// Similar to [`crate::file::Store::find()`] but a non-existing ref is treated as error. + pub fn find<'a, Name, E>(&self, _partial: Name) -> Result + where + Name: TryInto<&'a PartialNameRef, Error = E>, + Result<&'a PartialNameRef, E>: ResultExt, + { + todo!() } } diff --git a/gix-ref/src/store/general/init.rs b/gix-ref/src/store/general/init.rs index 48d985b47e4..1a7c18d58ef 100644 --- a/gix-ref/src/store/general/init.rs +++ b/gix-ref/src/store/general/init.rs @@ -1,36 +1,6 @@ -use std::path::PathBuf; - -mod error { - /// The error returned by [`crate::Store::at()`]. - #[derive(Debug)] - pub enum Error { - Io(std::io::Error), - } - - impl std::fmt::Display for Error { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Error::Io(_) => f.write_str("There was an error accessing the store's directory"), - } - } - } - - impl std::error::Error for Error { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - Error::Io(err) => Some(err), - } - } - } - - impl From for Error { - fn from(err: std::io::Error) -> Self { - Error::Io(err) - } - } -} +use gix_error::{Exn, Metadata, ResultExt}; -pub use error::Error; +use std::path::PathBuf; use crate::file; @@ -44,7 +14,7 @@ impl crate::Store { /// /// Note that if [`precompose_unicode`](crate::store::init::Options::precompose_unicode) is set in the options, /// the `git_dir` is also expected to use precomposed unicode, or else some operations that strip prefixes will fail. - pub fn at(git_dir: PathBuf, object_hash: gix_hash::Kind) -> Result { + pub fn at(git_dir: PathBuf, object_hash: gix_hash::Kind) -> Result { Self::at_opts(git_dir, object_hash, Default::default()) } @@ -53,13 +23,16 @@ impl crate::Store { /// /// Note that if [`precompose_unicode`](crate::store::init::Options::precompose_unicode) is set in the options, /// the `git_dir` is also expected to use precomposed unicode, or else some operations that strip prefixes will fail. + /// + /// Errors include metadata `path` (native path), the reference store directory. pub fn at_opts( git_dir: PathBuf, object_hash: gix_hash::Kind, opts: crate::store::init::Options, - ) -> Result { + ) -> Result { // for now, just try to read the directory - later we will do that naturally as we have to figure out if it's a ref-table or not. - std::fs::read_dir(&git_dir)?; + std::fs::read_dir(&git_dir) + .or_raise_erased(|| Metadata::new("Could not access reference store").with("path", git_dir.as_path()))?; Ok(crate::Store { inner: crate::store::State::Loose { store: file::Store::at_opts(git_dir, object_hash, opts), diff --git a/gix-ref/src/store/packed/buffer.rs b/gix-ref/src/store/packed/buffer.rs index 4d0795a1721..463b35097a1 100644 --- a/gix-ref/src/store/packed/buffer.rs +++ b/gix-ref/src/store/packed/buffer.rs @@ -1,3 +1,7 @@ +use std::path::PathBuf; + +use gix_error::{CorruptionError, ErrorExt, Exn, Metadata, ResultExt, message}; + use crate::store_impl::packed; impl AsRef<[u8]> for packed::Buffer { @@ -15,145 +19,94 @@ impl AsRef<[u8]> for packed::Backing { } } -/// -pub mod open { - use std::path::PathBuf; - - use crate::store_impl::packed; - - /// Initialization - impl packed::Buffer { - fn open_with_backing( - backing: packed::Backing, - path: PathBuf, - object_hash: gix_hash::Kind, - ) -> Result { - let (backing, offset) = { - let (offset, sorted) = { - let mut input = backing.as_ref(); - if *input.first().unwrap_or(&b' ') == b'#' { - let header = packed::decode::header(&mut input).map_err(|()| Error::HeaderParsing)?; - let offset = backing.as_ref().len() - input.len(); - (offset, header.sorted) - } else { - (0, false) - } - }; - - if !sorted { - // this implementation is likely slower than what git does, but it's less code, too. - let mut entries = - packed::Iter::new(&backing.as_ref()[offset..], object_hash)?.collect::, _>>()?; - entries.sort_by_key(|e| e.name.as_bstr()); - let mut serialized = Vec::::new(); - for entry in entries { - serialized.extend_from_slice(entry.target); - serialized.push(b' '); - serialized.extend_from_slice(entry.name.as_bstr()); - serialized.push(b'\n'); - if let Some(object) = entry.object { - serialized.push(b'^'); - serialized.extend_from_slice(object); - serialized.push(b'\n'); - } - } - (Backing::InMemory(serialized), 0) +/// Initialization +impl packed::Buffer { + fn open_with_backing(backing: packed::Backing, path: PathBuf, object_hash: gix_hash::Kind) -> Result { + let (backing, offset) = { + let (offset, sorted) = { + let mut input = backing.as_ref(); + if *input.first().unwrap_or(&b' ') == b'#' { + let header = packed::decode::header(&mut input).map_err(|()| { + CorruptionError::new("The header could not be parsed, even though first line started with '#'") + .raise_erased() + })?; + let offset = backing.as_ref().len() - input.len(); + (offset, header.sorted) } else { - (backing, offset) + (0, false) } }; - Ok(packed::Buffer { - offset, - data: backing, - path, - object_hash, - }) - } - - /// Open the file at `path`, parsing object ids as `object_hash`, and map it into memory if the file size is larger - /// than `use_memory_map_if_larger_than_bytes`. - /// - /// In order to allow fast lookups and optimizations, the contents of the packed refs must be sorted. - /// If that's not the case, they will be sorted on the fly with the data being written into a memory buffer. - pub fn open( - path: PathBuf, - use_memory_map_if_larger_than_bytes: u64, - object_hash: gix_hash::Kind, - ) -> Result { - let backing = if std::fs::metadata(&path)?.len() <= use_memory_map_if_larger_than_bytes { - packed::Backing::InMemory(std::fs::read(&path)?) - } else { - packed::Backing::Mapped( - // SAFETY: we have to take the risk of somebody changing the file underneath. Git never writes into the same file. - #[expect(unsafe_code)] - unsafe { - memmap2::MmapOptions::new().map_copy_read_only(&std::fs::File::open(&path)?)? - }, - ) - }; - Self::open_with_backing(backing, path, object_hash) - } - - /// Open a buffer from `bytes`, which is the content of a typical `packed-refs` file, parsing object ids as - /// `object_hash`. - /// - /// In order to allow fast lookups and optimizations, the contents of the packed refs must be sorted. - /// If that's not the case, they will be sorted on the fly. - pub fn from_bytes(bytes: &[u8], object_hash: gix_hash::Kind) -> Result { - let backing = packed::Backing::InMemory(bytes.into()); - Self::open_with_backing(backing, PathBuf::from(""), object_hash) - } - } - - mod error { - use crate::packed; - /// The error returned by [`open()`][super::packed::Buffer::open()]. - #[derive(Debug)] - #[expect(missing_docs)] - pub enum Error { - Iter(packed::iter::Error), - HeaderParsing, - Io(std::io::Error), - } - - impl std::fmt::Display for Error { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Error::Iter(_) => f.write_str( - "The packed-refs file did not have a header or wasn't sorted and could not be iterated", - ), - Error::HeaderParsing => { - f.write_str("The header could not be parsed, even though first line started with '#'") + if !sorted { + // this implementation is likely slower than what git does, but it's less code, too. + let mut entries = packed::Iter::new(&backing.as_ref()[offset..], object_hash) + .or_raise_erased(|| message("Could not iterate unsorted packed refs"))? + .collect::, _>>() + .or_erased()?; + entries.sort_by_key(|e| e.name.as_bstr()); + let mut serialized = Vec::::new(); + for entry in entries { + serialized.extend_from_slice(entry.target); + serialized.push(b' '); + serialized.extend_from_slice(entry.name.as_bstr()); + serialized.push(b'\n'); + if let Some(object) = entry.object { + serialized.push(b'^'); + serialized.extend_from_slice(object); + serialized.push(b'\n'); } - Error::Io(_) => f.write_str("The buffer could not be opened or read"), } + (packed::Backing::InMemory(serialized), 0) + } else { + (backing, offset) } - } - - impl std::error::Error for Error { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - Error::Iter(err) => Some(err), - Error::HeaderParsing => Some(&crate::CORRUPTION), - Error::Io(err) => Some(err), - } - } - } - - impl From for Error { - fn from(err: packed::iter::Error) -> Self { - Error::Iter(err) - } - } + }; + Ok(packed::Buffer { + offset, + data: backing, + path, + object_hash, + }) + } - impl From for Error { - fn from(err: std::io::Error) -> Self { - Error::Io(err) - } - } + /// Open the file at `path`, parsing object ids as `object_hash`, and map it into memory if the file size is larger + /// than `use_memory_map_if_larger_than_bytes`. + /// + /// In order to allow fast lookups and optimizations, the contents of the packed refs must be sorted. + /// If that's not the case, they will be sorted on the fly with the data being written into a memory buffer. + /// + /// I/O failures include metadata `path` (native path), the packed-refs file. + pub fn open( + path: PathBuf, + use_memory_map_if_larger_than_bytes: u64, + object_hash: gix_hash::Kind, + ) -> Result { + let backing = (|| -> std::io::Result { + Ok( + if std::fs::metadata(&path)?.len() <= use_memory_map_if_larger_than_bytes { + packed::Backing::InMemory(std::fs::read(&path)?) + } else { + packed::Backing::Mapped( + // SAFETY: Git replaces packed-refs rather than changing a mapped file in place. + #[expect(unsafe_code)] + unsafe { + memmap2::MmapOptions::new().map_copy_read_only(&std::fs::File::open(&path)?)? + }, + ) + }, + ) + })() + .or_raise_erased(|| Metadata::new("Could not open packed refs").with("path", path.as_path()))?; + Self::open_with_backing(backing, path, object_hash) } - pub use error::Error; - use crate::packed::Backing; + /// Open a buffer from `bytes`, which is the content of a typical `packed-refs` file, parsing object ids as + /// `object_hash`. + /// + /// In order to allow fast lookups and optimizations, the contents of the packed refs must be sorted. + /// If that's not the case, they will be sorted on the fly. + pub fn from_bytes(bytes: &[u8], object_hash: gix_hash::Kind) -> Result { + let backing = packed::Backing::InMemory(bytes.into()); + Self::open_with_backing(backing, PathBuf::from(""), object_hash) + } } diff --git a/gix-ref/src/store/packed/find.rs b/gix-ref/src/store/packed/find.rs index 67f45e2de1a..5a256328901 100644 --- a/gix-ref/src/store/packed/find.rs +++ b/gix-ref/src/store/packed/find.rs @@ -1,3 +1,5 @@ +use gix_error::{CorruptionError, ErrorExt, Exn, Metadata, ResultExt, message}; + use gix_object::bstr::{BStr, BString}; use crate::{FullNameRef, PartialNameRef, store_impl::packed}; @@ -8,12 +10,14 @@ impl packed::Buffer { /// /// Note that it will look it up verbatim and does not deal with namespaces or special prefixes like /// `main-worktree/` or `worktrees//`, as this is left to the caller. - pub fn try_find<'a, Name, E>(&self, name: Name) -> Result>, Error> + pub fn try_find<'a, Name, E>(&self, name: Name) -> Result>, Exn> where Name: TryInto<&'a PartialNameRef, Error = E>, - Error: From, + Result<&'a PartialNameRef, E>: ResultExt, { - let name = name.try_into()?; + let name = name + .try_into() + .or_raise_erased(|| message("The ref name or path is not a valid ref name"))?; let mut buf = BString::default(); for inbetween in &["", "tags", "heads", "remotes"] { let (name, was_absolute) = if name.looks_like_full_name(false) { @@ -36,38 +40,39 @@ impl packed::Buffer { Ok(None) } - pub(crate) fn try_find_full_name(&self, name: &FullNameRef) -> Result>, Error> { + /// Look up a resolved name. Decode failures include metadata `name` (bytes), the requested full name. + pub(crate) fn try_find_full_name(&self, name: &FullNameRef) -> Result>, Exn> { match self.binary_search_by(name.as_bstr()) { Ok(line_start) => { let mut input = &self.as_ref()[line_start..]; - Ok(Some( - packed::decode::reference(&mut input, self.object_hash) - .map_err(|err| Error::Parse(err.into_error()))?, - )) + packed::decode::reference(&mut input, self.object_hash).map(Some) } Err((parse_failure, _)) => { if parse_failure { - Err(Error::Parse(gix_error::Error::from_error( - gix_error::CorruptionError::new("Malformed packed reference record"), - ))) + Err(CorruptionError::new("Malformed packed reference record").raise()) } else { Ok(None) } } } + .or_raise_erased(|| Metadata::new("Could not decode packed reference").with("name", name.as_bstr())) } /// Find a reference with the given `name` and return it. - pub fn find<'a, Name, E>(&self, name: Name) -> Result, existing::Error> + pub fn find<'a, Name, E>(&self, name: Name) -> Result, Exn> where Name: TryInto<&'a PartialNameRef, Error = E>, - Error: From, + Result<&'a PartialNameRef, E>: ResultExt, { - match self.try_find(name) { - Ok(Some(r)) => Ok(r), - Ok(None) => Err(existing::Error::NotFound), - Err(err) => Err(existing::Error::Find(err)), - } + let name = name + .try_into() + .or_raise_erased(|| message("The ref name or path is not a valid ref name"))?; + self.try_find::<_, std::convert::Infallible>(name)?.ok_or_else(|| { + crate::file::find::NotFound { + name: name.to_partial_path().to_owned(), + } + .raise_erased() + }) } /// Perform a binary search where `Ok(pos)` is the beginning of the line that matches `name` perfectly and `Err(pos)` @@ -100,91 +105,6 @@ impl packed::Buffer { } } -mod error { - use std::convert::Infallible; - - /// The error returned by [`find()`][super::packed::Buffer::find()] - #[derive(Debug)] - #[expect(missing_docs)] - pub enum Error { - RefnameValidation(gix_error::Error), - Parse(gix_error::Error), - } - - impl std::fmt::Display for Error { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Error::RefnameValidation(_) => f.write_str("The ref name or path is not a valid ref name"), - Error::Parse(_) => f.write_str("The reference could not be parsed"), - } - } - } - - impl std::error::Error for Error { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - Error::RefnameValidation(err) => Some(err), - Error::Parse(err) => Some(err), - } - } - } - - impl From for Error { - fn from(err: crate::name::Error) -> Self { - Error::RefnameValidation(gix_error::Error::from_error(err)) - } - } - - impl From> for Error { - fn from(err: gix_error::Exn) -> Self { - Error::RefnameValidation(err.into_error()) - } - } - - impl From for Error { - fn from(_: Infallible) -> Self { - unreachable!("this impl is needed to allow passing a known valid partial path as parameter") - } - } -} -pub use error::Error; - -/// -pub mod existing { - - /// The error returned by [`find_existing()`][super::packed::Buffer::find()] - #[derive(Debug)] - #[expect(missing_docs)] - pub enum Error { - Find(super::Error), - NotFound, - } - - impl std::fmt::Display for Error { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Error::Find(_) => f.write_str("The find operation failed"), - Error::NotFound => f.write_str("The reference did not exist even though that was expected"), - } - } - } - - impl std::error::Error for Error { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - Error::Find(err) => Some(err), - Error::NotFound => Some(&crate::NOT_FOUND), - } - } - } - - impl From for Error { - fn from(err: super::Error) -> Self { - Error::Find(err) - } - } -} - pub(crate) fn transform_full_name_for_lookup(name: &FullNameRef) -> Option<&FullNameRef> { match name.category_and_short_name() { Some((c, sn)) => { diff --git a/gix-ref/src/store/packed/iter.rs b/gix-ref/src/store/packed/iter.rs index 9a55a6bd4f5..5be9caf549c 100644 --- a/gix-ref/src/store/packed/iter.rs +++ b/gix-ref/src/store/packed/iter.rs @@ -1,3 +1,5 @@ +use gix_error::{CorruptionError, ErrorExt, Exn, Metadata}; + use gix_object::bstr::{BString, ByteSlice}; use crate::store_impl::{packed, packed::decode}; @@ -5,16 +7,18 @@ use crate::store_impl::{packed, packed::decode}; /// packed-refs specific functionality impl packed::Buffer { /// Return an iterator of references stored in this packed refs buffer, ordered by reference name. + /// Header failures include metadata `input` (bytes), the first line without its newline. /// /// # Note /// /// There is no namespace support in packed iterators. It can be emulated using `iter_prefixed(…)`. - pub fn iter(&self) -> Result, packed::iter::Error> { + pub fn iter(&self) -> Result, Exn> { packed::Iter::new(self.as_ref(), self.object_hash) } /// Return an iterator yielding only references matching the given prefix, ordered by reference name. - pub fn iter_prefixed(&self, prefix: BString) -> Result, packed::iter::Error> { + /// Header failures include metadata `input` (bytes), the first selected line without its newline. + pub fn iter_prefixed(&self, prefix: BString) -> Result, Exn> { let first_record_with_prefix = self.binary_search_by(prefix.as_bstr()).unwrap_or_else(|(_, pos)| pos); packed::Iter::new_with_prefix( &self.as_ref()[first_record_with_prefix..], @@ -25,8 +29,9 @@ impl packed::Buffer { } impl<'a> Iterator for packed::Iter<'a> { - type Item = Result, Error>; + type Item = Result, Exn>; + /// Decode failures include metadata `line` (one-based line number within this iterator's input) and `input` (line bytes). fn next(&mut self) -> Option { if self.cursor.is_empty() { return None; @@ -35,7 +40,7 @@ impl<'a> Iterator for packed::Iter<'a> { let start = self.cursor; match decode::reference(&mut self.cursor, self.object_hash) { Ok(reference) => { - self.current_line += 1; + self.current_line += 1 + usize::from(reference.object.is_some()); if let Some(ref prefix) = self.prefix && !reference.name.as_bstr().starts_with_str(prefix) { @@ -54,14 +59,11 @@ impl<'a> Iterator for packed::Iter<'a> { let line_number = self.current_line; self.current_line += 1; - Some(Err(Error::Reference { - source: err.into_error(), - invalid_line: failed_line - .get(..failed_line.len().saturating_sub(1)) - .unwrap_or(failed_line) - .into(), - line_number, - })) + Some(Err(err.raise( + Metadata::new("Invalid packed reference") + .with("input", failed_line.strip_suffix(b"\n").unwrap_or(failed_line)) + .with("line", line_number), + ))) } } } @@ -70,18 +72,20 @@ impl<'a> Iterator for packed::Iter<'a> { impl<'a> packed::Iter<'a> { /// Return a new iterator after successfully parsing the possibly existing first line of the given `packed` refs buffer, /// parsing object ids as `object_hash`. - pub fn new(packed: &'a [u8], object_hash: gix_hash::Kind) -> Result { + /// Header failures include metadata `input` (bytes), the first line without its newline. + pub fn new(packed: &'a [u8], object_hash: gix_hash::Kind) -> Result> { Self::new_with_prefix(packed, object_hash, None) } /// Returns an iterator whose references will only match `prefix`. /// /// It assumes that the underlying `packed` buffer is indeed sorted and parses object ids as `object_hash`. + /// Header failures include metadata `input` (bytes), the first line without its newline. pub(in crate::store_impl::packed) fn new_with_prefix( packed: &'a [u8], object_hash: gix_hash::Kind, prefix: Option, - ) -> Result { + ) -> Result> { if packed.is_empty() { Ok(packed::Iter { cursor: packed, @@ -91,8 +95,11 @@ impl<'a> packed::Iter<'a> { }) } else if packed[0] == b'#' { let mut input = packed; - decode::header(&mut input).map_err(|()| Error::Header { - invalid_first_line: packed.lines().next().unwrap_or(packed).into(), + decode::header(&mut input).map_err(|()| { + CorruptionError::new("The packed-refs header could not be parsed").and_raise( + Metadata::new("Invalid packed reference header") + .with("input", packed.lines().next().unwrap_or(packed)), + ) })?; let refs = input; Ok(packed::Iter { @@ -111,47 +118,3 @@ impl<'a> packed::Iter<'a> { } } } - -mod error { - use gix_object::bstr::BString; - - /// The error returned by [`Iter`][super::packed::Iter], - #[derive(Debug)] - #[expect(missing_docs)] - pub enum Error { - Header { - invalid_first_line: BString, - }, - Reference { - invalid_line: BString, - line_number: usize, - source: gix_error::Error, - }, - } - - impl std::fmt::Display for Error { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Error::Header { invalid_first_line } => { - write!(f, "The header existed but could not be parsed: {invalid_first_line:?}") - } - Error::Reference { - invalid_line, - line_number, - .. - } => write!(f, "Invalid reference in line {line_number}: {invalid_line:?}"), - } - } - } - - impl std::error::Error for Error { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - Error::Header { .. } => Some(&crate::CORRUPTION), - Error::Reference { source, .. } => Some(source), - } - } - } -} - -pub use error::Error; diff --git a/gix-ref/src/store/packed/transaction.rs b/gix-ref/src/store/packed/transaction.rs index 390a2993ae4..1a714df4ca3 100644 --- a/gix-ref/src/store/packed/transaction.rs +++ b/gix-ref/src/store/packed/transaction.rs @@ -1,6 +1,6 @@ use std::{borrow::Cow, fmt::Formatter, io::Write}; -use gix_error::ResultExt; +use gix_error::{ErrorExt, Exn, Metadata, NotFoundError, ResultExt, message}; use crate::{ Namespace, Target, file, @@ -50,11 +50,12 @@ impl packed::Transaction { impl packed::Transaction { /// Prepare the transaction by checking all edits for applicability. /// Use `objects` to access objects for the purpose of peeling them - this is only used if packed-refs are involved. + /// Object lookup failures include metadata `object_id` (hex text) and `reference` (name bytes). pub fn prepare( mut self, edits: &mut dyn Iterator, objects: &dyn gix_object::Find, - ) -> Result { + ) -> Result { assert!(self.edits.is_none(), "BUG: cannot call prepare(…) more than once"); let buffer = &self.buffer; // Remove all edits which are deletions that aren't here in the first place @@ -108,9 +109,11 @@ impl packed::Transaction { { let mut next_id = new; edit.peeled = loop { - let data = objects - .try_find(&next_id, &mut buf) - .map_err(|err| prepare::Error::Resolve(err.into()))?; + let data = objects.try_find(&next_id, &mut buf).or_raise_erased(|| { + Metadata::new("Could not peel packed reference") + .with("object_id", next_id.to_string()) + .with("reference", edit.inner.name.as_bstr()) + })?; match data { Some(gix_object::Data { kind: gix_object::Kind::Tag, @@ -120,15 +123,23 @@ impl packed::Transaction { next_id = gix_object::TagRefIter::from_bytes(data, hash_kind) .target_id() .or_raise(|| gix_error::message!("Couldn't get target object id from tag {next_id}")) - .map_err(|err| prepare::Error::Resolve(err.into()))?; + .or_raise_erased(|| { + Metadata::new("Could not peel packed reference") + .with("object_id", next_id.to_string()) + .with("reference", edit.inner.name.as_bstr()) + })?; } Some(_) => { break if next_id == new { None } else { Some(next_id) }; } None => { - return Err(prepare::Error::Resolve( - format!("Couldn't find object with id {next_id}").into(), - )); + return Err(NotFoundError::new("Object could not be found") + .and_raise( + Metadata::new("Could not peel packed reference") + .with("object_id", next_id.to_string()) + .with("reference", edit.inner.name.as_bstr()), + ) + .erased()); } } }; @@ -141,7 +152,7 @@ impl packed::Transaction { .take() .map(gix_lock::File::close) .transpose() - .map_err(prepare::Error::CloseLock)?; + .or_raise_erased(|| message("Could not close unused packed reference lock"))?; } else { // NOTE that we don't do any additional checks here but apply all edits unconditionally. // This is because this transaction system is internal and will be used correctly from the @@ -155,16 +166,16 @@ impl packed::Transaction { /// /// Please note that actual edits invalidated existing packed buffers. /// Note: There is the potential to write changes into memory and return such a packed-refs buffer for reuse. - pub fn commit(self) -> Result<(), commit::Error> { + pub fn commit(self) -> Result<(), Exn> { let mut edits = self.edits.expect("BUG: cannot call commit() before prepare(…)"); if edits.is_empty() { return Ok(()); } let mut file = self.lock.expect("a write lock for applying changes"); - let refs_sorted: Box, packed::iter::Error>>> = + let refs_sorted: Box, Exn>>> = match self.buffer.as_ref() { - Some(buffer) => Box::new(buffer.iter()?), + Some(buffer) => Box::new(buffer.iter().or_erased()?), None => Box::new(std::iter::empty()), }; @@ -173,14 +184,15 @@ impl packed::Transaction { edits.sort_by(|l, r| l.inner.name.as_bstr().cmp(r.inner.name.as_bstr())); let mut peekable_sorted_edits = edits.iter().peekable(); - file.with_mut(|f| f.write_all(HEADER_LINE))?; + file.with_mut(|f| f.write_all(HEADER_LINE)) + .or_raise_erased(|| message("Could not write packed refs header"))?; let mut num_written_lines = 0; loop { match (refs_sorted.peek(), peekable_sorted_edits.peek()) { (Some(Err(_)), _) => { let err = refs_sorted.next().expect("next").expect_err("err"); - return Err(commit::Error::Iteration(err)); + return Err(err.erased()); } (None, None) => { break; @@ -188,7 +200,8 @@ impl packed::Transaction { (Some(Ok(_)), None) => { let pref = refs_sorted.next().expect("next").expect("no err"); num_written_lines += 1; - file.with_mut(|out| write_packed_ref(out, pref))?; + file.with_mut(|out| write_packed_ref(out, pref)) + .or_raise_erased(|| message("Could not write packed reference"))?; } (Some(Ok(pref)), Some(edit)) => { use std::cmp::Ordering::*; @@ -196,30 +209,36 @@ impl packed::Transaction { Less => { let pref = refs_sorted.next().expect("next").expect("valid"); num_written_lines += 1; - file.with_mut(|out| write_packed_ref(out, pref))?; + file.with_mut(|out| write_packed_ref(out, pref)) + .or_raise_erased(|| message("Could not write packed reference"))?; } Greater => { let edit = peekable_sorted_edits.next().expect("next"); - file.with_mut(|out| write_edit(out, edit, &mut num_written_lines))?; + file.with_mut(|out| write_edit(out, edit, &mut num_written_lines)) + .or_raise_erased(|| message("Could not write packed reference edit"))?; } Equal => { let _pref = refs_sorted.next().expect("next").expect("valid"); let edit = peekable_sorted_edits.next().expect("next"); - file.with_mut(|out| write_edit(out, edit, &mut num_written_lines))?; + file.with_mut(|out| write_edit(out, edit, &mut num_written_lines)) + .or_raise_erased(|| message("Could not write packed reference edit"))?; } } } (None, Some(_)) => { let edit = peekable_sorted_edits.next().expect("next"); - file.with_mut(|out| write_edit(out, edit, &mut num_written_lines))?; + file.with_mut(|out| write_edit(out, edit, &mut num_written_lines)) + .or_raise_erased(|| message("Could not write packed reference edit"))?; } } } if num_written_lines == 0 { - std::fs::remove_file(file.resource_path())?; + std::fs::remove_file(file.resource_path()) + .or_raise_erased(|| message("Could not delete empty packed refs"))?; } else { - file.commit()?; + file.commit() + .or_raise_erased(|| message("Could not commit packed refs"))?; } drop(refs_sorted); Ok(()) @@ -276,96 +295,3 @@ pub(crate) fn buffer_into_transaction( namespace, }) } - -/// -pub mod prepare { - /// The error used in [`Transaction::prepare(…)`][crate::file::Transaction::prepare()]. - #[derive(Debug)] - #[expect(missing_docs)] - pub enum Error { - CloseLock(std::io::Error), - Resolve(Box), - } - - impl std::fmt::Display for Error { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Error::CloseLock(_) => f.write_str("Could not close a lock which won't ever be committed"), - Error::Resolve(_) => f.write_str("The lookup of an object failed while peeling it"), - } - } - } - - impl std::error::Error for Error { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - Error::CloseLock(err) => Some(err), - Error::Resolve(err) => Some(&**err), - } - } - } - - impl From for Error { - fn from(err: std::io::Error) -> Self { - Error::CloseLock(err) - } - } - - impl From> for Error { - fn from(err: Box) -> Self { - Error::Resolve(err) - } - } -} - -/// -pub mod commit { - use crate::store_impl::packed; - - /// The error used in [`Transaction::commit(…)`][crate::file::Transaction::commit()]. - #[derive(Debug)] - #[expect(missing_docs)] - pub enum Error { - Commit(gix_lock::commit::Error), - Iteration(packed::iter::Error), - Io(std::io::Error), - } - - impl std::fmt::Display for Error { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Error::Commit(_) => f.write_str("Changes to the resource could not be committed"), - Error::Iteration(_) => f.write_str("Some references in the packed refs buffer could not be parsed"), - Error::Io(_) => f.write_str("Failed to write a ref line to the packed ref file"), - } - } - } - - impl std::error::Error for Error { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - Error::Commit(err) => Some(err), - Error::Iteration(err) => Some(err), - Error::Io(err) => Some(err), - } - } - } - - impl From> for Error { - fn from(err: gix_lock::commit::Error) -> Self { - Error::Commit(err) - } - } - - impl From for Error { - fn from(err: packed::iter::Error) -> Self { - Error::Iteration(err) - } - } - - impl From for Error { - fn from(err: std::io::Error) -> Self { - Error::Io(err) - } - } -} diff --git a/gix-ref/tests/refs/error.rs b/gix-ref/tests/refs/error.rs index 21c210cbfec..90dcde5bbea 100644 --- a/gix-ref/tests/refs/error.rs +++ b/gix-ref/tests/refs/error.rs @@ -7,14 +7,14 @@ fn missing_references_remain_classified_after_erasure() -> crate::Result { let packed = store.open_packed_buffer()?.expect("the fixture has packed refs"); let missing = std::ffi::OsStr::new("missing"); for err in [ - Error::from_error(store.find(missing).expect_err("the reference is absent")), - Error::from_error(store.find_loose(missing).expect_err("the reference is absent")), - Error::from_error( + Error::from(store.find(missing).expect_err("the reference is absent")), + Error::from(store.find_loose(missing).expect_err("the reference is absent")), + Error::from( store .find_packed(missing, Some(&packed)) .expect_err("the reference is absent"), ), - Error::from_error(packed.find(missing).expect_err("the reference is absent")), + Error::from(packed.find(missing).expect_err("the reference is absent")), ] { assert!( err.is_not_found(), @@ -37,18 +37,18 @@ fn peeling_missing_targets_is_classified() -> crate::Result { symbolic.target = gix_ref::Target::Symbolic("refs/heads/missing".try_into()?); let mut direct = store.find("main")?; for err in [ - Error::from_error( + Error::from( symbolic .follow(&store) .expect("HEAD is symbolic") .expect_err("the referent is absent"), ), - Error::from_error( + Error::from( symbolic .peel_to_id(&store, &gix_object::find::Never) .expect_err("the symbolic target is absent"), ), - Error::from_error( + Error::from( direct .peel_to_id(&store, &gix_object::find::Never) .expect_err("the object database is empty"), @@ -81,7 +81,7 @@ fn malformed_tags_are_corruption_instead_of_missing_objects() -> crate::Result { } let store = crate::file::store_at("make_ref_repository.sh")?; - let err = Error::from_error( + let err = Error::from( store .find("main")? .peel_to_id(&store, &MalformedTag) @@ -108,35 +108,33 @@ fn malformed_reference_data_is_classified() -> crate::Result { hash, )?; for err in [ - Error::from_error( + Error::from( gix_ref::file::loose::Reference::try_from_path("HEAD".try_into()?, b"invalid", hash) .expect_err("the loose ref is malformed"), ), - Error::from_error(packed::Buffer::from_bytes(b"# invalid\n", hash).expect_err("the header is malformed")), - Error::from_error(packed.find("main").expect_err("the packed ref is malformed")), - Error::from_error( + Error::from(packed::Buffer::from_bytes(b"# invalid\n", hash).expect_err("the header is malformed")), + Error::from(packed.find("main").expect_err("the packed ref is malformed")), + Error::from( packed .iter()? .next() .expect("one packed ref") .expect_err("the ref is malformed"), ), - Error::from_error( + Error::from( store .iter_packed(Some(&packed))? .find_map(Result::err) .expect("the overlay encounters the malformed packed ref"), ), - Error::from_error( + Error::from( store .find("loop-a")? .peel_to_id(&store, &gix_object::find::Never) .expect_err("the symbolic refs form a cycle"), ), - Error::from_error( - gix_ref::file::log::LineRef::from_bytes(b"invalid").expect_err("the reflog line is malformed"), - ), - Error::from_error( + Error::from(gix_ref::file::log::LineRef::from_bytes(b"invalid").expect_err("the reflog line is malformed")), + Error::from( gix_ref::file::log::iter::forward(b"invalid\n") .next() .expect("one reflog line") @@ -146,10 +144,6 @@ fn malformed_reference_data_is_classified() -> crate::Result { assert!(err.is_corrupted(), "malformed stored data is classified: {err}"); assert!(!err.is_not_found(), "malformed stored data is present"); } - assert!( - !Error::from_error(gix_ref::peel::to_object::Error::DepthLimitExceeded { max_depth: 5 }).is_corrupted(), - "a depth limit can also be reached by a valid symbolic reference chain" - ); Ok(()) } @@ -168,7 +162,7 @@ fn missing_transaction_targets_are_classified() -> crate::Result { "update", ), ] { - let err = Error::from_error( + let err = Error::from( store .transaction() .prepare([edit], Fail::Immediately, Fail::Immediately) @@ -191,15 +185,21 @@ fn invalid_reflog_input_is_classified() -> crate::Result { signature: committer(), message: "invalid\nmessage".into(), }; + let missing_committer = store + .transaction() + .prepare([create_at("refs/heads/new")], Fail::Immediately, Fail::Immediately)? + .commit(None) + .expect_err("writing a reflog requires a committer") + .into_error(); + assert!( + missing_committer + .downcast_any_ref::() + .is_some(), + "callers can request an identity without parsing the diagnostic message" + ); for err in [ Error::from_error(line.write_to(&mut Vec::new()).expect_err("newlines are forbidden")), - Error::from_error( - store - .transaction() - .prepare([create_at("refs/heads/new")], Fail::Immediately, Fail::Immediately)? - .commit(None) - .expect_err("writing a reflog requires a committer"), - ), + missing_committer, ] { assert!(err.is_validation(), "invalid reflog input is classified: {err}"); assert!(!err.is_corrupted(), "invalid input does not imply corrupt stored data"); @@ -207,29 +207,6 @@ fn invalid_reflog_input_is_classified() -> crate::Result { Ok(()) } -#[test] -fn existing_sources_and_retry_classifications_are_preserved() { - for kind in [std::io::ErrorKind::PermissionDenied, std::io::ErrorKind::TimedOut] { - let err = Error::from_error(gix_ref::file::find::existing::Error::Find( - gix_ref::file::find::Error::ReadFileContents { - source: std::io::Error::from(kind), - path: "refs/heads/main".into(), - }, - )); - assert_eq!( - err.downcast_any_ref::().map(std::io::Error::kind), - Some(kind), - "the concrete I/O cause remains accessible" - ); - assert_eq!( - err.can_retry(), - kind == std::io::ErrorKind::TimedOut, - "I/O retry policy is retained" - ); - assert!(!err.is_not_found(), "a failed lookup does not imply absence"); - } -} - #[test] fn malformed_packed_names_and_reflog_signatures_retain_parser_errors() -> crate::Result { let hash = crate::fixture_hash_kind(); @@ -237,7 +214,7 @@ fn malformed_packed_names_and_reflog_signatures_retain_parser_errors() -> crate: format!("# pack-refs with: sorted\n{} refs/heads/bad..name\n", hash.null()).as_bytes(), hash, )?; - let err = Error::from_error( + let err = Error::from( packed .iter()? .next() @@ -251,9 +228,8 @@ fn malformed_packed_names_and_reflog_signatures_retain_parser_errors() -> crate: ); let line = format!("{0} {0} invalid signature\tmessage", hash.null()); - let err = Error::from_error( - gix_ref::file::log::LineRef::from_bytes(line.as_bytes()).expect_err("the signature is invalid"), - ); + let err = + Error::from(gix_ref::file::log::LineRef::from_bytes(line.as_bytes()).expect_err("the signature is invalid")); assert!(err.is_corrupted()); assert!( err.downcast_any_ref::().is_some(), @@ -261,3 +237,70 @@ fn malformed_packed_names_and_reflog_signatures_retain_parser_errors() -> crate: ); Ok(()) } + +#[test] +fn custom_name_conversion_errors_keep_their_sources() -> crate::Result { + struct Name(E); + impl TryInto<&'static gix_ref::PartialNameRef> for Name { + type Error = E; + fn try_into(self) -> Result<&'static gix_ref::PartialNameRef, E> { + Err(self.0) + } + } + #[derive(Debug)] + struct Custom(std::io::Error); + impl std::fmt::Display for Custom { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("custom name conversion") + } + } + impl std::error::Error for Custom { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + Some(&self.0) + } + } + let store = crate::file::store_at("make_ref_repository.sh")?; + for kind in [std::io::ErrorKind::TimedOut, std::io::ErrorKind::NotFound] { + for err in [ + store.try_find(Name(Custom(kind.into()))).expect_err("conversion fails"), + store + .try_find(Name(Custom(kind.into()).raise())) + .expect_err("conversion fails"), + ] { + let err = err.into_error(); + assert!(err.downcast_any_ref::().is_some(), "custom type is preserved"); + assert_eq!(err.can_retry(), kind == std::io::ErrorKind::TimedOut); + assert_eq!(err.is_not_found(), kind == std::io::ErrorKind::NotFound); + } + } + Ok(()) +} + +#[test] +fn a_depth_limit_does_not_imply_corruption() -> crate::Result { + let (_keep, store) = crate::file::transaction::prepare_and_commit::empty_store()?; + let refs = store.git_dir().join("refs/heads"); + std::fs::create_dir_all(&refs)?; + for index in 0..6 { + std::fs::write( + refs.join(format!("r{index}")), + if index == 5 { + format!("{}\n", crate::fixture_hash_kind().null()) + } else { + format!("ref: refs/heads/r{}\n", index + 1) + }, + )?; + } + let err = store + .find("r0")? + .follow_to_object_packed(&store, None) + .expect_err("the valid symbolic chain exceeds the depth limit") + .into_error(); + assert!(!err.is_corrupted()); + assert!(!err.is_not_found()); + assert_eq!( + err.metadata().next().expect("limit details").values["max_depth"], + gix_error::Value::from(5_usize) + ); + Ok(()) +} diff --git a/gix-ref/tests/refs/file/log.rs b/gix-ref/tests/refs/file/log.rs index da170882f88..833e28055f1 100644 --- a/gix-ref/tests/refs/file/log.rs +++ b/gix-ref/tests/refs/file/log.rs @@ -77,8 +77,6 @@ mod iter { } mod with_buffer_too_small_for_single_line { - use std::error::Error; - #[test] fn single_line() -> crate::Result { let mut buf = [0u8; 128]; @@ -95,8 +93,8 @@ mod iter { iter.next() .expect("an error") .expect_err("buffer too small") - .source() - .expect("source") + .downcast_any_ref::() + .expect("original I/O failure") .to_string(), r#"buffer too small for line size, got until "0000000000000000 134385f6d781b7e97062102c6a483440bfda2a03 committer 946771200 +0000\tcommit (initial): c1""# ); @@ -221,9 +219,14 @@ mod iter { let mut iter = gix_ref::file::log::iter::forward(log_first_broken.as_bytes()); let err = iter.next().expect("error is not none").expect_err("the line is broken"); + assert!(err.is_corrupted()); + let mut details = err.metadata(); + let position = details.next().expect("line position"); + assert_eq!(position.values["line"], gix_error::Value::from(1_u64)); + assert_eq!(position.values["from_end"], gix_error::Value::from(false)); assert_eq!( - err.to_string(), - r#"In line 1: "0000000000000000000000000000000000000000 134385fbroken7062102c6a483440bfda2a03 committer 946771200 +0000\tcommit" did not match ' <> \t'"# + details.next().expect("decoder input").values["input"], + gix_error::Value::from(log_first_broken.lines().next().expect("first line").as_bytes()) ); assert!(iter.next().expect("a second line").is_ok(), "line parses ok"); assert!(iter.next().is_none(), "iterator exhausted"); diff --git a/gix-ref/tests/refs/file/reference.rs b/gix-ref/tests/refs/file/reference.rs index f90b15843f8..3687a7595f3 100644 --- a/gix-ref/tests/refs/file/reference.rs +++ b/gix-ref/tests/refs/file/reference.rs @@ -56,27 +56,6 @@ mod peel { hex_to_id, }; - #[test] - fn object_lookup_error_preserves_causes() { - use gix_error::ErrorExt; - - let lookup_error = std::io::Error::from(std::io::ErrorKind::PermissionDenied) - .and_raise(gix_error::message("Could not read the referenced object")) - .erased(); - let err = gix_error::Error::from_error(gix_ref::peel::to_id::Error::from(lookup_error)); - - assert_eq!( - err.downcast_any_ref::().map(std::io::Error::kind), - Some(std::io::ErrorKind::PermissionDenied), - "the peeling error retains the I/O cause beneath the lookup context" - ); - assert!( - err.iter_errors() - .any(|cause| cause.to_string() == "Could not read the referenced object"), - "the lookup context remains part of the error chain" - ); - } - #[test] fn one_level() -> crate::Result { let store = file::store()?; @@ -212,17 +191,19 @@ mod peel { assert_eq!(r.kind(), gix_ref::Kind::Symbolic, "there is something to peel"); assert_eq!(r, "refs/loop-a"); - assert!(matches!( - r.peel_to_id(&store, &gix_object::find::Never).unwrap_err(), - gix_ref::peel::to_id::Error::FollowToObject(gix_ref::peel::to_object::Error::Cycle { .. }) - )); + let err = r.peel_to_id(&store, &gix_object::find::Never).expect_err("cyclic refs"); + assert!(err.is_corrupted(), "a symbolic cycle is corruption"); + assert_eq!( + err.metadata().next().expect("cycle details").values["path"], + gix_error::Value::from(store.git_dir().join("refs/loop-a")) + ); assert_eq!(r, "refs/loop-a", "the ref is not changed on error"); let mut r: Reference = store.find_loose("loop-a")?.into(); let err = r .follow_to_object_packed(&store, store.cached_packed_buffer()?.as_ref().map(|p| &***p)) .unwrap_err(); - assert!(matches!(err, gix_ref::peel::to_object::Error::Cycle { .. })); + assert!(err.is_corrupted(), "following also reports the cycle"); Ok(()) } } @@ -243,16 +224,21 @@ mod parse { ) .expect_err("the loose reference content is invalid or unsupported"); assert_eq!( - err.to_string(), - $err, - "the error identifies why decoding failed" + err.metadata().next().expect("decode context").values["input"], + gix_error::Value::from($input.as_slice()), + "the original contents remain available" + ); + assert!( + err.iter_errors() + .any(|cause| cause.to_string().contains($err)), + "the error identifies why decoding failed: {err}" ); } }; } - mktest!(hex_id, b"foobar", "\"foobar\" could not be parsed"); - mktest!(ref_tag, b"reff: hello", "\"reff: hello\" could not be parsed"); + mktest!(hex_id, b"foobar", "Reference content could not be parsed"); + mktest!(ref_tag, b"reff: hello", "Reference content could not be parsed"); mktest!( reftable_placeholder, b"ref: refs/heads/.invalid\n", @@ -261,17 +247,17 @@ mod parse { mktest!( other_invalid_symbolic_target, b"ref: refs/heads/.invalid-other\n", - "The path \"refs/heads/.invalid-other\" to a symbolic reference within a ref file is invalid" + "Invalid symbolic reference target" ); mktest!( sha256_sized_id_for_sha1, b"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n", - "\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\\n\" could not be parsed" + "Reference content could not be parsed" ); mktest!( trailing_garbage_after_id, b"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaextra", - "\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaextra\" could not be parsed" + "Reference content could not be parsed" ); } mod valid { diff --git a/gix-ref/tests/refs/file/store/find.rs b/gix-ref/tests/refs/file/store/find.rs index bc246b8b2ae..4b61329b433 100644 --- a/gix-ref/tests/refs/file/store/find.rs +++ b/gix-ref/tests/refs/file/store/find.rs @@ -129,10 +129,12 @@ mod loose { Some(expected_path) => assert_eq!(reference?, expected_path), None => match reference { Ok(_) => panic!("Expected error"), - Err(gix_ref::file::find::existing::Error::NotFound { name }) => { - assert_eq!(name, Path::new(partial_name)); - } - Err(err) => panic!("Unexpected err: {err:?}"), + Err(err) => assert_eq!( + err.downcast_any_ref::() + .expect("absent reference") + .name, + Path::new(partial_name) + ), }, } } diff --git a/gix-ref/tests/refs/file/store/iter.rs b/gix-ref/tests/refs/file/store/iter.rs index 34f2061052e..a2147cf9798 100644 --- a/gix-ref/tests/refs/file/store/iter.rs +++ b/gix-ref/tests/refs/file/store/iter.rs @@ -256,9 +256,9 @@ fn loose_iter_with_broken_refs() -> crate::Result { "there is exactly one invalid item, and it didn't abort the iterator most importantly" ); #[cfg(not(windows))] - let msg = r#"The reference at "refs/broken" could not be instantiated"#; + let msg = r#"The reference at "refs/broken" could not be decoded"#; #[cfg(windows)] - let msg = r#"The reference at "refs\broken" could not be instantiated"#; + let msg = r#"The reference at "refs\\broken" could not be decoded"#; assert_eq!( actual[first_error].as_ref().expect_err("unparsable ref").to_string(), msg diff --git a/gix-ref/tests/refs/file/transaction/prepare_and_commit/create_or_update/collisions.rs b/gix-ref/tests/refs/file/transaction/prepare_and_commit/create_or_update/collisions.rs index 8e77e278f66..5747f89b2c1 100644 --- a/gix-ref/tests/refs/file/transaction/prepare_and_commit/create_or_update/collisions.rs +++ b/gix-ref/tests/refs/file/transaction/prepare_and_commit/create_or_update/collisions.rs @@ -34,7 +34,11 @@ fn conflicting_creation_without_packed_refs() -> crate::Result { Ok(_) if !case_sensitive => panic!("should fail as 'a' and 'A' clash"), Err(err) if case_sensitive => panic!("should work as case sensitivity allows 'a' and 'A' to coexist: {err:?}"), Err(err) if !case_sensitive => { - assert_eq!(err.to_string(), "A lock could not be obtained for reference \"refs/A\""); + assert!(err.can_retry()); + assert_eq!( + err.metadata().next().expect("failed edit").values["reference"], + gix_error::Value::from(b"refs/A".as_slice()) + ); } _ => unreachable!("actually everything is covered"), } @@ -79,9 +83,8 @@ fn packed_refs_lock_is_mandatory_for_multiple_ongoing_transactions_even_if_one_d let t2res = store .transaction() .prepare([delete_at(ref_name)], Fail::Immediately, Fail::Immediately); - assert_eq!( - &t2res.unwrap_err().to_string()[..54], - "The lock for the packed-ref file could not be obtained", + assert!( + t2res.expect_err("packed refs are locked").can_retry(), "if packed-refs are about to be created, other transactions always acquire a packed-refs lock as to not miss anything" ); Ok(()) @@ -107,9 +110,8 @@ fn conflicting_creation_into_packed_refs() -> crate::Result { ); if !case_sensitive(dir.path()) { - assert_eq!( - transaction.unwrap_err().to_string(), - "A lock could not be obtained for reference \"refs/A\"", + assert!( + transaction.expect_err("case-insensitive collision").can_retry(), "packed ref updates still acquire loose locks before their CAS read" ); return Ok(()); @@ -172,9 +174,8 @@ fn conflicting_creation_into_packed_refs() -> crate::Result { Fail::Immediately, ); - assert_eq!( - &t2res.unwrap_err().to_string()[..40], - "The lock for the packed-ref file could n", + assert!( + t2res.expect_err("packed refs are locked").can_retry(), "packed-refs files will always be locked if they are present as we have to look up their content" ); } @@ -188,9 +189,8 @@ fn conflicting_creation_into_packed_refs() -> crate::Result { .transaction() .prepare([delete_at("refs/A")], Fail::Immediately, Fail::Immediately); - assert_eq!( - &t2res.unwrap_err().to_string()[..40], - "The lock for the packed-ref file could n", + assert!( + t2res.expect_err("packed refs are locked").can_retry(), "once again, packed-refs save the day" ); } diff --git a/gix-ref/tests/refs/file/transaction/prepare_and_commit/create_or_update/mod.rs b/gix-ref/tests/refs/file/transaction/prepare_and_commit/create_or_update/mod.rs index 5205c34c9e2..033f0fafceb 100644 --- a/gix-ref/tests/refs/file/transaction/prepare_and_commit/create_or_update/mod.rs +++ b/gix-ref/tests/refs/file/transaction/prepare_and_commit/create_or_update/mod.rs @@ -1,5 +1,3 @@ -use std::error::Error; - use gix_date::parse::TimeBuf; use gix_hash::ObjectId; use gix_lock::acquire::Fail; @@ -90,15 +88,16 @@ fn reference_with_equally_named_empty_or_non_empty_directory_already_in_place_ca "HEAD was created despite a directory being in the way" ); } else { - match edits { - #[cfg_attr(target_os = "windows", allow(unused_variables))] - Err(transaction::commit::Error::LockCommit { source, full_name }) => { - assert_eq!(full_name, "HEAD"); - #[cfg(not(windows))] - assert_eq!(source.to_string(), "Directory not empty"); - } - _ => unreachable!("other errors shouldn't happen here"), - } + let err = edits.expect_err("the directory is not empty"); + assert_eq!( + err.metadata().next().expect("failed reference").values["reference"], + gix_error::Value::from(b"HEAD".as_slice()) + ); + #[cfg(not(windows))] + assert!( + err.iter_errors() + .any(|cause| cause.to_string() == "Directory not empty") + ); } } Ok(()) @@ -120,13 +119,12 @@ fn reference_with_old_value_must_exist_when_creating_it() -> crate::Result { Fail::Immediately, ); - match res { - Err(transaction::prepare::Error::MustExist { full_name, expected }) => { - assert_eq!(full_name, "HEAD"); - assert_eq!(expected, new_target); - } - _ => unreachable!("unexpected result"), - } + let err = res.expect_err("the previous reference must exist"); + assert!(err.is_not_found()); + assert_eq!( + err.metadata().next().expect("failed edit").values["reference"], + gix_error::Value::from(b"HEAD".as_slice()) + ); Ok(()) } @@ -146,13 +144,13 @@ fn reference_with_explicit_value_must_match_the_value_on_update() -> crate::Resu Fail::Immediately, Fail::Immediately, ); - match res { - Err(transaction::prepare::Error::ReferenceOutOfDate { full_name, actual, .. }) => { - assert_eq!(full_name, "HEAD"); - assert_eq!(actual, target); - } - _ => unreachable!("unexpected result"), - } + let err = res.expect_err("the transaction constraint is violated").into_error(); + let actual = err + .downcast_any_ref::() + .expect("typed recovery signal"); + assert_eq!(actual.full_name, "HEAD"); + assert_eq!(actual.actual, target); + assert!(!err.can_retry(), "retrying requires reconciling the current value"); Ok(()) } @@ -206,13 +204,13 @@ fn the_existing_must_match_constraint_requires_existing_references_to_have_the_g Fail::Immediately, Fail::Immediately, ); - match res { - Err(transaction::prepare::Error::ReferenceOutOfDate { full_name, actual, .. }) => { - assert_eq!(full_name, "HEAD"); - assert_eq!(actual, target); - } - _ => unreachable!("unexpected result"), - } + let err = res.expect_err("the transaction constraint is violated").into_error(); + let actual = err + .downcast_any_ref::() + .expect("typed recovery signal"); + assert_eq!(actual.full_name, "HEAD"); + assert_eq!(actual.actual, target); + assert!(!err.can_retry(), "retrying requires reconciling the current value"); Ok(()) } @@ -225,13 +223,13 @@ fn reference_with_must_not_exist_constraint_cannot_be_created_if_it_exists_alrea let res = store .transaction() .prepare(Some(create_at("HEAD")), Fail::Immediately, Fail::Immediately); - match res { - Err(transaction::prepare::Error::MustNotExist { full_name, actual, .. }) => { - assert_eq!(full_name, "HEAD"); - assert_eq!(actual, target); - } - _ => unreachable!("unexpected result"), - } + let err = res.expect_err("the transaction constraint is violated").into_error(); + let actual = err + .downcast_any_ref::() + .expect("typed recovery signal"); + assert_eq!(actual.full_name, "HEAD"); + assert_eq!(actual.actual, target); + assert!(!err.can_retry(), "retrying requires reconciling the current value"); Ok(()) } @@ -430,7 +428,9 @@ fn windows_device_name_is_illegal_with_enabled_windows_protections() -> crate::R ) .unwrap_err(); assert_eq!( - err.source().expect("inner").to_string(), + err.downcast_any_ref::() + .expect("original I/O failure") + .to_string(), format!("Illegal use of reserved Windows device name in \"{invalid_name}\""), "it's notable that the check also kicks in when the previous value doesn't matter - we expect a 'read' to happen anyway \ - it can't be optimized away as the previous value is stored in the transaction result right now." @@ -491,7 +491,9 @@ fn windows_device_name_check_runs_before_lock_acquisition() -> crate::Result { .unwrap_err(); assert_eq!( - err.source().expect("inner").to_string(), + err.downcast_any_ref::() + .expect("original I/O failure") + .to_string(), "Illegal use of reserved Windows device name in \"refs/heads/CON\"", "device-name validation must short-circuit before lock acquisition; otherwise the \ pre-existing lock file would surface as `LockAcquire(PermanentlyLocked)`" @@ -523,9 +525,17 @@ fn lock_failure_on_symbolic_referent_is_reported_for_the_symbolic_ref() -> crate ) .unwrap_err(); - assert!( - matches!(err, transaction::prepare::Error::LockAcquire { full_name, .. } if full_name == "HEAD"), - "the lock error should name the symbolic ref that initiated the dereferenced update" + assert!(err.can_retry(), "the original lock failure is classifiable"); + let details = err.metadata().next().expect("failed edit"); + assert_eq!( + details.values["reference"], + gix_error::Value::from(b"HEAD".as_slice()), + "the failed edit identifies the symbolic reference requested by the caller" + ); + assert_eq!( + details.values["referent"], + gix_error::Value::from(b"refs/heads/main".as_slice()), + "the actual locked referent is retained as well" ); Ok(()) } diff --git a/gix-ref/tests/refs/file/transaction/prepare_and_commit/delete.rs b/gix-ref/tests/refs/file/transaction/prepare_and_commit/delete.rs index c29dc4020c2..8dcd5e4ff29 100644 --- a/gix-ref/tests/refs/file/transaction/prepare_and_commit/delete.rs +++ b/gix-ref/tests/refs/file/transaction/prepare_and_commit/delete.rs @@ -7,7 +7,6 @@ use crate::{ }; use gix_date::parse::TimeBuf; use gix_lock::acquire::Fail; -use gix_ref::file::transaction::prepare::Error; use gix_ref::{ FullName, Reference, Target, file::ReferenceExt, @@ -37,13 +36,7 @@ fn delete_a_ref_which_is_gone_but_must_exist_fails() -> crate::Result { Fail::Immediately, Fail::Immediately, ); - match res { - Ok(_) => unreachable!("must exist, but it doesn't actually exist"), - Err(err) => assert_eq!( - err.to_string(), - "The reference \"DOES_NOT_EXIST\" for deletion did not exist or could not be parsed" - ), - } + assert!(res.expect_err("the reference must exist").is_not_found()); Ok(()) } @@ -99,18 +92,15 @@ fn delete_ref_with_incorrect_previous_value_fails() -> crate::Result { Fail::Immediately, ); - match res { - Err(err) => { - assert_eq!( - err.to_string(), - format!( - "The reference \"refs/heads/main\" should have content ref: refs/heads/main, actual content was {}", - hex_to_id("02a7a22d90d7c02fb494ed25551850b868e634f0") - ) - ); - } - Ok(_) => unreachable!("must be err"), - } + let err = res.expect_err("the expected target differs"); + let stale = err + .downcast_any_ref::() + .expect("stale reference recovery signal"); + assert_eq!(stale.full_name, "refs/heads/main"); + assert_eq!( + stale.actual, + Target::Object(hex_to_id("02a7a22d90d7c02fb494ed25551850b868e634f0")) + ); // everything stays as is let head = store.find_loose("HEAD")?; assert!(head.log_exists(&store)); @@ -197,16 +187,13 @@ fn rename_a_to_a_slash_b_in_one_transaction() -> crate::Result { ) .unwrap_err(); - match err { - Error::Io(err) => { - assert_eq!( - err.kind(), - std::io::ErrorKind::NotADirectory, - "For now this isn't supported in the same transaction, and path-prefix collisions are reported early." - ); - } - err => unreachable!("unexpected error variant: {err:?}"), - } + assert_eq!( + err.downcast_any_ref::() + .expect("original I/O error") + .kind(), + std::io::ErrorKind::NotADirectory, + "path-prefix collisions are reported early, without losing the I/O kind" + ); let edits = store .transaction() @@ -246,15 +233,7 @@ fn delete_broken_ref_that_must_exist_fails_as_it_is_no_valid_ref() -> crate::Res Fail::Immediately, Fail::Immediately, ); - match res { - Err(err) => { - assert_eq!( - err.to_string(), - "The reference \"HEAD\" for deletion did not exist or could not be parsed" - ); - } - Ok(_) => unreachable!("expected error"), - } + assert!(res.expect_err("a valid existing reference is required").is_not_found()); Ok(()) } diff --git a/gix-ref/tests/refs/file/worktree.rs b/gix-ref/tests/refs/file/worktree.rs index 2f85c95d0e9..a6a8b894f6a 100644 --- a/gix-ref/tests/refs/file/worktree.rs +++ b/gix-ref/tests/refs/file/worktree.rs @@ -429,7 +429,7 @@ mod writable { Fail::Immediately, Fail::Immediately, ), - Err(gix_ref::file::transaction::prepare::Error::LockAcquire { .. }) + Err(ref err) if err.can_retry() ), "prefixed refs resolve to the same name and will fail to be locked (so we don't check for this when doing dupe checking)" ); @@ -446,7 +446,7 @@ mod writable { Fail::Immediately, Fail::Immediately, ), - Err(gix_ref::file::transaction::prepare::Error::LockAcquire { .. }) + Err(ref err) if err.can_retry() )); } @@ -483,7 +483,7 @@ mod writable { Fail::Immediately, Fail::Immediately, ), - Err(gix_ref::file::transaction::prepare::Error::LockAcquire { .. }) + Err(ref err) if err.can_retry() ), "prefixed refs resolve to the same name and will fail to be locked (so we don't check for this when doing dupe checking)" ); diff --git a/gix-ref/tests/refs/packed/find.rs b/gix-ref/tests/refs/packed/find.rs index a7158c15282..8d070bf88b9 100644 --- a/gix-ref/tests/refs/packed/find.rs +++ b/gix-ref/tests/refs/packed/find.rs @@ -176,11 +176,11 @@ bogus refs/tags/gix-actor-v0.1.0 ); for failing_name in &["refs/tags/TEST-0.0.1", "refs/tags/gix-actor-v0.1.0"] { + let err = buf.try_find(*failing_name).expect_err("it should detect an error"); + assert!(err.is_corrupted()); assert_eq!( - buf.try_find(*failing_name) - .expect_err("it should detect an err") - .to_string(), - "The reference could not be parsed" + err.metadata().next().expect("failed lookup").values["name"], + gix_error::Value::from(failing_name.as_bytes()) ); } Ok(()) diff --git a/gix-ref/tests/refs/packed/iter.rs b/gix-ref/tests/refs/packed/iter.rs index 1c1b58d6e85..74e4b9b8318 100644 --- a/gix-ref/tests/refs/packed/iter.rs +++ b/gix-ref/tests/refs/packed/iter.rs @@ -105,22 +105,12 @@ buggy-hash refs/wrong let mut iter = packed::Iter::new(packed_refs, HASH_KIND)?; assert!(iter.next().expect("first ref").is_ok(), "first line is valid"); - assert_eq!( - iter.next() - .expect("second ref") - .expect_err("an error is produced") - .to_string(), - "Invalid reference in line 2: \"buggy-hash refs/wrong\"", - "second line is invalid", - ); - assert_eq!( - iter.next() - .expect("third ref") - .expect_err("an error is produced") - .to_string(), - "Invalid reference in line 3: \"^buggy-hash-too\"", - "third line is invalid", - ); + for (line, input) in [(2_u64, b"buggy-hash refs/wrong".as_slice()), (3, b"^buggy-hash-too")] { + let err = iter.next().expect("invalid line").expect_err("invalid reference"); + assert!(err.is_corrupted()); + assert_eq!(err.values["line"], gix_error::Value::from(line)); + assert_eq!(err.values["input"], gix_error::Value::from(input)); + } assert!(iter.next().expect("last ref").is_ok(), "last line is valid"); assert!(iter.next().is_none(), "exhausted"); Ok(()) @@ -145,3 +135,20 @@ fn performance() -> crate::Result { ); Ok(()) } + +#[test] +fn error_metadata_counts_peeled_lines_and_retains_unterminated_input() -> crate::Result { + let input = format!("{0} refs/tags/one\n^{0}\nbroken", HASH_KIND.null()); + let mut iter = packed::Iter::new(input.as_bytes(), HASH_KIND)?; + iter.next().expect("peeled tag")?; + let err = iter + .next() + .expect("last line") + .expect_err("malformed reference") + .into_error(); + let details = err.metadata().next().expect("line details survive conversion"); + assert_eq!(details.values["line"], gix_error::Value::from(3_u64)); + assert_eq!(details.values["input"], gix_error::Value::from(b"broken".as_slice())); + assert!(iter.next().is_none()); + Ok(()) +} diff --git a/gix-ref/tests/refs/packed/open.rs b/gix-ref/tests/refs/packed/open.rs index 74a60ca60f6..f9d480f8ac6 100644 --- a/gix-ref/tests/refs/packed/open.rs +++ b/gix-ref/tests/refs/packed/open.rs @@ -45,12 +45,11 @@ fn bogus_content_triggers_an_error() -> crate::Result { let packed_refs_data = b"starts with a bogus record, not a header anyway"; let (_keep, path) = write_packed_refs_with(packed_refs_data)?; - match gix_ref::packed::Buffer::open(path, 32, HASH_KIND) { - Ok(_) => unreachable!("unsorted buffers can't be opened"), - Err(err) => assert_eq!( - err.to_string(), - "The packed-refs file did not have a header or wasn't sorted and could not be iterated" - ), - } + let err = gix_ref::packed::Buffer::open(path, 32, HASH_KIND).expect_err("malformed packed refs"); + assert!(err.is_corrupted()); + assert_eq!( + err.metadata().next().expect("failed record").values["input"], + gix_error::Value::from(packed_refs_data.as_slice()) + ); Ok(()) } diff --git a/gix-tix/src/history.rs b/gix-tix/src/history.rs index 2165482af7d..4f005b61802 100644 --- a/gix-tix/src/history.rs +++ b/gix-tix/src/history.rs @@ -2164,11 +2164,12 @@ mod tests { #[test] fn only_missing_ref_reads_are_ignored() { let ref_error = |kind| { - gix::Exn::new(gix::refs::file::iter::loose_then_packed::Error::ReadFileContents { - source: std::io::Error::from(kind), - path: "refs/heads/racing".into(), - }) - .into_error() + gix::Exn::new(std::io::Error::from(kind)) + .raise( + gix::error::Metadata::new("Could not read reference") + .with("path", std::path::Path::new("refs/heads/racing")), + ) + .into_error() }; assert!( is_missing_ref(&ref_error(std::io::ErrorKind::NotFound)), diff --git a/gix/src/reference/iter.rs b/gix/src/reference/iter.rs index a526fe4ae80..98bb6fe5b83 100644 --- a/gix/src/reference/iter.rs +++ b/gix/src/reference/iter.rs @@ -1,7 +1,7 @@ //! #![allow(clippy::empty_docs)] -use gix_error::{ErrorExt, ResultExt}; +use gix_error::ResultExt; use gix_path::RelativePath; use gix_ref::file::ReferenceExt; @@ -96,7 +96,7 @@ impl Iter<'_, '_> { /// /// Doing this is necessary as the packed-refs buffer is already held by the iterator, disallowing the consumer of the iterator /// to peel the returned references themselves. - pub fn peeled(mut self) -> Result { + pub fn peeled(mut self) -> Result { self.peel_with_packed = self.repo.refs.cached_packed_buffer()?; self.peel = true; Ok(self) @@ -108,21 +108,15 @@ impl<'r> Iterator for Iter<'_, 'r> { fn next(&mut self) -> Option { self.inner.next().map(|res| { - res.map_err(|err| gix_error::Error::from(err.raise())) - .and_then(|mut r| { - if self.peel { - let repo = &self.repo; - r.peel_to_id_packed(&repo.refs, &repo.objects, self.peel_with_packed.as_ref().map(|p| &***p)) - .map_err(|err| gix_error::Error::from(err.raise())) - .map(|_| r) - } else { - Ok(r) - } - }) - .map(|r| crate::Reference::from_ref(r, self.repo)) + let mut r = res?; + if self.peel { + r.peel_to_id_packed( + &self.repo.refs, + &self.repo.objects, + self.peel_with_packed.as_ref().map(|p| &***p), + )?; + } + Ok(crate::Reference::from_ref(r, self.repo)) }) } } - -/// The error returned by [references()][crate::Repository::references()]. -pub type Error = gix_ref::packed::buffer::open::Error; diff --git a/gix/src/reference/mod.rs b/gix/src/reference/mod.rs index 9fc96afb726..85b63be0bbd 100644 --- a/gix/src/reference/mod.rs +++ b/gix/src/reference/mod.rs @@ -259,12 +259,13 @@ impl<'repo> Reference<'repo> { /// assert_eq!(branch, "refs/heads/main"); /// # Ok(()) } /// ``` - pub fn follow(&self) -> Option, gix_ref::file::find::existing::Error>> { + pub fn follow(&self) -> Option, crate::Error>> { self.inner.follow(&self.repo.refs).map(|res| { res.map(|r| Reference { inner: r, repo: self.repo, }) + .map_err(gix_error::Exn::into_error) }) } } diff --git a/gix/src/remote/connection/fetch/update_refs/mod.rs b/gix/src/remote/connection/fetch/update_refs/mod.rs index 93de2c61c63..91e946e432d 100644 --- a/gix/src/remote/connection/fetch/update_refs/mod.rs +++ b/gix/src/remote/connection/fetch/update_refs/mod.rs @@ -231,14 +231,7 @@ pub(crate) fn update( PreviousValue::MustExistAndMatch(existing.target().into_owned()), ) } - Err(err) - if matches!( - err.downcast_any_ref::(), - Some(gix_ref::peel::to_id::Error::FollowToObject( - gix_ref::peel::to_object::Error::Follow(_) - )) - ) => - { + Err(err) if err.downcast_any_ref::().is_some() => { // An unborn reference, always allow it to be changed to whatever the remote wants. ( if existing.target().try_name().map(gix_ref::FullNameRef::as_bstr) diff --git a/gix/src/remote/connection/fetch/update_refs/tests.rs b/gix/src/remote/connection/fetch/update_refs/tests.rs index f95df10c0c6..a44a4475184 100644 --- a/gix/src/remote/connection/fetch/update_refs/tests.rs +++ b/gix/src/remote/connection/fetch/update_refs/tests.rs @@ -346,8 +346,8 @@ mod update { .peel_to_id() .expect_err("the local symbolic reference points to a missing branch"); assert!( - peel_err.downcast_any_ref::().is_some(), - "chain mode retains the typed peel error used by update recovery" + peel_err.downcast_any_ref::().is_some(), + "the missing reference remains available for update recovery" ); let (mappings, specs) = mapping_from_spec("HEAD:refs/heads/existing-unborn-symbolic", &repo); assert_eq!(mappings.len(), 1); @@ -518,12 +518,41 @@ mod update { Ok(()) } + #[test] + fn symbolic_tags_with_malformed_referents_are_not_unborn() -> Result { + let (repo, _tmp) = repo_rw("two-origins"); + std::fs::write(repo.git_dir().join("refs/tags/broken"), b"ref: refs/tags/malformed\n")?; + let (mappings, specs) = mapping_from_spec("refs/heads/main:refs/tags/broken", &repo); + std::fs::write(repo.git_dir().join("refs/tags/malformed"), b"invalid")?; + for dry_run in [fetch::DryRun::Yes, fetch::DryRun::No] { + let err = fetch::refs::update( + &repo, + prefixed("action"), + &mappings, + &specs, + &[], + fetch::Tags::None, + dry_run, + fetch::WritePackedRefs::Never, + ) + .expect_err("malformed referents must not be treated as unborn"); + assert!(err.is_corrupted(), "the original decode failure is propagated"); + assert!(err.downcast_any_ref::().is_some()); + } + assert_eq!( + repo.find_reference("refs/tags/broken")?.target().into_owned(), + Target::Symbolic("refs/tags/malformed".try_into()?), + "the failed update preserves the symbolic tag" + ); + Ok(()) + } + #[test] fn symbolic_tags_with_missing_objects_are_not_unborn() -> Result { let (repo, _tmp) = repo_rw("two-origins"); let worktree = repo.workdir().expect("fixture has a worktree"); let missing_id = hex_to_id(&"1".repeat(repo.object_hash().len_in_hex())); - gix_testtools::git(worktree, "symbolic-ref refs/tags/broken refs/tags/missing")?; + std::fs::write(repo.git_dir().join("refs/tags/broken"), b"ref: refs/tags/missing\n")?; std::fs::write(repo.git_dir().join("refs/tags/missing"), format!("{missing_id}\n"))?; let git_error = gix_testtools::git(worktree, "fetch --no-tags origin refs/heads/main:refs/tags/broken") @@ -547,10 +576,9 @@ mod update { ) .expect_err("a missing object is a peeling failure, not an unborn reference"); assert!( - matches!( - err.downcast_any_ref::(), - Some(gix_ref::peel::to_id::Error::NotFound { oid, .. }) if *oid == missing_id - ), + err.is_not_found() + && err.metadata().any(|details| details.values.get("object_id") + == Some(&gix_error::Value::from(missing_id.to_string()))), "the missing-object peeling error is propagated: {err:?}" ); } diff --git a/gix/src/repository/reference.rs b/gix/src/repository/reference.rs index 641614d151f..e08a9fea7b1 100644 --- a/gix/src/repository/reference.rs +++ b/gix/src/repository/reference.rs @@ -308,22 +308,10 @@ impl crate::Repository { /// ``` pub fn find_reference<'a, Name, E>(&self, name: Name) -> Result, crate::Error> where - Name: TryInto<&'a PartialNameRef, Error = E> + Clone, - gix_ref::file::find::Error: From, + Name: TryInto<&'a PartialNameRef, Error = E>, + Result<&'a PartialNameRef, E>: ResultExt, { - // TODO: is there a way to just pass `partial_name` to `try_find_reference()`? Compiler freaks out then - // as it still wants to see `E` there, not `Infallible`. - let partial_name = name - .clone() - .try_into() - .map_err(gix_ref::file::find::Error::from) - .or_erased()?; - self.try_find_reference(name)?.ok_or_else(|| { - gix_error::Error::from_error(gix_error::NotFoundError::new(format!( - "The reference '{}' did not exist", - partial_name.as_bstr() - ))) - }) + Ok(Reference::from_ref(self.refs.find(name)?, self)) } /// Return a platform for iterating references. @@ -345,7 +333,7 @@ impl crate::Repository { /// assert_eq!(branches, vec!["refs/heads/main".to_owned()]); /// # Ok(()) } /// ``` - pub fn references(&self) -> Result, reference::iter::Error> { + pub fn references(&self) -> Result, crate::Error> { Ok(reference::iter::Platform { platform: self.refs.iter()?, repo: self, @@ -359,14 +347,8 @@ impl crate::Repository { pub fn try_find_reference<'a, Name, E>(&self, name: Name) -> Result>, crate::Error> where Name: TryInto<&'a PartialNameRef, Error = E>, - gix_ref::file::find::Error: From, + Result<&'a PartialNameRef, E>: ResultExt, { - match self.refs.try_find(name) { - Ok(r) => match r { - Some(r) => Ok(Some(Reference::from_ref(r, self))), - None => Ok(None), - }, - Err(err) => Err(gix_error::Error::from_error(err)), - } + Ok(self.refs.try_find(name)?.map(|r| Reference::from_ref(r, self))) } } diff --git a/gix/src/revision/spec/parse/delegate/mod.rs b/gix/src/revision/spec/parse/delegate/mod.rs index d2d14530e0c..27cbfe1e10a 100644 --- a/gix/src/revision/spec/parse/delegate/mod.rs +++ b/gix/src/revision/spec/parse/delegate/mod.rs @@ -86,11 +86,9 @@ impl<'repo> Delegate<'repo> { fn kind_to_spec( kind: Option, [first, second]: [Option; 2], - ) -> Result { - pub fn malformed() -> gix_error::Error { - message!("The rev-spec is malformed and misses a ref name") - .raise() - .into_error() + ) -> Result> { + pub fn malformed() -> Exn { + message!("The rev-spec is malformed and misses a ref name").raise() } use gix_revision::spec::Kind::*; Ok(match kind.unwrap_or_default() { @@ -110,11 +108,14 @@ impl<'repo> Delegate<'repo> { } let range = zero_or_one_objects_or_ambiguity_err(self.objs, self.prefix, self.repo)?; + // Finalizing symbolic refs can fail after parsing succeeds. Preserve those causes on failure, + // but keep ignoring errors from rejected candidates when a complete spec was produced. + let inner = kind_to_spec(self.kind, range).map_err(|err| err.chain_all(self.delayed_errors).into_error())?; Ok(crate::revision::Spec { path: self.paths[0].take().or(self.paths[1].take()), first_ref: self.refs[0].take(), second_ref: self.refs[1].take(), - inner: kind_to_spec(self.kind, range)?, + inner, repo: self.repo, }) } diff --git a/gix/src/revision/spec/parse/delegate/revision.rs b/gix/src/revision/spec/parse/delegate/revision.rs index 690d8711df5..d4ed67942ce 100644 --- a/gix/src/revision/spec/parse/delegate/revision.rs +++ b/gix/src/revision/spec/parse/delegate/revision.rs @@ -1,4 +1,4 @@ -use gix_error::{ErrorExt, Exn, ResultExt, bail, message}; +use gix_error::{ErrorExt, Exn, ResultExt, message}; use gix_hash::ObjectId; use gix_revision::spec::parse::{ delegate, @@ -20,16 +20,10 @@ impl delegate::Revision for Delegate<'_> { if self.has_delayed_err() && self.refs[self.idx].is_some() { return Err(message("Refusing call as there are delayed errors and a ref is available").raise_erased()); } - match self.repo.refs.find(name) { - Ok(r) => { - assert!(self.refs[self.idx].is_none(), "BUG: cannot set the same ref twice"); - self.refs[self.idx] = Some(r); - Ok(()) - } - Err(err) => { - bail!(err.raise_erased()) - } - } + let r = self.repo.refs.find(name)?; + assert!(self.refs[self.idx].is_none(), "BUG: cannot set the same ref twice"); + self.refs[self.idx] = Some(r); + Ok(()) } fn disambiguate_prefix( diff --git a/gix/tests/gix/reference/mod.rs b/gix/tests/gix/reference/mod.rs index 3c3927ed5a1..3353baf4c64 100644 --- a/gix/tests/gix/reference/mod.rs +++ b/gix/tests/gix/reference/mod.rs @@ -284,9 +284,9 @@ fn set_target_id() { assert!( head_ref .set_target_id(prev_id, "fails") - .unwrap_err() - .to_string() - .starts_with("Reference \"refs/heads/main\" was supposed to exist") + .expect_err("the reference was deleted") + .is_not_found(), + "updating a deleted reference requires reconciling its absence" ); } diff --git a/gix/tests/gix/remote/connect.rs b/gix/tests/gix/remote/connect.rs index 512c336925b..1add5362b85 100644 --- a/gix/tests/gix/remote/connect.rs +++ b/gix/tests/gix/remote/connect.rs @@ -17,9 +17,8 @@ mod http_authentication { .set("NO_PROXY", "*") .set("no_proxy", "*"); let directory = gix_testtools::tempfile::tempdir()?; - gix_testtools::git(directory.path(), "init --bare")?; let _cwd = gix_testtools::set_current_dir(directory.path())?; - let mut repo = gix::open_opts(directory.path(), gix::open::Options::isolated())?; + let mut repo = crate::init_repo_isolated(directory.path(), gix::create::Kind::Bare)?.to_thread_local(); // Like GCM, this helper needs the server's account hint to choose a cached credential. // The credentials are fictitious, and both the helper and gix must keep prompting disabled. diff --git a/gix/tests/gix/repository/object.rs b/gix/tests/gix/repository/object.rs index 956a39ceb13..2359c24f79e 100644 --- a/gix/tests/gix/repository/object.rs +++ b/gix/tests/gix/repository/object.rs @@ -740,11 +740,12 @@ mod commit { let empty_tree_id = repo.write_object(gix::objs::Tree::empty())?.detach(); let err = repo .commit("HEAD", "initial", empty_tree_id, [empty_tree_id]) - .unwrap_err(); + .expect_err("an initial commit cannot have a parent"); + assert!(err.is_not_found(), "the expected previous reference value is absent"); assert_eq!( - err.to_string(), - "Reference \"refs/heads/main\" was supposed to exist with value 4b825dc642cb6eb9a060e54bf8d69288fbee4904, but didn't.", - "cannot provide parent id in initial commit" + err.metadata().next().expect("the failed edit has context").values["reference"], + gix::error::Value::from(b"HEAD".as_slice()), + "the error identifies the requested reference" ); Ok(()) } diff --git a/gix/tests/gix/revision/spec/from_bytes/ambiguous.rs b/gix/tests/gix/revision/spec/from_bytes/ambiguous.rs index 2dec286d8cf..218080c562e 100644 --- a/gix/tests/gix/revision/spec/from_bytes/ambiguous.rs +++ b/gix/tests/gix/revision/spec/from_bytes/ambiguous.rs @@ -89,6 +89,42 @@ fn ranges_are_auto_disambiguated_by_committish() { } } +#[test] +fn resolved_ambiguity_does_not_hide_a_missing_symbolic_referent() -> crate::Result { + let fixture = gix_testtools::scripted_fixture_writable("make_rev_spec_parse_repos.sh")?; + let repo = gix::open_opts(fixture.path().join("ambiguous_blob_tree_commit"), crate::restricted())?; + std::fs::write(repo.git_dir().join("refs/heads/alias"), b"ref: refs/heads/missing\n")?; + + let err = repo + .rev_parse("000000000..alias") + .expect_err("the left endpoint resolves to a commit, but the right referent is missing"); + assert!( + err.is_not_found(), + "resolved ambiguity must not mask the missing referent: {err}" + ); + assert_eq!( + err.downcast_any_ref::() + .expect("the lookup error survives rejected-candidate errors") + .name, + std::path::Path::new("refs/heads/missing"), + "the final spec conversion retains the actual lookup failure" + ); + insta::assert_debug_snapshot!(err, @r#" + The rev-spec is malformed and misses a ref name + | + └─ Last encountered object 0000000000b was blob while trying to peel to commit + | + └─ Last encountered object 0000000000c was tree while trying to peel to commit + | + └─ Could not peel 'refs/heads/alias' to obtain its target + | + └─ The ref partially named "refs/heads/missing" could not be found + | + └─ Reference or object not found + "#); + Ok(()) +} + #[test] fn blob_and_tree_can_be_disambiguated_by_type() { let repo = repo("ambiguous_blob_tree_commit").unwrap(); diff --git a/gix/tests/gix/revision/spec/from_bytes/mod.rs b/gix/tests/gix/revision/spec/from_bytes/mod.rs index df45ab5a1af..ff9a8089378 100644 --- a/gix/tests/gix/revision/spec/from_bytes/mod.rs +++ b/gix/tests/gix/revision/spec/from_bytes/mod.rs @@ -113,23 +113,106 @@ fn missing_revision_keeps_reference_lookup_error_available_for_path_fallback() - .rev_parse("README.md") .expect_err("missing revspec must fail before callers can inspect the error chain"); + assert!( + err.is_not_found(), + "rev-parse preserves the reference lookup classification" + ); let not_found = err - .downcast_any_ref::() + .downcast_any_ref::() .expect("reference lookup failure remains available for downcasting after rev-parse"); - match not_found { - gix::refs::file::find::existing::Error::NotFound { name } => { - assert_eq!( - name, - std::path::Path::new("README.md"), - "the ref lookup error carries the unresolved revspec for path fallback" - ); - } - gix::refs::file::find::existing::Error::Find(_) => { - panic!("expected a missing ref error, got a lower-level ref lookup failure") - } + assert_eq!( + not_found.name, + std::path::Path::new("README.md"), + "the missing reference carries the unresolved revspec for path fallback" + ); + + Ok(()) +} + +#[test] +fn missing_symbolic_referents_keep_their_name() -> crate::Result { + let (repo, _keep) = crate::basic_rw_repo()?; + std::fs::write(repo.git_dir().join("refs/heads/alias"), b"ref: refs/heads/missing\n")?; + + for revspec in ["alias", "alias..HEAD", "HEAD..alias", "alias...HEAD", "HEAD...alias"] { + let err = repo.rev_parse(revspec).expect_err("the symbolic referent is missing"); + assert!( + err.is_not_found(), + "missing symbolic referents are classified as not found: {err:?}" + ); + assert_eq!( + err.downcast_any_ref::() + .expect("the missing referent remains available for path fallback") + .name, + std::path::Path::new("refs/heads/missing"), + "the missing reference name is not necessarily the input revspec" + ); } + Ok(()) +} + +#[test] +fn both_missing_symbolic_referents_are_retained() -> crate::Result { + let (repo, _keep) = crate::basic_rw_repo()?; + std::fs::write( + repo.git_dir().join("refs/heads/first"), + b"ref: refs/heads/missing-first\n", + )?; + std::fs::write( + repo.git_dir().join("refs/heads/second"), + b"ref: refs/heads/missing-second\n", + )?; + for revspec in ["first..second", "first...second"] { + let err = repo + .rev_parse(revspec) + .expect_err("both symbolic referents are missing"); + let missing_names: Vec<_> = err + .iter_errors() + .filter_map(|cause| cause.downcast_ref::()) + .map(|cause| cause.name.as_path()) + .collect(); + assert_eq!( + missing_names, + [ + std::path::Path::new("refs/heads/missing-first"), + std::path::Path::new("refs/heads/missing-second"), + ], + "final spec conversion preserves both lookup failures" + ); + } + Ok(()) +} + +#[test] +fn missing_objects_are_classified_without_a_missing_reference() -> crate::Result { + let (repo, _keep) = crate::basic_rw_repo()?; + let mut missing_commit_id = repo.object_hash().null(); + missing_commit_id.as_mut_slice()[0] = 1; + repo.reference( + "refs/heads/missing-object", + missing_commit_id, + gix::refs::transaction::PreviousValue::Any, + "", + )?; + + std::fs::write( + repo.git_dir().join("refs/heads/alias"), + b"ref: refs/heads/missing-object\n", + )?; + + for revspec in ["missing-object^{object}", "missing-object:README.md", "alias"] { + let err = repo.rev_parse(revspec).expect_err("the referenced object is missing"); + assert!( + err.is_not_found(), + "object lookup failures retain their classification: {err}" + ); + assert!( + err.downcast_any_ref::().is_none(), + "a missing object must not trigger missing-reference path fallback: {err}" + ); + } Ok(()) } @@ -243,9 +326,7 @@ fn invalid_head() { | └─ Could not peel 'HEAD' to obtain its target | - └─ Could not follow a single level of a symbolic reference - | | - | └─ The ref partially named "refs/heads/main" could not be found + └─ The ref partially named "refs/heads/main" could not be found | | | └─ Reference or object not found | @@ -253,9 +334,17 @@ fn invalid_head() { "#); let err = parse_spec("HEAD", &repo).unwrap_err(); - // The head couldn't be peeled, and there is nothing left to consume, but no - // object was resolved. - insta::assert_debug_snapshot!(err, @"The rev-spec is malformed and misses a ref name"); + assert!( + err.is_not_found(), + "final conversion retains the deferred lookup failure" + ); + insta::assert_debug_snapshot!(err, @r#" + The rev-spec is malformed and misses a ref name + | + └─ Could not peel 'HEAD' to obtain its target + | + └─ The ref partially named "refs/heads/main" could not be found + "#); } #[test] diff --git a/gix/tests/gix/submodule.rs b/gix/tests/gix/submodule.rs index b4b4a386292..e8f6e35631e 100644 --- a/gix/tests/gix/submodule.rs +++ b/gix/tests/gix/submodule.rs @@ -8,6 +8,8 @@ pub fn repo(name: &str) -> crate::Result { } mod open { + use std::io::Write; + use gix_sec::Trust; use gix::submodule; @@ -32,7 +34,10 @@ mod open { sm.open()?.is_some(), "deinitializing preserves the submodule repository" ); - gix_testtools::git(worktree, "config --file .git/modules/m1/config core.worktree ''")?; + std::fs::OpenOptions::new() + .append(true) + .open(repo.git_dir().join("modules/m1/config"))? + .write_all(b"\n[core]\n\tworktree = \n")?; let err = sm .open() diff --git a/tests/tools/src/repository/gix.rs b/tests/tools/src/repository/gix.rs index ab2f6e8f615..7bf3cc23ddc 100644 --- a/tests/tools/src/repository/gix.rs +++ b/tests/tools/src/repository/gix.rs @@ -189,7 +189,7 @@ fn resolve_symbolic(store: &gix_ref::file::Store, mut name: gix_ref::FullName) - is_broken: false, }); } - Err(gix_ref::file::find::Error::ReferenceCreation { .. }) => { + Err(err) if err.downcast_any_ref::().is_some() => { return Ok(SymbolicResolution { name, id: None, @@ -218,7 +218,9 @@ fn references(store: &gix_ref::file::Store) -> Result<(Vec, Vec reference, - Err(gix_ref::file::iter::loose_then_packed::Error::ReferenceCreation { .. }) => continue, + Err(err) if err.downcast_any_ref::().is_some() => { + continue; + } Err(err) => return Err(err.into()), }; if !reference.name.as_ref().as_bstr().starts_with(b"refs/") { diff --git a/tests/tools/tests/env.rs b/tests/tools/tests/env.rs index 037502d8c4a..d434cc5d330 100644 --- a/tests/tools/tests/env.rs +++ b/tests/tools/tests/env.rs @@ -108,6 +108,7 @@ mod isolate_git_environment { use std::{ collections::BTreeMap, ffi::{OsStr, OsString}, + io::Write, path::PathBuf, process::Command, }; @@ -251,8 +252,12 @@ mod isolate_git_environment { let outside = tempfile::tempdir()?; gix_testtools::git(repo.path(), "init -q")?; gix_testtools::git(outside.path(), "init -q")?; - gix_testtools::git(repo.path(), "config foo.bar intended")?; - gix_testtools::git(outside.path(), "config foo.bar outside")?; + for (directory, value) in [(repo.path(), "intended"), (outside.path(), "outside")] { + let mut config = std::fs::OpenOptions::new() + .append(true) + .open(directory.join(".git/config"))?; + writeln!(config, "\n[foo]\n\tbar = {value}")?; + } let mut command = gix_testtools::git_command(repo.path()); command.args(["config", "--local", "--get", "foo.bar"]); diff --git a/tests/tools/tests/tools/repository.rs b/tests/tools/tests/tools/repository.rs index 5905295e478..a4bd4d534dd 100644 --- a/tests/tools/tests/tools/repository.rs +++ b/tests/tools/tests/tools/repository.rs @@ -57,7 +57,7 @@ mod snapshot { let again = gix_testtools::repository::snapshot(fixture.path())?; assert_eq!(state, again, "taking a snapshot has no observable side effects"); - gix_testtools::git(fixture.path(), "checkout --theirs -- conflicted")?; + std::fs::write(fixture.path().join("conflicted"), b"conflict theirs\n")?; gix_testtools::git(fixture.path(), "add conflicted")?; assert!( gix_testtools::repository::snapshot(fixture.path())? From baff04f794a7889cde1dbf089de30667dc97caf5 Mon Sep 17 00:00:00 2001 From: Byron Date: Thu, 17 Sep 2026 14:58:06 +0100 Subject: [PATCH 48/51] change(gix-odb)!: replace forwarding errors with metadata contexts looked at the diff in detail, but went through quickly. Enough to give it my name, but really barely so. Some refactoring done as well, but nothing major. Return canonical `Exn` errors for loose and dynamic object lookup, alternate resolution, prefix lookup and integrity verification. Preserve original I/O, decoder, allocation, persistence and custom reader sources instead of forwarding them through operation-specific error enums. Use documented scalar `Metadata` contexts for native paths, object IDs, sizes, pack counts and recursion limits. Keep `alternate::Cycle` with its discovered directory chain, and preserve explicit retryability for interrupted verification or concurrent disk changes. An absent delta base remains not found, while a recursion limit alone implies neither absence nor corruption. Empty loose files are now classified as corruption. Remove empty error namespaces and redundant conversions in porcelain and CLI callers. Keep the genuine I/O boundary for store initialization and pack loading, using the existing adapter to retain both the I/O kind and the complete cause. Replace wrapper-construction tests with actual custom-reader, malformed-object, missing-delta and depth-limit failures. Validate metadata and classifications after conversion, including native path values and retained cycle details. --- gitoxide-core/src/pack/explode.rs | 14 +- gix-odb/src/alternate/mod.rs | 85 ++---- gix-odb/src/alternate/parse.rs | 103 +++---- gix-odb/src/lib.rs | 15 +- gix-odb/src/store_impls/dynamic/find.rs | 209 +++----------- gix-odb/src/store_impls/dynamic/header.rs | 43 ++- gix-odb/src/store_impls/dynamic/init.rs | 4 +- gix-odb/src/store_impls/dynamic/iter.rs | 8 +- gix-odb/src/store_impls/dynamic/load_index.rs | 145 +++------- gix-odb/src/store_impls/dynamic/mod.rs | 9 +- gix-odb/src/store_impls/dynamic/prefix.rs | 96 +------ gix-odb/src/store_impls/dynamic/structure.rs | 8 +- gix-odb/src/store_impls/dynamic/verify.rs | 135 ++++----- gix-odb/src/store_impls/dynamic/write.rs | 93 ++----- gix-odb/src/store_impls/loose/find.rs | 220 +++++---------- gix-odb/src/store_impls/loose/mod.rs | 6 +- gix-odb/src/store_impls/loose/verify.rs | 96 ++----- gix-odb/src/store_impls/loose/write.rs | 218 +++++---------- gix-odb/tests/odb/alternate.rs | 47 ++-- gix-odb/tests/odb/error.rs | 263 ++++++++++-------- gix-odb/tests/odb/store/loose.rs | 41 ++- gix-ref/src/store/file/find.rs | 7 +- gix-ref/src/store/file/loose/reflog.rs | 25 +- .../loose/reflog/create_or_update/tests.rs | 42 +++ gix-ref/src/store/file/overlay_iter.rs | 2 +- gix-ref/src/store/packed/transaction.rs | 29 +- gix-ref/tests/refs/error.rs | 49 ++++ gix-ref/tests/refs/file/store/find.rs | 30 ++ gix-ref/tests/refs/file/store/reflog.rs | 37 +++ gix/src/id.rs | 31 +-- .../remote/connection/fetch/receive_pack.rs | 2 +- .../revision/spec/parse/delegate/revision.rs | 3 +- gix/tests/gix/remote/fetch.rs | 13 +- .../gix/revision/spec/from_bytes/ambiguous.rs | 11 +- gix/tests/gix/revision/spec/from_bytes/mod.rs | 31 +-- .../gix/revision/spec/from_bytes/util.rs | 8 + 36 files changed, 864 insertions(+), 1314 deletions(-) diff --git a/gitoxide-core/src/pack/explode.rs b/gitoxide-core/src/pack/explode.rs index 5daf7766e18..784d2b8462a 100644 --- a/gitoxide-core/src/pack/explode.rs +++ b/gitoxide-core/src/pack/explode.rs @@ -197,12 +197,11 @@ pub fn pack_or_pack_index( move |object_kind, buf, index_entry, progress| { let written_id = out .write_buf(object_kind, buf) - .map_err(|err| { - err.raise(message!( + .or_raise_erased(|| { + message!( "Failed to write {object_kind} object {}", index_entry.oid - )) - .erased() + ) })?; if let Err(err) = written_id.verify(&index_entry.oid) { if let object::Kind::Tree = object_kind { @@ -219,11 +218,10 @@ pub fn pack_or_pack_index( if let Some(verifier) = loose_odb.as_ref() { let obj = verifier .try_find(&written_id, &mut read_buf) - .map_err(|err| { - err.and_raise(message!( + .or_raise_erased(|| { + message!( "The recently written file for loose object {written_id} could not be read" - )) - .erased() + ) })? .ok_or_else(|| { NotFoundError::new(format!( diff --git a/gix-odb/src/alternate/mod.rs b/gix-odb/src/alternate/mod.rs index eba8e0c9f94..cd9bdd8127f 100644 --- a/gix-odb/src/alternate/mod.rs +++ b/gix-odb/src/alternate/mod.rs @@ -18,68 +18,32 @@ //! Based on the [canonical implementation](https://github.com/git/git/blob/master/sha1-file.c#L598:L609). use std::{fs, io, path::PathBuf}; +use gix_error::{ErrorExt, Exn, Metadata, ResultExt}; use gix_path::realpath::MAX_SYMLINKS; -/// -pub mod parse; -pub use parse::function::parse; +mod parse; +pub use parse::parse; -/// Returned by [`resolve()`] +/// An alternate object directory points back into the chain being resolved. #[derive(Debug)] -#[allow(missing_docs)] -pub enum Error { - Io(io::Error), - Realpath(gix_error::Error), - Parse(parse::Error), - Cycle(Vec), +pub struct Cycle { + /// Canonical object directories in traversal order, with an implicit link from the last to the first. + pub paths: Vec, } -impl std::fmt::Display for Error { +impl std::fmt::Display for Cycle { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Error::Io(err) => std::fmt::Display::fmt(err, f), - Error::Realpath(err) => std::fmt::Display::fmt(err, f), - Error::Parse(err) => std::fmt::Display::fmt(err, f), - Error::Cycle(paths) => write!( - f, - "Alternates form a cycle: {} -> {}", - paths - .iter() - .map(|p| format!("'{}'", p.display())) - .collect::>() - .join(" -> "), - paths.first().expect("more than one directories").display() - ), + f.write_str("Alternates form a cycle")?; + for path in &self.paths { + write!(f, " -> {}", path.display())?; } + Ok(()) } } -impl std::error::Error for Error { +impl std::error::Error for Cycle { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - Error::Io(err) => Some(err), - Error::Realpath(err) => Some(err), - Error::Parse(err) => Some(err), - Error::Cycle(_) => Some(&crate::CORRUPTION), - } - } -} - -impl From for Error { - fn from(err: io::Error) -> Self { - Error::Io(err) - } -} - -impl From for Error { - fn from(err: gix_error::Exn) -> Self { - Error::Realpath(err.into_error()) - } -} - -impl From for Error { - fn from(err: parse::Error) -> Self { - Error::Parse(err) + Some(&crate::CORRUPTION) } } @@ -89,7 +53,9 @@ impl From for Error { /// if there are no alternates). /// An object directory that was resolved before is skipped, and it is an error if an alternate points back /// into the chain of directories that is currently being followed, as that would form a cycle. -pub fn resolve(objects_directory: PathBuf, current_dir: &std::path::Path) -> Result, Error> { +/// Read and parse failures include metadata `path` (native path), the alternates file. +/// Cycles retain their canonical directory chain in [`Cycle`]. +pub fn resolve(objects_directory: PathBuf, current_dir: &std::path::Path) -> Result, Exn> { let mut dirs = vec![(None, objects_directory.clone())]; let mut out = Vec::new(); let mut seen = Vec::new(); @@ -105,20 +71,29 @@ pub fn resolve(objects_directory: PathBuf, current_dir: &std::path::Path) -> Res .collect(); cycle.push(seen[seen_idx].0.clone()); cycle.reverse(); - return Err(Error::Cycle(cycle)); + return Err(Cycle { paths: cycle }.raise_erased()); } continue; } let idx = seen.len(); seen.push((dir_canonicalized, parent_idx)); - match fs::read(dir.join("info").join("alternates")) { + let path = dir.join("info").join("alternates"); + match fs::read(&path) { Ok(input) => { - for path in parse(&input)?.into_iter().rev() { + for path in parse(&input) + .or_raise_erased(|| Metadata::new("Could not parse alternates").with("path", path))? + .into_iter() + .rev() + { dirs.push((Some(idx), objects_directory.join(path))); } } Err(err) if err.kind() == io::ErrorKind::NotFound => {} - Err(err) => return Err(err.into()), + Err(err) => { + return Err(err + .and_raise(Metadata::new("Could not read alternates").with("path", path)) + .erased()); + } } if parent_idx.is_some() { out.push(dir); diff --git a/gix-odb/src/alternate/parse.rs b/gix-odb/src/alternate/parse.rs index 3940be55b5d..2fd576b83fd 100644 --- a/gix-odb/src/alternate/parse.rs +++ b/gix-odb/src/alternate/parse.rs @@ -1,73 +1,42 @@ -/// Returned as part of [`crate::alternate::Error::Parse`] -#[derive(Debug)] -#[allow(missing_docs)] -pub enum Error { - PathConversion { path: Vec, source: gix_error::Error }, -} - -impl std::fmt::Display for Error { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Error::PathConversion { path, .. } => write!( - f, - "Could not obtain an object path for the alternate directory '{}'", - String::from_utf8_lossy(path) - ), - } - } -} - -impl std::error::Error for Error { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - Error::PathConversion { source, .. } => Some(source), - } - } -} - -pub(super) mod function { - use super::Error; - use std::{borrow::Cow, path::PathBuf}; +use gix_error::{Exn, Metadata, ResultExt}; +use std::{borrow::Cow, path::PathBuf}; - use gix_object::bstr::ByteSlice; +use gix_object::bstr::ByteSlice; - /// Parse the raw contents of an `objects/info/alternates` file from `input` into paths. - /// - /// Empty entries and comments are ignored. Entries beginning with `"` use Git's C-style quoting, - /// which permits literal newlines in paths. Invalid quoting falls back to the raw entry. - pub fn parse(mut input: &[u8]) -> Result, Error> { - let mut out = Vec::new(); - while !input.is_empty() { - let entry = input.as_bstr(); - let end_of_line = || entry.find_byte(b'\n').unwrap_or(entry.len()); - let (path, consumed) = if entry.starts_with(b"#") { - (None, end_of_line()) - } else { - // Like Git, try unquoting before treating a newline as the next separator. - match entry.starts_with(b"\"").then(|| gix_quote::ansi_c::undo(entry)) { - Some(Ok((unquoted, consumed))) => (Some(unquoted), consumed), - _ => { - let consumed = end_of_line(); - (Some(Cow::Borrowed(entry[..consumed].as_bstr())), consumed) - } +/// Parse the raw contents of an `objects/info/alternates` file from `input` into paths. +/// +/// Empty entries and comments are ignored. Entries beginning with `"` use Git's C-style quoting, +/// which permits literal newlines in paths. Invalid quoting falls back to the raw entry. +/// Path conversion failures include metadata `input` (bytes), the original alternates entry. +pub fn parse(mut input: &[u8]) -> Result, Exn> { + let mut out = Vec::new(); + while !input.is_empty() { + let entry = input.as_bstr(); + let end_of_line = || entry.find_byte(b'\n').unwrap_or(entry.len()); + let (path, consumed) = if entry.starts_with(b"#") { + (None, end_of_line()) + } else { + // Like Git, try unquoting before treating a newline as the next separator. + match entry.starts_with(b"\"").then(|| gix_quote::ansi_c::undo(entry)) { + Some(Ok((unquoted, consumed))) => (Some(unquoted), consumed), + _ => { + let consumed = end_of_line(); + (Some(Cow::Borrowed(entry[..consumed].as_bstr())), consumed) } - }; - let original = &entry[..consumed]; - let maybe_nl = usize::from(consumed < input.len()); - input = &input[consumed + maybe_nl..]; + } + }; + let original = &entry[..consumed]; + let maybe_nl = usize::from(consumed < input.len()); + input = &input[consumed + maybe_nl..]; - let Some(path) = path.filter(|path| !path.is_empty()) else { - continue; - }; - out.push( - gix_path::try_from_bstr(path) - .map_err(|err| Error::PathConversion { - path: original.to_vec(), - source: err.into_error(), - })? - .into_owned(), - ); - } - Ok(out) + let Some(path) = path.filter(|path| !path.is_empty()) else { + continue; + }; + out.push( + gix_path::try_from_bstr(path) + .or_raise(|| Metadata::new("Could not obtain an alternate object directory").with("input", original))? + .into_owned(), + ); } + Ok(out) } diff --git a/gix-odb/src/lib.rs b/gix-odb/src/lib.rs index 051f0ce6375..e53ef088766 100644 --- a/gix-odb/src/lib.rs +++ b/gix-odb/src/lib.rs @@ -8,8 +8,12 @@ //! * access to packed objects //! * multiple loose objects and pack locations as gathered from `alternates` files. //! -//! Custom error sources retain missing-object, corruption, validation, resource-exhaustion and retry -//! classifications when raised or converted to [`gix_error::Error`]. +//! Errors preserve their original sources and classifications through [`gix_error::Exn`] and [`gix_error::Error`]. +//! [`gix_error::Metadata`] contexts carry diagnostic details such as native paths, object IDs as hex text, +//! and unsigned sizes or limits. Functions creating these contexts document their keys and value types. +//! Use [`gix_error::Exn::metadata()`] or [`gix_error::Error::metadata()`] to inspect each dictionary. +//! Alternate cycles retain their directory chain in [`alternate::Cycle`]; retryable verification failures +//! use [`gix_error::RetryableError`]. //! //! ## Write And Read Loose Objects //! @@ -63,16 +67,9 @@ use gix_features::threading::OwnShared; pub use gix_pack as pack; use gix_zlib::stream::deflate; -static NOT_FOUND: gix_error::NotFoundError = gix_error::NotFoundError { - message: std::borrow::Cow::Borrowed("Object not found"), -}; static CORRUPTION: gix_error::CorruptionError = gix_error::CorruptionError { message: std::borrow::Cow::Borrowed("Object database is malformed or inconsistent"), }; -// Global sources must be synchronized even when the `parallel` feature is disabled. -static RETRYABLE: std::sync::LazyLock = std::sync::LazyLock::new(|| { - gix_error::RetryableError::new(gix_error::message("Object database operation can be retried")) -}); mod store_impls; pub use store_impls::{dynamic as store, loose}; diff --git a/gix-odb/src/store_impls/dynamic/find.rs b/gix-odb/src/store_impls/dynamic/find.rs index 0f7543f1ca1..ae7761450a5 100644 --- a/gix-odb/src/store_impls/dynamic/find.rs +++ b/gix-odb/src/store_impls/dynamic/find.rs @@ -1,159 +1,51 @@ use std::ops::Deref; -use gix_error::ResultExt; +use gix_error::{ErrorExt, Exn, Metadata, NotFoundError, ResultExt}; use gix_pack::cache::DecodeEntry; use crate::store::{handle, load_index}; -pub(crate) mod error { - - use crate::loose; - - /// Returned by [`Handle::try_find()`][gix_pack::Find::try_find()] - #[derive(Debug)] - #[allow(missing_docs)] - pub enum Error { - Loose(loose::find::Error), - Pack(gix_error::Error), - LoadIndex(crate::store::load_index::Error), - LoadPack(std::io::Error), - EntryType(gix_error::Error), - DeltaBaseRecursionLimit { - /// the maximum recursion depth we encountered. - max_depth: usize, - /// The original object to lookup - id: gix_hash::ObjectId, - }, - DeltaBaseMissing { - /// the id of the base object which failed to lookup - base_id: gix_hash::ObjectId, - /// The original object to lookup - id: gix_hash::ObjectId, - }, - DeltaBaseLookup { - err: Box, - /// the id of the base object which failed to lookup - base_id: gix_hash::ObjectId, - /// The original object to lookup - id: gix_hash::ObjectId, - }, - } - - impl std::fmt::Display for Error { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Error::Loose(_) => { - f.write_str("An error occurred while obtaining an object from the loose object store") - } - Error::Pack(_) => { - f.write_str("An error occurred while obtaining an object from the packed object store") - } - Error::LoadIndex(err) => std::fmt::Display::fmt(err, f), - Error::LoadPack(err) => std::fmt::Display::fmt(err, f), - Error::EntryType(err) => std::fmt::Display::fmt(err, f), - Error::DeltaBaseRecursionLimit { max_depth, id } => { - write!( - f, - "Reached recursion limit of {max_depth} while resolving ref delta bases for {id}" - ) - } - Error::DeltaBaseMissing { base_id, id } => { - write!( - f, - "The base object {base_id} could not be found but is required to decode {id}" - ) - } - Error::DeltaBaseLookup { base_id, id, .. } => write!( - f, - "An error occurred when looking up a ref delta base object {base_id} to decode {id}" - ), - } - } - } - - impl std::error::Error for Error { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - Error::Loose(err) => Some(err), - Error::Pack(err) => Some(err), - Error::LoadIndex(err) => Some(err), - Error::LoadPack(err) => Some(err), - Error::EntryType(err) => Some(err), - Error::DeltaBaseLookup { err, .. } => Some(&**err), - Error::DeltaBaseMissing { .. } => Some(&crate::NOT_FOUND), - Error::DeltaBaseRecursionLimit { .. } => None, - } - } - } - - impl From for Error { - fn from(err: loose::find::Error) -> Self { - Error::Loose(err) - } - } - - impl From for Error { - fn from(err: gix_error::Exn) -> Self { - Error::Pack(err.into_error()) - } - } - - impl From for Error { - fn from(err: crate::store::load_index::Error) -> Self { - Error::LoadIndex(err) - } - } - - impl From for Error { - fn from(err: std::io::Error) -> Self { - Error::LoadPack(err) - } - } +#[derive(Copy, Clone)] +pub(crate) struct DeltaBaseRecursion<'a> { + pub depth: usize, + pub original_id: &'a gix_hash::oid, +} - impl From> for Error { - fn from(err: gix_error::Exn) -> Self { - Error::EntryType(err.into_error()) +impl<'a> DeltaBaseRecursion<'a> { + pub fn new(id: &'a gix_hash::oid) -> Self { + Self { + original_id: id, + depth: 0, } } - - #[derive(Copy, Clone)] - pub(crate) struct DeltaBaseRecursion<'a> { - pub depth: usize, - pub original_id: &'a gix_hash::oid, - } - - impl<'a> DeltaBaseRecursion<'a> { - pub fn new(id: &'a gix_hash::oid) -> Self { - Self { - original_id: id, - depth: 0, - } - } - pub fn inc_depth(mut self) -> Self { - self.depth += 1; - self - } + pub fn inc_depth(mut self) -> Self { + self.depth += 1; + self } +} - #[cfg(test)] - mod tests { - use super::*; +use crate::store::types::PackId; - #[test] - fn error_size() { - let actual = std::mem::size_of::(); - assert!(actual <= 88, "{actual} <= 88: should not grow without us noticing"); - } - } +/// Metadata `max_depth` (unsigned) and `object_id` (hex text) identify the recursion limit and original object. +pub(super) fn delta_base_recursion_limit_error(max_depth: usize, object_id: &gix_hash::oid) -> Metadata { + Metadata::new("Reached recursion limit while resolving ref delta bases") + .with("max_depth", max_depth) + .with("object_id", object_id.to_string()) } -pub use error::Error; -use crate::store::types::PackId; +/// Metadata `base_id` and `object_id` (hex text) identify the delta base and object being resolved. +pub(super) fn delta_base_lookup_error(base_id: &gix_hash::oid, object_id: &gix_hash::oid) -> Metadata { + Metadata::new("Could not resolve delta base object") + .with("base_id", base_id.to_string()) + .with("object_id", object_id.to_string()) +} impl super::Handle where S: Deref + Clone, { + /// Delta resolution failures include metadata `object_id` and `base_id` (hex text). + /// Recursion limits include `object_id` (hex text) and `max_depth` (unsigned). fn try_find_cached_inner<'a, 'b>( &'b self, mut id: &'b gix_hash::oid, @@ -161,14 +53,11 @@ where inflate: &mut gix_zlib::Inflate, pack_cache: &mut dyn DecodeEntry, snapshot: &mut load_index::Snapshot, - recursion: Option>, - ) -> Result, Option)>, Error> { + recursion: Option>, + ) -> Result, Option)>, Exn> { if let Some(r) = recursion { if r.depth >= self.max_recursion_depth { - return Err(Error::DeltaBaseRecursionLimit { - max_depth: self.max_recursion_depth, - id: r.original_id.to_owned(), - }); + return Err(delta_base_recursion_limit_error(self.max_recursion_depth, r.original_id).raise_erased()); } } else if !self.ignore_replacements && let Ok(pos) = self @@ -191,7 +80,7 @@ where { let pack = match possibly_pack { Some(pack) => pack, - None => match self.store.load_pack(pack_id, marker)? { + None => match self.store.load_pack(pack_id, marker).or_erased()? { Some(pack) => { *possibly_pack = Some(pack); possibly_pack.as_deref().expect("just put it in") @@ -214,7 +103,7 @@ where } }, }; - let entry = pack.entry(pack_offset)?; + let entry = pack.entry(pack_offset).or_erased()?; let header_size = entry.header_size(); let res = pack.decode_entry( entry, @@ -246,7 +135,7 @@ where .downcast_any_ref::() .map(|err| err.0) else { - return Err(err.into()); + return Err(err); }; // Only with multi-pack indices it's allowed to jump to refer to other packs within this // multi-pack. Otherwise this would constitute a thin pack which is only allowed in transit. @@ -260,6 +149,7 @@ where // The whole ordeal isn't as efficient as it could be due to memory allocation and // later mem-copying when trying again. let mut buf = Vec::new(); + let context = || delta_base_lookup_error(&base_id, id); let obj_kind = self .try_find_cached_inner( &base_id, @@ -268,17 +158,14 @@ where pack_cache, snapshot, recursion - .map(error::DeltaBaseRecursion::inc_depth) - .or_else(|| error::DeltaBaseRecursion::new(id).into()), + .map(DeltaBaseRecursion::inc_depth) + .or_else(|| DeltaBaseRecursion::new(id).into()), ) - .map_err(|err| Error::DeltaBaseLookup { - err: Box::new(err), - base_id, - id: id.to_owned(), - })? - .ok_or_else(|| Error::DeltaBaseMissing { - base_id, - id: id.to_owned(), + .or_raise_erased(context)? + .ok_or_else(|| { + NotFoundError::new("Delta base object is missing") + .and_raise(context()) + .erased() })? .0 .kind; @@ -309,7 +196,7 @@ where let pack = possibly_pack .as_ref() .expect("pack to still be available like just now"); - let entry = pack.entry(pack_offset)?; + let entry = pack.entry(pack_offset).or_erased()?; let header_size = entry.header_size(); pack.decode_entry( entry, @@ -364,10 +251,7 @@ where for lodb in snapshot.loose_dbs.iter() { // TODO: remove this double-lookup once the borrow checker allows it. if lodb.contains(id) { - return lodb - .try_find(id, buffer) - .map(|obj| obj.map(|obj| (obj, None))) - .map_err(Into::into); + return lodb.try_find(id, buffer).map(|obj| obj.map(|obj| (obj, None))); } } @@ -390,7 +274,7 @@ impl gix_pack::Find for super::Handle where S: Deref + Clone, { - // TODO: probably make this method fallible, but that would mean its own error type. + // TODO: make this method fallible to propagate index loading failures. fn contains(&self, id: &gix_hash::oid) -> bool { let mut snapshot = self.snapshot.borrow_mut(); loop { @@ -429,7 +313,6 @@ where let mut snapshot = self.snapshot.borrow_mut(); let mut inflate = self.inflate.borrow_mut(); self.try_find_cached_inner(id, buffer, &mut inflate, pack_cache, &mut snapshot, None) - .or_erased() } fn location_by_oid(&self, id: &gix_hash::oid, buf: &mut Vec) -> Option { diff --git a/gix-odb/src/store_impls/dynamic/header.rs b/gix-odb/src/store_impls/dynamic/header.rs index d9d0d1995a3..f44cb19b222 100644 --- a/gix-odb/src/store_impls/dynamic/header.rs +++ b/gix-odb/src/store_impls/dynamic/header.rs @@ -1,31 +1,32 @@ use std::ops::Deref; -use gix_error::ResultExt; +use gix_error::{ErrorExt, Exn, NotFoundError, ResultExt}; use gix_hash::oid; -use super::find::Error; use crate::{ find::Header, - store::{find::error::DeltaBaseRecursion, handle, load_index}, + store::{ + find::{DeltaBaseRecursion, delta_base_lookup_error, delta_base_recursion_limit_error}, + handle, load_index, + }, }; impl super::Handle where S: Deref + Clone, { + /// Delta resolution failures include metadata `object_id` and `base_id` (hex text). + /// Recursion limits include `object_id` (hex text) and `max_depth` (unsigned). pub(crate) fn try_header_inner<'b>( &'b self, mut id: &'b gix_hash::oid, inflate: &mut gix_zlib::Inflate, snapshot: &mut load_index::Snapshot, recursion: Option>, - ) -> Result, Error> { + ) -> Result, Exn> { if let Some(r) = recursion { if r.depth >= self.max_recursion_depth { - return Err(Error::DeltaBaseRecursionLimit { - max_depth: self.max_recursion_depth, - id: r.original_id.to_owned(), - }); + return Err(delta_base_recursion_limit_error(self.max_recursion_depth, r.original_id).raise_erased()); } } else if !self.ignore_replacements && let Ok(pos) = self @@ -48,7 +49,7 @@ where { let pack = match possibly_pack { Some(pack) => pack, - None => match self.store.load_pack(pack_id, marker)? { + None => match self.store.load_pack(pack_id, marker).or_erased()? { Some(pack) => { *possibly_pack = Some(pack); possibly_pack.as_deref().expect("just put it in") @@ -71,7 +72,7 @@ where } }, }; - let entry = pack.entry(pack_offset)?; + let entry = pack.entry(pack_offset).or_erased()?; let res = match pack.decode_header(entry, inflate, &|id| { index_file.pack_offset_by_id(id).and_then(|pack_offset| { pack.entry(pack_offset) @@ -85,12 +86,13 @@ where .downcast_any_ref::() .map(|err| err.0) else { - return Err(err.into()); + return Err(err); }; // Only with multi-pack indices it's allowed to jump to refer to other packs within this // multi-pack. Otherwise this would constitute a thin pack which is only allowed in transit. // However, if we somehow end up with that, we will resolve it safely, even though we could // avoid handling this case and error instead. + let context = || delta_base_lookup_error(&base_id, id); let hdr = self .try_header_inner( &base_id, @@ -100,14 +102,11 @@ where .map(DeltaBaseRecursion::inc_depth) .or_else(|| DeltaBaseRecursion::new(id).into()), ) - .map_err(|err| Error::DeltaBaseLookup { - err: Box::new(err), - base_id, - id: id.to_owned(), - })? - .ok_or_else(|| Error::DeltaBaseMissing { - base_id, - id: id.to_owned(), + .or_raise_erased(context)? + .ok_or_else(|| { + NotFoundError::new("Delta base object is missing") + .and_raise(context()) + .erased() })?; let handle::index_lookup::Outcome { object_index: @@ -136,7 +135,7 @@ where let pack = possibly_pack .as_ref() .expect("pack to still be available like just now"); - let entry = pack.entry(pack_offset)?; + let entry = pack.entry(pack_offset).or_erased()?; pack.decode_header(entry, inflate, &|id| { index_file .pack_offset_by_id(id) @@ -169,7 +168,7 @@ where for lodb in snapshot.loose_dbs.iter() { // TODO: remove this double-lookup once the borrow checker allows it. if lodb.contains(id) { - return lodb.try_header(id).map(|opt| opt.map(Into::into)).map_err(Into::into); + return lodb.try_header(id).map(|opt| opt.map(Into::into)); } } @@ -191,6 +190,6 @@ where fn try_header(&self, id: &oid) -> Result, gix_error::Exn> { let mut snapshot = self.snapshot.borrow_mut(); let mut inflate = self.inflate.borrow_mut(); - self.try_header_inner(id, &mut inflate, &mut snapshot, None).or_erased() + self.try_header_inner(id, &mut inflate, &mut snapshot, None) } } diff --git a/gix-odb/src/store_impls/dynamic/init.rs b/gix-odb/src/store_impls/dynamic/init.rs index 87fa34d2630..146e1534869 100644 --- a/gix-odb/src/store_impls/dynamic/init.rs +++ b/gix-odb/src/store_impls/dynamic/init.rs @@ -108,11 +108,11 @@ impl Store { Slots::Given(n) => n as usize, Slots::AsNeededByDiskState { multiplier, minimum } => { let mut db_paths = - crate::alternate::resolve(objects_dir.clone(), ¤t_dir).map_err(std::io::Error::other)?; + crate::alternate::resolve(objects_dir.clone(), ¤t_dir).map_err(super::exn_to_io)?; db_paths.insert(0, objects_dir.clone()); let num_slots = Store::collect_indices_and_mtime_sorted_by_size(db_paths, None, None, alloc_limit_bytes) - .map_err(std::io::Error::other)? + .map_err(super::exn_to_io)? .len(); let candidate = ((num_slots as f32 * multiplier) as usize).max(minimum); diff --git a/gix-odb/src/store_impls/dynamic/iter.rs b/gix-odb/src/store_impls/dynamic/iter.rs index 7b2428f4eee..8ba22e92319 100644 --- a/gix-odb/src/store_impls/dynamic/iter.rs +++ b/gix-odb/src/store_impls/dynamic/iter.rs @@ -1,3 +1,5 @@ +use gix_error::Exn; + use std::{ops::Deref, option::Option::None, sync::Arc, vec::IntoIter}; use gix_hash::ObjectId; @@ -75,7 +77,7 @@ impl AllObjects { impl AllObjects { /// Create a new iterator from a dynamic store, which will be forced to load all indices eagerly and in the current thread. - pub fn new(db: &dynamic::Store) -> Result { + pub fn new(db: &dynamic::Store) -> Result { let snapshot = db.load_all_indices()?; let packed_objects = snapshot @@ -222,14 +224,14 @@ where { /// Return an iterator over all, _possibly duplicate_, objects, first the ones in all packs of all linked databases (via alternates), /// followed by all loose objects. - pub fn iter(&self) -> Result { + pub fn iter(&self) -> Result { AllObjects::new(self.store_ref()) } } impl dynamic::Store { /// Like [`Handle::iter()`][super::Handle::iter()], but accessible directly on the store. - pub fn iter(&self) -> Result { + pub fn iter(&self) -> Result { AllObjects::new(self) } } diff --git a/gix-odb/src/store_impls/dynamic/load_index.rs b/gix-odb/src/store_impls/dynamic/load_index.rs index 812b5dacb57..6f1bd897dfb 100644 --- a/gix-odb/src/store_impls/dynamic/load_index.rs +++ b/gix-odb/src/store_impls/dynamic/load_index.rs @@ -10,6 +10,8 @@ use std::{ time::SystemTime, }; +use gix_error::{ErrorExt, Exn, Metadata, ResultExt}; + use crate::store::{IndexCtx, RefreshMode, handle, types}; pub(crate) struct Snapshot { @@ -21,100 +23,11 @@ pub(crate) struct Snapshot { pub(crate) marker: types::SlotIndexMarker, } -mod error { - use std::path::PathBuf; - - use gix_pack::multi_index::PackIndex; - - /// Returned by [`crate::at_opts()`] - #[derive(Debug)] - #[allow(missing_docs)] - pub enum Error { - Inaccessible(PathBuf), - Io(std::io::Error), - Alternate(crate::alternate::Error), - InsufficientSlots { - current: usize, - needed: usize, - }, - /// The problem here is that some logic assumes that more recent generations are higher than previous ones. If we would overflow, - /// we would break that invariant which can lead to the wrong object from being returned. It would probably be super rare, but… - /// let's not risk it. - GenerationOverflow, - TooManyPacksInMultiIndex { - actual: PackIndex, - limit: PackIndex, - index_path: PathBuf, - }, - } - - impl std::fmt::Display for Error { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Error::Inaccessible(path) => { - write!( - f, - "The objects directory at '{}' is not an accessible directory", - path.display() - ) - } - Error::Io(err) => std::fmt::Display::fmt(err, f), - Error::Alternate(err) => std::fmt::Display::fmt(err, f), - Error::InsufficientSlots { current, needed } => write!( - f, - "The slotmap turned out to be too small with {current} entries, would need {needed} more" - ), - Error::GenerationOverflow => write!( - f, - "Would have overflown amount of max possible generations of {}", - super::Generation::MAX - ), - #[allow(clippy::unnecessary_debug_formatting)] - Error::TooManyPacksInMultiIndex { - actual, - limit, - index_path, - } => write!( - f, - "Cannot numerically handle more than {limit} packs in a single multi-pack index, got {actual} in file {index_path:?}" - ), - } - } - } - - impl std::error::Error for Error { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - Error::Io(err) => Some(err), - Error::Alternate(err) => Some(err), - Error::Inaccessible(_) - | Error::InsufficientSlots { .. } - | Error::GenerationOverflow - | Error::TooManyPacksInMultiIndex { .. } => None, - } - } - } - - impl From for Error { - fn from(err: std::io::Error) -> Self { - Error::Io(err) - } - } - - impl From for Error { - fn from(err: crate::alternate::Error) -> Self { - Error::Alternate(err) - } - } -} - -pub use error::Error; - use crate::store::types::{Generation, IndexAndPacks, MutableIndexAndPack, PackId, SlotMapIndex}; impl super::Store { /// Load all indices, refreshing from disk only if needed. - pub(crate) fn load_all_indices(&self) -> Result { + pub(crate) fn load_all_indices(&self) -> Result { let mut snapshot = self.collect_snapshot(); while let Some(new_snapshot) = self.load_one_index(IndexCtx { refresh_mode: RefreshMode::Never, @@ -135,7 +48,7 @@ impl super::Store { marker, loose_compression, }: IndexCtx, - ) -> Result, Error> { + ) -> Result, Exn> { let index = self.index.load(); if !index.is_initialized() { return self.consolidate_with_disk_state( @@ -249,12 +162,14 @@ impl super::Store { /// refresh and possibly clear out our existing data structures, causing all pack ids to be invalidated. /// `load_new_index` is an optimization to at least provide one newly loaded pack after refreshing the slot map. + /// Capacity failures include metadata `current` (unsigned slot count), `needed` (unsigned additional slots), + /// or `limit` (unsigned maximum generation). pub(crate) fn consolidate_with_disk_state( &self, needs_init: bool, load_new_index: bool, loose_compression: gix_zlib::Compression, - ) -> Result, Error> { + ) -> Result, Exn> { let index = self.index.load(); let previous_index_state = Arc::as_ptr(&index) as usize; @@ -378,10 +293,10 @@ impl super::Store { while let Some((mut index_info, mtime, move_from_slot_idx)) = index_paths_to_add.pop_front() { 'increment_slot_index: loop { if num_indices_checked == self.files.len() { - return Err(Error::InsufficientSlots { - current: self.files.len(), - needed: index_paths_to_add.len() + 1, /*the one currently popped off*/ - }); + return Err(Metadata::new("The object database has too few index slots") + .with("current", self.files.len()) + .with("needed", index_paths_to_add.len() + 1) // include the index just popped off + .raise_erased()); } // Don't allow duplicate indicates, we need a 1:1 mapping. if new_slot_map_indices.contains(&next_possibly_free_index) { @@ -450,7 +365,12 @@ impl super::Store { ); let generation = if needs_generation_change { - index.generation.checked_add(1).ok_or(Error::GenerationOverflow)? + index.generation.checked_add(1).ok_or_else(|| { + // A wrapped generation could return an object from the wrong pack. + Metadata::new("Cannot advance the object database generation") + .with("limit", Generation::MAX) + .raise_erased() + })? } else { index.generation }; @@ -517,19 +437,25 @@ impl super::Store { }) } + /// Read failures include metadata `path` (native directory or index path). Multi-pack capacity failures + /// also include `actual` and `limit` (unsigned pack counts). pub(crate) fn collect_indices_and_mtime_sorted_by_size( db_paths: Vec, initial_capacity: Option, multi_pack_index_object_hash: Option, alloc_limit_bytes: Option, - ) -> Result, Error> { + ) -> Result, Exn> { let mut indices_by_modification_time = Vec::with_capacity(initial_capacity.unwrap_or_default()); for db_path in db_paths { let packs = db_path.join("pack"); - let entries = match std::fs::read_dir(packs) { + let entries = match std::fs::read_dir(&packs) { Ok(e) => e, Err(err) if err.kind() == std::io::ErrorKind::NotFound => continue, - Err(err) => return Err(err.into()), + Err(err) => { + return Err(err + .and_raise(Metadata::new("Could not read pack directory").with("path", packs)) + .erased()); + } }; let indices = entries .filter_map(Result::ok) @@ -540,8 +466,13 @@ impl super::Store { (ext == Some(OsStr::new("idx")) && p.with_extension("pack").is_file()) || (multi_pack_index_object_hash.is_some() && ext.is_none() && is_multipack_index(p)) }) - .map(|(p, md)| md.modified().map_err(Error::from).map(|mtime| (p, mtime, md.len()))) - .collect::, _>>()?; + .map(|(p, md)| { + let mtime = md.modified().or_raise_erased(|| { + Metadata::new("Could not read index modification time").with("path", p.as_path()) + })?; + Ok((p, mtime, md.len())) + }) + .collect::, Exn>>()?; let multi_index_info = multi_pack_index_object_hash .and_then(|hash| { @@ -557,11 +488,11 @@ impl super::Store { .flatten() .map(|t| { if t.0.num_indices() > PackId::max_packs_in_multi_index() { - Err(Error::TooManyPacksInMultiIndex { - index_path: p.to_owned(), - actual: t.0.num_indices(), - limit: PackId::max_packs_in_multi_index(), - }) + Err(Metadata::new("Too many packs in a single multi-pack index") + .with("path", p.as_path()) + .with("actual", t.0.num_indices()) + .with("limit", PackId::max_packs_in_multi_index()) + .raise_erased()) } else { Ok(t) } diff --git a/gix-odb/src/store_impls/dynamic/mod.rs b/gix-odb/src/store_impls/dynamic/mod.rs index 854f8e67237..11e9eab41b4 100644 --- a/gix-odb/src/store_impls/dynamic/mod.rs +++ b/gix-odb/src/store_impls/dynamic/mod.rs @@ -71,8 +71,7 @@ impl RefreshMode { } } -/// -pub mod find; +mod find; /// pub mod prefix; @@ -82,8 +81,7 @@ mod header; /// pub mod iter; -/// -pub mod write; +mod write; /// pub mod init; @@ -93,8 +91,7 @@ pub use types::Metrics; pub(crate) mod handle; -/// -pub mod load_index; +mod load_index; /// pub mod verify; diff --git a/gix-odb/src/store_impls/dynamic/prefix.rs b/gix-odb/src/store_impls/dynamic/prefix.rs index 7594f883d29..c5a11fd59a8 100644 --- a/gix-odb/src/store_impls/dynamic/prefix.rs +++ b/gix-odb/src/store_impls/dynamic/prefix.rs @@ -1,51 +1,12 @@ use std::{collections::HashSet, ops::Deref}; +use gix_error::{Exn, ResultExt, message}; use gix_object::Exists; -use crate::store::{Handle, load_index}; +use crate::store::Handle; /// pub mod lookup { - use crate::loose; - - /// Returned by [`Handle::lookup_prefix()`][crate::store::Handle::lookup_prefix()] - #[derive(Debug)] - #[allow(missing_docs)] - pub enum Error { - LooseWalkDir(loose::iter::Error), - LoadIndex(crate::store::load_index::Error), - } - - impl std::fmt::Display for Error { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Error::LooseWalkDir(_) => f.write_str("An error occurred looking up a prefix which requires iteration"), - Error::LoadIndex(err) => std::fmt::Display::fmt(err, f), - } - } - } - - impl std::error::Error for Error { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - Error::LooseWalkDir(err) => Some(err), - Error::LoadIndex(err) => Some(err), - } - } - } - - impl From for Error { - fn from(err: loose::iter::Error) -> Self { - Error::LooseWalkDir(err) - } - } - - impl From for Error { - fn from(err: crate::store::load_index::Error) -> Self { - Error::LoadIndex(err) - } - } - /// A way to indicate if a lookup, despite successful, was ambiguous or yielded exactly /// one result in the particular index. pub type Outcome = Result; @@ -89,46 +50,6 @@ pub mod disambiguate { self.hex_len } } - - /// Returned by [`Handle::disambiguate_prefix()`][crate::store::Handle::disambiguate_prefix()] - #[derive(Debug)] - #[allow(missing_docs)] - pub enum Error { - Contains(crate::store::find::Error), - Lookup(super::lookup::Error), - } - - impl std::fmt::Display for Error { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Error::Contains(_) => f.write_str( - "An error occurred while trying to determine if a full hash contained in the object database", - ), - Error::Lookup(err) => std::fmt::Display::fmt(err, f), - } - } - } - - impl std::error::Error for Error { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - Error::Contains(err) => Some(err), - Error::Lookup(err) => Some(err), - } - } - } - - impl From for Error { - fn from(err: crate::store::find::Error) -> Self { - Error::Contains(err) - } - } - - impl From for Error { - fn from(err: super::lookup::Error) -> Self { - Error::Lookup(err) - } - } } impl Handle @@ -137,7 +58,7 @@ where { /// Return the exact number of packed objects after loading all currently available indices /// as last seen on disk. - pub fn packed_object_count(&self) -> Result { + pub fn packed_object_count(&self) -> Result { let mut count = self.packed_object_count.borrow_mut(); match *count { Some(count) => Ok(count), @@ -158,10 +79,7 @@ where /// Given a prefix `candidate` with an object id and an initial `hex_len`, check if it only matches a single /// object within the entire object database and increment its `hex_len` by one until it is unambiguous. /// Return `Ok(None)` if no object with that prefix exists. - pub fn disambiguate_prefix( - &self, - mut candidate: disambiguate::Candidate, - ) -> Result, disambiguate::Error> { + pub fn disambiguate_prefix(&self, mut candidate: disambiguate::Candidate) -> Result, Exn> { let max_hex_len = candidate.id().kind().len_in_hex(); if candidate.hex_len() == max_hex_len { return Ok(self.exists(candidate.id()).then(|| candidate.to_prefix())); @@ -201,7 +119,7 @@ where &self, prefix: gix_hash::Prefix, mut candidates: Option<&mut HashSet>, - ) -> Result, lookup::Error> { + ) -> Result, Exn> { let mut candidate: Option = None; loop { let snapshot = self.snapshot.borrow(); @@ -215,7 +133,9 @@ where for lodb in snapshot.loose_dbs.iter() { // needed as it's the equivalent of a reborrow. - let lookup_result = lodb.lookup_prefix(prefix, candidates.as_deref_mut())?; + let lookup_result = lodb + .lookup_prefix(prefix, candidates.as_deref_mut()) + .or_raise_erased(|| message("Could not enumerate loose objects for prefix lookup"))?; if candidates.is_none() && !check_candidate(lookup_result, &mut candidate) { return Ok(Some(Err(()))); } diff --git a/gix-odb/src/store_impls/dynamic/structure.rs b/gix-odb/src/store_impls/dynamic/structure.rs index ec8e18b1f42..29cbbb6d974 100644 --- a/gix-odb/src/store_impls/dynamic/structure.rs +++ b/gix-odb/src/store_impls/dynamic/structure.rs @@ -1,6 +1,8 @@ use std::path::PathBuf; -use crate::{Store, store::load_index, types::IndexAndPacks}; +use gix_error::Exn; + +use crate::{Store, types::IndexAndPacks}; /// A record of a structural element of an object database. #[derive(Debug, Clone, PartialEq, Eq)] @@ -51,7 +53,7 @@ impl Store { /// Note that it may change as we collect information due to the highly volatile nature of the /// implementation. The likelihood of actual changes is low though as these still depend on something /// changing on disk and somebody reading at the same time. - pub fn structure(&self) -> Result, load_index::Error> { + pub fn structure(&self) -> Result, Exn> { let _span = gix_features::trace::detail!("gix_odb::Store::structure()"); let index = self.index.load(); if !index.is_initialized() { @@ -100,7 +102,7 @@ impl Store { /// This list might be empty if there are no alternates. /// /// Read more about alternates in the documentation of the [`resolve`][crate::alternate::resolve()] function. - pub fn alternate_db_paths(&self) -> Result, load_index::Error> { + pub fn alternate_db_paths(&self) -> Result, Exn> { let index = self.index.load(); if !index.is_initialized() { self.consolidate_with_disk_state(true, false /*load one new index*/, self.loose_compression)?; diff --git a/gix-odb/src/store_impls/dynamic/verify.rs b/gix-odb/src/store_impls/dynamic/verify.rs index b29f40b8181..e32f8cf1d30 100644 --- a/gix-odb/src/store_impls/dynamic/verify.rs +++ b/gix-odb/src/store_impls/dynamic/verify.rs @@ -4,6 +4,8 @@ use std::{ time::Instant, }; +use gix_error::{ErrorExt, Exn, Metadata, ResultExt, RetryableError, message}; + use gix_features::progress::{DynNestedProgress, MessageLevel, Progress}; use crate::{ @@ -21,64 +23,6 @@ pub mod integrity { /// Options for use in [`Store::verify_integrity()`][crate::Store::verify_integrity()]. pub type Options = pack::index::verify::integrity::Options; - /// Returned by [`Store::verify_integrity()`][crate::Store::verify_integrity()]. - #[derive(Debug)] - #[allow(missing_docs)] - pub enum Error { - MultiIndexIntegrity(gix_error::Error), - IndexIntegrity(gix_error::Error), - IndexOpen(gix_error::Error), - LooseObjectStoreIntegrity(crate::loose::verify::integrity::Error), - MultiIndexOpen(gix_error::Error), - PackOpen(gix_error::Error), - InitializeODB(crate::store::load_index::Error), - NeedsRetryDueToChangeOnDisk, - } - - impl std::fmt::Display for Error { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Error::MultiIndexIntegrity(err) => std::fmt::Display::fmt(err, f), - Error::IndexIntegrity(err) => std::fmt::Display::fmt(err, f), - Error::IndexOpen(err) => std::fmt::Display::fmt(err, f), - Error::LooseObjectStoreIntegrity(err) => std::fmt::Display::fmt(err, f), - Error::MultiIndexOpen(err) => std::fmt::Display::fmt(err, f), - Error::PackOpen(err) => std::fmt::Display::fmt(err, f), - Error::InitializeODB(err) => std::fmt::Display::fmt(err, f), - Error::NeedsRetryDueToChangeOnDisk => { - f.write_str("The disk on state changed while performing the operation, and we observed the change.") - } - } - } - } - - impl std::error::Error for Error { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - Error::MultiIndexIntegrity(err) => Some(err), - Error::IndexIntegrity(err) => Some(err), - Error::IndexOpen(err) => Some(err), - Error::LooseObjectStoreIntegrity(err) => Some(err), - Error::MultiIndexOpen(err) => Some(err), - Error::PackOpen(err) => Some(err), - Error::InitializeODB(err) => Some(err), - Error::NeedsRetryDueToChangeOnDisk => Some(&*crate::RETRYABLE), - } - } - } - - impl From for Error { - fn from(err: crate::loose::verify::integrity::Error) -> Self { - Error::LooseObjectStoreIntegrity(err) - } - } - - impl From for Error { - fn from(err: crate::store::load_index::Error) -> Self { - Error::InitializeODB(err) - } - } - #[derive(Debug, PartialEq, Eq, Hash, Ord, PartialOrd, Clone)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] /// Integrity information about loose object databases @@ -145,16 +89,18 @@ impl super::Store { /// /// Note that this will not force loading all indices or packs permanently, as we will only use the momentarily loaded disk state. /// This does, however, include all alternates. + /// Verification failures include metadata `path` (native index, pack, or loose object directory path). pub fn verify_integrity( &self, progress: &mut dyn DynNestedProgress, should_interrupt: &AtomicBool, options: integrity::Options, - ) -> Result + ) -> Result where C: pack::cache::DecodeEntry, F: Fn() -> C + Send + Clone, { + let changed = || RetryableError::new(message("The object database changed during verification")).raise_erased(); let _span = gix_features::trace::coarse!("gix_odb:Store::verify_integrity()"); let mut index = self.index.load(); if !index.is_initialized() { @@ -182,10 +128,10 @@ impl super::Store { for slot_index in &index.slot_indices { let slot = &self.files[*slot_index]; if slot.generation.load(Ordering::SeqCst) != index.generation { - return Err(integrity::Error::NeedsRetryDueToChangeOnDisk); + return Err(changed()); } let files = slot.files.load(); - let files = Option::as_ref(&files).ok_or(integrity::Error::NeedsRetryDueToChangeOnDisk)?; + let files = Option::as_ref(&files).ok_or_else(changed)?; let start = Instant::now(); let (mut child_progress, num_objects, index_path) = match files { @@ -194,8 +140,12 @@ impl super::Store { let index = match bundle.index.loaded() { Some(index) => index.deref(), None => { - index = pack::index::File::at(bundle.index.path(), self.object_hash) - .map_err(|err| integrity::Error::IndexOpen(err.into_error()))?; + index = pack::index::File::at(bundle.index.path(), self.object_hash).or_raise_erased( + || { + Metadata::new("Could not open pack index for verification") + .with("path", bundle.index.path()) + }, + )?; &index } }; @@ -204,7 +154,10 @@ impl super::Store { Some(pack) => pack.deref(), None => { pack = pack::data::File::at(bundle.data.path(), self.object_hash) - .map_err(|err| integrity::Error::PackOpen(err.into_error()))? + .or_raise_erased(|| { + Metadata::new("Could not open pack for verification") + .with("path", bundle.data.path()) + })? .with_alloc_limit_bytes(self.alloc_limit_bytes); &pack } @@ -222,7 +175,9 @@ impl super::Store { &mut child_progress, should_interrupt, ) - .map_err(|err| integrity::Error::IndexIntegrity(err.into_error()))?; + .or_raise_erased(|| { + Metadata::new("Could not verify pack index").with("path", index.path()) + })?; statistics.push(IndexStatistics { path: bundle.index.path().to_owned(), statistics: SingleOrMultiStatistics::Single( @@ -239,7 +194,10 @@ impl super::Store { Some(index) => index.deref(), None => { index = pack::multi_index::File::at(bundle.multi_index.path(), self.alloc_limit_bytes) - .map_err(|err| integrity::Error::MultiIndexOpen(err.into_error()))?; + .or_raise_erased(|| { + Metadata::new("Could not open multi-pack index for verification") + .with("path", bundle.multi_index.path()) + })?; &index } }; @@ -249,7 +207,9 @@ impl super::Store { ); let outcome = index .verify_integrity(&mut child_progress, should_interrupt, options.clone()) - .map_err(|err| integrity::Error::MultiIndexIntegrity(err.into_error()))?; + .or_raise_erased(|| { + Metadata::new("Could not verify multi-pack index").with("path", index.path()) + })?; let index_dir = bundle.multi_index.path().parent().expect("file in a directory"); statistics.push(IndexStatistics { @@ -284,26 +244,27 @@ impl super::Store { gix_features::progress::count("loose object stores"), ); let mut loose_object_stores = Vec::new(); - gix_features::trace::detail!("verify loose ODBs").into_scope( - || -> Result<_, crate::loose::verify::integrity::Error> { - for loose_db in &*index.loose_dbs { - let out = loose_db - .verify_integrity( - &mut progress.add_child_with_id( - loose_db.path().display().to_string(), - integrity::ProgressId::VerifyLooseObjectDbPath.into(), - ), - should_interrupt, - ) - .map(|statistics| integrity::LooseObjectStatistics { - path: loose_db.path().to_owned(), - statistics, - })?; - loose_object_stores.push(out); - } - Ok(()) - }, - )?; + gix_features::trace::detail!("verify loose ODBs").into_scope(|| -> Result<_, Exn> { + for loose_db in &*index.loose_dbs { + let out = loose_db + .verify_integrity( + &mut progress.add_child_with_id( + loose_db.path().display().to_string(), + integrity::ProgressId::VerifyLooseObjectDbPath.into(), + ), + should_interrupt, + ) + .or_raise_erased(|| { + Metadata::new("Could not verify loose object database").with("path", loose_db.path()) + }) + .map(|statistics| integrity::LooseObjectStatistics { + path: loose_db.path().to_owned(), + statistics, + })?; + loose_object_stores.push(out); + } + Ok(()) + })?; Ok(integrity::Outcome { loose_object_stores, diff --git a/gix-odb/src/store_impls/dynamic/write.rs b/gix-odb/src/store_impls/dynamic/write.rs index 1a6559f43de..2eaff876411 100644 --- a/gix-odb/src/store_impls/dynamic/write.rs +++ b/gix-odb/src/store_impls/dynamic/write.rs @@ -1,99 +1,45 @@ use std::{io::Read, ops::Deref}; -use gix_error::ResultExt; +use gix_error::Exn; use gix_hash::ObjectId; use gix_object::Kind; use crate::store; -mod error { - use crate::{loose, store}; - - /// The error returned by the [dynamic Store's][crate::Store] [`Write`](gix_object::Write) implementation. - #[derive(Debug)] - #[allow(missing_docs)] - pub enum Error { - LoadIndex(store::load_index::Error), - LooseWrite(loose::write::Error), - Io(std::io::Error), - } - - impl std::fmt::Display for Error { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Error::LoadIndex(err) => std::fmt::Display::fmt(err, f), - Error::LooseWrite(err) => std::fmt::Display::fmt(err, f), - Error::Io(err) => std::fmt::Display::fmt(err, f), - } - } - } - - impl std::error::Error for Error { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - Error::LoadIndex(err) => Some(err), - Error::LooseWrite(err) => Some(err), - Error::Io(err) => Some(err), - } - } - } - - impl From for Error { - fn from(err: store::load_index::Error) -> Self { - Error::LoadIndex(err) - } - } - - impl From for Error { - fn from(err: loose::write::Error) -> Self { - Error::LooseWrite(err) - } - } - - impl From for Error { - fn from(err: std::io::Error) -> Self { - Error::Io(err) - } - } -} -pub use error::Error; - use crate::store_impls::dynamic; impl gix_object::Write for store::Handle where S: Deref + Clone, { - fn write_stream(&self, kind: Kind, size: u64, from: &mut dyn Read) -> Result { + fn write_stream(&self, kind: Kind, size: u64, from: &mut dyn Read) -> Result { let mut snapshot = self.snapshot.borrow_mut(); - Ok(match snapshot.loose_dbs.first() { - Some(ldb) => ldb.write_stream(kind, size, from)?, + match snapshot.loose_dbs.first() { + Some(ldb) => ldb.write_stream(kind, size, from), None => { let new_snapshot = self .store - .load_one_index(self.index_ctx(snapshot.marker)) - .or_erased()? + .load_one_index(self.index_ctx(snapshot.marker))? .expect("there is always at least one ODB, and this code runs only once for initialization"); *snapshot = new_snapshot; - snapshot.loose_dbs[0].write_stream(kind, size, from)? + snapshot.loose_dbs[0].write_stream(kind, size, from) } - }) + } } - fn write_buf_with_known_id(&self, kind: Kind, from: &[u8], id: ObjectId) -> Result { + fn write_buf_with_known_id(&self, kind: Kind, from: &[u8], id: ObjectId) -> Result { let mut snapshot = self.snapshot.borrow_mut(); - Ok(match snapshot.loose_dbs.first() { - Some(ldb) => ldb.write_buf_with_known_id(kind, from, id)?, + match snapshot.loose_dbs.first() { + Some(ldb) => ldb.write_buf_with_known_id(kind, from, id), None => { let new_snapshot = self .store - .load_one_index(self.index_ctx(snapshot.marker)) - .or_erased()? + .load_one_index(self.index_ctx(snapshot.marker))? .expect("there is always at least one ODB, and this code runs only once for initialization"); *snapshot = new_snapshot; - snapshot.loose_dbs[0].write_buf_with_known_id(kind, from, id)? + snapshot.loose_dbs[0].write_buf_with_known_id(kind, from, id) } - }) + } } fn write_stream_with_known_id( @@ -102,19 +48,18 @@ where size: u64, from: &mut dyn Read, id: ObjectId, - ) -> Result { + ) -> Result { let mut snapshot = self.snapshot.borrow_mut(); - Ok(match snapshot.loose_dbs.first() { - Some(ldb) => ldb.write_stream_with_known_id(kind, size, from, id)?, + match snapshot.loose_dbs.first() { + Some(ldb) => ldb.write_stream_with_known_id(kind, size, from, id), None => { let new_snapshot = self .store - .load_one_index(self.index_ctx(snapshot.marker)) - .or_erased()? + .load_one_index(self.index_ctx(snapshot.marker))? .expect("there is always at least one ODB, and this code runs only once for initialization"); *snapshot = new_snapshot; - snapshot.loose_dbs[0].write_stream_with_known_id(kind, size, from, id)? + snapshot.loose_dbs[0].write_stream_with_known_id(kind, size, from, id) } - }) + } } } diff --git a/gix-odb/src/store_impls/loose/find.rs b/gix-odb/src/store_impls/loose/find.rs index a053e7f4030..0afb06bbb29 100644 --- a/gix-odb/src/store_impls/loose/find.rs +++ b/gix-odb/src/store_impls/loose/find.rs @@ -1,74 +1,11 @@ -use std::{cmp::Ordering, collections::HashSet, io, path::PathBuf}; +use std::{cmp::Ordering, collections::HashSet}; -use gix_error::{CorruptionError, ErrorExt, ResourceExhaustionError, ResourceExhaustionKind}; +use gix_error::{CorruptionError, ErrorExt, Exn, Metadata, ResourceExhaustionError, ResourceExhaustionKind, ResultExt}; use crate::store_impls::loose::{HEADER_MAX_SIZE, Store, hash_path}; -/// Returned by [`Store::try_find()`] -#[derive(Debug)] -#[allow(missing_docs)] -pub enum Error { - DecompressFile { - source: gix_error::Error, - path: PathBuf, - }, - SizeMismatch { - actual: u64, - expected: u64, - path: PathBuf, - }, - Decode(gix_error::Error), - OutOfMemory { - size: u64, - source: gix_error::Error, - }, - Io { - source: std::io::Error, - action: &'static str, - path: PathBuf, - }, -} - -impl std::fmt::Display for Error { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Error::DecompressFile { path, .. } => { - write!(f, "decompression of loose object at '{}' failed", path.display()) - } - Error::SizeMismatch { actual, expected, path } => write!( - f, - "file at '{}' showed invalid size of inflated data, expected {expected}, got {actual}", - path.display() - ), - Error::Decode(err) => std::fmt::Display::fmt(err, f), - Error::OutOfMemory { size, .. } => write!(f, "Cannot store {size} bytes in memory"), - Error::Io { action, path, .. } => write!(f, "Could not {action} data at '{}'", path.display()), - } - } -} - -impl std::error::Error for Error { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - Error::DecompressFile { source, .. } => Some(source), - Error::Decode(err) => Some(err), - Error::Io { source, .. } => Some(source), - Error::OutOfMemory { source, .. } => Some(source), - Error::SizeMismatch { .. } => Some(&crate::CORRUPTION), - } - } -} - -impl From> for Error { - fn from(err: gix_error::Exn) -> Self { - Error::Decode(err.into_error()) - } -} - /// Object lookup impl Store { - const OPEN_OR_MAP_ACTION: &'static str = "open or map"; - /// Returns true if the given id is contained in our repository. pub fn contains(&self, id: &gix_hash::oid) -> bool { debug_assert_eq!(self.object_hash, id.kind()); @@ -143,44 +80,42 @@ impl Store { /// /// Returns `Err` if there was an error locating or reading the object. Returns `Ok` if /// there was no such object. - pub fn try_find<'a>( - &self, - id: &gix_hash::oid, - out: &'a mut Vec, - ) -> Result>, Error> { + /// Failures include metadata `path` (native path), the loose object file. + pub fn try_find<'a>(&self, id: &gix_hash::oid, out: &'a mut Vec) -> Result>, Exn> { debug_assert_eq!(self.object_hash, id.kind()); self.find_inner(id, out) + .or_raise_erased(|| Metadata::new("Could not read loose object").with("path", self.object_path(id))) } /// Return only the decompressed size of the object and its kind without fully reading it into memory as tuple of `(size, kind)`. /// Returns `None` if `id` does not exist in the database. - pub fn try_header(&self, id: &gix_hash::oid) -> Result, Error> { + /// Failures include metadata `path` (native path), the loose object file. + pub fn try_header(&self, id: &gix_hash::oid) -> Result, Exn> { let path = hash_path(id, self.path.clone()); - let map = match self.map_loose_object(&path)? { + let context = || Metadata::new("Could not read loose object header").with("path", path.as_path()); + let map = match self.map_loose_object(&path).or_raise_erased(context)? { Some(map) => map, None => return Ok(None), }; let mut header = [0_u8; HEADER_MAX_SIZE]; let mut inflate = gix_zlib::Inflate::default(); - let (status, _consumed_in, consumed_out) = - inflate.once(&map, &mut header).map_err(|e| Error::DecompressFile { - source: e.into_error(), - path: path.to_owned(), - })?; + let (status, _consumed_in, consumed_out) = inflate.once(&map, &mut header).or_raise_erased(context)?; if status == gix_zlib::Status::BufError { - return Err(Error::DecompressFile { - source: gix_error::Error::from_error(CorruptionError::new( - "The zlib status indicated an error, status was 'BufError'", - )), - path, - }); + return Err( + CorruptionError::new("The zlib status indicated an error, status was 'BufError'") + .and_raise(context()) + .erased(), + ); } - let (kind, size, _header_size) = gix_object::decode::loose_header(&header[..consumed_out])?; + let (kind, size, _header_size) = + gix_object::decode::loose_header(&header[..consumed_out]).or_raise_erased(context)?; Ok(Some((size, kind))) } - fn find_inner<'a>(&self, id: &gix_hash::oid, out: &'a mut Vec) -> Result>, Error> { + /// Decode and allocation failures retain metadata `size` (requested bytes) or `actual` and `expected` + /// (inflated bytes); allocation limits also report `limit`. All counts are unsigned. + fn find_inner<'a>(&self, id: &gix_hash::oid, out: &'a mut Vec) -> Result>, Exn> { let path = hash_path(id, self.path.clone()); let map = match self.map_loose_object(&path)? { Some(map) => map, @@ -189,54 +124,38 @@ impl Store { let mut header = [0_u8; HEADER_MAX_SIZE]; let mut inflate = gix_zlib::Inflate::default(); - let (status, consumed_in, consumed_out) = - inflate.once(&map, &mut header).map_err(|e| Error::DecompressFile { - source: e.into_error(), - path: path.to_owned(), - })?; + let (status, consumed_in, consumed_out) = inflate.once(&map, &mut header).or_erased()?; if status == gix_zlib::Status::BufError { - return Err(Error::DecompressFile { - source: gix_error::Error::from_error(CorruptionError::new( - "The zlib status indicated an error, status was 'BufError'", - )), - path, - }); + return Err( + CorruptionError::new("The zlib status indicated an error, status was 'BufError'").raise_erased(), + ); } - let (kind, size, header_size) = gix_object::decode::loose_header(&header[..consumed_out])?; + let (kind, size, header_size) = gix_object::decode::loose_header(&header[..consumed_out]).or_erased()?; self.ensure_in_alloc_limit(size)?; - let size_usize = usize::try_from(size).map_err(|err| Error::OutOfMemory { - size, - source: err - .and_raise(ResourceExhaustionError::new( + let allocation = || allocation_error(size); + let size_usize = usize::try_from(size) + .or_raise(|| { + ResourceExhaustionError::new( ResourceExhaustionKind::AllocationFailure, "The object size cannot be represented in memory", - )) - .into_error(), - })?; - let decompressed_body_prefix_len = consumed_out.checked_sub(header_size).ok_or(Error::SizeMismatch { - actual: consumed_out as u64, - expected: header_size as u64, - path: path.clone(), - })?; + ) + }) + .or_raise_erased(allocation)?; + let decompressed_body_prefix_len = consumed_out + .checked_sub(header_size) + .ok_or_else(|| size_mismatch(consumed_out as u64, header_size as u64).erased())?; if decompressed_body_prefix_len > size_usize || (status == gix_zlib::Status::StreamEnd && decompressed_body_prefix_len != size_usize) { - return Err(Error::SizeMismatch { - expected: size, - actual: decompressed_body_prefix_len as u64, - path, - }); + return Err(size_mismatch(decompressed_body_prefix_len as u64, size).erased()); } // If the first inflate already reached the end of the stream, the fixed-size `header` buffer // contains the complete decompressed object, so we can skip a second streaming inflate pass. out.clear(); - out.try_reserve(size_usize).map_err(|err| Error::OutOfMemory { - size, - source: gix_error::Error::from_error(err), - })?; + out.try_reserve(size_usize).or_raise_erased(allocation)?; if status == gix_zlib::Status::StreamEnd { out.extend_from_slice(&header[header_size..consumed_out]); } else { @@ -249,18 +168,14 @@ impl Store { &mut inflate.state, &mut out[decompressed_body_prefix_len..], ) - .map_err(|e| Error::Io { - source: e, - action: "inflate", - path: path.to_owned(), - })?; + .or_erased()?; if num_decompressed_bytes as u64 + decompressed_body_prefix_len as u64 != size { - return Err(Error::SizeMismatch { - expected: size, - actual: num_decompressed_bytes as u64 + decompressed_body_prefix_len as u64, - path, - }); + return Err(size_mismatch( + num_decompressed_bytes as u64 + decompressed_body_prefix_len as u64, + size, + ) + .erased()); } } Ok(Some(gix_object::Data { @@ -270,38 +185,27 @@ impl Store { })) } - fn ensure_in_alloc_limit(&self, size: u64) -> Result<(), Error> { - if self.alloc_limit_bytes.is_some_and(|limit| size > limit as u64) { - return Err(Error::OutOfMemory { - size, - source: gix_error::Error::from_error(ResourceExhaustionError::new( - ResourceExhaustionKind::AllocationLimit, - "The object exceeds the configured allocation limit", - )), - }); + /// Allocation-limit failures include metadata `size` and `limit` (unsigned byte counts). + fn ensure_in_alloc_limit(&self, size: u64) -> Result<(), Exn> { + if let Some(limit) = self.alloc_limit_bytes.filter(|limit| size > *limit as u64) { + return Err(ResourceExhaustionError::new( + ResourceExhaustionKind::AllocationLimit, + "The object exceeds the configured allocation limit", + ) + .and_raise(allocation_error(size).with("limit", limit)) + .erased()); } Ok(()) } - fn map_loose_object(&self, path: &std::path::Path) -> Result, Error> { + fn map_loose_object(&self, path: &std::path::Path) -> Result, Exn> { let map = match mmap::read_only(path) { Ok(map) => map, Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None), - Err(err) => { - return Err(Error::Io { - action: Self::OPEN_OR_MAP_ACTION, - source: err, - path: path.to_owned(), - }); - } + Err(err) => return Err(err.raise_erased()), }; - if map.is_empty() { - return Err(Error::Io { - source: io::Error::other("empty loose object file"), - action: Self::OPEN_OR_MAP_ACTION, - path: path.to_owned(), - }); + return Err(CorruptionError::new("Empty loose object file").raise_erased()); } Ok(Some(map)) } @@ -319,3 +223,17 @@ mod mmap { } } } + +/// Metadata `size` (unsigned bytes) identifies the requested loose-object allocation. +fn allocation_error(size: u64) -> Metadata { + Metadata::new("Cannot store loose object in memory").with("size", size) +} + +/// Report invalid inflation sizes in metadata `actual` and `expected` (unsigned byte counts). +fn size_mismatch(actual: u64, expected: u64) -> Exn { + CorruptionError::new("Invalid size of inflated loose object").and_raise( + Metadata::new("Loose object size mismatch") + .with("actual", actual) + .with("expected", expected), + ) +} diff --git a/gix-odb/src/store_impls/loose/mod.rs b/gix-odb/src/store_impls/loose/mod.rs index 28fba9b9812..e98d1300648 100644 --- a/gix-odb/src/store_impls/loose/mod.rs +++ b/gix-odb/src/store_impls/loose/mod.rs @@ -92,8 +92,7 @@ fn hash_path(id: &gix_hash::oid, mut root: PathBuf) -> PathBuf { root } -/// -pub mod find; +mod find; /// pub mod iter; /// @@ -105,5 +104,4 @@ pub struct Iter { hash_hex_len: usize, } -/// -pub mod write; +mod write; diff --git a/gix-odb/src/store_impls/loose/verify.rs b/gix-odb/src/store_impls/loose/verify.rs index 04a190d08c5..3adb13ccef7 100644 --- a/gix-odb/src/store_impls/loose/verify.rs +++ b/gix-odb/src/store_impls/loose/verify.rs @@ -3,70 +3,14 @@ use std::{ time::Instant, }; +use gix_error::{ErrorExt, Exn, Metadata, ResultExt, RetryableError, message}; + use gix_features::progress::{Count, DynNestedProgress, Progress}; use crate::loose::Store; /// pub mod integrity { - /// The error returned by [`verify_integrity()`][super::Store::verify_integrity()]. - #[derive(Debug)] - #[allow(missing_docs)] - pub enum Error { - Iteration(crate::loose::iter::Error), - ObjectLookup { - source: crate::loose::find::Error, - id: gix_hash::ObjectId, - }, - ObjectDecode { - source: gix_error::ValidationError, - kind: gix_object::Kind, - id: gix_hash::ObjectId, - }, - ObjectHasher { - source: gix_error::CorruptionError, - kind: gix_object::Kind, - expected: gix_hash::ObjectId, - }, - ObjectEncodeMismatch { - source: gix_error::CorruptionError, - kind: gix_object::Kind, - }, - Retry, - Interrupted, - } - - impl std::fmt::Display for Error { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Error::Iteration(_) => f.write_str("Could not enumerate loose objects"), - Error::ObjectLookup { id, .. } => write!(f, "Could not read loose object {id}"), - Error::ObjectDecode { kind, id, .. } => write!(f, "{kind} object {id} could not be decoded"), - Error::ObjectHasher { kind, expected, .. } => { - write!(f, "{kind} object {expected} could not be hashed") - } - Error::ObjectEncodeMismatch { kind, .. } => { - write!(f, "{kind} object wasn't re-encoded without change") - } - Error::Retry => f.write_str("Objects were deleted during iteration - try again"), - Error::Interrupted => f.write_str("Interrupted"), - } - } - } - - impl std::error::Error for Error { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - Error::Iteration(err) => Some(err), - Error::ObjectLookup { source, .. } => Some(source), - Error::ObjectDecode { source, .. } => Some(source), - Error::ObjectHasher { source, .. } => Some(source), - Error::ObjectEncodeMismatch { source, .. } => Some(source), - Error::Retry | Error::Interrupted => Some(&*crate::RETRYABLE), - } - } - } - /// The outcome returned by [`verify_integrity()`][super::Store::verify_integrity()]. #[derive(Debug, PartialEq, Eq, Hash, Ord, PartialOrd, Clone)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] @@ -95,11 +39,12 @@ pub mod integrity { impl Store { /// Check all loose objects for their integrity checking their hash matches the actual data and by decoding them fully. + /// Verification failures include metadata `object_id` (hex text), plus `kind` (object kind text) after lookup. pub fn verify_integrity( &self, progress: &mut dyn DynNestedProgress, should_interrupt: &AtomicBool, - ) -> Result { + ) -> Result { let mut buf = Vec::new(); let mut num_objects = 0; @@ -107,32 +52,29 @@ impl Store { let mut progress = progress.add_child_with_id("Validating".into(), integrity::ProgressId::LooseObjects.into()); progress.init(None, gix_features::progress::count("loose objects")); for id in self.iter() { - let id = id.map_err(integrity::Error::Iteration)?; + let id = id.or_raise_erased(|| message("Could not enumerate loose objects"))?; let object = self .try_find(&id, &mut buf) - .map_err(|source| integrity::Error::ObjectLookup { source, id })? - .ok_or(integrity::Error::Retry)?; - gix_object::compute_hash(self.object_hash, object.kind, object.data) - .map_err(|source| integrity::Error::ObjectHasher { - source, - kind: object.kind, - expected: id, + .or_raise_erased(|| { + Metadata::new("Could not read loose object during verification").with("object_id", id.to_string()) })? - .verify(&id) - .map_err(|err| integrity::Error::ObjectEncodeMismatch { - source: err, - kind: object.kind, + .ok_or_else(|| { + RetryableError::new(message("Objects were deleted during iteration - try again")).raise_erased() })?; - object.decode().map_err(|err| integrity::Error::ObjectDecode { - source: err, - kind: object.kind, - id, - })?; + let context = || { + Metadata::new("Could not verify loose object") + .with("object_id", id.to_string()) + .with("kind", object.kind.to_string()) + }; + gix_object::compute_hash(self.object_hash, object.kind, object.data) + .and_then(|actual| actual.verify(&id)) + .or_raise_erased(context)?; + object.decode().or_raise_erased(context)?; progress.inc(); num_objects += 1; if should_interrupt.load(Ordering::SeqCst) { - return Err(integrity::Error::Interrupted); + return Err(RetryableError::new(std::io::Error::from(std::io::ErrorKind::Interrupted)).raise_erased()); } } progress.show_throughput(start); diff --git a/gix-odb/src/store_impls/loose/write.rs b/gix-odb/src/store_impls/loose/write.rs index e17c7580fb5..7af04ff0c74 100644 --- a/gix-odb/src/store_impls/loose/write.rs +++ b/gix-odb/src/store_impls/loose/write.rs @@ -1,6 +1,10 @@ -use std::{fs, io, io::Write, path::PathBuf}; +use std::{ + fs, io, + io::Write, + path::{Path, PathBuf}, +}; -use gix_error::ResultExt; +use gix_error::{ErrorExt, Exn, Metadata, ResultExt}; use gix_object::WriteTo; use gix_zlib::stream::deflate; use tempfile::NamedTempFile; @@ -8,180 +12,83 @@ use tempfile::NamedTempFile; use super::Store; use crate::store_impls::loose; -/// Returned by the [`gix_object::Write`] trait implementation of [`Store`] -#[derive(Debug)] -#[allow(missing_docs)] -pub enum Error { - Io { - source: std::io::Error, - message: &'static str, - path: PathBuf, - }, - IoRaw(io::Error), - Persist { - source: tempfile::PersistError, - target: PathBuf, - }, -} - -impl std::fmt::Display for Error { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Error::Io { message, path, .. } => write!(f, "Could not {message} '{}'", path.display()), - Error::IoRaw(_) => f.write_str("An IO error occurred while writing an object"), - Error::Persist { target, .. } => { - write!( - f, - "Could not turn temporary file into persisted file at '{}'", - target.display() - ) - } - } - } -} - -impl std::error::Error for Error { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - Error::Io { source, .. } => Some(source), - Error::IoRaw(err) => Some(err), - Error::Persist { source, .. } => Some(source), - } - } -} - -impl From for Error { - fn from(err: io::Error) -> Self { - Error::IoRaw(err) - } -} - impl gix_object::Write for Store { - fn write(&self, object: &dyn WriteTo) -> Result { - let mut to = self.dest().or_erased()?; + /// Write failures include metadata `path` (native path), the temporary object directory. + fn write(&self, object: &dyn WriteTo) -> Result { + let mut to = self.dest()?; to.write_all(&object.loose_header()) - .map_err(|err| Error::Io { - source: err, - message: "write header to tempfile in", - path: self.path.to_owned(), - }) - .or_erased()?; + .or_raise_erased(|| write_header_error(&self.path))?; object .write_to(&mut to) - .map_err(|err| Error::Io { - source: err, - message: "stream all data into tempfile in", - path: self.path.to_owned(), - }) - .or_erased()?; + .or_raise_erased(|| stream_data_error(&self.path))?; to.flush().or_erased()?; - self.finalize_object(to).or_erased() + self.finalize_object(to) } /// Write the given buffer in `from` to disk in one syscall at best. /// /// This will cost at least 4 IO operations. - fn write_buf(&self, kind: gix_object::Kind, from: &[u8]) -> Result { - let mut to = self.dest().or_erased()?; + /// Write failures include metadata `path` (native path), the temporary object directory. + fn write_buf(&self, kind: gix_object::Kind, from: &[u8]) -> Result { + let mut to = self.dest()?; to.write_all(&gix_object::encode::loose_header(kind, from.len() as u64)) - .map_err(|err| Error::Io { - source: err, - message: "write header to tempfile in", - path: self.path.to_owned(), - }) - .or_erased()?; + .or_raise_erased(|| write_header_error(&self.path))?; - to.write_all(from) - .map_err(|err| Error::Io { - source: err, - message: "stream all data into tempfile in", - path: self.path.to_owned(), - }) - .or_erased()?; + to.write_all(from).or_raise_erased(|| stream_data_error(&self.path))?; to.flush().or_erased()?; - self.finalize_object(to).or_erased() + self.finalize_object(to) } + /// Write failures include metadata `path` (native path), the temporary object directory. fn write_buf_with_known_id( &self, kind: gix_object::Kind, from: &[u8], id: gix_hash::ObjectId, - ) -> Result { - let mut to = self.compressed_tempfile().or_erased()?; + ) -> Result { + let mut to = self.compressed_tempfile()?; to.write_all(&gix_object::encode::loose_header(kind, from.len() as u64)) - .map_err(|err| Error::Io { - source: err, - message: "write header to tempfile in", - path: self.path.to_owned(), - }) - .or_erased()?; + .or_raise_erased(|| write_header_error(&self.path))?; - to.write_all(from) - .map_err(|err| Error::Io { - source: err, - message: "stream all data into tempfile in", - path: self.path.to_owned(), - }) - .or_erased()?; + to.write_all(from).or_raise_erased(|| stream_data_error(&self.path))?; to.flush().or_erased()?; - self.finalize_object_at(id, to).or_erased() + self.finalize_object_at(id, to) } /// Write the given stream in `from` to disk with at least one syscall. /// /// This will cost at least 4 IO operations. + /// Write failures include metadata `path` (native path), the temporary object directory. fn write_stream( &self, kind: gix_object::Kind, size: u64, mut from: &mut dyn io::Read, - ) -> Result { - let mut to = self.dest().or_erased()?; + ) -> Result { + let mut to = self.dest()?; to.write_all(&gix_object::encode::loose_header(kind, size)) - .map_err(|err| Error::Io { - source: err, - message: "write header to tempfile in", - path: self.path.to_owned(), - }) - .or_erased()?; + .or_raise_erased(|| write_header_error(&self.path))?; - io::copy(&mut from, &mut to) - .map_err(|err| Error::Io { - source: err, - message: "stream all data into tempfile in", - path: self.path.to_owned(), - }) - .or_erased()?; + io::copy(&mut from, &mut to).or_raise_erased(|| stream_data_error(&self.path))?; to.flush().or_erased()?; - self.finalize_object(to).or_erased() + self.finalize_object(to) } + /// Write failures include metadata `path` (native path), the temporary object directory. fn write_stream_with_known_id( &self, kind: gix_object::Kind, size: u64, mut from: &mut dyn io::Read, id: gix_hash::ObjectId, - ) -> Result { - let mut to = self.compressed_tempfile().or_erased()?; + ) -> Result { + let mut to = self.compressed_tempfile()?; to.write_all(&gix_object::encode::loose_header(kind, size)) - .map_err(|err| Error::Io { - source: err, - message: "write header to tempfile in", - path: self.path.to_owned(), - }) - .or_erased()?; + .or_raise_erased(|| write_header_error(&self.path))?; - io::copy(&mut from, &mut to) - .map_err(|err| Error::Io { - source: err, - message: "stream all data into tempfile in", - path: self.path.to_owned(), - }) - .or_erased()?; + io::copy(&mut from, &mut to).or_raise_erased(|| stream_data_error(&self.path))?; to.flush().or_erased()?; - self.finalize_object_at(id, to).or_erased() + self.finalize_object_at(id, to) } } @@ -199,12 +106,13 @@ impl Store { impl Store { /// A compressed tempfile, with auto-hashing. - fn dest(&self) -> Result, Error> { + fn dest(&self) -> Result, Exn> { Ok(gix_hash::io::Write::new(self.compressed_tempfile()?, self.object_hash)) } /// A compressed tempfile, without hasher. - fn compressed_tempfile(&self) -> Result { + /// Creation failures include metadata `path` (native path), the temporary object directory. + fn compressed_tempfile(&self) -> Result { #[cfg_attr(not(unix), allow(unused_mut))] let mut builder = tempfile::Builder::new(); #[cfg(unix)] @@ -214,32 +122,29 @@ impl Store { builder.permissions(perms); } Ok(deflate::Write::new( - builder.tempfile_in(&self.path).map_err(|err| Error::Io { - source: err, - message: "create named temp file in", - path: self.path.to_owned(), + builder.tempfile_in(&self.path).or_raise_erased(|| { + Metadata::new("Could not create temporary object file").with("path", self.path.as_path()) })?, self.compression, )) } + /// Hashing failures include metadata `path` (native path), the temporary object directory. fn finalize_object( &self, gix_hash::io::Write { hash, inner: file }: gix_hash::io::Write, - ) -> Result { - let id = hash.try_finalize().map_err(|err| Error::Io { - source: std::io::Error::other(gix_hash::io::from_hasher(err).into_error()), - message: "hash tempfile in", - path: self.path.to_owned(), - })?; + ) -> Result { + let id = hash + .try_finalize() + .map_err(gix_hash::io::from_hasher) + .or_raise_erased(|| { + Metadata::new("Could not hash temporary object file").with("path", self.path.as_path()) + })?; self.finalize_object_at(id, file) } - fn finalize_object_at( - &self, - id: gix_hash::ObjectId, - file: CompressedTempfile, - ) -> Result { + /// Publication failures include metadata `path` (native path), the object directory or destination file. + fn finalize_object_at(&self, id: gix_hash::ObjectId, file: CompressedTempfile) -> Result { let object_path = loose::hash_path(&id, self.path.clone()); let object_dir = object_path .parent() @@ -247,7 +152,11 @@ impl Store { if let Err(err) = fs::create_dir(object_dir) { match err.kind() { io::ErrorKind::AlreadyExists => {} - _ => return Err(err.into()), + _ => { + return Err(err + .and_raise(Metadata::new("Could not create object directory").with("path", object_dir)) + .erased()); + } } } let file = file.into_inner(); @@ -262,10 +171,17 @@ impl Store { return Ok(id); } } - res.map_err(|err| Error::Persist { - source: err, - target: object_path, - })?; + res.or_raise_erased(|| Metadata::new("Could not persist loose object").with("path", object_path))?; Ok(id) } } + +/// Metadata `path` (native path) identifies the temporary object directory for a header-write failure. +fn write_header_error(path: &Path) -> Metadata { + Metadata::new("Could not write loose object header").with("path", path) +} + +/// Metadata `path` (native path) identifies the temporary object directory for a data-streaming failure. +fn stream_data_error(path: &Path) -> Metadata { + Metadata::new("Could not stream loose object data").with("path", path) +} diff --git a/gix-odb/tests/odb/alternate.rs b/gix-odb/tests/odb/alternate.rs index 26c89c98617..ac84e412cb0 100644 --- a/gix-odb/tests/odb/alternate.rs +++ b/gix-odb/tests/odb/alternate.rs @@ -98,18 +98,19 @@ fn circular_alternates_are_detected_with_relative_paths() -> crate::Result { None, )?; - match alternate::resolve(from, &std::env::current_dir()?) { - Err(alternate::Error::Cycle(chain)) => { - assert_eq!( - chain - .into_iter() - .map(|p| p.file_name().expect("non-root").to_str().expect("utf8").to_owned()) - .collect::>(), - vec!["a", "b"] - ); - } - res => unreachable!("should be a specific kind of error: {:?}", res), - } + let err = alternate::resolve(from, &std::env::current_dir()?) + .expect_err("the relative alternate points back to its ancestor") + .into_error(); + assert!(err.is_corrupted(), "alternate cycles are malformed configuration"); + assert_eq!( + err.downcast_any_ref::() + .expect("the discovered cycle is retained") + .paths + .iter() + .map(|p| p.file_name().expect("non-root").to_str().expect("utf8")) + .collect::>(), + ["a", "b"] + ); Ok(()) } @@ -153,17 +154,17 @@ fn cycles_between_alternates_also_listed_by_the_root_are_detected() -> crate::Re None, )?; - match alternate::resolve(from, &std::env::current_dir()?) { - Err(alternate::Error::Cycle(chain)) => assert_eq!( - chain - .into_iter() - .map(|p| p.file_name().expect("non-root").to_str().expect("utf8").to_owned()) - .collect::>(), - vec!["a", "b"], - "the traversed A -> B -> A edges form a cycle even when A and B were first seen as siblings" - ), - res => unreachable!("should be a cycle error: {res:?}"), - } + let err = alternate::resolve(from, &std::env::current_dir()?).expect_err("the alternate chain is cyclic"); + assert_eq!( + err.downcast_any_ref::() + .expect("the discovered cycle is retained") + .paths + .iter() + .map(|p| p.file_name().expect("non-root").to_str().expect("utf8")) + .collect::>(), + ["a", "b"], + "the traversed A -> B -> A edges form a cycle even when A and B were first seen as siblings" + ); Ok(()) } diff --git a/gix-odb/tests/odb/error.rs b/gix-odb/tests/odb/error.rs index 56306774bf7..9688fcf1013 100644 --- a/gix-odb/tests/odb/error.rs +++ b/gix-odb/tests/odb/error.rs @@ -1,144 +1,167 @@ -use gix_error::{CorruptionError, Error, ErrorExt, message}; -use gix_odb::{alternate, loose, store}; +use std::io; + +use gix_error::{NotFoundError, RetryableError, Value}; +use gix_object::{Kind, Write}; #[test] -fn io_classification_survives_custom_wrappers() { - use std::io::ErrorKind; +fn write_failures_preserve_custom_sources_and_metadata() -> crate::Result { + #[derive(Debug)] + struct ReadFailure(RetryableError); + + impl std::fmt::Display for ReadFailure { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("custom reader failed") + } + } + + impl std::error::Error for ReadFailure { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + Some(&self.0) + } + } + + struct FailingRead; + impl io::Read for FailingRead { + fn read(&mut self, _buf: &mut [u8]) -> io::Result { + Err(io::Error::other(ReadFailure(RetryableError::new(NotFoundError::new( + "temporarily missing input", + ))))) + } + } - for kind in [ - ErrorKind::NotFound, - ErrorKind::Interrupted, - ErrorKind::OutOfMemory, - ErrorKind::PermissionDenied, - ] { - let io = || std::io::Error::from(kind); - let load_index = || store::load_index::Error::Io(io()); + let dir = gix_testtools::tempfile::tempdir()?; + let loose = gix_odb::loose::Store::at(dir.path(), gix_testtools::object_hash()); + let dynamic = crate::odb_at(dir.path())?; + for store in [&loose as &dyn Write, &dynamic] { for err in [ - load_index().raise_erased(), - store::load_index::Error::Alternate(alternate::Error::Io(io())).raise_erased(), - alternate::Error::from(io().raise_erased()).raise_erased(), - store::find::Error::LoadIndex(load_index()).raise_erased(), - store::find::Error::LoadPack(io()).raise_erased(), - store::write::Error::Io(io()).raise_erased(), - store::write::Error::LoadIndex(load_index()).raise_erased(), - store::write::Error::LooseWrite(loose::write::Error::IoRaw(io())).raise_erased(), - store::prefix::disambiguate::Error::Lookup(store::prefix::lookup::Error::LoadIndex(load_index())) - .raise_erased(), - store::verify::integrity::Error::InitializeODB(load_index()).raise_erased(), + store + .write_stream(Kind::Blob, 1, &mut FailingRead) + .expect_err("the input reader failed"), + store + .write_stream_with_known_id( + Kind::Blob, + 1, + &mut FailingRead, + gix_testtools::object_hash().empty_blob(), + ) + .expect_err("a known object ID does not mask input failures"), ] { - assert_eq!( - err.can_retry(), - gix_error::can_retry(&io()), - "custom wrappers retain the retry policy for {kind:?}" - ); - assert_eq!( - err.is_not_found(), - kind == ErrorKind::NotFound, - "custom wrappers retain missing-resource failures" + assert!( + err.is_not_found() && err.can_retry(), + "custom sources retain both predicates" ); let err = err.into_error(); - assert_eq!( - err.can_retry(), - gix_error::can_retry(&io()), - "conversion retains the retry policy for {kind:?}" + assert!( + err.is_not_found() && err.can_retry(), + "conversion preserves both predicates" ); - assert_eq!( - err.is_not_found(), - kind == ErrorKind::NotFound, - "conversion retains missing-resource failures" + assert!( + err.downcast_any_ref::().is_some(), + "the custom cause remains accessible" ); + let context = err.metadata().next().expect("the write adds context"); assert_eq!( - err.is_resource_exhausted(), - kind == ErrorKind::OutOfMemory, - "conversion retains allocation failures" + context.message, "Could not stream loose object data", + "streaming failures share the same operation context" ); assert_eq!( - err.downcast_any_ref::() - .expect("retain the immediate I/O cause") - .kind(), - kind + context.values, + maplit::btreemap! { "path".into() => Value::from(dir.path()) }, + "the schema retains only the native object directory" ); } } + Ok(()) } #[test] -fn custom_wrappers_retain_classified_errors_and_branches() { - let corrupt = || Error::from_error(CorruptionError::new("malformed object")); - for err in [ - loose::find::Error::Decode(corrupt()).raise_erased(), - store::find::Error::EntryType(Error::from_error(CorruptionError::new("invalid pack entry type"))) - .raise_erased(), - store::verify::integrity::Error::MultiIndexIntegrity(corrupt()).raise_erased(), - store::verify::integrity::Error::IndexIntegrity(corrupt()).raise_erased(), - store::verify::integrity::Error::IndexOpen(corrupt()).raise_erased(), - store::verify::integrity::Error::MultiIndexOpen(corrupt()).raise_erased(), - store::verify::integrity::Error::PackOpen( - message("pack failed") - .raise() - .chain(message("unrelated cause")) - .chain(CorruptionError::new("malformed pack")) - .into_error(), - ) - .raise_erased(), - ] { - assert!( - err.is_corrupted(), - "custom wrappers retain classified causes, including branches" - ); - assert!(err.into_error().is_corrupted(), "conversion retains classified causes"); - } -} +fn delta_lookup_distinguishes_missing_bases_from_recursion_limits() -> crate::Result { + use std::io::Write; -#[test] -fn custom_leaf_errors_expose_their_classification() { - let blob_id = gix_hash::ObjectId::empty_blob(gix_hash::Kind::Sha1); - let err = store::find::Error::DeltaBaseMissing { - base_id: blob_id, - id: blob_id, - } - .raise(); - assert!(err.is_not_found(), "a missing delta base is a missing object"); - assert!( - err.into_error().is_not_found(), - "conversion retains the missing-object classification" - ); + use gix_object::Find; + use gix_pack::data; - for err in [ - loose::find::Error::SizeMismatch { - actual: 0, - expected: 1, - path: "object".into(), - } - .raise_erased(), - alternate::Error::Cycle(vec!["objects".into()]).raise_erased(), - ] { - assert!( - err.is_corrupted(), - "inconsistent object sizes and alternate cycles are corruption" - ); - assert!(err.into_error().is_corrupted()); + let dir = gix_testtools::tempfile::tempdir()?; + let pack_dir = dir.path().join("pack"); + std::fs::create_dir(&pack_dir)?; + let object_hash = gix_testtools::object_hash(); + let blob_id = object_hash.empty_blob(); + let base_id = object_hash.null(); + + // A single ref-delta would produce an empty blob, but its base is absent. + let mut pack = data::header::encode(data::Version::V2, 1).to_vec(); + data::entry::Header::RefDelta { base_id }.write_to(2, &mut pack)?; + let mut compressed = gix_zlib::stream::deflate::Write::new(Vec::new(), gix_zlib::Compression::DEFAULT); + compressed.write_all(&[0, 0])?; + compressed.flush()?; + pack.extend(compressed.into_inner()); + let mut hasher = gix_hash::hasher(object_hash); + hasher.update(&pack); + let pack_id = hasher.try_finalize()?; + pack.extend_from_slice(pack_id.as_slice()); + + // A v1 index names the unresolved object without needing to decode it first. + let mut index = Vec::new(); + for first_byte in 0..=255 { + index.extend_from_slice(&u32::from(first_byte >= blob_id.first_byte()).to_be_bytes()); } - let err = alternate::Error::Parse(alternate::parse::Error::PathConversion { - path: vec![0xff], - source: Error::from_error(gix_error::ValidationError::new("invalid path encoding")), - }) - .raise(); - assert!(err.is_validation(), "unrepresentable alternate paths are invalid input"); - assert!(err.into_error().is_validation()); + index.extend_from_slice(&(data::header::SIZE as u32).to_be_bytes()); + index.extend_from_slice(blob_id.as_slice()); + index.extend_from_slice(pack_id.as_slice()); + let mut hasher = gix_hash::hasher(object_hash); + hasher.update(&index); + index.extend_from_slice(hasher.try_finalize()?.as_slice()); + std::fs::write(pack_dir.join(format!("pack-{pack_id}.pack")), pack)?; + std::fs::write(pack_dir.join(format!("pack-{pack_id}.idx")), index)?; - for err in [ - loose::verify::integrity::Error::Retry.raise_erased(), - loose::verify::integrity::Error::Interrupted.raise_erased(), - store::verify::integrity::Error::NeedsRetryDueToChangeOnDisk.raise_erased(), - store::verify::integrity::Error::LooseObjectStoreIntegrity(loose::verify::integrity::Error::Retry) - .raise_erased(), - ] { - assert!( - err.can_retry(), - "verification can retry after interruption or concurrent changes" - ); - assert!(err.is_retryable(), "custom errors explicitly advertise retryability"); - assert!(err.into_error().can_retry(), "conversion retains retryability"); + for max_depth in [8, 0] { + let mut store = crate::odb_at(dir.path())?; + store.max_recursion_depth = max_depth; + for err in [ + store + .try_find(&blob_id, &mut Vec::new()) + .expect_err("the delta cannot be resolved"), + gix_odb::Header::try_header(&store, &blob_id).expect_err("the base kind is unavailable"), + ] { + let err = err.into_error(); + assert_eq!( + err.is_not_found(), + max_depth != 0, + "hitting a limit does not establish absence" + ); + assert!(!err.is_corrupted() && !err.can_retry()); + let context = err.metadata().next().expect("the failed lookup carries context"); + assert_eq!( + context.message, "Could not resolve delta base object", + "object and header lookup share the same delta-resolution context" + ); + assert_eq!( + context.values, + maplit::btreemap! { + "base_id".into() => Value::from(base_id.to_string()), + "object_id".into() => Value::from(blob_id.to_string()), + }, + "delta-resolution metadata retains both object IDs as hex text" + ); + if max_depth == 0 { + let limit = err + .metadata() + .find(|context| context.values.contains_key("max_depth")) + .expect("the nested recursion failure retains its limit"); + assert_eq!( + limit.message, "Reached recursion limit while resolving ref delta bases", + "both lookup paths report the same recursion-limit context" + ); + assert_eq!( + limit.values, + maplit::btreemap! { + "max_depth".into() => Value::U64(0), + "object_id".into() => Value::from(blob_id.to_string()), + }, + "recursion-limit metadata retains the original object and unsigned limit" + ); + } + } } + Ok(()) } diff --git a/gix-odb/tests/odb/store/loose.rs b/gix-odb/tests/odb/store/loose.rs index 2075deb961b..bb8a7fc7fff 100644 --- a/gix-odb/tests/odb/store/loose.rs +++ b/gix-odb/tests/odb/store/loose.rs @@ -59,6 +59,14 @@ fn verify_integrity() { .verify_integrity(&mut progress::Discard, &AtomicBool::new(false)) .unwrap(); assert_eq!(outcome.num_objects, 7); + let err = db + .verify_integrity(&mut progress::Discard, &AtomicBool::new(true)) + .expect_err("verification was interrupted") + .into_error(); + assert!( + err.is_retryable() && err.can_retry(), + "interrupted verification can be retried" + ); } mod write { @@ -287,8 +295,8 @@ mod lookup_prefix { } mod find { + use gix_error::Value; use gix_object::{BlobRef, CommitRef, Kind, TagRef, TreeRef, bstr::ByteSlice, tree::EntryKind}; - use gix_odb::loose; use crate::{ hex_to_id, hex_to_id_for_hash, @@ -321,7 +329,7 @@ mod find { ); assert!(db.try_find(&id, &mut buf).is_err(), "it must not panic"); assert!(db.try_header(&id).is_err(), "it must not panic"); - let err = gix_error::Error::from_error( + let err = gix_error::Error::from( db.verify_integrity( &mut gix_features::progress::Discard, &std::sync::atomic::AtomicBool::new(false), @@ -330,7 +338,7 @@ mod find { ); assert!(!err.can_retry(), "corrupt objects do not become valid when retried"); assert!( - err.downcast_any_ref::().is_some(), + err.downcast_any_ref::().is_some(), "verification preserves the original lookup error" ); @@ -359,9 +367,15 @@ mod find { .try_find(&blob_id, &mut buf) .expect_err("the completed stream contains no body despite its advertised size"); assert!( - matches!(&err, loose::find::Error::SizeMismatch { .. }), + err.is_corrupted(), "a completed object with an oversized header is corrupt: {err}" ); + let sizes = err + .metadata() + .find(|context| context.values.contains_key("expected")) + .expect("the mismatch records both sizes"); + assert_eq!(sizes.values["expected"], Value::from(size)); + assert_eq!(sizes.values["actual"], Value::U64(0)); assert_eq!(buf.capacity(), 0, "invalid sizes must be rejected before allocation"); } Ok(()) @@ -479,8 +493,23 @@ cjHJZXWmV4CcRfmLsXzU8s2cR9A0DBvOxhPD1TlKC2JhBFXigjuL9U4Rbq9tdegB let err = db .try_find(&id, &mut buf) .expect_err("the object exceeds the configured allocation limit"); - assert!(matches!(&err, loose::find::Error::OutOfMemory { size: 56915, .. })); - let err = gix_error::Error::from_error(err); + let allocation = err + .metadata() + .find(|context| context.values.contains_key("size")) + .expect("the allocation limit retains its byte counts"); + assert_eq!( + allocation.message, "Cannot store loose object in memory", + "allocation-limit failures retain the shared allocation context" + ); + assert_eq!( + allocation.values, + maplit::btreemap! { + "limit".into() => Value::U64(1), + "size".into() => Value::U64(56915), + }, + "allocation limits add the unsigned byte limit to the requested size" + ); + let err = gix_error::Error::from(err); assert_eq!( err.classify() .map(|classification| classification.class()) diff --git a/gix-ref/src/store/file/find.rs b/gix-ref/src/store/file/find.rs index b1c186b5b16..4bbc947d91b 100644 --- a/gix-ref/src/store/file/find.rs +++ b/gix-ref/src/store/file/find.rs @@ -185,7 +185,7 @@ impl file::Store { Err(err) if err.kind() == io::ErrorKind::NotADirectory => return Ok(None), Err(err) => { return Err(err - .and_raise(Metadata::new("Could not read reference").with("path", self.reference_path(full_name))) + .and_raise(read_reference_error(self.reference_path(full_name))) .erased()); } }; @@ -416,6 +416,11 @@ impl file::Store { } } +/// Metadata `path` (native path) identifies the reference file that could not be read. +pub(super) fn read_reference_error(path: impl Into) -> Metadata { + Metadata::new("Could not read reference").with("path", path.into()) +} + /// A reference lookup found no matching name, including a missing symbolic referent. #[derive(Debug)] pub struct NotFound { diff --git a/gix-ref/src/store/file/loose/reflog.rs b/gix-ref/src/store/file/loose/reflog.rs index c2d697902e2..6494bd3c160 100644 --- a/gix-ref/src/store/file/loose/reflog.rs +++ b/gix-ref/src/store/file/loose/reflog.rs @@ -39,7 +39,7 @@ impl file::Store { .try_into() .or_raise_erased(|| message("The reflog name or path is not a valid ref name"))?; self.reflog_iter_rev_inner(name, buf) - .or_raise_erased(|| Metadata::new("Could not read reflog").with("path", self.reflog_path(name))) + .or_raise_erased(|| read_reflog_error(self.reflog_path(name))) } pub(crate) fn reflog_iter_rev_inner<'b>( @@ -77,7 +77,7 @@ impl file::Store { .try_into() .or_raise_erased(|| message("The reflog name or path is not a valid ref name"))?; self.reflog_iter_inner(name, buf) - .or_raise_erased(|| Metadata::new("Could not read reflog").with("path", self.reflog_path(name))) + .or_raise_erased(|| read_reflog_error(self.reflog_path(name))) } pub(crate) fn reflog_iter_inner<'b>( @@ -110,6 +110,11 @@ impl file::Store { } } +/// Metadata `path` (native path) identifies the resolved reflog path that could not be read. +fn read_reflog_error(path: PathBuf) -> Metadata { + Metadata::new("Could not read reflog").with("path", path) +} + /// pub mod create_or_update { use std::{ @@ -163,16 +168,9 @@ pub mod create_or_update { gix_tempfile::remove_dir::empty_depth_first(log_path.clone()) .and_then(|_| options.open(&log_path)) .map(Some) - .or_raise_erased(|| { - Metadata::new("Could not open reflog for appending") - .with("path", log_path.as_path()) - })? + .or_raise_erased(|| open_reflog_for_appending_error(log_path.as_path()))? } else { - return Err(err - .and_raise( - Metadata::new("Could not open reflog for appending").with("path", log_path), - ) - .erased()); + return Err(err.and_raise(open_reflog_for_appending_error(log_path)).erased()); } } }; @@ -225,6 +223,11 @@ pub mod create_or_update { } } + /// Metadata `path` (native path) identifies the reflog that could not be opened for appending. + fn open_reflog_for_appending_error(path: impl Into) -> Metadata { + Metadata::new("Could not open reflog for appending").with("path", path.into()) + } + /// A reflog entry requires a committer identity which wasn't provided. #[derive(Debug)] pub struct MissingCommitter; diff --git a/gix-ref/src/store/file/loose/reflog/create_or_update/tests.rs b/gix-ref/src/store/file/loose/reflog/create_or_update/tests.rs index 8b2fd14e36c..22819df853f 100644 --- a/gix-ref/src/store/file/loose/reflog/create_or_update/tests.rs +++ b/gix-ref/src/store/file/loose/reflog/create_or_update/tests.rs @@ -183,6 +183,48 @@ fn missing_reflog_creates_it_even_if_similarly_named_empty_dir_exists_and_append Ok(()) } +#[test] +fn non_empty_reflog_directory_preserves_open_error_context() -> Result { + let (_keep, store) = empty_store(WriteReflog::Normal)?; + let name: &FullNameRef = "refs/heads/main".try_into()?; + let path = store.reflog_path(name); + std::fs::create_dir_all(&path)?; + std::fs::write(path.join("keep"), b"not an empty directory")?; + + let err = store + .reflog_create_or_append( + name, + None, + &gix_testtools::object_hash().null(), + None, + b"update".as_bstr(), + false, + ) + .expect_err("a non-empty directory cannot be replaced with a reflog") + .into_error(); + let details = err.metadata().next().expect("reflog open context"); + assert_eq!( + details.message, "Could not open reflog for appending", + "directory recovery retains the open message" + ); + assert_eq!(details.values.len(), 1, "open context contains only the path"); + assert_eq!( + details.values["path"], + gix_error::Value::Path(path), + "the reflog path remains a native path" + ); + assert!( + err.downcast_any_ref::().is_some(), + "the directory removal error remains accessible" + ); + assert!(!err.is_not_found(), "the conflicting directory exists"); + assert!( + !err.is_corrupted(), + "a directory collision does not imply corrupt reflog contents" + ); + Ok(()) +} + #[test] fn reflog_write_normalizes_committer_name_and_email_like_git() -> Result { let (_keep, store) = empty_store(WriteReflog::Always)?; diff --git a/gix-ref/src/store/file/overlay_iter.rs b/gix-ref/src/store/file/overlay_iter.rs index 9f06edf7fbd..6e0d1ed8c09 100644 --- a/gix-ref/src/store/file/overlay_iter.rs +++ b/gix-ref/src/store/file/overlay_iter.rs @@ -81,7 +81,7 @@ impl<'p> LooseThenPacked<'p, '_> { buf.clear(); f.read_to_end(buf) }) - .or_raise_erased(|| Metadata::new("Could not read reference").with("path", refpath.as_path()))?; + .or_raise_erased(|| file::find::read_reference_error(refpath.as_path()))?; loose::Reference::try_from_path(name, buf, self.object_hash) .or_raise_erased(|| { let relative_path = refpath diff --git a/gix-ref/src/store/packed/transaction.rs b/gix-ref/src/store/packed/transaction.rs index 1a714df4ca3..fed58bfd9a0 100644 --- a/gix-ref/src/store/packed/transaction.rs +++ b/gix-ref/src/store/packed/transaction.rs @@ -3,7 +3,7 @@ use std::{borrow::Cow, fmt::Formatter, io::Write}; use gix_error::{ErrorExt, Exn, Metadata, NotFoundError, ResultExt, message}; use crate::{ - Namespace, Target, file, + FullNameRef, Namespace, Target, file, store_impl::{packed, packed::Edit}, transaction::{Change, RefEdit}, }; @@ -109,11 +109,9 @@ impl packed::Transaction { { let mut next_id = new; edit.peeled = loop { - let data = objects.try_find(&next_id, &mut buf).or_raise_erased(|| { - Metadata::new("Could not peel packed reference") - .with("object_id", next_id.to_string()) - .with("reference", edit.inner.name.as_bstr()) - })?; + let data = objects + .try_find(&next_id, &mut buf) + .or_raise_erased(|| peel_reference_error(&next_id, edit.inner.name.as_ref()))?; match data { Some(gix_object::Data { kind: gix_object::Kind::Tag, @@ -123,22 +121,14 @@ impl packed::Transaction { next_id = gix_object::TagRefIter::from_bytes(data, hash_kind) .target_id() .or_raise(|| gix_error::message!("Couldn't get target object id from tag {next_id}")) - .or_raise_erased(|| { - Metadata::new("Could not peel packed reference") - .with("object_id", next_id.to_string()) - .with("reference", edit.inner.name.as_bstr()) - })?; + .or_raise_erased(|| peel_reference_error(&next_id, edit.inner.name.as_ref()))?; } Some(_) => { break if next_id == new { None } else { Some(next_id) }; } None => { return Err(NotFoundError::new("Object could not be found") - .and_raise( - Metadata::new("Could not peel packed reference") - .with("object_id", next_id.to_string()) - .with("reference", edit.inner.name.as_bstr()), - ) + .and_raise(peel_reference_error(&next_id, edit.inner.name.as_ref())) .erased()); } } @@ -245,6 +235,13 @@ impl packed::Transaction { } } +/// Metadata `object_id` (hex text) and `reference` (name bytes) identify the object and packed reference being peeled. +fn peel_reference_error(object_id: &gix_hash::oid, reference: &FullNameRef) -> Metadata { + Metadata::new("Could not peel packed reference") + .with("object_id", object_id.to_string()) + .with("reference", reference.as_bstr()) +} + fn write_packed_ref(out: &mut dyn std::io::Write, pref: packed::Reference<'_>) -> std::io::Result<()> { write!(out, "{} ", pref.target)?; out.write_all(pref.name.as_bstr())?; diff --git a/gix-ref/tests/refs/error.rs b/gix-ref/tests/refs/error.rs index 90dcde5bbea..b5e2df9a821 100644 --- a/gix-ref/tests/refs/error.rs +++ b/gix-ref/tests/refs/error.rs @@ -32,6 +32,54 @@ fn missing_references_remain_classified_after_erasure() -> crate::Result { #[test] fn peeling_missing_targets_is_classified() -> crate::Result { + use gix_lock::acquire::Fail; + use gix_ref::{file::transaction::PackedRefs, transaction::RefEdit}; + + let (_keep, packed_store) = crate::file::transaction::prepare_and_commit::empty_store()?; + let blob_id = crate::fixture_hash_kind().empty_blob(); + let name = "refs/tags/missing"; + let packed_err = packed_store + .transaction() + .packed_refs(PackedRefs::DeletionsAndNonSymbolicUpdates(Box::new( + gix_object::find::Never, + ))) + .prepare( + [RefEdit::update( + name.try_into()?, + gix_ref::Target::Object(blob_id), + PreviousValue::Any, + "", + )], + Fail::Immediately, + Fail::Immediately, + ) + .expect_err("the object to pack is absent") + .into_error(); + let details = packed_err.metadata().next().expect("packed peeling context"); + assert_eq!( + details.message, "Could not peel packed reference", + "the peeling message is unchanged" + ); + assert_eq!( + details.values.len(), + 2, + "peeling context contains only the object and reference" + ); + assert_eq!( + details.values["object_id"], + gix_error::Value::String(blob_id.to_string()), + "object ids remain hex text" + ); + assert_eq!( + details.values["reference"], + gix_error::Value::Bytes(name.into()), + "reference names remain bytes" + ); + assert!( + packed_err.downcast_any_ref::().is_some(), + "the original missing-object error remains accessible" + ); + let store = crate::file::store_at("make_ref_repository.sh")?; let mut symbolic = store.find("HEAD")?; symbolic.target = gix_ref::Target::Symbolic("refs/heads/missing".try_into()?); @@ -53,6 +101,7 @@ fn peeling_missing_targets_is_classified() -> crate::Result { .peel_to_id(&store, &gix_object::find::Never) .expect_err("the object database is empty"), ), + packed_err, ] { assert!( err.is_not_found(), diff --git a/gix-ref/tests/refs/file/store/find.rs b/gix-ref/tests/refs/file/store/find.rs index 4b61329b433..15723514d4f 100644 --- a/gix-ref/tests/refs/file/store/find.rs +++ b/gix-ref/tests/refs/file/store/find.rs @@ -194,6 +194,36 @@ mod loose { assert!(reference.is_none(), "{}", reason); } } + + let mut store = store; + store.prohibit_windows_device_names = true; + let name = "refs/heads/CON"; + let err = store + .try_find_loose(name) + .expect_err("reserved device names cannot be read when prohibited") + .into_error(); + let details = err.metadata().next().expect("reference read context"); + assert_eq!( + details.message, "Could not read reference", + "the read message is unchanged" + ); + assert_eq!(details.values.len(), 1, "read context contains only the path"); + assert_eq!( + details.values["path"], + gix_error::Value::Path(store.git_dir().join(name)), + "the resolved reference path remains a native path" + ); + assert_eq!( + err.downcast_any_ref::() + .expect("the original read error is retained") + .kind(), + std::io::ErrorKind::Other, + "rejecting a device name retains the original I/O error kind" + ); + assert!( + !err.is_corrupted(), + "a prohibited path does not imply corrupt ref contents" + ); Ok(()) } diff --git a/gix-ref/tests/refs/file/store/reflog.rs b/gix-ref/tests/refs/file/store/reflog.rs index eb1a256715b..f4ab07b8d8c 100644 --- a/gix-ref/tests/refs/file/store/reflog.rs +++ b/gix-ref/tests/refs/file/store/reflog.rs @@ -12,6 +12,43 @@ fn store() -> crate::Result { mod iter_and_iter_rev { use crate::file::store::reflog::store; + #[cfg(unix)] + #[test] + fn read_failures_preserve_context() -> crate::Result { + let store = store()?; + let name = "refs/heads/main/child"; + let mut buf = vec![0; 256]; + for err in [ + store.reflog_iter(name, &mut buf).err().expect("main is a file"), + store.reflog_iter_rev(name, &mut buf).err().expect("main is a file"), + ] { + let err = err.into_error(); + let details = err.metadata().next().expect("reflog read context"); + assert_eq!( + details.message, "Could not read reflog", + "both directions retain the read message" + ); + assert_eq!(details.values.len(), 1, "read context contains only the path"); + assert_eq!( + details.values["path"], + gix_error::Value::Path(store.git_dir().join("logs").join(name)), + "both directions retain the resolved reflog path as a native path" + ); + assert_eq!( + err.downcast_any_ref::() + .expect("the original open error is retained") + .kind(), + std::io::ErrorKind::NotADirectory, + "a file at a path prefix retains its I/O error kind on Unix" + ); + assert!( + !err.is_corrupted(), + "a path collision does not imply corrupt reflog contents" + ); + } + Ok(()) + } + #[test] fn non_existing_and_directory_returns_none() -> crate::Result { let store = store()?; diff --git a/gix/src/id.rs b/gix/src/id.rs index 6ae41c38a7b..b709fcbd2f3 100644 --- a/gix/src/id.rs +++ b/gix/src/id.rs @@ -1,7 +1,5 @@ //! #![allow(clippy::empty_docs)] -use gix_error::ResultExt; - use std::ops::Deref; use gix_hash::{ObjectId, oid}; @@ -44,28 +42,19 @@ impl<'repo> Id<'repo> { /// Turn this object id into a shortened id with a length in hex as configured by `core.abbrev`. pub fn shorten(&self) -> Result { - let hex_len = self - .repo - .config - .hex_len - .map_or_else( - || self.repo.objects.packed_object_count().map(calculate_auto_hex_len), - Ok, - ) - .or_erased()?; + let hex_len = self.repo.config.hex_len.map_or_else( + || self.repo.objects.packed_object_count().map(calculate_auto_hex_len), + Ok, + )?; let prefix = gix_odb::store::prefix::disambiguate::Candidate::new(self.inner, hex_len) .expect("BUG: internal hex-len must always be valid"); - self.repo - .objects - .disambiguate_prefix(prefix) - .or_erased()? - .ok_or_else(|| { - gix_error::Error::from_error(gix_error::message!( - "Id could not be shortened as the object with id {} could not be found", - self.inner - )) - }) + self.repo.objects.disambiguate_prefix(prefix)?.ok_or_else(|| { + gix_error::Error::from_error(gix_error::message!( + "Id could not be shortened as the object with id {} could not be found", + self.inner + )) + }) } /// Turn this object id into a shortened id with a length in hex as configured by `core.abbrev`, or default diff --git a/gix/src/remote/connection/fetch/receive_pack.rs b/gix/src/remote/connection/fetch/receive_pack.rs index 8e35b705bc6..a86302951c2 100644 --- a/gix/src/remote/connection/fetch/receive_pack.rs +++ b/gix/src/remote/connection/fetch/receive_pack.rs @@ -163,7 +163,7 @@ where }; let cache = graph_repo.commit_graph_if_enabled().ok().flatten(); let mut graph = graph_repo.revision_graph(cache.as_ref()); - let alternates = repo.objects.store_ref().alternate_db_paths().or_erased()?; + let alternates = repo.objects.store_ref().alternate_db_paths()?; let mut negotiate = Negotiate { objects: &graph_repo.objects, refs: &graph_repo.refs, diff --git a/gix/src/revision/spec/parse/delegate/revision.rs b/gix/src/revision/spec/parse/delegate/revision.rs index d4ed67942ce..5a4a231f745 100644 --- a/gix/src/revision/spec/parse/delegate/revision.rs +++ b/gix/src/revision/spec/parse/delegate/revision.rs @@ -41,8 +41,7 @@ impl delegate::Revision for Delegate<'_> { Ok(Some(Err(()))) } else { self.repo.objects.lookup_prefix(prefix, candidates.as_mut()) - } - .or_erased()?; + }?; match ok { None => Err(message!("An object prefixed {prefix} could not be found").raise_erased()), diff --git a/gix/tests/gix/remote/fetch.rs b/gix/tests/gix/remote/fetch.rs index b7b4dd7e8a2..f8e4ff2e0e2 100644 --- a/gix/tests/gix/remote/fetch.rs +++ b/gix/tests/gix/remote/fetch.rs @@ -155,7 +155,7 @@ mod blocking_and_async_io { if gix_testtools::run_in_isolated_process()? { return Ok(()); } - use gix::{config::tree::User, interrupt::IS_INTERRUPTED}; + use gix::{config::tree::User, error::Value, interrupt::IS_INTERRUPTED}; use gix_odb::store::init::Slots; use gix_testtools::tempfile; fn create_empty_commit(repo: &gix::Repository) -> anyhow::Result<()> { @@ -244,9 +244,14 @@ mod blocking_and_async_io { { Ok(out) => check_fetch_output(&local_repo, out, expected_object_count)?, Err(err) => { - assert!( - err.to_string() - .starts_with("The slotmap turned out to be too small with ") + let context = err.metadata().next().expect("index capacity details"); + assert_eq!( + (&context.values["current"], &context.values["needed"]), + ( + &Value::from(max_packs), + &Value::from(round_to_create_pack + 1 - usize::from(max_packs)) + ), + "each fetch adds one pack beyond the configured capacity" ); // But opening a new repo will always be able to read all objects // as it dynamically sizes the otherwise static slotmap. diff --git a/gix/tests/gix/revision/spec/from_bytes/ambiguous.rs b/gix/tests/gix/revision/spec/from_bytes/ambiguous.rs index 218080c562e..022167be449 100644 --- a/gix/tests/gix/revision/spec/from_bytes/ambiguous.rs +++ b/gix/tests/gix/revision/spec/from_bytes/ambiguous.rs @@ -1,8 +1,8 @@ use super::repo; use crate::{ revision::spec::from_bytes::{ - parse_spec, parse_spec_better_than_baseline, parse_spec_no_baseline, parse_spec_no_baseline_opts, - parse_spec_opts, rev_parse, + normalize_repo_path, parse_spec, parse_spec_better_than_baseline, parse_spec_no_baseline, + parse_spec_no_baseline_opts, parse_spec_opts, rev_parse, }, util::hex_to_id_sha1_only, }; @@ -31,8 +31,11 @@ fn prefix() { { let repo = repo("blob.bad").unwrap(); assert_eq!( - parse_spec("bad0", &repo).unwrap_err().probable_cause().to_string(), - "Short id bad0 is ambiguous. Candidates are:\n\tbad0853 lookup error: An error occurred while obtaining an object from the loose object store\n\tbad0bd4 lookup error: An error occurred while obtaining an object from the loose object store", + normalize_repo_path( + &parse_spec("bad0", &repo).unwrap_err().probable_cause().to_string(), + &repo + ), + "Short id bad0 is ambiguous. Candidates are:\n\tbad0853 lookup error: Could not read loose object, \"path\"=\"$GIT_DIR/objects/ba/d0853730d9d114ac789f0ce89039d224bf66c9\"\n\tbad0bd4 lookup error: Could not read loose object, \"path\"=\"$GIT_DIR/objects/ba/d0bd4672dee1b4d3b8088534ed5a0362bc8d59\"", "git provides a much worse hint messages and fails to provide information for both bad objects" ); } diff --git a/gix/tests/gix/revision/spec/from_bytes/mod.rs b/gix/tests/gix/revision/spec/from_bytes/mod.rs index ff9a8089378..2216f29bf84 100644 --- a/gix/tests/gix/revision/spec/from_bytes/mod.rs +++ b/gix/tests/gix/revision/spec/from_bytes/mod.rs @@ -238,14 +238,10 @@ fn bad_objects_are_valid_until_they_are_actually_read_from_the_odb() { ("Unknown object kind", Some(b"bad".as_slice())), "Now we enforce the object to exist and be valid, as ultimately it wants to match with a certain type" ); - insta::assert_snapshot!(format!("{err:#?}").replace('\\', "/"), @r#" + insta::assert_snapshot!(normalize_repo_path(&format!("{err:#?}"), &repo), @r#" delegate.peel_until(ValidObject) failed: "{object}" | - └─ An error occurred while obtaining an object from the loose object store - | - └─ ValidationError { message: "The object header contained an unknown object kind.", input: None } - | - └─ ValidationError { message: "Unknown object kind", input: Some("bad") } + └─ Could not read loose object, "path"="$GIT_DIR/objects/e3/2851d29feb48953c6f40b2e06d630a3c49608a" | └─ The object header contained an unknown object kind. | @@ -260,29 +256,10 @@ fn bad_objects_are_valid_until_they_are_actually_read_from_the_odb() { Spec::from_id(hex_to_id_sha1_only("cafea31147e840161a1860c50af999917ae1536b").attach(&repo)) ); let err = parse_spec("cafea^{object}", &repo).unwrap_err(); - let actual = { - let mut actual = format!("{err:#?}") - .replace("\\\\", "/") - .replace('\\', "/") - .replace("windows", "unix"); - let marker = "make_rev_spec_parse_repos/"; - let mut search_from = 0; - while let Some(start) = actual[search_from..].find(marker) { - let start = search_from + start + marker.len(); - let Some(end) = actual[start..].find("/blob.corrupt") else { - break; - }; - actual.replace_range(start..start + end, "$HASH/$SEED-unix"); - search_from = start + "$HASH/$SEED-unix".len(); - } - actual - }; - insta::assert_snapshot!(actual, @r#" + insta::assert_snapshot!(normalize_repo_path(&format!("{err:#?}"), &repo), @r#" delegate.peel_until(ValidObject) failed: "{object}" | - └─ An error occurred while obtaining an object from the loose object store - | - └─ decompression of loose object at 'tests/fixtures/generated-do-not-edit/make_rev_spec_parse_repos/$HASH/$SEED-unix/blob.corrupt/objects/ca/fea31147e840161a1860c50af999917ae1536b' failed + └─ Could not read loose object, "path"="$GIT_DIR/objects/ca/fea31147e840161a1860c50af999917ae1536b" | └─ Could not decode zip stream | diff --git a/gix/tests/gix/revision/spec/from_bytes/util.rs b/gix/tests/gix/revision/spec/from_bytes/util.rs index 3b616ec0572..3d6269d0a2d 100644 --- a/gix/tests/gix/revision/spec/from_bytes/util.rs +++ b/gix/tests/gix/revision/spec/from_bytes/util.rs @@ -7,6 +7,14 @@ use gix_revision::spec::Kind; const FIXTURE_NAME: &str = "make_rev_spec_parse_repos.sh"; +/// Replace fixture paths in diagnostics independently of hash kind, seed, and platform. +pub fn normalize_repo_path(message: &str, repo: &gix::Repository) -> String { + message + .replace("\\\\", "/") + .replace('\\', "/") + .replace(&repo.git_dir().to_string_lossy().replace('\\', "/"), "$GIT_DIR") +} + fn git_has_correct_pattern_revision_order(version: (u8, u8, u8)) -> bool { // Git 57fb139b5e accidentally reversed `:/` traversal order in 2.47.x. // Git 0ff919e87a restored youngest-first traversal starting with 2.48.0. From faf85101e1b2c67ca8bbaa60bc6ae360034694c1 Mon Sep 17 00:00:00 2001 From: Byron Date: Thu, 24 Sep 2026 14:22:21 +0200 Subject: [PATCH 49/51] feat(gix-testtools): redact unstable values in diagnostic snapshots Complete error snapshots expose useful context and causes, but temporary paths, dynamically assigned ports, object IDs, and platform-specific I/O messages make their output unstable. Add `redact_debug_snapshot()` to apply explicit replacements, reuse object-ID normalization, and render OS errors by their portable `ErrorKind`. Handle plain and debug-escaped Windows paths while retaining meaningful suffixes and unrelated backslashes. Return an owned `Debug` value so `insta` displays the diagnostic without additional quoting or newline escaping. Cover redaction, object identity, and OS error formatting, and convert the environment, SBOM argument, and Rust fixture error assertions to complete inline snapshots. The helper is available before downstream diagnostic snapshot migrations need it. Assisted-by: GPT 6.0 Co-authored-by: GPT 6.0 --- tests/tools/src/lib.rs | 80 +++++++++++++++++++++++++ tests/tools/tests/env.rs | 11 ++-- tests/tools/tests/sbom.rs | 57 ++++++++++++++---- tests/tools/tests/tools/main.rs | 73 +++++++++++++++++++++- tests/tools/tests/tools/rust_fixture.rs | 7 +-- 5 files changed, 207 insertions(+), 21 deletions(-) diff --git a/tests/tools/src/lib.rs b/tests/tools/src/lib.rs index 3a8f0bf3cc9..ae550ac6fa0 100644 --- a/tests/tools/src/lib.rs +++ b/tests/tools/src/lib.rs @@ -1992,6 +1992,86 @@ pub fn normalize_debug_snapshot(value: &dyn std::fmt::Debug) -> (String, Vec impl std::fmt::Debug + use<> { + struct Diagnostic(String); + + impl std::fmt::Debug for Diagnostic { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.0) + } + } + + let mut text = format!("{value:#?}"); + for (from, to) in replacements { + let escaped = format!("{from:?}"); + let windows_path = from.contains('\\') || (cfg!(windows) && from.contains('/')); + for prefix in [&escaped[1..escaped.len() - 1], *from] { + if !windows_path { + text = text.replace(prefix, to); + continue; + } + let paths: Vec<_> = text + .match_indices(prefix) + .map(|(start, _)| { + let suffix_start = start + prefix.len(); + let quote = text[..start].chars().next_back().filter(|ch| matches!(ch, '\'' | '"')); + // ponytail: whitespace delimits unquoted paths; supply a complete path if it contains spaces. + let suffix_len = match quote { + Some('"') if text[..start].ends_with(r#"\""#) => text[suffix_start..].find(r#"\""#), + Some(quote) => text[suffix_start..].find(quote), + None => text[suffix_start..].find(|ch: char| ch.is_whitespace() || matches!(ch, '\'' | '"')), + } + .unwrap_or(text.len() - suffix_start); + let end = suffix_start + suffix_len; + let suffix = text[suffix_start..end].replace(r"\\", "/").replace('\\', "/"); + (start, end, format!("{to}{suffix}")) + }) + .collect(); + for (start, end, replacement) in paths.into_iter().rev() { + text.replace_range(start..end, &replacement); + } + } + } + let os_errors: Vec<_> = text + .split("(os error ") + .skip(1) + .filter_map(|tail| tail.split_once(')')?.0.parse::().ok()) + .map(std::io::Error::from_raw_os_error) + .collect(); + for err in os_errors { + text = text.replace(&err.to_string(), &format!("{:?}", err.kind())); + } + let os_debug: Vec<_> = text + .match_indices("Os {\n") + .filter_map(|(start, _)| { + let line = text[start + "Os {\n".len()..].lines().next()?; + let code = line.trim().strip_prefix("code: ")?.strip_suffix(',')?.parse().ok()?; + let err = std::io::Error::from_raw_os_error(code); + let indent = " ".repeat((line.len() - line.trim_start().len()).saturating_sub(4)); + Some(( + format!("{err:#?}").replace('\n', &format!("\n{indent}")), + format!("{:?}", err.kind()), + )) + }) + .collect(); + for (from, to) in os_debug { + text = text.replace(&from, &to); + } + Diagnostic(normalize_hashes(&text).0) +} + /// Normalize 40- and 64-character hexadecimal object IDs in `input`. /// /// This is like [`normalize_debug_snapshot()`], but operates on already-formatted text. diff --git a/tests/tools/tests/env.rs b/tests/tools/tests/env.rs index d434cc5d330..d5ba24b54e7 100644 --- a/tests/tools/tests/env.rs +++ b/tests/tools/tests/env.rs @@ -353,11 +353,12 @@ mod isolate_git_environment { "an early successful return restores the entire environment" ); let error = leave_scope(true).expect_err("the deliberate error leaves the guarded scope"); - assert_eq!( - error.to_string(), - "deliberate scoped error", - "the error comes from inside the scope, not guard creation" - ); + insta::assert_debug_snapshot!(error, "the error comes from inside the scope, not guard creation", @r#" + Custom { + kind: Other, + error: "deliberate scoped error", + } + "#); assert_eq!(snapshot(), before, "error propagation restores the entire environment"); Ok(()) } diff --git a/tests/tools/tests/sbom.rs b/tests/tools/tests/sbom.rs index 8d042b42be7..aa20f5a2e93 100644 --- a/tests/tools/tests/sbom.rs +++ b/tests/tools/tests/sbom.rs @@ -127,20 +127,57 @@ fn help_and_invalid_selection() -> Result { String::from_utf8_lossy(&help.stdout).contains("--package"), "help explains package selection" ); - for (args, diagnostic) in [ - (vec!["--package", "missing"], "unknown workspace package"), - (vec!["--package", "app", "--features", "missing"], "unknown feature"), - (vec!["--features", "http"], "--package"), - (vec!["--package"], "requires a value"), + let mut diagnostics = Vec::new(); + for args in [ + vec!["--package", "missing"], + vec!["--package", "app", "--features", "missing"], + vec!["--features", "http"], + vec!["--package"], ] { let output = jtt(root.path(), &args)?; assert!(!output.status.success(), "invalid selection must fail: {args:?}"); - assert!( - String::from_utf8_lossy(&output.stderr).contains(diagnostic), - "diagnostic explains {args:?}: {}", - String::from_utf8_lossy(&output.stderr) - ); + diagnostics.push(( + args, + gix_testtools::redact_debug_snapshot(&format_args!("{}", String::from_utf8_lossy(&output.stderr)), &[]), + )); } + insta::assert_debug_snapshot!(diagnostics, "invalid SBOM selections identify the rejected package, feature, or argument", @r#" + [ + ( + [ + "--package", + "missing", + ], + Error: "unknown workspace package Some(\"missing\")" + , + ), + ( + [ + "--package", + "app", + "--features", + "missing", + ], + Error: "unknown feature \"missing\" for \"app\"" + , + ), + ( + [ + "--features", + "http", + ], + Error: "feature selection requires --package; workspace inventories always enable all features" + , + ), + ( + [ + "--package", + ], + Error: "--package requires a value" + , + ), + ] + "#); Ok(()) } diff --git a/tests/tools/tests/tools/main.rs b/tests/tools/tests/tools/main.rs index d2a06528e21..1fcdf0edff0 100644 --- a/tests/tools/tests/tools/main.rs +++ b/tests/tools/tests/tools/main.rs @@ -1,5 +1,5 @@ mod isolation { - use gix_testtools::{Creation, Result}; + use gix_testtools::{Creation, Result, redact_debug_snapshot}; #[test] fn script_configuration_adds_to_the_isolation() -> Result { @@ -36,6 +36,77 @@ mod isolation { ); Ok(()) } + + #[test] + fn redact_debug_snapshot_preserves_diagnostics_and_object_identity() { + for (input, replacements, expected) in [ + ( + "read /tmp/random/repo/objects/pack failed: NotFound\nsource: /tmp/random/repo/config", + &[("/tmp/random/repo", "")][..], + "read /objects/pack failed: NotFound\nsource: /config", + ), + ( + r#"path: "C:\\Temp\\random\\repo\\config", input: "a\\b""#, + &[(r"C:\Temp\random\repo\config", "/config")][..], + r#"path: "/config", input: "a\\b""#, + ), + ( + r#"path: "C:\\Temp\\random\\repo\\dir with spaces\\config", input: "a\\b""#, + &[(r"C:\Temp\random\repo", "")][..], + r#"path: "/dir with spaces/config", input: "a\\b""#, + ), + ( + r"Alternates form a cycle -> C:\Temp\repo\a -> C:\Temp\repo\b", + &[(r"C:\Temp\repo", "")][..], + "Alternates form a cycle -> /a -> /b", + ), + ( + r#"path: "tests/fixtures\\repo\\sub\\config", input: "a\\b""#, + &[(r"tests/fixtures\repo", "")][..], + r#"path: "/sub/config", input: "a\\b""#, + ), + ( + r#"message: "Could not read \"C:\\Temp\\repo\\config\"", input: "a\\b""#, + &[(r"C:\Temp\repo", "")][..], + r#"message: "Could not read \"/config\"", input: "a\\b""#, + ), + ( + "connect 127.0.0.1:49152: ConnectionRefused; expected port 443", + &[("127.0.0.1:49152", "127.0.0.1:")][..], + "connect 127.0.0.1:: ConnectionRefused; expected port 443", + ), + ] { + let snapshot = redact_debug_snapshot(&format_args!("{input}"), replacements); + assert_eq!( + format!("{snapshot:#?}"), + expected, + "explicit redactions preserve the surrounding diagnostic without extra quoting" + ); + } + let os_error = std::io::Error::from_raw_os_error(2); + let snapshot = redact_debug_snapshot(&format_args!("read failed: {os_error}"), &[]); + assert_eq!( + format!("{snapshot:#?}"), + format!("read failed: {:?}", os_error.kind()), + "platform-dependent OS text is reduced to its portable error kind" + ); + let snapshot = redact_debug_snapshot(&vec![os_error], &[]); + insta::assert_debug_snapshot!(snapshot, "nested pretty-debug OS errors omit platform-specific codes and messages", @" + [ + NotFound, + ] + "); + + #[cfg(all(feature = "sha1", feature = "sha256"))] + { + let input = "expected e69de29bb2d1d6434b8b29ae775ad8c2e48c5391, actual 473a0f4c3be8a93681a267e3b1e9a7dcda1185436fe141f7749120a303721813; expected e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"; + let snapshot = redact_debug_snapshot(&format_args!("{input}"), &[]); + assert_eq!( + format!("{snapshot:#?}"), + "expected Oid(1), actual Oid(2); expected Oid(1)" + ); + } + } } mod repository; mod rust_fixture; diff --git a/tests/tools/tests/tools/rust_fixture.rs b/tests/tools/tests/tools/rust_fixture.rs index 4dd4712f3c8..b34dcf2c9e1 100644 --- a/tests/tools/tests/tools/rust_fixture.rs +++ b/tests/tools/tests/tools/rust_fixture.rs @@ -101,9 +101,6 @@ fn rust_fixture_closure_error_propagates() { Err::<(), _>("intentional error".into()) }); - let err_msg = res.unwrap_err().to_string(); - assert!( - err_msg.contains("intentional error"), - "Error message should contain the original error, got: {err_msg}" - ); + let err = res.expect_err("the fixture closure failed"); + insta::assert_debug_snapshot!(err, "fixture setup preserves the closure's error", @r#""intentional error""#); } From f79ab74b7e12c84de81cc5b5e6eb5f31c39b688c Mon Sep 17 00:00:00 2001 From: Byron Date: Thu, 24 Sep 2026 19:02:40 +0200 Subject: [PATCH 50/51] Exclude Rust tests and examples from `CodeQL` analysis Keep Rust security findings focused on production code by excluding `examples/`, `tests/`, `fuzz/`, and `benches/` throughout the workspace. Scope `paths-ignore` to the Rust job, whose `build-mode: none` supports these filters. Inline unit tests remain included. Validated YAML parsing, recursive path coverage, and `git diff --check`. Assisted-by: GPT 6.0 Co-authored-by: GPT 6.0 --- .github/workflows/codeql.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index a9792f69dc7..4e78be9f393 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -56,6 +56,13 @@ jobs: build-mode: none - language: rust build-mode: none + # Path exclusions work with `none`; inline unit tests remain in scope. + config: | + paths-ignore: + - '**/examples/**' + - '**/tests/**' + - '**/fuzz/**' + - '**/benches/**' # CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'rust', 'swift' # Use `c-cpp` to analyze code written in C, C++ or both # Use 'java-kotlin' to analyze code written in Java, Kotlin or both @@ -82,6 +89,7 @@ jobs: with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} + config: ${{ matrix.config }} # If you wish to specify custom queries, you can do so here or in a config file. # By default, queries listed here will override any specified in a config file. # Prefix the list here with "+" to use these queries and those in the config file. From da0f21d3f48266eb47a23548d5ae4163b78651dc Mon Sep 17 00:00:00 2001 From: Codex Date: Tue, 22 Sep 2026 16:03:26 +0200 Subject: [PATCH 51/51] change(gix-error)!: unify diagnostics as `Message` and preserve causes I refackiewed gix-error with the usual care, and skimmed through all the downstream changes, rubber-stamping it. Definitely cleaned up a few bits, like config-key related error handling, and error related types. A single failure could require separate errors for its message, category, and diagnostic values. Classification wrappers could obscure the concrete cause, while cause-selection heuristics could choose a different sibling after adding context or flattening an exception tree. Represent each failure with one diagnostic and preserve its actual causal structure. Make `Message` the shared diagnostic type, with a message, optional `Class`, and named `MetadataValue`s. Classified constructors and the `with_class()` and `with()` builders attach categories and details without extra error layers. Plain messages and string conversions remain unclassified. `Metadata` is now the value dictionary; `Exn::metadata()` and `Error::metadata()` yield only non-empty dictionaries in traversal order, keeping each context separate and preserving bytes, paths, and numeric types. Introduce `ClassificationMarker` to classify existing concrete errors or provide classification-only sources. Classification still inspects these markers, while diagnostic traversal, downcasts, cause selection, and reports skip them and retain their real descendants. Preserve concrete I/O errors and their original kinds for recovery decisions. Give `Frame`, `Exn`, and `Error` the same `probable_cause()` policy: follow the unique causal path to a leaf or the first diagnostic branch, retaining the aggregate instead of choosing an arbitrary sibling. Include native sources, I/O payloads, nested errors, and explicitly raised children in both tree and chain representations. Preserve caller locations through transparent markers, avoid duplicate native causes in reports, and honor alternate `TestError` formatting. Marker-only errors retain a classification fallback. Add `ExnResult` and `ExnMessageResult`, re-export them from `gix`, and adopt them throughout the workspace. Keep callback bounds erased and preserve concrete error types where callers need their payloads. Group supporting classification and display types in `gix_error::types`, and frame inspection and erasure in `gix_error::exn`. Migrate constructors, signatures, and recovery checks together. Replace the `gix::config::key::Error` family with ordinary diagnostics and central `gix::Error` results. Provide `config::key::error()` and `error_with_value()` for `key`, optional `environment_override`, and optional `input` metadata. Retain parser causes, rejected numeric values, accepted special values, and configuration leniency. Collapse artificial error layers elsewhere while preserving concrete failures and partial outcomes. Make complete diagnostics reviewable through inline `insta` snapshots, retaining assertions for classifications, retry policies, concrete causes, and stored data. Use the shared snapshot redaction helper for unstable paths, ports, object IDs, and platform-specific OS errors. Update migration guidance, examples, and CLI snapshots, and use static X.509 assertion messages to avoid dumping verifier identities and raw output. BREAKING CHANGE: replace `ValidationError`, `CorruptionError`, `NotFoundError`, `ResourceExhaustionError`, and `RetryableError` with `Message` diagnostics or transparent `ClassificationMarker`s. The former `Metadata` error becomes a dictionary, `Value` becomes `MetadataValue`, and `Something` is removed. Access offending input through named metadata. Import `Frame` and `Untyped` from `exn`, and classification/display helpers from `types`. Replace free `can_retry()` and `can_retry_lenient()` calls with methods on `classify(error)`. Configuration error aliases are removed; diagnostic traversal omits markers, metadata iteration skips empty maps, and probable-cause selection stops at the first diagnostic branch. Co-authored-by: Byron --- AGENTS.md | 10 +- Cargo.lock | 44 + Cargo.toml | 1 - etc/plan/gix-error.md | 189 --- gitoxide-core/src/hours/mod.rs | 5 +- gitoxide-core/src/index/checkout.rs | 6 +- gitoxide-core/src/net.rs | 5 +- gitoxide-core/src/pack/create.rs | 4 +- gitoxide-core/src/pack/explode.rs | 17 +- gitoxide-core/src/pack/receive.rs | 9 +- gitoxide-core/src/query/engine/update.rs | 10 +- gitoxide-core/src/repository/credential.rs | 7 +- .../src/repository/revision/explain.rs | 30 +- gix-actor/Cargo.toml | 1 + gix-actor/src/identity.rs | 11 +- gix-actor/src/signature/decode.rs | 25 +- gix-actor/src/signature/mod.rs | 29 +- gix-actor/tests/actor/identity.rs | 19 +- gix-actor/tests/actor/signature.rs | 30 +- gix-archive/src/write.rs | 19 +- gix-attributes/Cargo.toml | 1 + gix-attributes/src/name.rs | 9 +- gix-attributes/src/parse.rs | 34 +- gix-attributes/src/search/attributes.rs | 5 +- gix-attributes/tests/attributes/parse.rs | 239 +-- gix-bitmap/src/ewah.rs | 21 +- gix-blame/Cargo.toml | 1 + gix-blame/src/file/function.rs | 22 +- gix-blame/src/file/tests.rs | 5 +- gix-blame/src/types.rs | 15 +- gix-blame/tests/blame.rs | 6 +- gix-chunk/src/file/decode.rs | 39 +- gix-chunk/src/file/index.rs | 24 +- gix-chunk/src/lib.rs | 2 +- gix-chunk/tests/decode.rs | 79 + gix-command/Cargo.toml | 1 + gix-command/src/parse.rs | 20 +- gix-command/tests/command/command_line.rs | 88 +- gix-command/tests/command/prepare.rs | 3 +- gix-command/tests/command/spawn.rs | 36 +- gix-commitgraph/Cargo.toml | 1 + gix-commitgraph/src/file/commit.rs | 22 +- gix-commitgraph/src/file/init.rs | 6 +- gix-commitgraph/src/file/verify.rs | 12 +- gix-commitgraph/src/init.rs | 22 +- gix-commitgraph/src/lib.rs | 5 +- gix-commitgraph/src/verify.rs | 6 +- gix-commitgraph/tests/commitgraph.rs | 14 +- gix-config-value/Cargo.toml | 4 + gix-config-value/src/boolean.rs | 25 +- gix-config-value/src/color.rs | 26 +- gix-config-value/src/integer.rs | 18 +- gix-config-value/src/path.rs | 36 +- gix-config-value/src/types.rs | 3 + gix-config-value/tests/value/boolean.rs | 9 +- gix-config-value/tests/value/color.rs | 6 +- gix-config-value/tests/value/integer.rs | 3 +- gix-config-value/tests/value/path.rs | 50 +- gix-config/src/file/access/comfort.rs | 33 +- gix-config/src/file/access/mutate.rs | 58 +- gix-config/src/file/access/raw.rs | 103 +- gix-config/src/file/access/read_only.rs | 17 +- gix-config/src/file/includes/mod.rs | 29 +- gix-config/src/file/init/comfort.rs | 7 +- gix-config/src/file/init/from_env.rs | 25 +- gix-config/src/file/init/from_paths.rs | 11 +- gix-config/src/file/init/mod.rs | 5 +- gix-config/src/file/mutable/mod.rs | 3 +- gix-config/src/file/mutable/multi_value.rs | 15 +- gix-config/src/file/mutable/section.rs | 38 +- gix-config/src/file/mutable/value.rs | 8 +- gix-config/src/file/section/body.rs | 9 +- gix-config/src/file/section/mod.rs | 15 +- gix-config/src/file/util.rs | 6 +- gix-config/src/lookup.rs | 2 +- gix-config/src/parse/comment.rs | 7 +- gix-config/src/parse/error.rs | 6 +- gix-config/src/parse/event.rs | 9 +- gix-config/src/parse/format.rs | 5 +- gix-config/src/parse/from_bytes/tests.rs | 8 +- gix-config/src/parse/mod.rs | 31 +- gix-config/src/parse/section/header.rs | 27 +- gix-config/src/parse/section/mod.rs | 16 +- gix-config/src/parse/tests.rs | 8 +- gix-config/tests/config/file/access/mutate.rs | 49 +- .../config/file/access/raw/raw_multi_value.rs | 27 +- .../tests/config/file/access/raw/raw_value.rs | 47 +- .../file/access/raw/set_existing_raw_value.rs | 10 +- .../config/file/access/raw/set_raw_value.rs | 11 +- .../tests/config/file/access/read_only.rs | 48 +- gix-config/tests/config/file/impls.rs | 3 +- gix-config/tests/config/file/init/comfort.rs | 9 +- gix-config/tests/config/file/init/from_env.rs | 38 +- .../includes/conditional/gitdir/mod.rs | 81 +- .../includes/conditional/gitdir/util.rs | 19 +- .../includes/conditional/hasconfig.rs | 13 +- .../from_paths/includes/conditional/mod.rs | 5 +- .../includes/conditional/onbranch.rs | 11 +- .../init/from_paths/includes/unconditional.rs | 49 +- .../tests/config/file/init/from_paths/mod.rs | 21 +- gix-config/tests/config/file/init/from_str.rs | 6 +- gix-config/tests/config/file/mod.rs | 3 +- .../tests/config/file/mutable/multi_value.rs | 26 +- .../tests/config/file/mutable/section.rs | 82 +- gix-config/tests/config/file/mutable/value.rs | 23 +- .../tests/config/file/resolve_includes.rs | 3 +- gix-config/tests/config/file/write.rs | 6 +- gix-config/tests/config/parse/error.rs | 78 +- gix-config/tests/config/parse/section.rs | 42 +- gix-credentials/Cargo.toml | 1 + gix-credentials/examples/custom-helper.rs | 5 +- .../examples/git-credential-lite.rs | 3 +- gix-credentials/src/helper/invoke.rs | 11 +- gix-credentials/src/helper/mod.rs | 5 +- gix-credentials/src/program/main.rs | 30 +- gix-credentials/src/protocol/context/mod.rs | 7 +- gix-credentials/src/protocol/context/serde.rs | 37 +- gix-credentials/src/protocol/mod.rs | 5 +- gix-credentials/tests/helper/cascade.rs | 20 +- gix-credentials/tests/helper/context.rs | 23 +- gix-credentials/tests/helper/invoke.rs | 10 +- gix-credentials/tests/helper/mod.rs | 18 +- gix-credentials/tests/program/main.rs | 25 +- gix-credentials/tests/protocol/context.rs | 8 +- gix-date/Cargo.toml | 1 + gix-date/src/parse/function.rs | 11 +- gix-date/src/parse/mod.rs | 6 +- gix-date/src/parse/relative.rs | 26 +- gix-date/tests/time/main.rs | 7 +- gix-date/tests/time/parse/mod.rs | 9 +- gix-date/tests/time/parse/relative.rs | 20 +- gix-diff/src/blob/pipeline.rs | 10 +- gix-diff/src/blob/platform.rs | 45 +- gix-diff/src/index/function.rs | 14 +- gix-diff/src/rewrites/tracker.rs | 14 +- gix-diff/src/tree/mod.rs | 4 +- gix-diff/src/tree_with_rewrites/change.rs | 3 +- gix-diff/src/tree_with_rewrites/function.rs | 7 +- gix-diff/tests/diff/blob/pipeline.rs | 17 +- gix-diff/tests/diff/blob/platform.rs | 51 +- gix-diff/tests/diff/blob/slider.rs | 21 +- gix-diff/tests/diff/blob/textconv.rs | 3 +- gix-diff/tests/diff/blob/unified_diff.rs | 33 +- gix-diff/tests/diff/index.rs | 50 +- gix-diff/tests/diff/main.rs | 3 +- gix-diff/tests/diff/rewrites/tracker.rs | 38 +- gix-diff/tests/diff/tree.rs | 23 +- gix-diff/tests/diff/tree_with_rewrites.rs | 58 +- gix-dir/Cargo.toml | 1 + gix-dir/src/walk/classify.rs | 6 +- gix-dir/src/walk/function.rs | 18 +- gix-dir/src/walk/readdir.rs | 4 +- gix-dir/tests/dir/walk.rs | 161 +- gix-dir/tests/walk_utils/mod.rs | 18 +- gix-discover/Cargo.toml | 3 +- gix-discover/src/is.rs | 23 +- gix-discover/src/parse.rs | 15 +- gix-discover/src/path.rs | 3 +- gix-discover/src/upwards/mod.rs | 28 +- gix-discover/tests/discover/is_git.rs | 29 +- gix-discover/tests/discover/parse.rs | 17 +- gix-discover/tests/discover/path.rs | 53 +- .../tests/discover/upwards/ceiling_dirs.rs | 28 +- gix-discover/tests/discover/upwards/mod.rs | 66 +- gix-discover/tests/isolated.rs | 13 + gix-error/Cargo.toml | 1 + gix-error/src/concrete/chain.rs | 18 +- gix-error/src/concrete/classify.rs | 205 +-- gix-error/src/concrete/message.rs | 38 +- gix-error/src/concrete/metadata.rs | 178 ++- gix-error/src/concrete/mod.rs | 1 - gix-error/src/concrete/validate.rs | 65 - gix-error/src/error.rs | 252 +-- gix-error/src/exn/ext.rs | 36 +- gix-error/src/exn/impls.rs | 305 ++-- gix-error/src/exn/macros.rs | 4 +- gix-error/src/exn/mod.rs | 64 +- gix-error/src/lib.rs | 344 ++-- gix-error/src/test.rs | 38 +- gix-error/src/types.rs | 7 + gix-error/tests/auto_chain_error.rs | 286 +++- gix-error/tests/error/classification.rs | 1076 ++++++++++++- gix-error/tests/error/error.rs | 402 +++-- gix-error/tests/error/exn.rs | 1006 +++++++++--- gix-error/tests/error/main.rs | 1 + gix-error/tests/error/metadata.rs | 810 +++++++++- gix-error/tests/error/probable_cause.rs | 1387 +++++++++++++++++ gix-error/tests/error/test.rs | 95 ++ gix-features/Cargo.toml | 4 + .../tests/parallel/in_parallel_with_slice.rs | 12 +- gix-features/tests/pipe.rs | 34 +- gix-filter/Cargo.toml | 1 + gix-filter/src/driver/apply.rs | 6 +- gix-filter/src/driver/delayed.rs | 8 +- gix-filter/src/driver/init.rs | 6 +- gix-filter/src/driver/process/client.rs | 10 +- gix-filter/src/driver/process/server.rs | 6 +- gix-filter/src/driver/shutdown.rs | 3 +- gix-filter/src/eol/convert_to_git.rs | 7 +- gix-filter/src/eol/convert_to_worktree.rs | 3 +- gix-filter/src/ident.rs | 8 +- gix-filter/src/pipeline/convert.rs | 9 +- gix-filter/src/pipeline/util.rs | 16 +- gix-filter/src/worktree/encode_to_git.rs | 15 +- gix-filter/src/worktree/encode_to_worktree.rs | 10 +- gix-filter/src/worktree/encoding.rs | 5 +- gix-filter/tests/filter/driver.rs | 89 +- gix-filter/tests/filter/eol/convert_to_git.rs | 36 +- .../tests/filter/eol/convert_to_worktree.rs | 13 +- gix-filter/tests/filter/ident.rs | 20 +- .../tests/filter/pipeline/convert_to_git.rs | 6 +- .../filter/pipeline/convert_to_worktree.rs | 7 +- gix-filter/tests/filter/pipeline/mod.rs | 3 +- gix-filter/tests/filter/worktree.rs | 47 +- gix-fs/Cargo.toml | 2 + gix-fs/src/stack.rs | 20 +- gix-fs/tests/fs/dir/create.rs | 139 +- gix-fs/tests/fs/dir/remove.rs | 34 +- gix-fs/tests/fs/read_dir.rs | 4 +- gix-fs/tests/fs/stack.rs | 149 +- gix-fsck/src/lib.rs | 4 +- gix-hash/Cargo.toml | 1 + gix-hash/src/change_id.rs | 15 +- gix-hash/src/hasher.rs | 11 +- gix-hash/src/io.rs | 13 +- gix-hash/src/object_id.rs | 21 +- gix-hash/src/oid.rs | 15 +- gix-hash/src/prefix.rs | 59 +- gix-hash/src/verify.rs | 9 +- gix-hash/tests/hash/change_id.rs | 15 +- gix-hash/tests/hash/hasher.rs | 27 +- gix-hash/tests/hash/object_id.rs | 35 +- gix-hash/tests/hash/prefix.rs | 29 +- gix-ignore/tests/ignore/search.rs | 3 +- gix-index/src/decode/entries.rs | 6 +- gix-index/src/decode/header.rs | 18 +- gix-index/src/decode/mod.rs | 19 +- gix-index/src/extension/decode.rs | 3 +- .../extension/end_of_index_entry/decode.rs | 3 +- .../src/extension/end_of_index_entry/write.rs | 3 +- gix-index/src/extension/link.rs | 19 +- gix-index/src/extension/tree/verify.rs | 48 +- gix-index/src/extension/tree/write.rs | 4 +- gix-index/src/file/init.rs | 6 +- gix-index/src/file/verify.rs | 4 +- gix-index/src/file/write.rs | 5 +- gix-index/src/init.rs | 6 +- gix-index/src/verify.rs | 12 +- gix-index/src/write.rs | 4 +- gix-index/tests/index/file/access.rs | 3 +- gix-index/tests/index/file/fuzzed.rs | 17 +- gix-index/tests/index/file/init.rs | 15 + gix-index/tests/index/file/read.rs | 10 +- gix-index/tests/index/file/write.rs | 11 +- gix-index/tests/index/fs.rs | 3 +- gix-index/tests/index/fuzzed.rs | 43 +- gix-index/tests/index/init.rs | 40 +- gix-lock/Cargo.toml | 2 + gix-lock/src/acquire.rs | 20 +- gix-lock/tests/lock/file.rs | 7 + gix-lock/tests/lock/marker.rs | 35 +- gix-mailmap/Cargo.toml | 3 +- gix-mailmap/src/lib.rs | 1 + gix-mailmap/src/parse.rs | 32 +- gix-mailmap/tests/parse/mod.rs | 30 +- gix-merge/Cargo.toml | 1 + gix-merge/fuzz/fuzz_targets/tree.rs | 9 +- gix-merge/src/blob/pipeline.rs | 16 +- gix-merge/src/blob/platform/merge.rs | 16 +- gix-merge/src/blob/platform/prepare_merge.rs | 4 +- gix-merge/src/blob/platform/set_resource.rs | 3 +- gix-merge/src/commit/function.rs | 4 +- gix-merge/src/commit/virtual_merge_base.rs | 6 +- gix-merge/src/tree/function/change.rs | 5 +- gix-merge/src/tree/function/resolve.rs | 16 +- gix-merge/src/tree/utils.rs | 23 +- gix-merge/tests/merge/blob/builtin_driver.rs | 3 +- gix-merge/tests/merge/blob/mod.rs | 3 +- gix-merge/tests/merge/blob/pipeline.rs | 24 +- gix-merge/tests/merge/blob/platform.rs | 45 +- gix-merge/tests/merge/tree/cartesian.rs | 23 +- gix-merge/tests/merge/tree/mod.rs | 3 +- gix-negotiate/src/consecutive.rs | 22 +- gix-negotiate/src/lib.rs | 13 +- gix-negotiate/src/noop.rs | 13 +- gix-negotiate/src/skipping.rs | 24 +- gix-negotiate/tests/baseline/mod.rs | 5 +- gix-note/Cargo.toml | 1 + gix-note/src/lib.rs | 60 +- gix-note/tests/note.rs | 63 +- gix-object/benches/edit_tree.rs | 4 +- gix-object/src/commit/decode.rs | 4 +- gix-object/src/commit/mod.rs | 18 +- gix-object/src/commit/ref_iter.rs | 27 +- gix-object/src/data.rs | 14 +- gix-object/src/encode.rs | 11 +- gix-object/src/find.rs | 34 +- gix-object/src/kind.rs | 7 +- gix-object/src/lib.rs | 28 +- gix-object/src/object/convert.rs | 29 +- gix-object/src/object/mod.rs | 36 +- gix-object/src/parse.rs | 21 +- gix-object/src/signature/sign.rs | 35 +- gix-object/src/signature/verify.rs | 49 +- gix-object/src/tag/decode.rs | 8 +- gix-object/src/tag/mod.rs | 7 +- gix-object/src/tag/ref_iter.rs | 19 +- gix-object/src/tag/write.rs | 8 +- gix-object/src/traits/_impls.rs | 32 +- gix-object/src/traits/find.rs | 51 +- gix-object/src/traits/mod.rs | 17 +- gix-object/src/tree/editor.rs | 23 +- gix-object/src/tree/ref_iter.rs | 24 +- gix-object/src/tree/write.rs | 20 +- gix-object/tests/object/commit/from_bytes.rs | 40 +- gix-object/tests/object/commit/iter.rs | 45 +- gix-object/tests/object/commit/mod.rs | 7 +- gix-object/tests/object/commit/signature.rs | 2 +- gix-object/tests/object/encode.rs | 14 +- gix-object/tests/object/object_ref.rs | 8 +- gix-object/tests/object/tag/mod.rs | 59 +- gix-object/tests/object/tag/signature.rs | 5 +- gix-object/tests/object/tree/editor.rs | 52 +- gix-object/tests/object/tree/entries.rs | 3 +- gix-object/tests/object/tree/from_bytes.rs | 13 +- gix-object/tests/object/tree/iter.rs | 16 +- gix-odb/Cargo.toml | 1 + gix-odb/src/alternate/mod.rs | 12 +- gix-odb/src/alternate/parse.rs | 9 +- gix-odb/src/cache.rs | 22 +- gix-odb/src/lib.rs | 17 +- gix-odb/src/memory.rs | 16 +- gix-odb/src/sink.rs | 8 +- gix-odb/src/store_impls/dynamic/find.rs | 37 +- gix-odb/src/store_impls/dynamic/header.rs | 15 +- gix-odb/src/store_impls/dynamic/iter.rs | 10 +- gix-odb/src/store_impls/dynamic/load_index.rs | 28 +- gix-odb/src/store_impls/dynamic/prefix.rs | 14 +- gix-odb/src/store_impls/dynamic/structure.rs | 6 +- gix-odb/src/store_impls/dynamic/verify.rs | 23 +- gix-odb/src/store_impls/dynamic/write.rs | 8 +- gix-odb/src/store_impls/loose/find.rs | 85 +- gix-odb/src/store_impls/loose/verify.rs | 21 +- gix-odb/src/store_impls/loose/write.rs | 66 +- gix-odb/src/traits.rs | 21 +- gix-odb/tests/odb/alternate.rs | 31 +- gix-odb/tests/odb/error.rs | 348 ++++- gix-odb/tests/odb/memory.rs | 13 +- gix-odb/tests/odb/regression.rs | 7 +- gix-odb/tests/odb/sink.rs | 3 +- gix-odb/tests/odb/store/dynamic.rs | 50 +- gix-odb/tests/odb/store/linked.rs | 8 +- gix-odb/tests/odb/store/loose.rs | 311 +++- gix-pack/src/bundle/find.rs | 6 +- gix-pack/src/bundle/init.rs | 10 +- gix-pack/src/bundle/mod.rs | 4 +- gix-pack/src/bundle/write/mod.rs | 112 +- gix-pack/src/bundle/write/types.rs | 4 +- gix-pack/src/cache/delta/from_offsets.rs | 12 +- gix-pack/src/cache/delta/traverse/mod.rs | 12 +- gix-pack/src/cache/delta/traverse/resolve.rs | 67 +- gix-pack/src/cache/delta/tree.rs | 37 +- gix-pack/src/cache/mod.rs | 3 +- gix-pack/src/data/delta.rs | 32 +- gix-pack/src/data/entry/decode.rs | 34 +- gix-pack/src/data/file/decode/entry.rs | 37 +- gix-pack/src/data/file/decode/header.rs | 16 +- gix-pack/src/data/file/decode/mod.rs | 11 +- gix-pack/src/data/file/init.rs | 10 +- gix-pack/src/data/file/verify.rs | 4 +- gix-pack/src/data/header.rs | 7 +- gix-pack/src/data/input/bytes_to_entries.rs | 14 +- gix-pack/src/data/input/entries_to_bytes.rs | 12 +- gix-pack/src/data/input/entry.rs | 6 +- .../data/input/lookup_ref_delta_objects.rs | 9 +- gix-pack/src/data/output/bytes.rs | 10 +- gix-pack/src/data/output/count/objects/mod.rs | 24 +- .../src/data/output/entry/iter_from_counts.rs | 4 +- gix-pack/src/data/output/entry/mod.rs | 8 +- gix-pack/src/find_traits.rs | 29 +- gix-pack/src/index/encode.rs | 4 +- gix-pack/src/index/init.rs | 16 +- gix-pack/src/index/traverse/mod.rs | 23 +- gix-pack/src/index/traverse/reduce.rs | 6 +- gix-pack/src/index/traverse/with_index.rs | 13 +- gix-pack/src/index/traverse/with_lookup.rs | 9 +- gix-pack/src/index/verify.rs | 20 +- gix-pack/src/index/write/mod.rs | 20 +- gix-pack/src/multi_index/chunk.rs | 30 +- gix-pack/src/multi_index/init.rs | 26 +- gix-pack/src/multi_index/verify.rs | 24 +- gix-pack/src/multi_index/write.rs | 12 +- gix-pack/src/testing.rs | 3 +- gix-pack/src/verify.rs | 8 +- gix-pack/tests/pack/bundle.rs | 50 +- gix-pack/tests/pack/data/file.rs | 32 +- gix-pack/tests/pack/data/fuzzed.rs | 8 +- gix-pack/tests/pack/data/header.rs | 3 +- gix-pack/tests/pack/data/input.rs | 43 +- .../pack/data/output/count_and_entries.rs | 38 +- gix-pack/tests/pack/data/output/mod.rs | 3 +- gix-pack/tests/pack/index.rs | 57 +- gix-pack/tests/pack/iter.rs | 13 +- gix-pack/tests/pack/main.rs | 12 - gix-pack/tests/pack/malformed.rs | 104 +- gix-pack/tests/pack/multi_index/fuzzed.rs | 16 +- gix-pack/tests/pack/multi_index/verify.rs | 3 +- gix-pack/tests/pack/multi_index/write.rs | 92 +- gix-packetline/Cargo.toml | 2 + gix-packetline/src/async_io/encode.rs | 9 +- gix-packetline/src/async_io/read.rs | 17 +- gix-packetline/src/async_io/sidebands.rs | 9 +- gix-packetline/src/blocking_io/encode.rs | 6 +- gix-packetline/src/blocking_io/read.rs | 16 +- gix-packetline/src/blocking_io/sidebands.rs | 5 +- gix-packetline/src/decode.rs | 29 +- gix-packetline/src/lib.rs | 22 +- gix-packetline/src/read.rs | 8 +- gix-packetline/tests/packetline/decode.rs | 34 +- gix-packetline/tests/packetline/encode.rs | 23 +- gix-packetline/tests/packetline/main-async.rs | 10 - .../tests/packetline/main-blocking.rs | 10 - gix-packetline/tests/packetline/read/mod.rs | 78 +- .../tests/packetline/read/sideband.rs | 50 +- gix-packetline/tests/packetline/write.rs | 10 +- gix-path/Cargo.toml | 2 + gix-path/src/convert.rs | 23 +- gix-path/src/realpath.rs | 14 +- gix-path/src/relative_path.rs | 29 +- gix-path/tests/path/convert/mod.rs | 18 + gix-path/tests/path/realpath.rs | 35 +- gix-path/tests/path/relative_path.rs | 139 +- gix-pathspec/Cargo.toml | 1 + gix-pathspec/fuzz/fuzz_targets/parse.rs | 2 +- gix-pathspec/src/defaults.rs | 10 +- gix-pathspec/src/lib.rs | 5 +- gix-pathspec/src/parse.rs | 84 +- gix-pathspec/src/pattern.rs | 23 +- gix-pathspec/src/search/init.rs | 9 +- gix-pathspec/tests/defaults.rs | 17 +- gix-pathspec/tests/normalize/mod.rs | 45 +- gix-pathspec/tests/parse/invalid.rs | 335 +++- gix-pathspec/tests/parse/mod.rs | 3 +- gix-pathspec/tests/parse/valid.rs | 7 +- gix-pathspec/tests/search/mod.rs | 56 +- gix-prompt/Cargo.toml | 1 + gix-prompt/examples/credentials.rs | 3 +- gix-prompt/src/lib.rs | 10 +- gix-prompt/src/unix.rs | 11 +- gix-prompt/tests/prompt.rs | 6 +- gix-protocol/Cargo.toml | 2 + gix-protocol/src/command.rs | 22 +- gix-protocol/src/fetch/function.rs | 39 +- gix-protocol/src/fetch/negotiate.rs | 32 +- gix-protocol/src/fetch/refmap/init.rs | 26 +- gix-protocol/src/fetch/response/io.rs | 25 +- gix-protocol/src/fetch/response/mod.rs | 23 +- gix-protocol/src/fetch/types.rs | 7 +- gix-protocol/src/handshake/function.rs | 7 +- gix-protocol/src/handshake/mod.rs | 5 +- gix-protocol/src/handshake/refs/async_io.rs | 5 +- .../src/handshake/refs/blocking_io.rs | 5 +- gix-protocol/src/handshake/refs/shared.rs | 55 +- gix-protocol/src/ls_refs.rs | 25 +- gix-protocol/tests/protocol/command.rs | 68 +- gix-protocol/tests/protocol/fetch/_impl.rs | 3 +- .../tests/protocol/fetch/arguments.rs | 8 +- gix-protocol/tests/protocol/fetch/ref_map.rs | 10 +- gix-protocol/tests/protocol/fetch/response.rs | 44 +- gix-protocol/tests/protocol/fetch/v1.rs | 9 +- gix-protocol/tests/protocol/fetch/v2.rs | 15 +- gix-protocol/tests/protocol/handshake.rs | 9 +- gix-quote/src/ansi_c.rs | 37 +- gix-ref/src/lib.rs | 21 +- gix-ref/src/store/file/find.rs | 32 +- gix-ref/src/store/file/log/iter.rs | 16 +- gix-ref/src/store/file/log/line.rs | 22 +- .../src/store/file/loose/reference/decode.rs | 21 +- .../src/store/file/loose/reference/logiter.rs | 4 +- gix-ref/src/store/file/loose/reflog.rs | 37 +- .../loose/reflog/create_or_update/tests.rs | 19 +- gix-ref/src/store/file/overlay_iter.rs | 24 +- gix-ref/src/store/file/packed.rs | 19 +- gix-ref/src/store/file/raw_ext.rs | 79 +- gix-ref/src/store/file/transaction/commit.rs | 18 +- gix-ref/src/store/file/transaction/prepare.rs | 24 +- gix-ref/src/store/general/handle/find.rs | 6 +- gix-ref/src/store/general/init.rs | 10 +- gix-ref/src/store/packed/buffer.rs | 14 +- gix-ref/src/store/packed/decode/mod.rs | 9 +- gix-ref/src/store/packed/find.rs | 15 +- gix-ref/src/store/packed/iter.rs | 36 +- gix-ref/src/store/packed/transaction.rs | 36 +- gix-ref/tests/refs/error.rs | 434 +++++- gix-ref/tests/refs/file/log.rs | 76 +- gix-ref/tests/refs/file/mod.rs | 20 +- gix-ref/tests/refs/file/reference.rs | 49 +- gix-ref/tests/refs/file/store/access.rs | 5 +- gix-ref/tests/refs/file/store/find.rs | 64 +- gix-ref/tests/refs/file/store/iter.rs | 159 +- gix-ref/tests/refs/file/store/mod.rs | 3 +- gix-ref/tests/refs/file/store/reflog.rs | 52 +- gix-ref/tests/refs/file/transaction/mod.rs | 5 +- .../create_or_update/collisions.rs | 73 +- .../create_or_update/mod.rs | 167 +- .../transaction/prepare_and_commit/delete.rs | 64 +- gix-ref/tests/refs/file/worktree.rs | 142 +- gix-ref/tests/refs/main.rs | 3 +- gix-ref/tests/refs/packed/find.rs | 33 +- gix-ref/tests/refs/packed/iter.rs | 92 +- gix-ref/tests/refs/packed/mod.rs | 3 +- gix-ref/tests/refs/packed/open.rs | 16 +- gix-ref/tests/refs/store.rs | 7 +- gix-ref/tests/refs/transaction.rs | 44 +- gix-refspec/src/match_group/validate.rs | 10 +- gix-refspec/src/parse.rs | 48 +- gix-refspec/tests/refspec/match_group.rs | 42 +- gix-refspec/tests/refspec/matching.rs | 57 +- gix-refspec/tests/refspec/parse/fetch.rs | 75 +- gix-refspec/tests/refspec/parse/invalid.rs | 122 +- gix-refspec/tests/refspec/parse/mod.rs | 29 +- gix-refspec/tests/refspec/parse/push.rs | 54 +- gix-revision/fuzz/fuzz_targets/parse.rs | 24 +- gix-revision/src/describe.rs | 8 +- gix-revision/src/merge_base/function.rs | 8 +- gix-revision/src/merge_base/mod.rs | 5 +- gix-revision/src/spec/parse/delegate.rs | 22 +- gix-revision/src/spec/parse/function.rs | 270 ++-- gix-revision/src/spec/parse/mod.rs | 4 +- gix-revision/tests/revision/describe/mod.rs | 8 +- gix-revision/tests/revision/merge_base.rs | 35 +- .../revision/spec/parse/anchor/at_symbol.rs | 55 +- .../spec/parse/anchor/colon_symbol.rs | 7 +- .../revision/spec/parse/anchor/refnames.rs | 2 +- .../tests/revision/spec/parse/kind.rs | 63 +- gix-revision/tests/revision/spec/parse/mod.rs | 34 +- .../spec/parse/navigate/caret_symbol.rs | 115 +- .../spec/parse/navigate/tilde_symbol.rs | 7 +- gix-revwalk/src/graph/commit.rs | 30 +- gix-revwalk/src/graph/mod.rs | 26 +- gix-sec/tests/sec/identity.rs | 11 +- gix-shallow/src/lib.rs | 13 +- gix-status/Cargo.toml | 1 + gix-status/src/index_as_worktree/function.rs | 15 +- gix-status/src/index_as_worktree/traits.rs | 14 +- .../src/index_as_worktree_with_renames/mod.rs | 14 +- gix-status/src/stack.rs | 10 +- gix-status/tests/status/index_as_worktree.rs | 29 +- gix-status/tests/status/stack.rs | 47 +- gix-submodule/Cargo.toml | 1 + gix-submodule/src/access.rs | 114 +- gix-submodule/src/config.rs | 14 +- gix-submodule/src/is_active_platform.rs | 3 +- gix-submodule/src/lib.rs | 14 +- gix-submodule/tests/file/baseline.rs | 5 +- gix-submodule/tests/file/mod.rs | 181 ++- gix-tempfile/Cargo.toml | 4 + gix-tempfile/tests/tempfile/handle.rs | 52 +- gix-tix/Cargo.toml | 2 +- gix-tix/src/change_id.rs | 6 +- gix-tix/src/edit/create.rs | 2 + gix-tix/src/edit/head.rs | 2 + gix-tix/src/edit/rebase.rs | 17 + gix-tix/src/edit/reword.rs | 8 + gix-tix/src/edit/split.rs | 2 + gix-tix/src/edit/stash.rs | 4 +- gix-tix/src/edit/todo.rs | 4 + gix-tix/src/edit/undo.rs | 11 +- gix-tix/src/enrich.rs | 18 +- gix-tix/src/history.rs | 30 +- gix-tix/src/lib.rs | 14 +- gix-transport/Cargo.toml | 2 + .../src/client/async_io/bufread_ext.rs | 17 +- gix-transport/src/client/async_io/connect.rs | 6 +- .../src/client/blocking_io/bufread_ext.rs | 16 +- .../src/client/blocking_io/connect.rs | 6 +- gix-transport/src/client/blocking_io/file.rs | 27 +- .../src/client/blocking_io/http/curl/mod.rs | 21 +- .../client/blocking_io/http/curl/remote.rs | 40 +- .../src/client/blocking_io/http/mod.rs | 71 +- .../src/client/blocking_io/http/redirect.rs | 8 +- .../client/blocking_io/http/reqwest/mod.rs | 6 +- .../client/blocking_io/http/reqwest/remote.rs | 18 +- .../src/client/blocking_io/http/traits.rs | 8 +- .../src/client/blocking_io/ssh/mod.rs | 6 +- .../src/client/blocking_io/ssh/tests.rs | 196 ++- gix-transport/src/client/capabilities.rs | 5 +- gix-transport/src/client/git/async_io.rs | 4 +- gix-transport/src/client/git/blocking_io.rs | 10 +- gix-transport/src/client/non_io_types.rs | 246 ++- gix-transport/src/client/traits.rs | 8 +- gix-transport/src/lib.rs | 8 - .../tests/blocking-transport-http-reqwest.rs | 46 +- gix-transport/tests/blocking-transport.rs | 1 + .../tests/client/blocking_io/http/mod.rs | 203 ++- gix-transport/tests/client/capabilities.rs | 7 +- gix-transport/tests/client/git.rs | 110 +- gix-traverse/src/commit/mod.rs | 8 +- gix-traverse/src/commit/simple.rs | 31 +- gix-traverse/src/commit/topo/init.rs | 6 +- gix-traverse/src/commit/topo/iter.rs | 38 +- gix-traverse/src/tree/breadthfirst.rs | 6 +- gix-traverse/src/tree/depthfirst.rs | 9 +- .../traverse/commit/simple/adjusted_dates.rs | 15 +- .../traverse/commit/simple/different_date.rs | 7 +- .../simple/different_date_intermixed.rs | 7 +- .../tests/traverse/commit/simple/hide.rs | 38 +- .../tests/traverse/commit/simple/mod.rs | 11 +- .../tests/traverse/commit/simple/same_date.rs | 21 +- gix-traverse/tests/traverse/commit/topo.rs | 44 +- gix-traverse/tests/traverse/tree.rs | 15 +- gix-url/Cargo.toml | 1 + gix-url/fuzz/fuzz_targets/parse.rs | 4 +- gix-url/src/expand_path.rs | 11 +- gix-url/src/lib.rs | 29 +- gix-url/src/parse.rs | 86 +- gix-url/src/simple_url.rs | 35 +- gix-url/tests/url/expand_path.rs | 1 + gix-url/tests/url/parse/file.rs | 9 +- gix-url/tests/url/parse/http.rs | 10 +- gix-url/tests/url/parse/invalid.rs | 142 +- gix-url/tests/url/parse/mod.rs | 5 +- gix-validate/Cargo.toml | 3 +- gix-validate/src/lib.rs | 10 +- gix-validate/src/path.rs | 2 +- gix-validate/src/reference.rs | 2 +- gix-validate/src/submodule.rs | 2 +- gix-validate/src/tag.rs | 2 +- gix-validate/tests/validate/main.rs | 21 +- gix-validate/tests/validate/path.rs | 167 +- gix-validate/tests/validate/reference.rs | 125 +- gix-validate/tests/validate/submodule.rs | 41 +- gix-validate/tests/validate/tag.rs | 156 +- gix-worktree-state/Cargo.toml | 1 + gix-worktree-state/src/checkout/chunk.rs | 17 +- gix-worktree-state/src/checkout/entry.rs | 18 +- gix-worktree-state/src/checkout/function.rs | 6 +- .../tests/worktree-state/checkout.rs | 98 +- gix-worktree-stream/Cargo.toml | 1 + gix-worktree-stream/src/entry.rs | 4 +- gix-worktree-stream/src/from_tree/mod.rs | 4 +- gix-worktree-stream/src/from_tree/traverse.rs | 20 +- gix-worktree-stream/tests/stream.rs | 38 +- gix-worktree/Cargo.toml | 1 + .../tests/worktree/stack/attributes.rs | 3 +- .../tests/worktree/stack/create_directory.rs | 92 +- gix-worktree/tests/worktree/stack/ignore.rs | 11 +- gix-worktree/tests/worktree/stack/mod.rs | 3 +- gix-zlib/Cargo.toml | 1 + gix-zlib/src/decompress.rs | 15 +- gix-zlib/src/inflate.rs | 4 +- gix-zlib/src/stream/deflate.rs | 14 +- gix-zlib/src/stream/inflate.rs | 4 +- gix-zlib/tests/zlib/inflate.rs | 6 +- gix-zlib/tests/zlib/stream/inflate.rs | 18 +- gix/Cargo.toml | 2 +- gix/examples/stats.rs | 2 +- gix/src/clone/access.rs | 10 +- gix/src/clone/checkout.rs | 8 +- gix/src/clone/fetch/mod.rs | 14 +- gix/src/clone/fetch/util.rs | 56 +- gix/src/clone/mod.rs | 22 +- gix/src/commit/mod.rs | 24 +- gix/src/commit/sign.rs | 22 +- gix/src/commit/verify.rs | 14 +- gix/src/config/cache/access.rs | 80 +- gix/src/config/cache/incubate.rs | 25 +- gix/src/config/cache/init.rs | 19 +- gix/src/config/cache/util.rs | 31 +- gix/src/config/file_mut.rs | 11 +- gix/src/config/mod.rs | 292 +--- gix/src/config/overrides.rs | 9 +- gix/src/config/snapshot/access.rs | 30 +- gix/src/config/snapshot/credential_helpers.rs | 31 +- gix/src/config/tree/keys.rs | 232 ++- gix/src/config/tree/sections/checkout.rs | 30 +- gix/src/config/tree/sections/core.rs | 146 +- gix/src/config/tree/sections/diff.rs | 63 +- gix/src/config/tree/sections/extensions.rs | 17 +- gix/src/config/tree/sections/fetch.rs | 31 +- gix/src/config/tree/sections/gitoxide.rs | 22 +- gix/src/config/tree/sections/gpg.rs | 20 +- gix/src/config/tree/sections/http.rs | 91 +- gix/src/config/tree/sections/index.rs | 23 +- gix/src/config/tree/sections/merge.rs | 16 +- gix/src/config/tree/sections/notes.rs | 16 +- gix/src/config/tree/sections/pack.rs | 33 +- gix/src/config/tree/sections/protocol.rs | 38 +- gix/src/config/tree/sections/push.rs | 19 +- gix/src/config/tree/sections/remote.rs | 19 +- gix/src/config/tree/sections/ssh.rs | 19 +- gix/src/config/tree/sections/status.rs | 16 +- gix/src/config/tree/traits.rs | 23 +- gix/src/create.rs | 37 +- gix/src/diff.rs | 13 +- gix/src/dirwalk/iter.rs | 8 +- gix/src/dirwalk/mod.rs | 5 +- gix/src/discover.rs | 10 +- gix/src/ext/tree.rs | 6 +- gix/src/filter.rs | 26 +- gix/src/head/mod.rs | 4 +- gix/src/head/peel.rs | 26 +- gix/src/id.rs | 14 +- gix/src/init.rs | 22 +- gix/src/lib.rs | 31 +- gix/src/note.rs | 30 +- gix/src/object/blob.rs | 11 +- gix/src/object/commit.rs | 28 +- gix/src/object/mod.rs | 33 +- gix/src/object/peel.rs | 12 +- gix/src/object/tag.rs | 7 +- gix/src/object/tree/diff/change.rs | 4 +- gix/src/object/tree/diff/for_each.rs | 14 +- gix/src/object/tree/diff/mod.rs | 6 +- gix/src/object/tree/editor.rs | 34 +- gix/src/object/tree/entry.rs | 6 +- gix/src/object/tree/mod.rs | 26 +- gix/src/object/tree/traverse.rs | 8 +- gix/src/open/options.rs | 4 +- gix/src/open/repository.rs | 61 +- gix/src/pathspec.rs | 8 +- gix/src/reference/edits.rs | 9 +- gix/src/reference/iter.rs | 20 +- gix/src/reference/mod.rs | 36 +- gix/src/reference/remote.rs | 8 +- gix/src/remote/access.rs | 8 +- gix/src/remote/build.rs | 22 +- gix/src/remote/connect.rs | 30 +- gix/src/remote/connection/access.rs | 10 +- gix/src/remote/connection/fetch/config.rs | 18 +- gix/src/remote/connection/fetch/mod.rs | 8 +- .../remote/connection/fetch/receive_pack.rs | 15 +- .../connection/fetch/update_refs/mod.rs | 8 +- .../connection/fetch/update_refs/tests.rs | 60 +- gix/src/remote/connection/ref_map.rs | 13 +- gix/src/remote/init.rs | 34 +- gix/src/remote/name.rs | 20 +- gix/src/remote/save.rs | 8 +- gix/src/remote/url/scheme_permission.rs | 8 +- gix/src/repository/attributes.rs | 8 +- gix/src/repository/blame.rs | 4 +- gix/src/repository/branch.rs | 6 +- gix/src/repository/checkout.rs | 4 +- gix/src/repository/config/branch.rs | 44 +- gix/src/repository/config/mod.rs | 35 +- gix/src/repository/config/transport.rs | 28 +- gix/src/repository/diff.rs | 10 +- gix/src/repository/dirwalk.rs | 12 +- gix/src/repository/filter.rs | 6 +- gix/src/repository/graph.rs | 7 +- gix/src/repository/identity.rs | 10 +- gix/src/repository/impls.rs | 20 +- gix/src/repository/index.rs | 17 +- gix/src/repository/init.rs | 4 +- gix/src/repository/location.rs | 20 +- gix/src/repository/mailmap.rs | 8 +- gix/src/repository/merge.rs | 18 +- gix/src/repository/note.rs | 4 +- gix/src/repository/object.rs | 58 +- gix/src/repository/pathspec.rs | 11 +- gix/src/repository/reference.rs | 46 +- gix/src/repository/remote.rs | 68 +- gix/src/repository/revision.rs | 34 +- gix/src/repository/shallow.rs | 4 +- gix/src/repository/submodule.rs | 12 +- gix/src/repository/worktree.rs | 22 +- gix/src/revision/spec/parse/delegate/mod.rs | 18 +- .../revision/spec/parse/delegate/navigate.rs | 14 +- .../revision/spec/parse/delegate/revision.rs | 21 +- gix/src/revision/spec/parse/mod.rs | 7 +- gix/src/revision/walk.rs | 17 +- gix/src/status/index_worktree.rs | 23 +- gix/src/status/iter/mod.rs | 11 +- gix/src/status/iter/types.rs | 7 +- gix/src/status/mod.rs | 10 +- gix/src/status/tree_index.rs | 6 +- gix/src/submodule/mod.rs | 68 +- gix/src/worktree/mod.rs | 22 +- gix/src/worktree/proxy.rs | 8 +- gix/tests/gix-init.rs | 231 ++- gix/tests/gix/clone.rs | 159 +- gix/tests/gix/commit.rs | 40 +- gix/tests/gix/config/file_mut.rs | 65 +- gix/tests/gix/config/key.rs | 143 ++ gix/tests/gix/config/mod.rs | 1 + gix/tests/gix/config/tree.rs | 613 +++++--- gix/tests/gix/diff.rs | 3 +- gix/tests/gix/head.rs | 11 +- gix/tests/gix/id.rs | 55 +- gix/tests/gix/init.rs | 67 +- gix/tests/gix/object/commit.rs | 7 +- gix/tests/gix/object/tree/diff.rs | 117 +- gix/tests/gix/object/tree/mod.rs | 27 +- gix/tests/gix/reference/mod.rs | 71 +- gix/tests/gix/reference/remote.rs | 21 +- gix/tests/gix/remote/connect.rs | 39 +- gix/tests/gix/remote/fetch.rs | 51 +- gix/tests/gix/remote/ref_map.rs | 3 +- gix/tests/gix/remote/save.rs | 26 +- gix/tests/gix/repository/blame.rs | 5 +- gix/tests/gix/repository/branch.rs | 27 +- .../config_snapshot/credential_helpers.rs | 28 +- .../repository/config/config_snapshot/mod.rs | 30 +- gix/tests/gix/repository/config/identity.rs | 11 +- gix/tests/gix/repository/config/mod.rs | 52 +- gix/tests/gix/repository/config/remote.rs | 40 +- gix/tests/gix/repository/excludes.rs | 20 +- gix/tests/gix/repository/filter.rs | 15 +- gix/tests/gix/repository/merge.rs | 3 +- gix/tests/gix/repository/mod.rs | 32 +- gix/tests/gix/repository/note.rs | 17 +- gix/tests/gix/repository/object.rs | 127 +- gix/tests/gix/repository/open.rs | 191 ++- gix/tests/gix/repository/pathspec.rs | 3 +- gix/tests/gix/repository/reference.rs | 45 +- gix/tests/gix/repository/remote.rs | 109 +- gix/tests/gix/repository/shallow.rs | 18 +- gix/tests/gix/repository/submodule.rs | 15 +- gix/tests/gix/repository/worktree.rs | 33 +- .../gix/revision/spec/from_bytes/ambiguous.rs | 212 +-- gix/tests/gix/revision/spec/from_bytes/mod.rs | 118 +- .../gix/revision/spec/from_bytes/reflog.rs | 48 +- .../gix/revision/spec/from_bytes/regex.rs | 59 +- .../gix/revision/spec/from_bytes/traverse.rs | 59 +- .../gix/revision/spec/from_bytes/util.rs | 25 +- gix/tests/gix/status.rs | 90 +- gix/tests/gix/submodule.rs | 125 +- gix/tests/repository.rs | 89 +- src/shared.rs | 5 +- tests/it/Cargo.toml | 2 +- .../no-repo/pack/clone/fail-ambiguous-host | 4 +- .../no-repo/pack/clone/fail-ambiguous-path | 2 +- .../fail-ambiguous-username/explicit-ssh | 4 +- .../fail-ambiguous-username/implicit-ssh | 4 +- .../no-repo/pack/receive/fail-ambiguous-host | 4 +- .../no-repo/pack/receive/fail-ambiguous-path | 2 +- .../fail-ambiguous-username/explicit-ssh | 4 +- .../fail-ambiguous-username/implicit-ssh | 4 +- .../invalid-branch-name-failure | 4 +- tests/snapshots/porcelain/init/fail | 2 +- .../init/fail-with-multi-element-directory | 2 +- tests/tools/src/lib.rs | 27 +- tests/tools/tests/tools/main.rs | 15 + 843 files changed, 19460 insertions(+), 10690 deletions(-) delete mode 100644 etc/plan/gix-error.md create mode 100644 gix-chunk/tests/decode.rs delete mode 100644 gix-error/src/concrete/validate.rs create mode 100644 gix-error/src/types.rs create mode 100644 gix-error/tests/error/probable_cause.rs create mode 100644 gix/tests/gix/config/key.rs diff --git a/AGENTS.md b/AGENTS.md index 248896f114e..3b835eb4acc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -40,6 +40,12 @@ uses `gix-error` (look at its `Cargo.toml`); if it does, follow the patterns bel - **Error types**: use `gix_error::Exn`, `gix_error::Exn`, or the appropriate concrete error directly, importing types under their canonical names as needed. Do not introduce crate-specific or operation-specific forwarding aliases or renamed error exports. +- **Exception results**: use `ExnResult` for results with `Exn` errors. + It defaults to `T = ()` and `E = gix_error::exn::Untyped`: use `ExnResult` for erased errors, + bare `ExnResult` for erased unit results, and `ExnMessageResult` for message contexts. + `ExnMessageResult` also defaults to unit success. Preserve concrete error types when adapting signatures. + Always import `ExnResult` and `ExnMessageResult` directly from `gix_error` (or their `gix` re-exports) + and use their bare names in signatures. - **Porcelain errors**: use the central `gix::Error` re-export at public API boundaries. Keep the underlying error type and any `Exn` parameter when adapting an existing signature. - **Static messages**: `gix_error::message("something failed")` @@ -47,7 +53,7 @@ uses `gix-error` (look at its `Cargo.toml`); if it does, follow the patterns bel - **Wrapping callee errors with context**: `.or_raise(|| message("context about what failed"))?` - **Standalone error (no callee)**: `Err(message("something went wrong").raise())` - **Wrapping an `impl Error` with context**: `err.and_raise(message("context"))` -- **Closure/callback bounds**: use `Result` (bare), not `Exn`; +- **Closure/callback bounds**: use `ExnResult` with the default erased error type; inside the function, convert with `.or_raise(|| message("..."))?`; inside the closure, convert typed to bare with `.or_erased()` - **`Exn` does NOT implement `std::error::Error`** — this is by design. @@ -55,7 +61,7 @@ uses `gix-error` (look at its `Cargo.toml`); if it does, follow the patterns bel - Example: `std::io::Error::other(exn.into_error())` - **In tests** returning `gix_testtools::Result` (= `Result<(), Box>`), `Exn` can't be used with `?` directly — use `.map_err(|e| e.into_error())?` -- **Common imports**: `use gix_error::{message, ErrorExt, ResultExt};` +- **Common imports**: `use gix_error::{message, ErrorExt, ExnMessageResult, ExnResult, ResultExt};` - See `gix-error/src/lib.rs` module docs for a full migration guide from `thiserror` ### Commit Messages diff --git a/Cargo.lock b/Cargo.lock index 1d874383a89..0decce5ed9f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1618,6 +1618,7 @@ dependencies = [ "gix-error", "gix-hash", "gix-testtools", + "insta", "pretty_assertions", "serde", ] @@ -1659,6 +1660,7 @@ dependencies = [ "gix-quote", "gix-testtools", "gix-trace", + "insta", "serde", "smallvec", "unicode-bom", @@ -1692,6 +1694,7 @@ dependencies = [ "gix-trace", "gix-traverse", "gix-worktree", + "insta", "pretty_assertions", "smallvec", ] @@ -1713,6 +1716,7 @@ dependencies = [ "gix-quote", "gix-testtools", "gix-trace", + "insta", ] [[package]] @@ -1726,6 +1730,7 @@ dependencies = [ "gix-error", "gix-hash", "gix-testtools", + "insta", "memmap2", "nonempty", "serde", @@ -1766,6 +1771,8 @@ dependencies = [ "document-features", "gix-error", "gix-path", + "gix-testtools", + "insta", "libc", "serde", ] @@ -1787,6 +1794,7 @@ dependencies = [ "gix-testtools", "gix-trace", "gix-url", + "insta", "serde", ] @@ -1799,6 +1807,7 @@ dependencies = [ "gix-error", "gix-hash", "gix-testtools", + "insta", "itoa", "jiff", "pretty_assertions", @@ -1853,6 +1862,7 @@ dependencies = [ "gix-trace", "gix-utils", "gix-worktree", + "insta", "pretty_assertions", ] @@ -1869,6 +1879,7 @@ dependencies = [ "gix-ref", "gix-sec", "gix-testtools", + "insta", "is_ci", "serial_test", "tempfile", @@ -1882,6 +1893,7 @@ dependencies = [ "bstr", "document-features", "gix-error", + "gix-testtools", "insta", ] @@ -1895,8 +1907,10 @@ dependencies = [ "crossbeam-channel", "document-features", "gix-path", + "gix-testtools", "gix-trace", "gix-utils", + "insta", "libc", "once_cell", "parking_lot", @@ -1927,6 +1941,7 @@ dependencies = [ "gix-trace", "gix-utils", "gix-worktree", + "insta", "smallvec", ] @@ -1939,7 +1954,9 @@ dependencies = [ "gix-error", "gix-features", "gix-path", + "gix-testtools", "gix-utils", + "insta", "is_ci", "serde", "tempfile", @@ -1983,6 +2000,7 @@ dependencies = [ "gix-features", "gix-hash", "gix-testtools", + "insta", "serde", "sha1dc", "sha2", @@ -2069,7 +2087,9 @@ version = "24.0.0" dependencies = [ "gix-error", "gix-tempfile", + "gix-testtools", "gix-utils", + "insta", "tempfile", ] @@ -2093,6 +2113,7 @@ dependencies = [ "gix-date", "gix-error", "gix-testtools", + "insta", "serde", ] @@ -2124,6 +2145,7 @@ dependencies = [ "gix-trace", "gix-utils", "gix-worktree", + "insta", "nonempty", "pretty_assertions", "serde", @@ -2157,6 +2179,7 @@ dependencies = [ "gix-object", "gix-odb", "gix-testtools", + "insta", ] [[package]] @@ -2210,6 +2233,7 @@ dependencies = [ "gix-quote", "gix-testtools", "gix-zlib", + "insta", "maplit", "memmap2", "parking_lot", @@ -2265,7 +2289,9 @@ dependencies = [ "gix-macros", "gix-odb", "gix-pack", + "gix-testtools", "gix-trace", + "insta", "pin-project-lite", "serde", ] @@ -2276,8 +2302,10 @@ version = "0.12.6" dependencies = [ "bstr", "gix-error", + "gix-testtools", "gix-trace", "gix-validate", + "insta", "serial_test", "tempfile", "windows 0.62.2", @@ -2296,6 +2324,7 @@ dependencies = [ "gix-glob", "gix-path", "gix-testtools", + "insta", "serial_test", ] @@ -2308,6 +2337,7 @@ dependencies = [ "gix-config-value", "gix-error", "gix-testtools", + "insta", "parking_lot", "rustix", "serial_test", @@ -2338,9 +2368,11 @@ dependencies = [ "gix-refspec", "gix-revwalk", "gix-shallow", + "gix-testtools", "gix-trace", "gix-transport", "gix-utils", + "insta", "nonempty", "serde", ] @@ -2487,6 +2519,7 @@ dependencies = [ "gix-testtools", "gix-worktree", "hashbrown 0.16.1", + "insta", "portable-atomic", "pretty_assertions", "windows-sys 0.61.2", @@ -2505,6 +2538,7 @@ dependencies = [ "gix-refspec", "gix-testtools", "gix-url", + "insta", ] [[package]] @@ -2514,6 +2548,8 @@ dependencies = [ "dashmap", "document-features", "gix-fs", + "gix-testtools", + "insta", "libc", "parking_lot", "signal-hook 0.4.4", @@ -2608,8 +2644,10 @@ dependencies = [ "gix-path", "gix-quote", "gix-sec", + "gix-testtools", "gix-transport", "gix-url", + "insta", "parking_lot", "pin-project-lite", "reqwest", @@ -2651,6 +2689,7 @@ dependencies = [ "gix-path", "gix-testtools", "gix-utils", + "insta", "percent-encoding", "serde", "serde_json", @@ -2674,6 +2713,7 @@ dependencies = [ "bstr", "gix-error", "gix-testtools", + "insta", ] [[package]] @@ -2696,6 +2736,7 @@ dependencies = [ "gix-path", "gix-testtools", "gix-validate", + "insta", "serde", ] @@ -2716,6 +2757,7 @@ dependencies = [ "gix-testtools", "gix-worktree", "gix-worktree-state", + "insta", "io-close", "walkdir", ] @@ -2737,6 +2779,7 @@ dependencies = [ "gix-traverse", "gix-worktree", "gix-worktree-stream", + "insta", "parking_lot", ] @@ -2746,6 +2789,7 @@ version = "0.1.0" dependencies = [ "bstr", "gix-error", + "insta", "serde", "zlib-rs", ] diff --git a/Cargo.toml b/Cargo.toml index 77abe48ec98..9c586acd93d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -381,7 +381,6 @@ explicit_deref_methods = "allow" # x22 inconsistent_struct_constructor = "allow" # x18 range_plus_one = "allow" # x17 inefficient_to_string = "allow" # x14 -from_iter_instead_of_collect = "allow" # x13 unused_self = "allow" # x10 many_single_char_names = "allow" # x10 manual_string_new = "allow" # x10 diff --git a/etc/plan/gix-error.md b/etc/plan/gix-error.md deleted file mode 100644 index 0f8b67aa1e8..00000000000 --- a/etc/plan/gix-error.md +++ /dev/null @@ -1,189 +0,0 @@ -# gix-error / `Exn` Migration Plan - -Source issue: [GitoxideLabs/gitoxide#2351](https://github.com/GitoxideLabs/gitoxide/issues/2351) -Imported on: 2026-04-22 -Working assumption: the checkboxes in this file reflect the current `gix-error` branch in this checkout, not only the historical state of the upstream issue. - -## Mission - -Finish the migration from `thiserror`-based error enums to `gix-error` / `Exn`, while preserving three caller-facing properties: - -- typed validation failures stay identifiable as `gix_error::ValidationError` -- repository-open failures keep a distinct `NotARepository` path -- crate-local plumbing errors stay cheap and composable until they are intentionally erased at the `gix` boundary - -## Constraints - -- Use the current workspace as the source of truth for completion. -- Treat upstream PR history as context only. Several linked PRs are merged upstream, but that work is not fully reflected in this branch. -- Keep migration leaf-first so downstream breakage stays local. - -## Reconciled Status - -- [x] Proof of concept completed in [#2352](https://github.com/GitoxideLabs/gitoxide/pull/2352), merged on January 12, 2026. -- [x] `anyhow` / source-chain integration completed in [#2383](https://github.com/GitoxideLabs/gitoxide/pull/2383), merged on January 19, 2026. -- [ ] Make `cargo nextest --workflow` run without `--exclude gix-error`. - Evidence: `.github/workflows/ci.yml` still excludes `gix-error`. -- [ ] Replace `thiserror` with `gix-error` everywhere. - Evidence: 33 crates in this branch still carry a `thiserror` dependency and/or `thiserror::Error` usage. -- [x] Keep `NotARepository` distinct from generic open failures. - Evidence: `gix::open::Error::NotARepository` exists and is asserted in tests. -- [ ] Use `gix_error::Error` in tests when that simplifies `Exn`-heavy paths. - Evidence: partially adopted, but not clearly finished as a repo-wide sweep. -- [x] Make `gix-validate` failures identifiable as `gix_error::ValidationError`. - Evidence: `gix-error` exports `ValidationError`, and downstream crates already use it directly. - -## Current Snapshot - -Workspace scan basis: - -- `thiserror` dependency present in `Cargo.toml` -- `thiserror::Error` mentions under `src/**/*.rs` - -Result on 2026-04-22: - -- 32 crates are done -- 33 crates are still pending - -## Linked Upstream PRs - -- [x] [#2352](https://github.com/GitoxideLabs/gitoxide/pull/2352) `gix-error` punch-through -- [x] [#2373](https://github.com/GitoxideLabs/gitoxide/pull/2373) Convert more crates to `gix-error` -- [x] [#2378](https://github.com/GitoxideLabs/gitoxide/pull/2378) `gix-commitgraph` to `gix-error` -- [x] [#2383](https://github.com/GitoxideLabs/gitoxide/pull/2383) `anyhow` integration for `gix-error` -- [x] [#2389](https://github.com/GitoxideLabs/gitoxide/pull/2389) custom error implementation follow-up -- [x] [#2390](https://github.com/GitoxideLabs/gitoxide/pull/2390) make validate errors non-exhaustive -- [x] [#2396](https://github.com/GitoxideLabs/gitoxide/pull/2396) `gix-actor` -- [x] [#2400](https://github.com/GitoxideLabs/gitoxide/pull/2400) more `gix-error` -- [x] [#2423](https://github.com/GitoxideLabs/gitoxide/pull/2423) batch 1, part 1 - -## Migration Rules - -- Replace `thiserror` in `Cargo.toml` with `gix-error`. -- Use canonical `gix-error` types directly in signatures, with imports where helpful. Forwarding aliases used during individual crate migrations are removed in the final cleanup pass. -- Convert validation/parsing-only paths to `gix_error::ValidationError`. -- Replace `#[from]` / `#[source]` propagation with `.or_raise(...)` or `.ok_or_raise(...)`. -- Keep `gix_error::Error` as the erased boundary type, mainly at `gix` and in tests that benefit from downcasting or frame inspection. -- When migrating a crate, run its local checks and at least one downstream compile pass. - -## Execution Order - -### Batch 1: leaves - -- [ ] `gix-hash` - 7 -- [ ] `gix-url` - 3 -- [ ] `gix-packetline` - 3 -- [ ] `gix-features` - 3 -- [ ] `gix-path` - 2 -- [ ] `gix-attributes` - 2 -- [x] `gix-quote` -- [ ] `gix-lock` - 1 -- [x] `gix-fs` -- [x] `gix-bitmap` -- [x] `gix-mailmap` - -### Batch 2: simple dependents - -- [ ] `gix-object` - 11 -- [ ] `gix-config-value` - 2 -- [ ] `gix-shallow` - 2 -- [ ] `gix-refspec` - 1 - -### Batch 3: ref / filter layer - -- [ ] `gix-ref` - 22 -- [ ] `gix-filter` - 18 -- [ ] `gix-revwalk` - 4 -- [ ] `gix-pathspec` - 3 -- [ ] `gix-prompt` - 1 - -### Batch 4: config and discovery - -- [ ] `gix-traverse` - 3 -- [ ] `gix-config` - 11 -- [ ] `gix-credentials` - 5 -- [ ] `gix-discover` - 4 - -### Batch 5: transport and index-adjacent - -- [ ] `gix-index` - 11 -- [ ] `gix-transport` - 10 -- [x] `gix-worktree-stream` -- [ ] `gix-submodule` - 6 - -### Batch 6: diff / protocol tier - -- [ ] `gix-diff` - 8 -- [ ] `gix-protocol` - 8 -- [ ] `gix-dir` - 1 -- [ ] `gix-worktree-state` - 1 -- [x] `gix-archive` - -### Batch 7: heavier consumers - -- [ ] `gix-pack` - 23 -- [ ] `gix-merge` - 8 -- [ ] `gix-status` - 3 -- [ ] `gix-blame` - 1 - -### Batch 8: object database - -- [ ] `gix-odb` - 11 - -### Batch 9: top-level API - -- [ ] `gix` - 138 - -## Already Done Outside The Active Queue - -- [x] `gix-actor` -- [x] `gix-chunk` -- [x] `gix-command` -- [x] `gix-commitgraph` -- [x] `gix-date` -- [x] `gix-error` -- [x] `gix-fetchhead` -- [x] `gix-fsck` -- [x] `gix-glob` -- [x] `gix-hashtable` -- [x] `gix-ignore` -- [x] `gix-lfs` -- [x] `gix-macros` -- [x] `gix-negotiate` -- [x] `gix-note` -- [x] `gix-rebase` -- [x] `gix-revision` -- [x] `gix-sec` -- [x] `gix-sequencer` -- [x] `gix-tempfile` -- [x] `gix-tix` -- [x] `gix-trace` -- [x] `gix-tui` -- [x] `gix-utils` -- [x] `gix-validate` -- [x] `gix-worktree` - -## Immediate Next Moves - -- [ ] Finish Batch 1 in this branch before assuming the upstream batch-1 PR history is present locally. -- [ ] Remove the `gix-error` special-case from `.github/workflows/ci.yml`. -- [ ] Re-scan counts after each crate or mini-batch instead of trusting the original issue numbers. -- [ ] Only move `gix` itself after all plumbing crates beneath it are clean. - -## Exit Criteria - -- [ ] No crate in this workspace depends on `thiserror`. -- [ ] No `src/**/*.rs` file in this workspace mentions `thiserror::Error`. -- [ ] `cargo nextest --workflow` no longer excludes `gix-error`. -- [ ] The `gix` boundary still returns `gix_error::Error` where type erasure is desired. -- [ ] Validation-heavy crates still expose typed validation failures where callers need them. - -## Final Cleanup at the Stack Tip - -Remove the forwarding aliases retained during crate-by-crate migration in one self-contained breaking commit, including all downstream adaptations. - -- [x] Remove direct and indirect forwarding aliases, renamed error exports, and test-only synonyms. -- [x] Remove namespaces and files that only exposed those aliases, and update documentation links. -- [x] Keep the central `gix::{Error, Exn}` and `gix::error` exports, canonical unrenamed re-exports, required associated types, and aliases with additional structure. -- [x] Preserve concrete error types, `Exn` parameters, feature-dependent alternatives, messages, and error chains. -- [x] Pass `etc/scripts/ci-check-local.sh --thorough` on the completed tip commit. diff --git a/gitoxide-core/src/hours/mod.rs b/gitoxide-core/src/hours/mod.rs index f974b8e2326..05ffcf17222 100644 --- a/gitoxide-core/src/hours/mod.rs +++ b/gitoxide-core/src/hours/mod.rs @@ -1,3 +1,4 @@ +use gix::ExnMessageResult; use std::{collections::BTreeSet, io, path::Path, time::Instant}; use anyhow::bail; @@ -85,7 +86,7 @@ fn parse_trailer_identity(trailer: gix::objs::commit::message::body::TrailerRef< fn commit_author_identities( commit_data: &[u8], object_hash: gix::hash::Kind, -) -> Result<(gix::actor::SignatureRef<'_>, SmallVec<[ParsedIdentity<'_>; 2]>), gix::error::ValidationError> { +) -> ExnMessageResult<(gix::actor::SignatureRef<'_>, SmallVec<[ParsedIdentity<'_>; 2]>)> { let commit = gix::objs::CommitRef::from_bytes(commit_data, object_hash)?; let author = commit.author()?.trim(); let mut authors = smallvec![ParsedIdentity::Borrowed(gix::actor::IdentityRef::from(author))]; @@ -254,7 +255,7 @@ where } commit_idx += 1; } - Err(err) if err.downcast_any_ref::().is_some() => { + Err(err) if err.is_not_found() => { is_shallow = true; break; } diff --git a/gitoxide-core/src/index/checkout.rs b/gitoxide-core/src/index/checkout.rs index 6bd4654b8bf..a0b747bc957 100644 --- a/gitoxide-core/src/index/checkout.rs +++ b/gitoxide-core/src/index/checkout.rs @@ -4,7 +4,7 @@ use std::{ }; use anyhow::bail; -use gix::{NestedProgress, Progress, worktree::state::checkout}; +use gix::{ExnResult, NestedProgress, Progress, worktree::state::checkout}; use crate::{ index, @@ -183,7 +183,7 @@ impl gix::objs::Find for EmptyOrDb where Find: gix::objs::Find, { - fn try_find<'a>(&self, id: &gix::oid, buf: &'a mut Vec) -> Result>, gix::Exn> { + fn try_find<'a>(&self, id: &gix::oid, buf: &'a mut Vec) -> ExnResult>> { if self.empty_files { // We always want to query the ODB here… let Some(kind) = self.db.try_find(id, buf)?.map(|d| d.kind) else { @@ -206,7 +206,7 @@ where struct Empty; impl gix::objs::Find for Empty { - fn try_find<'a>(&self, id: &gix::oid, buffer: &'a mut Vec) -> Result>, gix::Exn> { + fn try_find<'a>(&self, id: &gix::oid, buffer: &'a mut Vec) -> ExnResult>> { buffer.clear(); Ok(Some(gix::objs::Data { kind: gix::object::Kind::Blob, diff --git a/gitoxide-core/src/net.rs b/gitoxide-core/src/net.rs index b01af00fbc1..0e6c55ad2c9 100644 --- a/gitoxide-core/src/net.rs +++ b/gitoxide-core/src/net.rs @@ -1,5 +1,8 @@ use std::str::FromStr; +#[cfg(any(feature = "async-client", feature = "blocking-client"))] +use gix::ExnMessageResult; + #[cfg(feature = "async-client")] use gix::protocol::transport::client::async_io as io_mode; #[cfg(feature = "blocking-client")] @@ -45,7 +48,7 @@ mod impls { pub async fn connect( url: Url, options: io_mode::connect::Options, -) -> Result>, gix::Exn> +) -> ExnMessageResult>> where Url: TryInto, E: std::error::Error + Send + Sync + 'static, diff --git a/gitoxide-core/src/pack/create.rs b/gitoxide-core/src/pack/create.rs index d5170fee45b..837d2c0a303 100644 --- a/gitoxide-core/src/pack/create.rs +++ b/gitoxide-core/src/pack/create.rs @@ -1,11 +1,11 @@ use std::{ffi::OsStr, io, path::Path, str::FromStr, time::Instant}; use anyhow::anyhow; -use gix::error::ResultExt; use gix::{ Count, NestedProgress, Progress, hash, hash::ObjectId, interrupt, objs::bstr::ByteVec, odb::pack, parallel::InOrderIter, prelude::Finalize, progress, traverse, }; +use gix::{ExnResult, error::ResultExt}; use crate::OutputFormat; @@ -107,7 +107,7 @@ where P: NestedProgress, P::SubProgress: 'static, { - type ObjectIdIter = dyn Iterator> + Send; + type ObjectIdIter = dyn Iterator> + Send; let repo = gix::discover(repository_path)?; let pack_compression = repo.pack_compression()?; diff --git a/gitoxide-core/src/pack/explode.rs b/gitoxide-core/src/pack/explode.rs index 784d2b8462a..372003d0f66 100644 --- a/gitoxide-core/src/pack/explode.rs +++ b/gitoxide-core/src/pack/explode.rs @@ -6,7 +6,10 @@ use std::{ }; use anyhow::{Result, anyhow}; -use gix::error::{ErrorExt, NotFoundError, ResultExt, message}; +use gix::{ + ExnResult, + error::{ErrorExt, ResultExt, message}, +}; use gix::{ NestedProgress, hash::ObjectId, @@ -76,21 +79,21 @@ enum OutputWriter { } impl gix::objs::Write for OutputWriter { - fn write_buf(&self, kind: object::Kind, from: &[u8]) -> Result { + fn write_buf(&self, kind: object::Kind, from: &[u8]) -> ExnResult { match self { OutputWriter::Loose(db) => db.write_buf(kind, from), OutputWriter::Sink(db) => db.write_buf(kind, from), } } - fn write_buf_with_known_id(&self, kind: object::Kind, from: &[u8], id: ObjectId) -> Result { + fn write_buf_with_known_id(&self, kind: object::Kind, from: &[u8], id: ObjectId) -> ExnResult { match self { OutputWriter::Loose(db) => db.write_buf_with_known_id(kind, from, id), OutputWriter::Sink(db) => db.write_buf_with_known_id(kind, from, id), } } - fn write_stream(&self, kind: object::Kind, size: u64, from: &mut dyn Read) -> Result { + fn write_stream(&self, kind: object::Kind, size: u64, from: &mut dyn Read) -> ExnResult { match self { OutputWriter::Loose(db) => db.write_stream(kind, size, from), OutputWriter::Sink(db) => db.write_stream(kind, size, from), @@ -103,7 +106,7 @@ impl gix::objs::Write for OutputWriter { size: u64, from: &mut dyn Read, id: ObjectId, - ) -> Result { + ) -> ExnResult { match self { OutputWriter::Loose(db) => db.write_stream_with_known_id(kind, size, from, id), OutputWriter::Sink(db) => db.write_stream_with_known_id(kind, size, from, id), @@ -211,7 +214,7 @@ pub fn pack_or_pack_index( )); } else { return Err(err - .and_raise(message!("{object_kind} object wasn't re-encoded without change")) + .raise(message!("{object_kind} object wasn't re-encoded without change")) .erased()); } } @@ -224,7 +227,7 @@ pub fn pack_or_pack_index( ) })? .ok_or_else(|| { - NotFoundError::new(format!( + gix::error::not_found(format!( "The recently written file for loose object {written_id} could not be found" )) .raise_erased() diff --git a/gitoxide-core/src/pack/receive.rs b/gitoxide-core/src/pack/receive.rs index 6650d687541..3b88dc221c8 100644 --- a/gitoxide-core/src/pack/receive.rs +++ b/gitoxide-core/src/pack/receive.rs @@ -5,12 +5,15 @@ use std::{ }; use crate::{OutputFormat, net, pack::receive::protocol::fetch::negotiate}; -use gix::error::{ResultExt, message}; #[cfg(feature = "async-client")] use gix::protocol::transport::client::async_io::connect; #[cfg(feature = "blocking-client")] use gix::protocol::transport::client::blocking_io::connect; use gix::{DynNestedProgress, config::tree::Key, protocol::bisync}; +use gix::{ + ExnMessageResult, + error::{ResultExt, message}, +}; pub use gix::{ NestedProgress, Progress, hash::ObjectId, @@ -164,7 +167,7 @@ struct Negotiate<'a> { } impl gix::protocol::fetch::Negotiate for Negotiate<'_> { - fn mark_complete_and_common_ref(&mut self) -> Result> { + fn mark_complete_and_common_ref(&mut self) -> ExnMessageResult { Ok(negotiate::Action::MustNegotiate { remote_ref_target_known: vec![], /* we don't really negotiate */ }) @@ -184,7 +187,7 @@ impl gix::protocol::fetch::Negotiate for Negotiate<'_> { _state: &mut negotiate::one_round::State, _arguments: &mut Arguments, _previous_response: Option<&Response>, - ) -> Result<(negotiate::Round, bool), gix::Exn> { + ) -> ExnMessageResult<(negotiate::Round, bool)> { Ok(( negotiate::Round { haves_sent: 0, diff --git a/gitoxide-core/src/query/engine/update.rs b/gitoxide-core/src/query/engine/update.rs index e835e718ee4..69fad65d04f 100644 --- a/gitoxide-core/src/query/engine/update.rs +++ b/gitoxide-core/src/query/engine/update.rs @@ -7,7 +7,7 @@ use std::{ use anyhow::{anyhow, bail}; use gix::{ - Count, Progress, + Count, ExnResult, Progress, bstr::{BStr, BString, ByteSlice}, diff::{blob::platform::prepare_diff::Operation, rewrites::CopySource}, features::progress, @@ -384,11 +384,7 @@ pub fn update( where Find: gix::prelude::Find + Clone, { - fn try_find<'b>( - &self, - id: &gix::oid, - buf: &'b mut Vec, - ) -> Result>, gix::Exn> { + fn try_find<'b>(&self, id: &gix::oid, buf: &'b mut Vec) -> ExnResult>> { let obj = self.inner.try_find(id, buf)?; let Some(obj) = obj else { return Ok(None) }; if !obj.kind.is_commit() { @@ -444,7 +440,7 @@ pub fn update( break; } } - Err(traverse_err) if traverse_err.downcast_any_ref::().is_some() => { + Err(traverse_err) if traverse_err.is_not_found() => { writeln!(err, "shallow repository - commit history is truncated").ok(); break; } diff --git a/gitoxide-core/src/repository/credential.rs b/gitoxide-core/src/repository/credential.rs index d4229c18605..aff1ac697bc 100644 --- a/gitoxide-core/src/repository/credential.rs +++ b/gitoxide-core/src/repository/credential.rs @@ -1,18 +1,19 @@ +use gix::ExnResult; pub fn function(repo: Option, action: gix::credentials::program::main::Action) -> anyhow::Result<()> { use gix::credentials::program::main::Action::*; - use gix::error::{OptionExt, ResultExt, ValidationError, message}; + use gix::error::{OptionExt, ResultExt, message}; gix::credentials::program::main( Some(action.as_str().into()), std::io::stdin(), std::io::stdout(), gix::credentials::protocol::ContextOptions::default(), - |action, context| -> Result<_, gix::Exn> { + |action, context| -> ExnResult<_> { let url = context .url .clone() .or_else(|| context.to_url()) .ok_or_raise_erased(|| { - ValidationError::new("Either 'url' field or both 'protocol' and 'host' fields must be provided") + gix::error::validation("Either 'url' field or both 'protocol' and 'host' fields must be provided") })?; let url = gix::url::parse(&url).or_erased()?; diff --git a/gitoxide-core/src/repository/revision/explain.rs b/gitoxide-core/src/repository/revision/explain.rs index f2c042e6c86..f5098eab6c3 100644 --- a/gitoxide-core/src/repository/revision/explain.rs +++ b/gitoxide-core/src/repository/revision/explain.rs @@ -1,6 +1,6 @@ use anyhow::bail; use gix::{ - Exn, + ExnResult, bstr::{BStr, BString}, revision::plumbing::{ spec, @@ -41,7 +41,7 @@ impl<'a> Explain<'a> { err: None, } } - fn prefix(&mut self) -> Result<(), Exn> { + fn prefix(&mut self) -> ExnResult { self.call += 1; write!(self.out, "{:02}. ", self.call).ok(); Ok(()) @@ -57,18 +57,14 @@ impl<'a> Explain<'a> { } impl delegate::Revision for Explain<'_> { - fn find_ref(&mut self, name: &BStr) -> Result<(), Exn> { + fn find_ref(&mut self, name: &BStr) -> ExnResult { self.prefix()?; self.ref_name = Some(name.into()); writeln!(self.out, "Lookup the '{name}' reference").ok(); Ok(()) } - fn disambiguate_prefix( - &mut self, - prefix: gix::hash::Prefix, - hint: Option>, - ) -> Result<(), Exn> { + fn disambiguate_prefix(&mut self, prefix: gix::hash::Prefix, hint: Option>) -> ExnResult { self.prefix()?; self.oid_prefix = Some(prefix); writeln!( @@ -86,7 +82,7 @@ impl delegate::Revision for Explain<'_> { Ok(()) } - fn reflog(&mut self, query: ReflogLookup) -> Result<(), Exn> { + fn reflog(&mut self, query: ReflogLookup) -> ExnResult { self.prefix()?; self.has_implicit_anchor = true; let ref_name: &BStr = self.ref_name.as_ref().map_or_else(|| "HEAD".into(), AsRef::as_ref); @@ -103,14 +99,14 @@ impl delegate::Revision for Explain<'_> { Ok(()) } - fn nth_checked_out_branch(&mut self, branch_no: usize) -> Result<(), Exn> { + fn nth_checked_out_branch(&mut self, branch_no: usize) -> ExnResult { self.prefix()?; self.has_implicit_anchor = true; writeln!(self.out, "Find the {branch_no}th checked-out branch of 'HEAD'").ok(); Ok(()) } - fn sibling_branch(&mut self, kind: SiblingBranch) -> Result<(), Exn> { + fn sibling_branch(&mut self, kind: SiblingBranch) -> ExnResult { self.prefix()?; self.has_implicit_anchor = true; let ref_info = match self.ref_name.as_ref() { @@ -132,7 +128,7 @@ impl delegate::Revision for Explain<'_> { } impl delegate::Navigate for Explain<'_> { - fn traverse(&mut self, kind: Traversal) -> Result<(), Exn> { + fn traverse(&mut self, kind: Traversal) -> ExnResult { self.prefix()?; let name = self.revision_name(); writeln!( @@ -147,7 +143,7 @@ impl delegate::Navigate for Explain<'_> { Ok(()) } - fn peel_until(&mut self, kind: PeelTo<'_>) -> Result<(), Exn> { + fn peel_until(&mut self, kind: PeelTo<'_>) -> ExnResult { self.prefix()?; writeln!( self.out, @@ -163,7 +159,7 @@ impl delegate::Navigate for Explain<'_> { Ok(()) } - fn find(&mut self, regex: &BStr, negated: bool) -> Result<(), Exn> { + fn find(&mut self, regex: &BStr, negated: bool) -> ExnResult { self.prefix()?; self.has_implicit_anchor = true; let negate_text = if negated { "does not match" } else { "matches" }; @@ -188,7 +184,7 @@ impl delegate::Navigate for Explain<'_> { Ok(()) } - fn index_lookup(&mut self, path: &BStr, stage: u8) -> Result<(), Exn> { + fn index_lookup(&mut self, path: &BStr, stage: u8) -> ExnResult { self.prefix()?; self.has_implicit_anchor = true; writeln!( @@ -210,7 +206,7 @@ impl delegate::Navigate for Explain<'_> { } impl delegate::Kind for Explain<'_> { - fn kind(&mut self, kind: spec::Kind) -> Result<(), Exn> { + fn kind(&mut self, kind: spec::Kind) -> ExnResult { self.prefix()?; self.call = 0; writeln!( @@ -232,7 +228,7 @@ impl delegate::Kind for Explain<'_> { } impl Delegate for Explain<'_> { - fn done(&mut self) -> Result<(), Exn> { + fn done(&mut self) -> ExnResult { if !self.has_implicit_anchor && self.ref_name.is_none() && self.oid_prefix.is_none() { self.err = Some("Incomplete specification lacks its anchor, like a reference or object name".into()); } diff --git a/gix-actor/Cargo.toml b/gix-actor/Cargo.toml index 53ed2a4b336..52c5e330fa8 100644 --- a/gix-actor/Cargo.toml +++ b/gix-actor/Cargo.toml @@ -33,6 +33,7 @@ serde = { version = "1.0.114", optional = true, default-features = false, featur document-features = { version = "0.2.0", optional = true } [dev-dependencies] +insta = "1.46.3" pretty_assertions = "1.0.0" gix-testtools = { path = "../tests/tools", default-features = false } gix-hash = { path = "../gix-hash" } diff --git a/gix-actor/src/identity.rs b/gix-actor/src/identity.rs index 81c09a598f9..3851e19246e 100644 --- a/gix-actor/src/identity.rs +++ b/gix-actor/src/identity.rs @@ -1,4 +1,5 @@ use bstr::ByteSlice; +use gix_error::ExnMessageResult; use crate::{Identity, IdentityRef, signature::decode}; @@ -6,7 +7,7 @@ impl<'a> IdentityRef<'a> { /// Deserialize an identity from the given `data`. /// /// Typical input is `Name 1700000000 +0000`. - pub fn from_bytes(mut data: &'a [u8]) -> Result { + pub fn from_bytes(mut data: &'a [u8]) -> ExnMessageResult { Self::from_bytes_consuming(&mut data) } @@ -14,7 +15,7 @@ impl<'a> IdentityRef<'a> { /// /// Typical input is `Name 1700000000 +0000`; on success, /// `data` points to the bytes immediately after the closing `>`. - pub fn from_bytes_consuming(data: &mut &'a [u8]) -> Result { + pub fn from_bytes_consuming(data: &mut &'a [u8]) -> ExnMessageResult { decode::identity(data) } @@ -41,6 +42,9 @@ mod write { /// Output impl Identity { /// Serialize this instance to `out` in the git serialization format for signatures (but without timestamp). + /// Invalid name or email bytes are retained as `input` in the I/O error. + /// After [wrapping](gix_error::Error::from_error()), inspect them with + /// [metadata](gix_error::Error::metadata()). pub fn write_to(&self, out: &mut dyn std::io::Write) -> std::io::Result<()> { self.to_ref().write_to(out) } @@ -48,6 +52,9 @@ mod write { impl IdentityRef<'_> { /// Serialize this instance to `out` in the git serialization format for signatures (but without timestamp). + /// Invalid name or email bytes are retained as `input` in the I/O error. + /// After [wrapping](gix_error::Error::from_error()), inspect them with + /// [metadata](gix_error::Error::metadata()). pub fn write_to(&self, out: &mut dyn std::io::Write) -> std::io::Result<()> { out.write_all(validated_token(self.name).map_err(std::io::Error::other)?)?; out.write_all(b" ")?; diff --git a/gix-actor/src/signature/decode.rs b/gix-actor/src/signature/decode.rs index 3889756734a..ba63165be49 100644 --- a/gix-actor/src/signature/decode.rs +++ b/gix-actor/src/signature/decode.rs @@ -1,11 +1,12 @@ pub(crate) mod function { use bstr::ByteSlice; - use gix_error::ValidationError; + use gix_error::ExnMessageResult; + use gix_error::validation; use crate::{IdentityRef, SignatureRef}; /// Parse a signature from the bytes input `i`, and change it to point to the unparsed bytes afterwards. - pub fn decode<'a>(i: &mut &'a [u8]) -> Result, ValidationError> { + pub fn decode<'a>(i: &mut &'a [u8]) -> ExnMessageResult> { let identity = identity(i)?; if i.first() == Some(&b' ') { *i = &i[1..]; @@ -26,23 +27,23 @@ pub(crate) mod function { } /// Parse an identity from the bytes input `i` (like `name `). - pub fn identity<'a>(i: &mut &'a [u8]) -> Result, ValidationError> { + pub fn identity<'a>(i: &mut &'a [u8]) -> ExnMessageResult> { let eol_idx = i.find_byte(b'\n').unwrap_or(i.len()); let right_delim_idx = i[..eol_idx] .rfind_byte(b'>') - .ok_or_else(|| ValidationError::new("Closing '>' not found"))?; + .ok_or_else(|| validation("Closing '>' not found"))?; let i_name_and_email = &i[..right_delim_idx]; let skip_from_right = i_name_and_email.iter().rev().take_while(|b| **b == b'>').count(); let left_delim_idx = i_name_and_email .find_byte(b'<') - .ok_or_else(|| ValidationError::new("Opening '<' not found"))?; + .ok_or_else(|| validation("Opening '<' not found"))?; let skip_from_left = i[left_delim_idx..].iter().take_while(|b| **b == b'<').count(); let mut name = i[..left_delim_idx].as_bstr(); name = name.strip_suffix(b" ").unwrap_or(name).as_bstr(); let email = i .get(left_delim_idx + skip_from_left..right_delim_idx - skip_from_right) - .ok_or_else(|| ValidationError::new("Skipped parts run into each other"))? + .ok_or_else(|| validation("Skipped parts run into each other"))? .as_bstr(); *i = i.get(right_delim_idx + 1..).unwrap_or(&[]); Ok(IdentityRef { name, email }) @@ -57,11 +58,11 @@ pub use function::identity; #[cfg(test)] mod tests { mod parse_signature { - use gix_error::ValidationError; + use gix_error::ExnMessageResult; use crate::SignatureRef; - fn decode(mut i: &[u8]) -> Result<(&[u8], SignatureRef<'_>), ValidationError> { + fn decode(mut i: &[u8]) -> ExnMessageResult<(&[u8], SignatureRef<'_>)> { SignatureRef::from_bytes_consuming(&mut i).map(|signature| (i, signature)) } @@ -142,12 +143,8 @@ mod tests { #[test] fn invalid_signature() { - assert_eq!( - decode(b"hello < 12345 -1215") - .expect_err("parse fails as > is missing") - .to_string(), - "Closing '>' not found" - ); + insta::assert_debug_snapshot!(decode(b"hello < 12345 -1215") + .expect_err("parse fails as > is missing"), "invalid signature", @"Closing '>' not found"); } #[test] diff --git a/gix-actor/src/signature/mod.rs b/gix-actor/src/signature/mod.rs index a5fcb560646..71c4232b0d5 100644 --- a/gix-actor/src/signature/mod.rs +++ b/gix-actor/src/signature/mod.rs @@ -1,5 +1,6 @@ mod _ref { use bstr::ByteSlice; + use gix_error::ExnMessageResult; use crate::{IdentityRef, Signature, SignatureRef, signature::decode}; @@ -8,7 +9,7 @@ mod _ref { /// Deserialize a signature from the given `data`. /// /// Typical input is `Name 1700000000 +0000`. - pub fn from_bytes(mut data: &'a [u8]) -> Result, gix_error::ValidationError> { + pub fn from_bytes(mut data: &'a [u8]) -> ExnMessageResult> { Self::from_bytes_consuming(&mut data) } @@ -17,12 +18,12 @@ mod _ref { /// Typical input is `Name 1700000000 +0000`; on /// success, `data` points to the bytes immediately after the parsed /// signature. - pub fn from_bytes_consuming(data: &mut &'a [u8]) -> Result, gix_error::ValidationError> { + pub fn from_bytes_consuming(data: &mut &'a [u8]) -> ExnMessageResult> { decode(data) } /// Try to parse the timestamp and create an owned instance from this shared one. - pub fn to_owned(&self) -> Result { + pub fn to_owned(&self) -> ExnMessageResult { Ok(Signature { name: self.name.to_owned(), email: self.email.to_owned(), @@ -65,8 +66,8 @@ mod _ref { /// Parse the `time` field for access to the passed time since unix epoch, and the time offset. /// The format is expected to be [raw](gix_date::parse_header()). - pub fn time(&self) -> Result { - self.time.parse() + pub fn time(&self) -> ExnMessageResult { + Ok(self.time.parse()?) } } } @@ -104,12 +105,16 @@ mod convert { pub(crate) mod write { use bstr::{BStr, ByteSlice}; use gix_date::parse::TimeBuf; + use gix_error::ExnMessageResult; use crate::{Signature, SignatureRef}; /// Output impl Signature { /// Serialize this instance to `out` in the git serialization format for actors. + /// Invalid signature field bytes are retained as `input` in the I/O error. + /// After [wrapping](gix_error::Error::from_error()), inspect them with + /// [metadata](gix_error::Error::metadata()). pub fn write_to(&self, out: &mut dyn std::io::Write) -> std::io::Result<()> { let mut buf = TimeBuf::default(); self.to_ref(&mut buf).write_to(out) @@ -122,6 +127,9 @@ pub(crate) mod write { impl SignatureRef<'_> { /// Serialize this instance to `out` in the git serialization format for actors. + /// Invalid signature field bytes are retained as `input` in the I/O error. + /// After [wrapping](gix_error::Error::from_error()), inspect them with + /// [metadata](gix_error::Error::metadata()). pub fn write_to(&self, out: &mut dyn std::io::Write) -> std::io::Result<()> { out.write_all(validated_token(self.name).map_err(std::io::Error::other)?)?; out.write_all(b" ")?; @@ -136,12 +144,13 @@ pub(crate) mod write { } } - pub(crate) fn validated_token(name: &BStr) -> Result<&BStr, gix_error::ValidationError> { + pub(crate) fn validated_token(name: &BStr) -> ExnMessageResult<&BStr> { if name.find_byteset(b"<>\n").is_some() { - return Err(gix_error::ValidationError::new_with_input( - "Signature name or email must not contain '<', '>' or \\n", - name, - )); + return Err( + gix_error::validation("Signature name or email must not contain '<', '>' or \\n") + .with("input", name) + .into(), + ); } Ok(name) } diff --git a/gix-actor/tests/actor/identity.rs b/gix-actor/tests/actor/identity.rs index 7f600c01949..6a3a96bfe96 100644 --- a/gix-actor/tests/actor/identity.rs +++ b/gix-actor/tests/actor/identity.rs @@ -22,6 +22,7 @@ fn round_trip() -> gix_testtools::Result { #[test] fn lenient_parsing() -> gix_testtools::Result { + let mut error_snapshots = Vec::new(); for (input, expected_email) in [ ( "First Last<> >", @@ -41,11 +42,19 @@ fn lenient_parsing() -> gix_testtools::Result { let signature: Identity = identity.into(); let mut output = Vec::new(); let err = signature.write_to(&mut output).unwrap_err(); - assert_eq!( - err.to_string(), - format!(r#"Signature name or email must not contain '<', '>' or \n: {expected_email:?}"#), - "this isn't roundtrippable as the name is technically incorrect - must not contain brackets" - ); + error_snapshots.push(gix_testtools::redact_debug_snapshot(&(err), &[])); } + insta::assert_debug_snapshot!(error_snapshots, "lenient parsing", @r#" + [ + Custom { + kind: Other, + error: Signature name or email must not contain '<', '>' or \n, "input"="fl > ", + }, + Custom { + kind: Other, + error: Signature name or email must not contain '<', '>' or \n, "input"="fl ' or \\\\n\", input: Some(\"invalid < middlename\") } })" - ); + insta::assert_debug_snapshot!(signature.write_to(&mut Vec::new()).expect_err("the signature is invalid"), "signature names reject angle brackets", @r#" + Custom { + kind: Other, + error: Signature name or email must not contain '<', '>' or \n, "input"="invalid < middlename", + } + "#); } #[test] @@ -23,10 +25,12 @@ mod write_to { email: "server>.example.com".into(), time: Time::default(), }; - assert_eq!( - format!("{:?}", signature.write_to(&mut Vec::new())), - "Err(Custom { kind: Other, error: ValidationError { message: \"Signature name or email must not contain '<', '>' or \\\\n\", input: Some(\"server>.example.com\") } })" - ); + insta::assert_debug_snapshot!(signature.write_to(&mut Vec::new()).expect_err("the signature is invalid"), "signature email addresses reject angle brackets", @r#" + Custom { + kind: Other, + error: Signature name or email must not contain '<', '>' or \n, "input"="server>.example.com", + } + "#); } #[test] @@ -36,10 +40,12 @@ mod write_to { email: "name@example.com".into(), time: Time::default(), }; - assert_eq!( - format!("{:?}", signature.write_to(&mut Vec::new())), - "Err(Custom { kind: Other, error: ValidationError { message: \"Signature name or email must not contain '<', '>' or \\\\n\", input: Some(\"hello\\nnewline\") } })" - ); + insta::assert_debug_snapshot!(signature.write_to(&mut Vec::new()).expect_err("the signature is invalid"), "signature names reject newlines", @r#" + Custom { + kind: Other, + error: Signature name or email must not contain '<', '>' or \n, "input"="hello\nnewline", + } + "#); } } } diff --git a/gix-archive/src/write.rs b/gix-archive/src/write.rs index 5cead321253..035a396cbac 100644 --- a/gix-archive/src/write.rs +++ b/gix-archive/src/write.rs @@ -1,6 +1,7 @@ #[cfg(any(feature = "tar", feature = "tar_gz", feature = "zip"))] use gix_error::ResultExt; use gix_error::{ErrorExt, message}; +use gix_error::{ExnMessageResult, ExnResult}; use gix_worktree_stream::{Entry, Stream}; use crate::{Format, Options}; @@ -22,9 +23,9 @@ pub fn write_stream( mut next_entry: NextFn, out: impl std::io::Write, opts: Options, -) -> Result<(), gix_error::Exn> +) -> ExnMessageResult where - NextFn: FnMut(&mut Stream) -> Result>, gix_error::Exn>, + NextFn: FnMut(&mut Stream) -> ExnResult>>, { if opts.format == Format::InternalTransientNonPersistable { return Err(message("The internal format cannot be used as an archive, it's merely a debugging tool").raise()); @@ -39,11 +40,7 @@ where } impl State { - pub fn new( - format: Format, - mtime: gix_date::SecondsSinceUnixEpoch, - out: W, - ) -> Result> { + pub fn new(format: Format, mtime: gix_date::SecondsSinceUnixEpoch, out: W) -> ExnMessageResult { match format { Format::InternalTransientNonPersistable => unreachable!("handled earlier"), Format::Zip { .. } => { @@ -152,9 +149,9 @@ pub fn write_stream_seek( mut next_entry: NextFn, out: impl std::io::Write + std::io::Seek, opts: Options, -) -> Result<(), gix_error::Exn> +) -> ExnMessageResult where - NextFn: FnMut(&mut Stream) -> Result>, gix_error::Exn>, + NextFn: FnMut(&mut Stream) -> ExnResult>>, { let compression_level = match opts.format { Format::Zip { compression_level } => compression_level.map(i64::from), @@ -208,7 +205,7 @@ fn append_zip_entry( mtime: rawzip::time::UtcDateTime, compression_level: Option, tree_prefix: Option<&bstr::BString>, -) -> Result<(), gix_error::Exn> { +) -> ExnMessageResult { use bstr::ByteSlice; let path = add_prefix(entry.relative_path(), tree_prefix).into_owned(); let unix_permissions = if entry.mode.is_executable() { 0o755 } else { 0o644 }; @@ -303,7 +300,7 @@ fn append_tar_entry( mut entry: gix_worktree_stream::Entry<'_>, mtime_seconds_since_epoch: i64, opts: &Options, -) -> Result<(), gix_error::Exn> { +) -> ExnMessageResult { let mut header = tar::Header::new_gnu(); header.set_mtime(mtime_seconds_since_epoch as u64); header.set_entry_type(tar_entry_type(entry.mode)); diff --git a/gix-attributes/Cargo.toml b/gix-attributes/Cargo.toml index 8d4bb8c49d7..4edcfdfe1c9 100644 --- a/gix-attributes/Cargo.toml +++ b/gix-attributes/Cargo.toml @@ -41,6 +41,7 @@ serde = { version = "1.0.114", optional = true, default-features = false, featur document-features = { version = "0.2.1", optional = true } [dev-dependencies] +insta = "1.46.3" criterion = "0.8.2" gix-testtools = { path = "../tests/tools", default-features = false, features = ["sha1"] } gix-fs = { path = "../gix-fs" } diff --git a/gix-attributes/src/name.rs b/gix-attributes/src/name.rs index bdb47346b6d..300e77d090e 100644 --- a/gix-attributes/src/name.rs +++ b/gix-attributes/src/name.rs @@ -24,8 +24,10 @@ impl AsRef for NameRef<'_> { } impl<'a> TryFrom<&'a BStr> for NameRef<'a> { - type Error = gix_error::ValidationError; + type Error = gix_error::Message; + /// Invalid name bytes are stored as `input` in [`gix_error::Message::values`]. + /// After [wrapping](gix_error::Error::from_error()), inspect them with [metadata](gix_error::Error::metadata()). fn try_from(attr: &'a BStr) -> Result { fn attr_valid(attr: &BStr) -> bool { if attr.is_empty() || attr.first() == Some(&b'-') { @@ -39,10 +41,7 @@ impl<'a> TryFrom<&'a BStr> for NameRef<'a> { attr_valid(attr) .then(|| NameRef(attr.to_str().expect("no illformed utf8"))) .ok_or_else(|| { - gix_error::ValidationError::new_with_input( - "Attribute has non-ascii characters or starts with '-'", - attr, - ) + gix_error::validation("Attribute has non-ascii characters or starts with '-'").with("input", attr) }) } } diff --git a/gix-attributes/src/parse.rs b/gix-attributes/src/parse.rs index 02cf334b81d..f3d9a26c801 100644 --- a/gix-attributes/src/parse.rs +++ b/gix-attributes/src/parse.rs @@ -1,7 +1,7 @@ use std::borrow::Cow; use bstr::{BStr, ByteSlice}; -use gix_error::{ErrorExt, ResultExt, ValidationError}; +use gix_error::{ErrorExt, ExnMessageResult, ResultExt, validation}; use crate::{AssignmentRef, Name, NameRef, StateRef}; @@ -28,13 +28,15 @@ pub struct Iter<'a> { impl<'a> Iter<'a> { /// Create a new instance to parse attribute assignments from `input`. + /// Iterator errors store invalid name bytes as `input` in [`gix_error::Message::values`]. + /// After [wrapping](gix_error::Error::from_error()), inspect them with [metadata](gix_error::Error::metadata()). pub fn new(input: &'a BStr) -> Self { Iter { attrs: input.split(is_blank as fn(&u8) -> bool), } } - fn parse_attr(&self, attr: &'a [u8]) -> Result, gix_error::ValidationError> { + fn parse_attr(&self, attr: &'a [u8]) -> ExnMessageResult> { let mut tokens = attr.splitn(2, |b| *b == b'='); let attr = tokens.next().expect("attr itself").as_bstr(); let possibly_value = tokens.next(); @@ -49,16 +51,16 @@ impl<'a> Iter<'a> { } } -fn check_attr(attr: &BStr) -> Result, gix_error::ValidationError> { - NameRef::try_from(attr).and_then(|name| { +fn check_attr(attr: &BStr) -> ExnMessageResult> { + Ok(NameRef::try_from(attr).and_then(|name| { (!name.as_str().starts_with("builtin_")).then_some(name).ok_or_else(|| { - gix_error::ValidationError::new_with_input("Attribute name uses the reserved 'builtin_' prefix", attr) + gix_error::validation("Attribute name uses the reserved 'builtin_' prefix").with("input", attr) }) - }) + })?) } impl<'a> Iterator for Iter<'a> { - type Item = Result, gix_error::ValidationError>; + type Item = ExnMessageResult>; fn next(&mut self) -> Option { let attr = self.attrs.find(|a| !a.is_empty())?; @@ -69,6 +71,7 @@ impl<'a> Iterator for Iter<'a> { /// Instantiation impl<'a> Lines<'a> { /// Create a new instance to parse all attributes in all lines of the input `bytes`. + /// Iterator errors include invalid macro name or pattern bytes as `input` [metadata](gix_error::Exn::metadata()). pub fn new(bytes: &'a [u8]) -> Self { let bom = unicode_bom::Bom::from(bytes); Lines { @@ -79,7 +82,7 @@ impl<'a> Lines<'a> { } impl<'a> Iterator for Lines<'a> { - type Item = Result<(Kind, Iter<'a>, usize), gix_error::Exn>; + type Item = ExnMessageResult<(Kind, Iter<'a>, usize)>; fn next(&mut self) -> Option { fn skip_blanks(line: &BStr) -> &BStr { @@ -100,10 +103,7 @@ impl<'a> Iterator for Lines<'a> { } } -fn parse_line( - line: &BStr, - line_number: usize, -) -> Option, usize), gix_error::Exn>> { +fn parse_line(line: &BStr, line_number: usize) -> Option, usize)>> { if line.is_empty() { return None; } @@ -122,15 +122,15 @@ fn parse_line( let kind_res = match line.strip_prefix(b"[attr]").filter(|name| !name.is_empty()) { Some(macro_name) => check_attr(macro_name.into()) - .or_raise(|| ValidationError::new(format!("Macro in line {line_number} has an invalid name"))) + .or_raise(|| validation(format!("Macro in line {line_number} has an invalid name"))) .map(|name| Kind::Macro(name.to_owned())), None => { let pattern = gix_glob::Pattern::from_bytes(line.as_ref())?; if pattern.mode.contains(gix_glob::pattern::Mode::NEGATIVE) { - Err(ValidationError::new_with_input( - format!(r"Line {line_number} has a negative pattern, for literal characters use \!"), - line.as_ref(), - ) + Err(validation(format!( + r"Line {line_number} has a negative pattern, for literal characters use \!" + )) + .with("input", line.as_ref()) .raise()) } else { Ok(Kind::Pattern(pattern)) diff --git a/gix-attributes/src/search/attributes.rs b/gix-attributes/src/search/attributes.rs index c59a728d8ff..2cdcd321a22 100644 --- a/gix-attributes/src/search/attributes.rs +++ b/gix-attributes/src/search/attributes.rs @@ -1,3 +1,4 @@ +use gix_error::ExnMessageResult; use std::path::{Path, PathBuf}; use bstr::{BStr, ByteSlice}; @@ -128,7 +129,7 @@ impl Pattern for Attributes { fn bytes_to_patterns(&self, bytes: &[u8], _source: &std::path::Path) -> Vec> { fn into_owned_assignments<'a>( - attrs: impl Iterator, gix_error::ValidationError>>, + attrs: impl Iterator>>, ) -> Option { let res = attrs .map(|res| { @@ -137,7 +138,7 @@ impl Pattern for Attributes { inner: a.to_owned(), }) }) - .collect::>(); + .collect::>(); match res { Ok(res) => Some(res), Err(_err) => { diff --git a/gix-attributes/tests/attributes/parse.rs b/gix-attributes/tests/attributes/parse.rs index 69188c844b4..61293844578 100644 --- a/gix-attributes/tests/attributes/parse.rs +++ b/gix-attributes/tests/attributes/parse.rs @@ -1,6 +1,7 @@ -use bstr::{BString, ByteSlice}; +use bstr::BString; use gix_attributes::{StateRef, parse, state::ValueRef}; -use gix_error::{ResultExt, ValidationError}; +use gix_error::ExnMessageResult; +use gix_error::{Message, ResultExt, validation}; use gix_glob::pattern::Mode; use gix_testtools::fixture_bytes; @@ -95,18 +96,9 @@ fn exclamation_marks_must_be_escaped_or_error_unlike_gitignore() { line(r"\!hello"), (pattern(r"!hello", Mode::NO_SUB_DIR, None), vec![], 1) ); - assert!(has_validation_message( - try_line(r"!hello"), - r"Line 1 has a negative pattern, for literal characters use \!" - )); + insta::assert_debug_snapshot!(assert_validation(try_line(r"!hello")), "exclamation marks must be escaped or error unlike gitignore", @r#"Line 1 has a negative pattern, for literal characters use \!, "input"="!hello""#); assert!(lenient_lines(r#"!hello"#).is_empty()); - assert!( - has_validation_message( - try_line(r#""!hello""#), - r"Line 1 has a negative pattern, for literal characters use \!" - ), - "even in quotes they trigger…" - ); + insta::assert_debug_snapshot!(assert_validation(try_line(r#""!hello""#)), "even in quotes they trigger…", @r#"Line 1 has a negative pattern, for literal characters use \!, "input"="!hello""#); assert!(lenient_lines(r#""!hello""#).is_empty()); assert_eq!( line(r#""\\!hello""#), @@ -196,84 +188,103 @@ fn the_macro_prefix_without_a_name_is_a_pattern() { #[test] fn custom_macros_must_be_valid_attribute_names() { - assert!(has_validation_message( - try_line(r"[attr]-prefixdash"), - "Macro in line 1 has an invalid name" - )); + insta::assert_debug_snapshot!(assert_validation(try_line(r"[attr]-prefixdash")), "custom macros must be valid attribute names", @r#" + Macro in line 1 has an invalid name + | + └─ Attribute has non-ascii characters or starts with '-', "input"="-prefixdash" + "#); assert!(lenient_lines(r"[attr]-prefixdash").is_empty()); - assert!(has_validation_message( - try_line(r"[attr]!exclamation"), - "Macro in line 1 has an invalid name" - )); - assert!(has_validation_message( - try_line(r"[attr]assignment=value"), - "Macro in line 1 has an invalid name" - )); - assert!(has_validation_message( - try_line(r"[attr]你好"), - "Macro in line 1 has an invalid name" - )); + insta::assert_debug_snapshot!(assert_validation(try_line(r"[attr]!exclamation")), "custom macros must be valid attribute names", @r#" + Macro in line 1 has an invalid name + | + └─ Attribute has non-ascii characters or starts with '-', "input"="!exclamation" + "#); + insta::assert_debug_snapshot!(assert_validation(try_line(r"[attr]assignment=value")), "custom macros must be valid attribute names", @r#" + Macro in line 1 has an invalid name + | + └─ Attribute has non-ascii characters or starts with '-', "input"="assignment=value" + "#); + insta::assert_debug_snapshot!(assert_validation(try_line(r"[attr]你好")), "custom macros must be valid attribute names", @r#" + Macro in line 1 has an invalid name + | + └─ Attribute has non-ascii characters or starts with '-', "input"="你好" + "#); assert!(lenient_lines(r"[attr]你好").is_empty()); } #[test] fn invalid_names_retain_line_context_and_the_validation_cause() { - for (input, context) in [ - ("p 你好", "Attribute in line 1 has an invalid name"), - ("[attr]你好", "Macro in line 1 has an invalid name"), - ] { + let mut error_snapshots = Vec::new(); + for input in ["p 你好", "[attr]你好"] { let err = try_line(input).unwrap_err(); - assert_eq!(err, context); + error_snapshots.push(gix_testtools::redact_debug_snapshot(&(err), &[])); let cause = err .iter() .skip(1) - .find_map(|frame| frame.error().downcast_ref::()) + .find_map(|frame| frame.error().downcast_ref::()) .expect("invalid names remain a typed validation cause"); - assert_eq!(cause.message, "Attribute has non-ascii characters or starts with '-'"); + assert_eq!(cause.class, Some(gix_error::Class::Validation)); assert_eq!( - cause.input.as_ref().map(|input| input.as_bstr()), - Some("你好".as_bytes().as_bstr()) + cause.values.get("input"), + Some(&gix_error::MetadataValue::Bytes("你好".into())) ); } + insta::assert_debug_snapshot!(error_snapshots, "invalid names retain line context and the validation cause", @r#" + [ + Attribute in line 1 has an invalid name + | + └─ Attribute has non-ascii characters or starts with '-', "input"="你好", + Macro in line 1 has an invalid name + | + └─ Attribute has non-ascii characters or starts with '-', "input"="你好", + ] + "#); } #[test] fn attribute_names_must_not_begin_with_dash_and_must_be_ascii_only() { - assert!(has_validation_message( - try_line(r"p !-a"), - "Attribute in line 1 has an invalid name" - )); + insta::assert_debug_snapshot!(assert_validation(try_line(r"p !-a")), "attribute names must not begin with dash and must be ascii only", @r#" + Attribute in line 1 has an invalid name + | + └─ Attribute has non-ascii characters or starts with '-', "input"="-a" + "#); assert!(lenient_lines(r"p !-a").is_empty()); - assert!( - has_validation_message(try_line(r#"p !!a"#), "Attribute in line 1 has an invalid name"), - "exclamation marks aren't allowed either" - ); + insta::assert_debug_snapshot!(assert_validation(try_line(r#"p !!a"#)), "exclamation marks aren't allowed either", @r#" + Attribute in line 1 has an invalid name + | + └─ Attribute has non-ascii characters or starts with '-', "input"="!a" + "#); assert!(lenient_lines(r#"p !!a"#).is_empty()); - assert!( - has_validation_message(try_line(r#"p 你好"#), "Attribute in line 1 has an invalid name"), - "nor is utf-8 encoded characters - gitoxide could consider to relax this when established" - ); + insta::assert_debug_snapshot!(assert_validation(try_line(r#"p 你好"#)), "nor is utf-8 encoded characters - gitoxide could consider to relax this when established", @r#" + Attribute in line 1 has an invalid name + | + └─ Attribute has non-ascii characters or starts with '-', "input"="你好" + "#); assert!(lenient_lines(r#"p 你好"#).is_empty()); } #[test] fn attribute_names_must_not_be_empty() { - assert!( - has_validation_message(try_line(r"p text =lf"), "Attribute in line 1 has an invalid name"), - "a blank in front of the equals sign leaves the assignment without a name" - ); - assert!( - has_validation_message(try_line(r"p ="), "Attribute in line 1 has an invalid name"), - "an assignment that is nothing but an equals sign has no name either" - ); - assert!( - has_validation_message(try_line(r"p -"), "Attribute in line 1 has an invalid name"), - "prefixes need a name to apply to" - ); - assert!( - has_validation_message(try_line(r"p !"), "Attribute in line 1 has an invalid name"), - "the unspecified prefix needs one as well" - ); + insta::assert_debug_snapshot!(assert_validation(try_line(r"p text =lf")), "a blank in front of the equals sign leaves the assignment without a name", @r#" + Attribute in line 1 has an invalid name + | + └─ Attribute has non-ascii characters or starts with '-', "input"="" + "#); + insta::assert_debug_snapshot!(assert_validation(try_line(r"p =")), "an assignment that is nothing but an equals sign has no name either", @r#" + Attribute in line 1 has an invalid name + | + └─ Attribute has non-ascii characters or starts with '-', "input"="" + "#); + insta::assert_debug_snapshot!(assert_validation(try_line(r"p -")), "prefixes need a name to apply to", @r#" + Attribute in line 1 has an invalid name + | + └─ Attribute has non-ascii characters or starts with '-', "input"="" + "#); + insta::assert_debug_snapshot!(assert_validation(try_line(r"p !")), "the unspecified prefix needs one as well", @r#" + Attribute in line 1 has an invalid name + | + └─ Attribute has non-ascii characters or starts with '-', "input"="" + "#); assert!( gix_attributes::NameRef::try_from(bstr::BStr::new(b"")).is_err(), "names can't be created empty either, which `attr_name_valid()` rejects via `namelen <= 0`" @@ -282,28 +293,22 @@ fn attribute_names_must_not_be_empty() { #[test] fn attribute_names_must_not_use_the_reserved_builtin_prefix() { - assert!( - has_validation_message( - try_line(r"p builtin_objectmode"), - "Attribute in line 1 has an invalid name" - ), - "Git reserves 'builtin_' for built-in attributes and drops lines that assign to it" - ); + insta::assert_debug_snapshot!(assert_validation(try_line(r"p builtin_objectmode")), "Git reserves 'builtin_' for built-in attributes and drops lines that assign to it", @r#" + Attribute in line 1 has an invalid name + | + └─ Attribute name uses the reserved 'builtin_' prefix, "input"="builtin_objectmode" + "#); assert!(lenient_lines(r"p builtin_objectmode").is_empty()); - assert!( - has_validation_message( - try_line(r"p -builtin_objectmode"), - "Attribute in line 1 has an invalid name" - ), - "the prefix is checked after '-' and '!' are stripped, just like in `parse_attr()`" - ); - assert!( - has_validation_message( - try_line(r"[attr]builtin_macro -text"), - "Macro in line 1 has an invalid name" - ), - "macro names are checked against the reserved namespace as well" - ); + insta::assert_debug_snapshot!(assert_validation(try_line(r"p -builtin_objectmode")), "the prefix is checked after '-' and '!' are stripped, just like in `parse_attr()`", @r#" + Attribute in line 1 has an invalid name + | + └─ Attribute name uses the reserved 'builtin_' prefix, "input"="builtin_objectmode" + "#); + insta::assert_debug_snapshot!(assert_validation(try_line(r"[attr]builtin_macro -text")), "macro names are checked against the reserved namespace as well", @r#" + Macro in line 1 has an invalid name + | + └─ Attribute name uses the reserved 'builtin_' prefix, "input"="builtin_macro" + "#); assert_eq!( line(r"p builtin"), (pattern("p", Mode::NO_SUB_DIR, None), vec![set("builtin")], 1), @@ -361,25 +366,26 @@ fn only_ascii_blanks_separate_attributes() { ), "a non-breaking space belongs to the value it appears in" ); - assert!( - has_validation_message( - try_line("p text\u{a0}eol=lf"), - "Attribute in line 1 has an invalid name" - ), - "in a name it makes the whole name invalid" - ); - assert!( - has_validation_message(try_line("p a\u{b}b"), "Attribute in line 1 has an invalid name"), - "a vertical tab is part of the name, not a separator" - ); - assert!( - has_validation_message(try_line("p a\u{c}b"), "Attribute in line 1 has an invalid name"), - "a form feed is part of the name, not a separator" - ); - assert!( - has_validation_message(try_line("p a\u{2028}b"), "Attribute in line 1 has an invalid name"), - "vertical tabs, form feeds and unicode line separators aren't blanks either" - ); + insta::assert_debug_snapshot!(assert_validation(try_line("p text\u{a0}eol=lf")), "in a name it makes the whole name invalid", @r#" + Attribute in line 1 has an invalid name + | + └─ Attribute has non-ascii characters or starts with '-', "input"="text\u{a0}eol" + "#); + insta::assert_debug_snapshot!(assert_validation(try_line("p a\u{b}b")), "a vertical tab is part of the name, not a separator", @r#" + Attribute in line 1 has an invalid name + | + └─ Attribute has non-ascii characters or starts with '-', "input"="a\x0bb" + "#); + insta::assert_debug_snapshot!(assert_validation(try_line("p a\u{c}b")), "a form feed is part of the name, not a separator", @r#" + Attribute in line 1 has an invalid name + | + └─ Attribute has non-ascii characters or starts with '-', "input"="a\x0cb" + "#); + insta::assert_debug_snapshot!(assert_validation(try_line("p a\u{2028}b")), "vertical tabs, form feeds and unicode line separators aren't blanks either", @r#" + Attribute in line 1 has an invalid name + | + └─ Attribute has non-ascii characters or starts with '-', "input"="a\u{2028}b" + "#); } #[test] @@ -483,11 +489,16 @@ fn pattern(name: &str, flags: gix_glob::pattern::Mode, first_wildcard_pos: Optio }) } -fn has_validation_message(result: Result>, expected: &str) -> bool { - result.is_err_and(|err| err.message == expected) +fn assert_validation(result: ExnMessageResult) -> gix_error::Exn { + let err = result.err().expect("attribute input must be rejected"); + assert!( + err.is_validation(), + "invalid attribute syntax retains its classification" + ); + err } -fn try_line(input: &str) -> Result, gix_error::Exn> { +fn try_line(input: &str) -> ExnMessageResult> { let mut lines = gix_attributes::parse(input.as_bytes()); let res = expand(lines.next().unwrap())?; assert!(lines.next().is_none(), "expected only one line"); @@ -502,7 +513,7 @@ fn byte_line(input: &[u8]) -> ExpandedAttribute<'_> { try_byte_line(input).unwrap() } -fn try_byte_line(input: &[u8]) -> Result, gix_error::Exn> { +fn try_byte_line(input: &[u8]) -> ExnMessageResult> { let mut lines = gix_attributes::parse(input); let res = expand(lines.next().unwrap())?; assert!(lines.next().is_none(), "expected only one line"); @@ -516,17 +527,15 @@ fn lenient_lines(input: &str) -> Vec> { .collect() } -fn try_lines(input: &str) -> Result>, gix_error::Exn> { +fn try_lines(input: &str) -> ExnMessageResult>> { gix_attributes::parse(input.as_bytes()).map(expand).collect() } -fn expand( - input: Result<(parse::Kind, parse::Iter<'_>, usize), gix_error::Exn>, -) -> Result, gix_error::Exn> { +fn expand(input: ExnMessageResult<(parse::Kind, parse::Iter<'_>, usize)>) -> ExnMessageResult> { let (pattern, attrs, line_no) = input?; let attrs = attrs .map(|r| r.map(|attr| (attr.name.as_str().into(), attr.state))) .collect::, _>>() - .or_raise(|| ValidationError::new(format!("Attribute in line {line_no} has an invalid name")))?; + .or_raise(|| validation(format!("Attribute in line {line_no} has an invalid name")))?; Ok((pattern, attrs, line_no)) } diff --git a/gix-bitmap/src/ewah.rs b/gix-bitmap/src/ewah.rs index 079f8797a78..b2f7fceece1 100644 --- a/gix-bitmap/src/ewah.rs +++ b/gix-bitmap/src/ewah.rs @@ -1,10 +1,11 @@ +use gix_error::ExnMessageResult; /// Decode `data` as EWAH bitmap. -pub fn decode(data: &[u8]) -> Result<(Vec, &[u8]), gix_error::Exn> { +pub fn decode(data: &[u8]) -> ExnMessageResult<(Vec, &[u8])> { use crate::decode; - use gix_error::{OptionExt, message}; + use gix_error::{OptionExt, validation}; - let (num_bits, data) = decode::u32(data).ok_or_raise(|| message("eof reading amount of bits").into())?; - let (len, data) = decode::u32(data).ok_or_raise(|| message("eof reading chunk length").into())?; + let (num_bits, data) = decode::u32(data).ok_or_raise(|| validation("eof reading amount of bits"))?; + let (len, data) = decode::u32(data).ok_or_raise(|| validation("eof reading chunk length"))?; let len = len as usize; // NOTE: git does this by copying all bytes first, and then it will change the endianness in a separate loop. @@ -12,10 +13,10 @@ pub fn decode(data: &[u8]) -> Result<(Vec, &[u8]), gix_error::Exn()) - .ok_or_raise(|| message("chunk length overflows size calculation").into())?; + .ok_or_raise(|| validation("chunk length overflows size calculation"))?; let (mut bits, data) = data .split_at_checked(word_bytes_len) - .ok_or_raise(|| message("eof while reading bit data").into())?; + .ok_or_raise(|| validation("eof while reading bit data"))?; let mut buf = std::vec::Vec::::with_capacity(len); for _ in 0..len { let (bit_num, rest) = bits.split_at(std::mem::size_of::()); @@ -23,7 +24,7 @@ pub fn decode(data: &[u8]) -> Result<(Vec, &[u8]), gix_error::Exn Result<(Vec, &[u8]), gix_error::Exn std::io::Result<()> { let len = u32::try_from(self.bits.len()) - .or_raise(|| ValidationError::new("bit word count exceeds u32::MAX")) + .or_raise(|| validation("bit word count exceeds u32::MAX")) .map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidInput, err.into_error()))?; let rlw = u32::try_from(self.rlw) - .or_raise(|| ValidationError::new("run length word offset exceeds u32::MAX")) + .or_raise(|| validation("run length word offset exceeds u32::MAX")) .map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidInput, err.into_error()))?; out.write_all(&self.num_bits.to_be_bytes())?; diff --git a/gix-blame/Cargo.toml b/gix-blame/Cargo.toml index e91253a329a..105af2bcc34 100644 --- a/gix-blame/Cargo.toml +++ b/gix-blame/Cargo.toml @@ -32,6 +32,7 @@ gix-traverse = { version = "^0.61.0", path = "../gix-traverse" } smallvec = "1.15.1" [dev-dependencies] +insta = "1.46.3" gix-hash = { path = "../gix-hash", features = ["sha1", "sha256"] } gix-ref = { path = "../gix-ref", features = ["sha1", "sha256"] } gix-filter = { path = "../gix-filter" } diff --git a/gix-blame/src/file/function.rs b/gix-blame/src/file/function.rs index 2a790631bd4..4ab550e081f 100644 --- a/gix-blame/src/file/function.rs +++ b/gix-blame/src/file/function.rs @@ -1,7 +1,7 @@ use std::num::NonZeroU32; use gix_diff::{blob::TokenSource, tree::Visit}; -use gix_error::{ErrorExt, NotFoundError, OptionExt, ResultExt, message}; +use gix_error::{ErrorExt, ExnResult, OptionExt, ResultExt, message, not_found}; use gix_hash::ObjectId; use gix_object::{ FindExt, @@ -72,7 +72,7 @@ pub fn file( resource_cache: &mut gix_diff::blob::Platform, file_path: &BStr, options: Options, -) -> Result { +) -> ExnResult { let _span = gix_trace::coarse!("gix_blame::file()", ?file_path, ?start); let mut stats = Statistics::default(); @@ -580,7 +580,7 @@ fn tree_diff_at_file_path( lhs_tree_buf: &mut Vec, rhs_tree_buf: &mut Vec, rewrites: Option, -) -> Result, gix_error::Exn> { +) -> ExnResult> { let parent_tree_id = find_commit(cache, &odb, &parent_id, commit_buf) .or_raise_erased(|| message("Could not find existing iterator over a tree"))? .tree_id() @@ -637,7 +637,7 @@ fn tree_diff_without_rewrites_at_file_path( state: &mut gix_diff::tree::State, parent_tree_iter: gix_object::TreeRefIter<'_>, tree_iter: gix_object::TreeRefIter<'_>, -) -> Result, gix_error::Exn> { +) -> ExnResult> { struct FindChangeToPath { inner: gix_diff::tree::Recorder, interesting_path: BString, @@ -740,7 +740,7 @@ fn tree_diff_with_rewrites_at_file_path( parent_tree_iter: gix_object::TreeRefIter<'_>, tree_iter: gix_object::TreeRefIter<'_>, rewrites: gix_diff::Rewrites, -) -> Result, gix_error::Exn> { +) -> ExnResult> { let mut change: Option = None; let options: gix_diff::tree_with_rewrites::Options = gix_diff::tree_with_rewrites::Options { @@ -753,7 +753,7 @@ fn tree_diff_with_rewrites_at_file_path( resource_cache, state, &odb, - |change_ref| -> Result<_, gix_error::Exn> { + |change_ref| -> ExnResult<_> { if change_ref.location() == file_path { change = Some(change_ref.into_owned()); Ok(std::ops::ControlFlow::Break(())) @@ -781,7 +781,7 @@ fn blob_changes( previous_file_path: &BStr, diff_algorithm: gix_diff::blob::Algorithm, stats: &mut Statistics, -) -> Result, gix_error::Exn> { +) -> ExnResult> { resource_cache .set_resource( previous_oid, @@ -869,7 +869,7 @@ fn find_path_entry_in_commit( buf: &mut Vec, buf2: &mut Vec, stats: &mut Statistics, -) -> Result, gix_error::Exn> { +) -> ExnResult> { let tree_id = find_commit(cache, odb, commit, buf) .or_raise_erased(|| message("Could not find existing iterator over a tree"))? .tree_id() @@ -897,7 +897,7 @@ fn collect_parents( odb: &impl gix_object::Find, cache: Option<&gix_commitgraph::Graph>, buf: &mut Vec, -) -> Result { +) -> ExnResult { let mut parent_ids: ParentIds = Default::default(); match commit { gix_traverse::commit::Either::CachedCommit(commit) => { @@ -952,12 +952,12 @@ fn initial_state( buf: &mut Vec, buf2: &mut Vec, stats: &mut Statistics, -) -> Result { +) -> ExnResult { match start { Start::Commit(suspect) => { let blamed_file_entry_id = find_path_entry_in_commit(&odb, &suspect, file_path, cache, buf, buf2, stats)? .ok_or_raise_erased(|| { - NotFoundError::new(format!( + not_found(format!( "The file to blame at '{file_path}' wasn't found in the first commit at {suspect}" )) })?; diff --git a/gix-blame/src/file/tests.rs b/gix-blame/src/file/tests.rs index 1e308e4a611..84dd21afa0e 100644 --- a/gix-blame/src/file/tests.rs +++ b/gix-blame/src/file/tests.rs @@ -992,10 +992,7 @@ mod blame_ranges { fn create_with_invalid_range() { let ranges = BlameRanges::from_one_based_inclusive_range(0..=10); - assert_eq!( - ranges.expect_err("zero isn't a valid one-based line number").message, - "Invalid line range was given, line range is expected to be a 1-based inclusive range in the format ','" - ); + insta::assert_debug_snapshot!(ranges.expect_err("zero isn't a valid one-based line number"), "create with invalid range", @"Invalid line range was given, line range is expected to be a 1-based inclusive range in the format ','"); } #[test] diff --git a/gix-blame/src/types.rs b/gix-blame/src/types.rs index 635fb50a937..c9d28350d75 100644 --- a/gix-blame/src/types.rs +++ b/gix-blame/src/types.rs @@ -1,4 +1,5 @@ -use gix_error::ValidationError; +use gix_error::ExnMessageResult; +use gix_error::validation; use gix_hash::ObjectId; use gix_object::bstr::BString; use smallvec::SmallVec; @@ -54,7 +55,7 @@ impl BlameRanges { /// /// Note that the input range is 1-based inclusive, as used by git, and /// the output is a zero-based `BlameRanges` instance. - pub fn from_one_based_inclusive_range(range: RangeInclusive) -> Result { + pub fn from_one_based_inclusive_range(range: RangeInclusive) -> ExnMessageResult { let zero_based_range = Self::inclusive_to_zero_based_exclusive(range)?; Ok(Self::PartialFile(vec![zero_based_range])) } @@ -65,7 +66,7 @@ impl BlameRanges { /// the output is a zero-based `BlameRanges` instance. /// /// If the input vector is empty, the result will be `WholeFile`. - pub fn from_one_based_inclusive_ranges(ranges: Vec>) -> Result { + pub fn from_one_based_inclusive_ranges(ranges: Vec>) -> ExnMessageResult { if ranges.is_empty() { return Ok(Self::WholeFile); } @@ -82,11 +83,11 @@ impl BlameRanges { } /// Convert a 1-based inclusive range to a 0-based exclusive range. - fn inclusive_to_zero_based_exclusive(range: RangeInclusive) -> Result, ValidationError> { + fn inclusive_to_zero_based_exclusive(range: RangeInclusive) -> ExnMessageResult> { if range.start() == &0 { - return Err(ValidationError::new( + return Err(validation( "Invalid line range was given, line range is expected to be a 1-based inclusive range in the format ','", - )); + ).into()); } let start = range.start() - 1; let end = *range.end(); @@ -98,7 +99,7 @@ impl BlameRanges { /// Add a single range to blame. /// /// The new range will be merged with any overlapping existing ranges. - pub fn add_one_based_inclusive_range(&mut self, new_range: RangeInclusive) -> Result<(), ValidationError> { + pub fn add_one_based_inclusive_range(&mut self, new_range: RangeInclusive) -> ExnMessageResult { let zero_based_range = Self::inclusive_to_zero_based_exclusive(new_range)?; self.merge_zero_based_exclusive_range(zero_based_range); diff --git a/gix-blame/tests/blame.rs b/gix-blame/tests/blame.rs index 83e65a6d6fe..5bbcc66c04c 100644 --- a/gix-blame/tests/blame.rs +++ b/gix-blame/tests/blame.rs @@ -1,5 +1,7 @@ use std::{collections::BTreeMap, path::PathBuf}; +use gix_error::ExnResult; + use gix_blame::BlameRanges; use gix_hash::ObjectId; use gix_object::bstr; @@ -218,7 +220,7 @@ impl Fixture { &mut self, source_file_name: &bstr::BStr, options: gix_blame::Options, - ) -> Result { + ) -> ExnResult { gix_blame::file( &self.odb, gix_blame::Start::Commit(self.suspect), @@ -234,7 +236,7 @@ impl Fixture { source_file_name: &bstr::BStr, contents: Vec, options: gix_blame::Options, - ) -> Result { + ) -> ExnResult { gix_blame::file( &self.odb, gix_blame::Start::Contents { diff --git a/gix-chunk/src/file/decode.rs b/gix-chunk/src/file/decode.rs index 9f316066a19..e297aa36a64 100644 --- a/gix-chunk/src/file/decode.rs +++ b/gix-chunk/src/file/decode.rs @@ -1,5 +1,6 @@ -use gix_error::ValidationError; +use gix_error::ExnMessageResult; use gix_error::bstr::ByteSlice; +use gix_error::{ErrorExt, validation}; use std::ops::Range; use crate::{file, file::index}; @@ -7,11 +8,12 @@ use crate::{file, file::index}; impl file::Index { /// Provided a mapped file at the beginning via `data`, starting at `toc_offset` decode all chunk information to return /// an index with `num_chunks` chunks. - pub fn from_bytes(data: &[u8], toc_offset: usize, num_chunks: u32) -> Result { + pub fn from_bytes(data: &[u8], toc_offset: usize, num_chunks: u32) -> ExnMessageResult { if num_chunks == 0 { - return Err(ValidationError::new( + return Err(validation( "Empty chunk indices are not allowed as the point of chunked files is to have chunks.", - )); + ) + .into()); } let data_len: u64 = data.len() as u64; @@ -19,42 +21,47 @@ impl file::Index { let mut toc_entry = &data[toc_offset..]; let expected_min_size = (num_chunks as usize + 1) * file::Index::ENTRY_SIZE; if toc_entry.len() < expected_min_size { - Err(format!( + return Err(validation(format!( "The table of contents would be {expected_min_size} bytes, but got only {toc_entry_len}", toc_entry_len = toc_entry.len() - ))?; + )) + .raise()); } for chunk_idx in 0..num_chunks { let (kind, offset) = toc_entry.split_at(4); let kind = to_kind(kind); if kind == crate::SENTINEL { - Err(format!( + return Err(validation(format!( "Sentinel value encountered while processing chunks {chunk_idx} of {num_chunks}" - ))?; + )) + .raise()); } if chunks.iter().any(|c: &index::Entry| c.kind == kind) { - Err(format!( + return Err(validation(format!( "The chunk of kind '{}' was encountered more than once", kind.as_bstr() - ))?; + )) + .raise()); } let offset = be_u64(offset); if offset > data_len { - Err(format!( + return Err(validation(format!( "The chunk offset {offset} went past the file of length {data_len} - was it truncated?", - ))?; + )) + .raise()); } toc_entry = &toc_entry[file::Index::ENTRY_SIZE..]; let next_offset = be_u64(&toc_entry[4..]); if next_offset > data_len { - Err(format!( + return Err(validation(format!( "The chunk offset {next_offset} went past the file of length {data_len} - was it truncated?" - ))?; + )) + .raise()); } if next_offset <= offset { - Err("All chunk offsets must be incrementing.")?; + return Err(validation("All chunk offsets must be incrementing.").raise()); } chunks.push(index::Entry { kind, @@ -67,7 +74,7 @@ impl file::Index { let sentinel = to_kind(&toc_entry[..4]); if sentinel != crate::SENTINEL { - Err(format!("Sentinel value wasn't found, saw '{}'", sentinel.as_bstr()))?; + return Err(validation(format!("Sentinel value wasn't found, saw '{}'", sentinel.as_bstr())).raise()); } Ok(file::Index { diff --git a/gix-chunk/src/file/index.rs b/gix-chunk/src/file/index.rs index f2ead63d19c..1cc6877a2a7 100644 --- a/gix-chunk/src/file/index.rs +++ b/gix-chunk/src/file/index.rs @@ -1,3 +1,4 @@ +use gix_error::ExnMessageResult; use std::ops::Range; use crate::Id; @@ -25,11 +26,12 @@ impl Index { } /// Find a chunk of `kind` and return its offset into the data if found - pub fn offset_by_id(&self, kind: Id) -> Result, Message> { - self.chunks + pub fn offset_by_id(&self, kind: Id) -> ExnMessageResult> { + Ok(self + .chunks .iter() .find_map(|c| (c.kind == kind).then(|| c.offset.clone())) - .ok_or_else(make_message(kind)) + .ok_or_else(make_message(kind))?) } /// Find a chunk of `kind` and return its offset as usize range into the data if found. @@ -39,11 +41,12 @@ impl Index { /// /// - if the usize conversion fails, which isn't expected as memory maps can't be created if files are too large /// to require such offsets. - pub fn usize_offset_by_id(&self, kind: Id) -> Result, Message> { - self.chunks + pub fn usize_offset_by_id(&self, kind: Id) -> ExnMessageResult> { + Ok(self + .chunks .iter() .find_map(|c| (c.kind == kind).then(|| crate::range::into_usize_or_panic(c.offset.clone()))) - .ok_or_else(make_message(kind)) + .ok_or_else(make_message(kind))?) } /// Like [`Index::usize_offset_by_id()`] but with support for validation and transformation using a function. @@ -51,16 +54,17 @@ impl Index { &self, kind: Id, validate: impl FnOnce(Range) -> T, - ) -> Result { - self.chunks + ) -> ExnMessageResult { + Ok(self + .chunks .iter() .find_map(|c| (c.kind == kind).then(|| crate::range::into_usize_or_panic(c.offset.clone()))) .map(validate) - .ok_or_else(make_message(kind)) + .ok_or_else(make_message(kind))?) } /// Find a chunk of `kind` and return its data slice based on its offset. - pub fn data_by_id<'a>(&self, data: &'a [u8], kind: Id) -> Result<&'a [u8], Message> { + pub fn data_by_id<'a>(&self, data: &'a [u8], kind: Id) -> ExnMessageResult<&'a [u8]> { let offset = self.offset_by_id(kind)?; Ok(&data[crate::range::into_usize(offset) .ok_or_else(|| message("The offsets into the file couldn't be represented by usize"))?]) diff --git a/gix-chunk/src/lib.rs b/gix-chunk/src/lib.rs index 9cc8d27754c..fc0a8e70c24 100644 --- a/gix-chunk/src/lib.rs +++ b/gix-chunk/src/lib.rs @@ -5,7 +5,7 @@ //! ## Examples //! //! ``` -//! # fn main() -> Result<(), Box> { +//! # fn main() -> Result<(), Box> { //! use std::io::Write; //! //! let mut index = gix_chunk::file::Index::for_writing(); diff --git a/gix-chunk/tests/decode.rs b/gix-chunk/tests/decode.rs new file mode 100644 index 00000000000..b9b47153b71 --- /dev/null +++ b/gix-chunk/tests/decode.rs @@ -0,0 +1,79 @@ +use gix_chunk::{Id, SENTINEL, file::Index}; + +#[test] +fn malformed_chunk_tables_are_validation_errors() { + let cases = [ + ( + "empty index", + Vec::new(), + 0, + "Empty chunk indices are not allowed as the point of chunked files is to have chunks.", + ), + ( + "truncated table", + Vec::new(), + 1, + "The table of contents would be 24 bytes, but got only 0", + ), + ( + "early sentinel", + chunk_file(&[(SENTINEL, 24), (SENTINEL, 26)]), + 1, + "Sentinel value encountered while processing chunks 0 of 1", + ), + ( + "duplicate chunk", + chunk_file(&[(*b"DATA", 36), (*b"DATA", 37), (SENTINEL, 38)]), + 2, + "The chunk of kind 'DATA' was encountered more than once", + ), + ( + "chunk offset past the file", + chunk_file(&[(*b"DATA", 27), (SENTINEL, 26)]), + 1, + "The chunk offset 27 went past the file of length 26 - was it truncated?", + ), + ( + "next chunk offset past the file", + chunk_file(&[(*b"DATA", 24), (SENTINEL, 27)]), + 1, + "The chunk offset 27 went past the file of length 26 - was it truncated?", + ), + ( + "equal offsets", + chunk_file(&[(*b"DATA", 24), (SENTINEL, 24)]), + 1, + "All chunk offsets must be incrementing.", + ), + ( + "decreasing offsets", + chunk_file(&[(*b"DATA", 25), (SENTINEL, 24)]), + 1, + "All chunk offsets must be incrementing.", + ), + ( + "missing sentinel", + chunk_file(&[(*b"DATA", 24), (*b"MISS", 26)]), + 1, + "Sentinel value wasn't found, saw 'MISS'", + ), + ]; + + for (case, data, num_chunks, expected_message) in cases { + let err = Index::from_bytes(&data, 0, num_chunks) + .err() + .expect("malformed chunk tables must be rejected"); + assert_eq!(err.to_string(), expected_message, "{case} must retain its diagnostic"); + assert!(err.is_validation(), "{case} must be classified as invalid input: {err}"); + } +} + +fn chunk_file(entries: &[(Id, u64)]) -> Vec { + let mut data = Vec::new(); + for (kind, offset) in entries { + data.extend_from_slice(kind); + data.extend_from_slice(&offset.to_be_bytes()); + } + data.extend_from_slice(b"ab"); + data +} diff --git a/gix-command/Cargo.toml b/gix-command/Cargo.toml index 95d16d9a7af..222227c9720 100644 --- a/gix-command/Cargo.toml +++ b/gix-command/Cargo.toml @@ -23,4 +23,5 @@ gix-quote = { version = "^0.8.0", path = "../gix-quote" } bstr = { version = "1.12.0", default-features = false, features = ["std", "unicode"] } [dev-dependencies] +insta = "1.46.3" gix-testtools = { path = "../tests/tools", features = ["sha1"] } diff --git a/gix-command/src/parse.rs b/gix-command/src/parse.rs index 9fd0de63076..7471fd4b9ca 100644 --- a/gix-command/src/parse.rs +++ b/gix-command/src/parse.rs @@ -1,7 +1,7 @@ use std::ffi::OsString; use bstr::{BStr, BString}; -use gix_error::{Exn, ResultExt}; +use gix_error::{ExnResult, ResultExt}; /// The result of [`command_line()`]. #[derive(Debug, Clone, PartialEq, Eq)] @@ -16,7 +16,9 @@ pub struct Outcome { /// The error returned when a command line cannot be parsed into a command. /// -/// Its [`gix_error::ValidationError`] source preserves the classification after type erasure. +/// Its source is a classification-only [`gix_error::ClassificationMarker`]. +/// Use [`gix_error::classify()`] or `is_validation()` on [`gix_error::Exn`] and [`gix_error::Error`] to check the classification. +/// Downcast to this type for the parser failure. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Error { /// A quote was opened but never closed. @@ -44,11 +46,7 @@ impl std::fmt::Display for Error { impl std::error::Error for Error { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - static INVALID_COMMAND: gix_error::ValidationError = gix_error::ValidationError { - message: std::borrow::Cow::Borrowed("Invalid command line"), - input: None, - }; - Some(&INVALID_COMMAND) + Some(const { &gix_error::ClassificationMarker::VALIDATION }) } } @@ -72,7 +70,7 @@ struct Word { /// shell identifier. Assignment-only input is rejected because it contains no command to execute. Environment /// assignment names are strings, while their values, the command, and arguments are converted losslessly to OS /// strings or rejected if the platform cannot represent them. -pub fn command_line(input: &BStr) -> Result> { +pub fn command_line(input: &BStr) -> ExnResult { let mut words = parse_words(input)?; let assignment_count = words .iter() @@ -90,7 +88,7 @@ pub fn command_line(input: &BStr) -> Result> { into_os_string(word.value[separator + 1..].to_owned().into())?, )) }) - .collect::>>()?; + .collect::>()?; Ok(Outcome { env, command, @@ -98,7 +96,7 @@ pub fn command_line(input: &BStr) -> Result> { }) } -pub(crate) fn arguments(input: &BStr) -> Result, Exn> { +pub(crate) fn arguments(input: &BStr) -> ExnResult, Error> { parse_words(input)? .into_iter() .map(|word| into_os_string(word.value)) @@ -198,7 +196,7 @@ fn push_unquoted( value.push(byte); } -fn into_os_string(value: BString) -> Result> { +fn into_os_string(value: BString) -> ExnResult { gix_path::try_from_bstring(value) .map(std::path::PathBuf::into_os_string) .or_raise(|| Error::UnrepresentableOsString) diff --git a/gix-command/tests/command/command_line.rs b/gix-command/tests/command/command_line.rs index 950e74c4c41..2b2f975e1f6 100644 --- a/gix-command/tests/command/command_line.rs +++ b/gix-command/tests/command/command_line.rs @@ -64,47 +64,105 @@ fn invalid_assignment_names_are_arguments() -> gix_testtools::Result { #[test] fn unterminated_quotes_are_rejected() { + let mut error_snapshots = Vec::new(); for input in ["cmd '", "cmd \"", "cmd \"\\"] { - assert_eq!( - *parse::command_line(input.into()).expect_err("unterminated quote"), - parse::Error::MissingClosingQuote - ); + let err = parse::command_line(input.into()).expect_err("unterminated quote"); + assert_eq!(*err, parse::Error::MissingClosingQuote); + error_snapshots.push((input, gix_testtools::redact_debug_snapshot(&err, &[]))); } + insta::assert_debug_snapshot!(error_snapshots, "unterminated quotes are rejected", @r#" + [ + ( + "cmd '", + missing closing quote, + ), + ( + "cmd \"", + missing closing quote, + ), + ( + "cmd \"\\", + missing closing quote, + ), + ] + "#); } #[test] fn dangling_unquoted_escape_is_rejected() { - assert_eq!( - *parse::command_line("cmd arg\\".into()).expect_err("dangling escape"), - parse::Error::MissingEscapedByte - ); + let err = parse::command_line("cmd arg\\".into()).expect_err("dangling escape"); + assert_eq!(*err, parse::Error::MissingEscapedByte); + insta::assert_debug_snapshot!(err, "dangling unquoted escape is rejected", @"missing byte after escape"); } #[test] fn a_command_is_required() { + let mut error_snapshots = Vec::new(); for input in ["", " ", "\t\n", "# comment", "\\\n", "tool=name", "FOO=one BAR=two"] { - assert_eq!( - *parse::command_line(input.into()).expect_err("no command"), - parse::Error::MissingCommand, - "{input:?}" - ); + let err = parse::command_line(input.into()).expect_err("no command"); + assert_eq!(*err, parse::Error::MissingCommand, "{input:?}"); + error_snapshots.push((input, gix_testtools::redact_debug_snapshot(&err, &[]))); } + insta::assert_debug_snapshot!(error_snapshots, "a command is required", @r##" + [ + ( + "", + missing command, + ), + ( + " ", + missing command, + ), + ( + "\t\n", + missing command, + ), + ( + "# comment", + missing command, + ), + ( + "\\\n", + missing command, + ), + ( + "tool=name", + missing command, + ), + ( + "FOO=one BAR=two", + missing command, + ), + ] + "##); } #[test] fn parse_errors_retain_their_classification() { + let mut error_snapshots = Vec::new(); for input in ["cmd '", "cmd arg\\", "FOO=one"] { let err = parse::command_line(input.into()).expect_err("the command line is invalid"); let cause = *err; + error_snapshots.push(gix_testtools::redact_debug_snapshot(&(err), &[])); assert!(err.is_validation(), "invalid commands classify as validation failures"); - let err = err.into_error(); - assert!(err.is_validation(), "conversion preserves the classification"); + assert_eq!( + err.probable_cause().downcast_ref::(), + Some(&cause), + "the parser error, not its classification marker, is the probable cause" + ); assert_eq!( err.downcast_any_ref::(), Some(&cause), "the original parser error remains available" ); } + insta::assert_debug_snapshot!(error_snapshots, "parse errors retain their classification", @" + [ + missing closing quote, + missing byte after escape, + missing command, + ] + "); } #[test] diff --git a/gix-command/tests/command/prepare.rs b/gix-command/tests/command/prepare.rs index 21a7ac983a3..388c5a9d50a 100644 --- a/gix-command/tests/command/prepare.rs +++ b/gix-command/tests/command/prepare.rs @@ -1,3 +1,4 @@ +use crate::Result; use std::sync::LazyLock; fn default_shell() -> &'static str { @@ -236,7 +237,7 @@ fn invalid_utf8_commands_are_checked_for_shell_syntax() { } #[test] -fn relative_existing_paths_with_shell_syntax_still_use_the_shell() -> crate::Result { +fn relative_existing_paths_with_shell_syntax_still_use_the_shell() -> Result { let temp = gix_testtools::tempfile::Builder::new() .prefix("$HOME") .tempdir_in(".")?; diff --git a/gix-command/tests/command/spawn.rs b/gix-command/tests/command/spawn.rs index 3a7cd7d1b0a..ed96f4ef86b 100644 --- a/gix-command/tests/command/spawn.rs +++ b/gix-command/tests/command/spawn.rs @@ -1,7 +1,8 @@ +use crate::Result; use bstr::ByteSlice; #[test] -fn environment_variables_are_passed_one_by_one() -> crate::Result { +fn environment_variables_are_passed_one_by_one() -> Result { let out = gix_command::prepare("echo $FIRST $SECOND") .env("FIRST", "first") .env("SECOND", "second") @@ -13,7 +14,7 @@ fn environment_variables_are_passed_one_by_one() -> crate::Result { } #[test] -fn disallow_shell() -> crate::Result { +fn disallow_shell() -> Result { let out = gix_command::prepare("PATH= echo hi") .command_may_be_shell_script_disallow_manual_argument_splitting() .spawn()? @@ -32,7 +33,7 @@ fn disallow_shell() -> crate::Result { } #[test] -fn script_with_dollar_at() -> crate::Result { +fn script_with_dollar_at() -> Result { let out = std::process::Command::from( gix_command::prepare(r#"echo "$@""#) .command_may_be_shell_script() @@ -49,7 +50,7 @@ fn script_with_dollar_at() -> crate::Result { } #[test] -fn direct_command_execution_searches_in_path() -> crate::Result { +fn direct_command_execution_searches_in_path() -> Result { assert!( gix_command::prepare(if cfg!(unix) { "ls" } else { "attrib.exe" }) .spawn()? @@ -61,16 +62,17 @@ fn direct_command_execution_searches_in_path() -> crate::Result { #[cfg(unix)] #[test] -fn direct_command_with_absolute_command_path() -> crate::Result { +fn direct_command_with_absolute_command_path() -> Result { assert!(gix_command::prepare("/usr/bin/env").spawn()?.wait()?.success()); Ok(()) } mod with_shell { + use crate::Result; use gix_testtools::bstr::ByteSlice; #[test] - fn command_in_path_with_args() -> crate::Result { + fn command_in_path_with_args() -> Result { // `ls` is occasionaly a builtin, as in busybox ash, but it is usually external. assert!( gix_command::prepare(if cfg!(unix) { "ls -l" } else { "attrib.exe /d" }) @@ -84,7 +86,7 @@ mod with_shell { #[cfg(unix)] #[test] - fn shell_builtin_or_command_in_path() -> crate::Result { + fn shell_builtin_or_command_in_path() -> Result { let out = gix_command::prepare("echo") .command_may_be_shell_script() .spawn()? @@ -96,7 +98,7 @@ mod with_shell { #[cfg(unix)] #[test] - fn shell_builtin_or_command_in_path_with_single_extra_arg() -> crate::Result { + fn shell_builtin_or_command_in_path_with_single_extra_arg() -> Result { let out = gix_command::prepare("printf") .command_may_be_shell_script() .arg("1") @@ -109,7 +111,7 @@ mod with_shell { #[cfg(unix)] #[test] - fn shell_builtin_or_command_in_path_with_multiple_extra_args() -> crate::Result { + fn shell_builtin_or_command_in_path_with_multiple_extra_args() -> Result { let out = gix_command::prepare("printf") .command_may_be_shell_script() .arg("%s") @@ -122,7 +124,7 @@ mod with_shell { } #[test] - fn force_shell_builtin() -> crate::Result { + fn force_shell_builtin() -> Result { let out = gix_command::prepare("echo").with_shell().spawn()?.wait_with_output()?; assert!(out.status.success()); assert_eq!(out.stdout.as_bstr(), "\n"); @@ -130,7 +132,7 @@ mod with_shell { } #[test] - fn force_shell_builtin_with_single_extra_arg() -> crate::Result { + fn force_shell_builtin_with_single_extra_arg() -> Result { let out = gix_command::prepare("printf") .with_shell() .arg("1") @@ -142,7 +144,7 @@ mod with_shell { } #[test] - fn force_shell_builtin_with_multiple_extra_args() -> crate::Result { + fn force_shell_builtin_with_multiple_extra_args() -> Result { let out = gix_command::prepare("printf") .with_shell() .arg("%s") @@ -155,7 +157,7 @@ mod with_shell { } #[test] - fn sh_shell_specific_script_code() -> crate::Result { + fn sh_shell_specific_script_code() -> Result { assert!( gix_command::prepare(":;:;:") .command_may_be_shell_script() @@ -167,7 +169,7 @@ mod with_shell { } #[test] - fn sh_shell_specific_script_code_with_single_extra_arg() -> crate::Result { + fn sh_shell_specific_script_code_with_single_extra_arg() -> Result { let out = gix_command::prepare(":;printf") .command_may_be_shell_script() .arg("1") @@ -179,7 +181,7 @@ mod with_shell { } #[test] - fn sh_shell_specific_script_code_with_multiple_extra_args() -> crate::Result { + fn sh_shell_specific_script_code_with_multiple_extra_args() -> Result { let out = gix_command::prepare(":;printf") .command_may_be_shell_script() .arg("%s") @@ -193,7 +195,7 @@ mod with_shell { #[cfg(unix)] #[test] - fn dollar_zero_in_minus_c_is_basename_of_default_shell() -> crate::Result { + fn dollar_zero_in_minus_c_is_basename_of_default_shell() -> Result { let out = gix_command::prepare(r#"printf %s "$0""#) .command_may_be_shell_script() .spawn()? @@ -209,7 +211,7 @@ mod with_shell { #[cfg(unix)] #[test] - fn dollar_zero_in_minus_c_reflects_with_shell_program() -> crate::Result { + fn dollar_zero_in_minus_c_reflects_with_shell_program() -> Result { let out = std::process::Command::from( gix_command::prepare(r#"printf %s "$0""#) .command_may_be_shell_script() diff --git a/gix-commitgraph/Cargo.toml b/gix-commitgraph/Cargo.toml index 164f4b11eeb..110488ada61 100644 --- a/gix-commitgraph/Cargo.toml +++ b/gix-commitgraph/Cargo.toml @@ -36,6 +36,7 @@ serde = { version = "1.0.114", optional = true, default-features = false, featur document-features = { version = "0.2.0", optional = true } [dev-dependencies] +insta = "1.46.3" gix-testtools = { path = "../tests/tools", default-features = false } gix-date = { path = "../gix-date" } gix-hash = { path = "../gix-hash", features = ["sha1", "sha256"] } diff --git a/gix-commitgraph/src/file/commit.rs b/gix-commitgraph/src/file/commit.rs index dee517ae361..0648ee0508b 100644 --- a/gix-commitgraph/src/file/commit.rs +++ b/gix-commitgraph/src/file/commit.rs @@ -3,7 +3,8 @@ use crate::{ File, Position, file::{self, EXTENDED_EDGES_MASK, LAST_EXTENDED_EDGE_MASK, NO_PARENT}, }; -use gix_error::{Message, message}; +use gix_error::ExnMessageResult; +use gix_error::message; use std::{ fmt::{Debug, Formatter}, slice::Chunks, @@ -77,7 +78,7 @@ impl<'a> Commit<'a> { } /// Returns the first parent of this commit. - pub fn parent1(&self) -> Result, Message> { + pub fn parent1(&self) -> ExnMessageResult> { self.iter_parents().next().transpose() } @@ -122,7 +123,7 @@ pub struct Parents<'a> { } impl Iterator for Parents<'_> { - type Item = Result; + type Item = ExnMessageResult; fn next(&mut self) -> Option { let state = std::mem::replace(&mut self.state, ParentIteratorState::Exhausted); @@ -133,7 +134,8 @@ impl Iterator for Parents<'_> { _ => Some(Err(message!( "commit {} has a second parent but not a first parent", self.commit_data.id() - ))), + ) + .into())), }, ParentEdge::GraphPosition(pos) => { self.state = ParentIteratorState::Second; @@ -142,7 +144,8 @@ impl Iterator for Parents<'_> { ParentEdge::ExtraEdgeIndex(_) => Some(Err(message!( "commit {}'s first parent is an extra edge index, which is invalid", self.commit_data.id(), - ))), + ) + .into())), }, ParentIteratorState::Second => match self.commit_data.parent2 { ParentEdge::None => None, @@ -164,13 +167,15 @@ impl Iterator for Parents<'_> { Some(Err(message!( "commit {}'s extra edges overflows the commit-graph file's extra edges list", self.commit_data.id() - ))) + ) + .into())) } } else { Some(Err(message!( "commit {} has extra edges, but commit-graph file has no extra edges list", self.commit_data.id() - ))) + ) + .into())) } } }, @@ -188,7 +193,8 @@ impl Iterator for Parents<'_> { Some(Err(message!( "commit {}'s extra edges overflows the commit-graph file's extra edges list", self.commit_data.id() - ))) + ) + .into())) } } ParentIteratorState::Exhausted => None, diff --git a/gix-commitgraph/src/file/init.rs b/gix-commitgraph/src/file/init.rs index ee98d0b7ad6..e74cf005595 100644 --- a/gix-commitgraph/src/file/init.rs +++ b/gix-commitgraph/src/file/init.rs @@ -1,6 +1,6 @@ use std::path::{Path, PathBuf}; -use gix_error::{ErrorExt, Exn, Message, ResultExt, message}; +use gix_error::{ErrorExt, Exn, ExnMessageResult, Message, ResultExt, message}; use crate::{ File, @@ -17,7 +17,7 @@ const MIN_FILE_SIZE: usize = HEADER_LEN impl File { /// Try to parse the commit graph file at `path`. - pub fn at(path: impl AsRef) -> Result> { + pub fn at(path: impl AsRef) -> ExnMessageResult { Self::try_from(path.as_ref()) } @@ -26,7 +26,7 @@ impl File { /// /// Note that `path` is only used for verification of the hash its basename contains, but otherwise /// is not of importance. - pub fn new(data: memmap2::Mmap, path: PathBuf) -> Result> { + pub fn new(data: memmap2::Mmap, path: PathBuf) -> ExnMessageResult { let data_size = data.len(); if data_size < MIN_FILE_SIZE { return Err(message("Commit-graph file too small even for an empty graph").raise()); diff --git a/gix-commitgraph/src/file/verify.rs b/gix-commitgraph/src/file/verify.rs index 19ff767a84a..9138e3a2213 100644 --- a/gix-commitgraph/src/file/verify.rs +++ b/gix-commitgraph/src/file/verify.rs @@ -5,7 +5,7 @@ use std::{ path::Path, }; -use gix_error::{ErrorExt, Exn, Message, ResultExt, message}; +use gix_error::{ErrorExt, ExnMessageResult, ExnResult, ResultExt, message}; use crate::{File, GENERATION_NUMBER_INFINITY, GENERATION_NUMBER_MAX, file}; @@ -35,9 +35,9 @@ impl File { /// Traverse all [commits][file::Commit] stored in this file and call `processor(commit) -> Result<(), Error>` on it. /// /// If the `processor` fails, the iteration will be stopped and the entire call results in the respective error. - pub fn traverse<'a, Processor>(&'a self, mut processor: Processor) -> Result> + pub fn traverse<'a, Processor>(&'a self, mut processor: Processor) -> ExnMessageResult where - Processor: FnMut(&file::Commit<'a>) -> Result<(), Exn>, + Processor: FnMut(&file::Commit<'a>) -> ExnResult, { self.verify_checksum()?; verify_split_chain_filename_hash(&self.path, self.checksum())?; @@ -102,8 +102,8 @@ impl File { /// Assure the [`checksum`][File::checksum()] matches the actual checksum over all content of this file, excluding the trailing /// checksum itself. /// - /// Return the actual checksum on success or [`Exn`] if there is a mismatch. - pub fn verify_checksum(&self) -> Result> { + /// Return the actual checksum on success or [`Exn`](gix_error::Exn) if there is a mismatch. + pub fn verify_checksum(&self) -> ExnMessageResult { let data_len_without_trailer = self.data.len() - self.hash_len; let mut hasher = gix_hash::hasher(self.object_hash()); hasher.update(&self.data[..data_len_without_trailer]); @@ -119,7 +119,7 @@ impl File { /// If the given path's filename matches "graph-{hash}.graph", check that `hash` matches the /// expected hash. -fn verify_split_chain_filename_hash(path: &Path, expected: &gix_hash::oid) -> Result<(), Exn> { +fn verify_split_chain_filename_hash(path: &Path, expected: &gix_hash::oid) -> ExnMessageResult { path.file_name() .and_then(std::ffi::OsStr::to_str) .and_then(|filename| filename.strip_suffix(".graph")) diff --git a/gix-commitgraph/src/init.rs b/gix-commitgraph/src/init.rs index ac0de2b3d5f..6d4b2fbde48 100644 --- a/gix-commitgraph/src/init.rs +++ b/gix-commitgraph/src/init.rs @@ -1,4 +1,5 @@ use crate::{File, Graph, MAX_COMMITS}; +use gix_error::ExnMessageResult; use gix_error::{ErrorExt, Exn, Message, ResultExt, message}; use std::{ io::{BufRead, BufReader}, @@ -10,12 +11,12 @@ impl Graph { /// Instantiate a commit graph from `path` which may be a directory containing graph files or the graph file itself. /// /// Filesystem errors retain their [`std::io::Error`] source, including when `path` does not exist. - pub fn at(path: &Path) -> Result> { + pub fn at(path: &Path) -> ExnMessageResult { Self::try_from(path) } /// Instantiate a commit graph from the directory containing all of its files. - pub fn from_commit_graphs_dir(path: &Path) -> Result> { + pub fn from_commit_graphs_dir(path: &Path) -> ExnMessageResult { let commit_graphs_dir = path; let chain_file_path = commit_graphs_dir.join("commit-graph-chain"); let chain_file = std::fs::File::open(&chain_file_path).or_raise(|| { @@ -38,31 +39,31 @@ impl Graph { .or_raise(|| message!("Could not open commit-graph file at '{}'", graph_file_path.display()))?, ); } - Ok(Self::new(files)?) + Self::new(files) } /// Instantiate a commit graph from a `.git/objects/info/commit-graph` or /// `.git/objects/info/commit-graphs/graph-*.graph` file. - pub fn from_file(path: &Path) -> Result> { + pub fn from_file(path: &Path) -> ExnMessageResult { let file = File::at(path).or_raise(|| message!("Could not open commit-graph file at '{}'", path.display()))?; - Ok(Self::new(vec![file])?) + Self::new(vec![file]) } /// Instantiate a commit graph from an `.git/objects/info` directory. - pub fn from_info_dir(info_dir: &Path) -> Result> { + pub fn from_info_dir(info_dir: &Path) -> ExnMessageResult { Self::from_file(&info_dir.join("commit-graph")) .or_else(|_| Self::from_commit_graphs_dir(&info_dir.join("commit-graphs"))) } /// Create a new commit graph from a list of `files`. - pub fn new(files: Vec) -> Result { + pub fn new(files: Vec) -> ExnMessageResult { let files = nonempty::NonEmpty::from_vec(files) .ok_or_else(|| message!("Commit-graph must contain at least one file"))?; let num_commits: u64 = files.iter().map(|f| u64::from(f.num_commits())).sum(); if num_commits > u64::from(MAX_COMMITS) { return Err(message!( "Commit-graph files contain {num_commits} commits altogether, but only {MAX_COMMITS} commits are allowed" - )); + ).into()); } let mut f1 = files.first(); @@ -74,7 +75,8 @@ impl Graph { hash1 = f1.object_hash(), path2 = f2.path().display(), hash2 = f2.object_hash(), - )); + ) + .into()); } f1 = f2; } @@ -86,7 +88,7 @@ impl Graph { impl TryFrom<&Path> for Graph { type Error = Exn; - fn try_from(path: &Path) -> Result { + fn try_from(path: &Path) -> std::result::Result { let metadata = path .metadata() .or_raise(|| message!("Could not access commit-graph path at '{}'", path.display()))?; diff --git a/gix-commitgraph/src/lib.rs b/gix-commitgraph/src/lib.rs index 7368250c25d..f1708eac173 100644 --- a/gix-commitgraph/src/lib.rs +++ b/gix-commitgraph/src/lib.rs @@ -15,9 +15,10 @@ #![cfg_attr(all(doc, feature = "document-features"), feature(doc_cfg))] #![deny(missing_docs, unsafe_code)] -use gix_error::{Exn, Message}; use std::path::Path; +use gix_error::ExnMessageResult; + /// A single commit-graph file. /// /// All operations on a `File` are local to that graph file. Since a commit graph can span multiple @@ -45,7 +46,7 @@ pub struct Graph { } /// Instantiate a commit graph from an `.git/objects/info` directory, or one of the various commit-graph files. -pub fn at(path: impl AsRef) -> Result> { +pub fn at(path: impl AsRef) -> ExnMessageResult { Graph::at(path.as_ref()) } diff --git a/gix-commitgraph/src/verify.rs b/gix-commitgraph/src/verify.rs index 52160867df7..e27c0a07538 100644 --- a/gix-commitgraph/src/verify.rs +++ b/gix-commitgraph/src/verify.rs @@ -4,7 +4,7 @@ use std::{ collections::BTreeMap, }; -use gix_error::{ErrorExt, Exn, Message, ResultExt, message}; +use gix_error::{ErrorExt, ExnMessageResult, ResultExt, message}; use crate::{ GENERATION_NUMBER_MAX, Graph, Position, @@ -34,7 +34,7 @@ impl Graph { pub fn verify_integrity( &self, mut processor: impl FnMut(&file::Commit<'_>) -> Result<(), E>, - ) -> Result> + ) -> ExnMessageResult where E: std::error::Error + Send + Sync + 'static, { @@ -93,7 +93,7 @@ impl Graph { let file_stats = file.traverse(|commit| { let mut max_parent_generation = 0u32; for parent_pos in commit.iter_parents() { - let parent_pos = parent_pos.map_err(|err| err.raise_erased())?; + let parent_pos = parent_pos.map_err(gix_error::Exn::erased)?; if parent_pos >= next_file_start_pos { return Err(message!( "Commit {} has parent position {parent_pos} that is out of range (should be in range 0-{})", diff --git a/gix-commitgraph/tests/commitgraph.rs b/gix-commitgraph/tests/commitgraph.rs index fb54c578b3f..b736c40d2c8 100644 --- a/gix-commitgraph/tests/commitgraph.rs +++ b/gix-commitgraph/tests/commitgraph.rs @@ -16,6 +16,11 @@ fn missing_path_is_not_found() -> gix_testtools::Result { let err = gix_commitgraph::at(dir.path().join("missing")) .err() .expect("a missing path cannot contain a commit-graph"); + insta::assert_debug_snapshot!(gix_testtools::redact_debug_snapshot(&(err), &[(&(dir.path()).to_string_lossy(), "")]), "callers can distinguish a missing optional cache from other failures", @" + Could not access commit-graph path at '/missing' + | + └─ NotFound + "); assert!( err.is_not_found(), "callers can distinguish a missing optional cache from other failures" @@ -34,11 +39,12 @@ fn checksum_mismatches_retain_their_classification() -> gix_testtools::Result { let graph = gix_commitgraph::File::at(path).map_err(gix_error::Exn::into_error)?; let err = graph.verify_checksum().expect_err("the checksum no longer matches"); + insta::assert_debug_snapshot!(gix_testtools::redact_debug_snapshot(&(err), &[]), "a checksum mismatch is corruption", @" + commit-graph checksum does not match + | + └─ Hash was Oid(1), but should have been Oid(2) + "); assert!(err.is_corrupted(), "a checksum mismatch is corruption"); - assert!( - err.into_error().is_corrupted(), - "conversion preserves the checksum failure" - ); Ok(()) } diff --git a/gix-config-value/Cargo.toml b/gix-config-value/Cargo.toml index 2c93a2ee23a..cdfcf5508d7 100644 --- a/gix-config-value/Cargo.toml +++ b/gix-config-value/Cargo.toml @@ -34,3 +34,7 @@ libc = "0.2" [package.metadata.docs.rs] all-features = true features = ["document-features"] + +[dev-dependencies] +gix-testtools = { path = "../tests/tools", default-features = false, features = ["sha1"] } +insta = "1.46.3" diff --git a/gix-config-value/src/boolean.rs b/gix-config-value/src/boolean.rs index bb8ae1b5db1..006698ed0d3 100644 --- a/gix-config-value/src/boolean.rs +++ b/gix-config-value/src/boolean.rs @@ -1,24 +1,21 @@ use std::{borrow::Cow, ffi::OsString, fmt::Display}; use bstr::{BStr, BString}; -use gix_error::{ErrorExt, ResultExt, ValidationError}; +use gix_error::{ErrorExt, Message, ResultExt, validation}; use crate::{Boolean, Integer}; -fn bool_err(input: impl Into) -> ValidationError { - ValidationError::new_with_input( - "Booleans need to be 'no', 'off', 'false', '' or 'yes', 'on', 'true' or any number", - input, - ) +fn bool_err(input: impl Into) -> Message { + validation("Booleans need to be 'no', 'off', 'false', '' or 'yes', 'on', 'true' or any number") + .with("input", gix_error::MetadataValue::Bytes(input.into())) } impl TryFrom for Boolean { - type Error = gix_error::Exn; + type Error = gix_error::Exn; fn try_from(value: OsString) -> Result { - let value = gix_path::os_str_into_bstr(&value).or_raise(|| { - ValidationError::new_with_input("Illformed UTF-8", std::path::Path::new(&value).display().to_string()) - })?; + let value = gix_path::os_str_into_bstr(&value) + .or_raise(|| validation("Illformed UTF-8").with("input", value.as_encoded_bytes()))?; Self::try_from(value) } } @@ -38,7 +35,7 @@ impl TryFrom for Boolean { /// Instead of this, obtain booleans with `config.boolean(…)`, which handles the case were no separator is /// present correctly. impl TryFrom<&BStr> for Boolean { - type Error = gix_error::Exn; + type Error = gix_error::Exn; fn try_from(value: &BStr) -> Result { if parse_true(value) { @@ -54,7 +51,7 @@ impl TryFrom<&BStr> for Boolean { } impl TryFrom<&str> for Boolean { - type Error = gix_error::Exn; + type Error = gix_error::Exn; fn try_from(value: &str) -> Result { Self::try_from(BStr::new(value)) @@ -71,14 +68,14 @@ impl Boolean { } impl TryFrom> for Boolean { - type Error = gix_error::Exn; + type Error = gix_error::Exn; fn try_from(c: Cow<'_, BStr>) -> Result { Self::try_from(c.as_ref()) } } impl TryFrom for Boolean { - type Error = gix_error::Exn; + type Error = gix_error::Exn; fn try_from(value: BString) -> Result { Self::try_from(BStr::new(&value)) } diff --git a/gix-config-value/src/color.rs b/gix-config-value/src/color.rs index dfd8423d514..c42335edacb 100644 --- a/gix-config-value/src/color.rs +++ b/gix-config-value/src/color.rs @@ -1,7 +1,7 @@ use std::{borrow::Cow, fmt::Display, str::FromStr}; use bstr::{BStr, BString}; -use gix_error::{ErrorExt, ResultExt, ValidationError}; +use gix_error::{ErrorExt, Message, ResultExt, validation}; use crate::Color; @@ -31,15 +31,13 @@ impl Display for Color { } } -fn color_err(input: impl Into) -> ValidationError { - ValidationError::new_with_input( - "Colors are specific color values and their attributes, like 'brightred', or 'blue'", - input, - ) +fn color_err(input: impl Into) -> Message { + validation("Colors are specific color values and their attributes, like 'brightred', or 'blue'") + .with("input", gix_error::MetadataValue::Bytes(input.into())) } impl TryFrom<&BStr> for Color { - type Error = gix_error::Exn; + type Error = gix_error::Exn; fn try_from(s: &BStr) -> Result { let s = std::str::from_utf8(s).or_raise(|| color_err(s))?; @@ -90,7 +88,7 @@ impl TryFrom<&BStr> for Color { } impl TryFrom<&str> for Color { - type Error = gix_error::Exn; + type Error = gix_error::Exn; fn try_from(value: &str) -> Result { Self::try_from(BStr::new(value)) @@ -98,7 +96,7 @@ impl TryFrom<&str> for Color { } impl TryFrom> for Color { - type Error = gix_error::Exn; + type Error = gix_error::Exn; fn try_from(c: Cow<'_, BStr>) -> Result { Self::try_from(c.as_ref()) @@ -106,7 +104,7 @@ impl TryFrom> for Color { } impl TryFrom for Color { - type Error = gix_error::Exn; + type Error = gix_error::Exn; fn try_from(value: BString) -> Result { Self::try_from(BStr::new(&value)) @@ -231,7 +229,7 @@ fn parse_hex(hex: &[u8]) -> Option<(u8, u8, u8)> { } impl FromStr for Name { - type Err = gix_error::Exn; + type Err = gix_error::Exn; fn from_str(s: &str) -> Result { const BASIC: &[(&str, Name, Name)] = &[ @@ -287,7 +285,7 @@ impl FromStr for Name { } impl TryFrom<&BStr> for Name { - type Error = gix_error::Exn; + type Error = gix_error::Exn; fn try_from(s: &BStr) -> Result { Self::from_str(std::str::from_utf8(s).or_raise(|| color_err(s))?) @@ -384,7 +382,7 @@ impl serde::Serialize for Attribute { } impl FromStr for Attribute { - type Err = gix_error::Exn; + type Err = gix_error::Exn; fn from_str(mut s: &str) -> Result { let inverted = if let Some(rest) = s.strip_prefix("no-").or_else(|| s.strip_prefix("no")) { @@ -423,7 +421,7 @@ impl FromStr for Attribute { } impl TryFrom<&BStr> for Attribute { - type Error = gix_error::Exn; + type Error = gix_error::Exn; fn try_from(s: &BStr) -> Result { Self::from_str(std::str::from_utf8(s).or_raise(|| color_err(s))?) diff --git a/gix-config-value/src/integer.rs b/gix-config-value/src/integer.rs index a1b33b7f45a..f48d9d2f140 100644 --- a/gix-config-value/src/integer.rs +++ b/gix-config-value/src/integer.rs @@ -1,7 +1,7 @@ use std::{borrow::Cow, fmt::Display, str::FromStr}; use bstr::{BStr, BString, ByteSlice}; -use gix_error::{ErrorExt, ResultExt, ValidationError}; +use gix_error::{ErrorExt, Message, ResultExt, validation}; use crate::Integer; @@ -48,11 +48,9 @@ impl serde::Serialize for Integer { } } -fn int_err(input: impl Into) -> ValidationError { - ValidationError::new_with_input( - "Integers needs to be positive or negative numbers which may have a suffix like 1k, 42, or 50G", - input, - ) +fn int_err(input: impl Into) -> Message { + validation("Integers needs to be positive or negative numbers which may have a suffix like 1k, 42, or 50G") + .with("input", gix_error::MetadataValue::Bytes(input.into())) } /// Parse `input` the way `git_parse_signed()` does, which hands the value to @@ -86,7 +84,7 @@ fn parse_like_git(input: &str) -> Option { } impl TryFrom<&BStr> for Integer { - type Error = gix_error::Exn; + type Error = gix_error::Exn; fn try_from(s: &BStr) -> Result { let s = std::str::from_utf8(s).or_raise(|| int_err(s))?; @@ -116,7 +114,7 @@ impl TryFrom<&BStr> for Integer { } impl TryFrom<&str> for Integer { - type Error = gix_error::Exn; + type Error = gix_error::Exn; fn try_from(value: &str) -> Result { Self::try_from(BStr::new(value)) @@ -124,7 +122,7 @@ impl TryFrom<&str> for Integer { } impl TryFrom> for Integer { - type Error = gix_error::Exn; + type Error = gix_error::Exn; fn try_from(c: Cow<'_, BStr>) -> Result { Self::try_from(c.as_ref()) @@ -132,7 +130,7 @@ impl TryFrom> for Integer { } impl TryFrom for Integer { - type Error = gix_error::Exn; + type Error = gix_error::Exn; fn try_from(value: BString) -> Result { Self::try_from(BStr::new(&value)) diff --git a/gix-config-value/src/path.rs b/gix-config-value/src/path.rs index 6c18dad8bb6..64299102617 100644 --- a/gix-config-value/src/path.rs +++ b/gix-config-value/src/path.rs @@ -1,7 +1,7 @@ use std::{borrow::Cow, path::PathBuf}; use bstr::{BStr, BString, ByteSlice}; -use gix_error::{ErrorExt, NotFoundError, OptionExt, ResultExt, ValidationError}; +use gix_error::{ErrorExt, ExnResult, OptionExt, ResultExt, not_found, validation}; use crate::Path; @@ -144,6 +144,8 @@ impl Path { /// /// Any other, non-empty path value is returned unchanged and error is returned in case of an empty path value or if the required /// input wasn't provided. + /// UTF-8 conversion failures include the invalid path or username bytes as `input` + /// [metadata](gix_error::Exn::metadata()). pub fn interpolate( self, interpolate::Context { @@ -151,22 +153,18 @@ impl Path { home_dir, home_for_user, }: interpolate::Context<'_>, - ) -> Result { + ) -> ExnResult { if self.is_empty() { - return Err(NotFoundError::new("path is missing").raise_erased()); + return Err(not_found("path is missing").raise_erased()); } const PREFIX: &[u8] = b"%(prefix)/"; if self.starts_with(PREFIX) { - let git_install_dir = - git_install_dir.ok_or_raise_erased(|| NotFoundError::new("git install dir is missing"))?; + let git_install_dir = git_install_dir.ok_or_raise_erased(|| not_found("git install dir is missing"))?; let (_prefix, path_without_trailing_slash) = self.split_at(PREFIX.len()); let path_without_trailing_slash = gix_path::try_from_bstring(path_without_trailing_slash).or_raise_erased(|| { - ValidationError::new_with_input( - "Ill-formed UTF-8 in path past %(prefix)", - path_without_trailing_slash, - ) + validation("Ill-formed UTF-8 in path past %(prefix)").with("input", path_without_trailing_slash) })?; Ok(git_install_dir.join(path_without_trailing_slash)) } else if let Some(val) = self.strip_prefix(b"~") { @@ -177,7 +175,7 @@ impl Path { let (mut home, what) = if username.is_empty() { ( home_dir - .ok_or_raise_erased(|| NotFoundError::new("home dir is missing"))? + .ok_or_raise_erased(|| not_found("home dir is missing"))? .to_path_buf(), "path past ~/", ) @@ -185,15 +183,16 @@ impl Path { ( Self::home_for_username( username, - home_for_user.ok_or_raise_erased(|| NotFoundError::new("home for user lookup is missing"))?, + home_for_user.ok_or_raise_erased(|| not_found("home for user lookup is missing"))?, )?, "path past ~user/", ) }; if let Some(path) = path { - home.push(gix_path::try_from_byte_slice(path).or_raise_erased(|| { - ValidationError::new_with_input(format!("Ill-formed UTF-8 in {what}"), path) - })?); + home.push( + gix_path::try_from_byte_slice(path) + .or_raise_erased(|| validation(format!("Ill-formed UTF-8 in {what}")).with("input", path))?, + ); } Ok(home) } else { @@ -201,12 +200,9 @@ impl Path { } } - fn home_for_username( - username: &[u8], - home_for_user: fn(&str) -> Option, - ) -> Result { + fn home_for_username(username: &[u8], home_for_user: fn(&str) -> Option) -> ExnResult { let username = std::str::from_utf8(username) - .or_raise_erased(|| ValidationError::new_with_input("Ill-formed UTF-8 in username", username))?; - home_for_user(username).ok_or_raise_erased(|| NotFoundError::new("pwd user info is missing")) + .or_raise_erased(|| validation("Ill-formed UTF-8 in username").with("input", username))?; + home_for_user(username).ok_or_raise_erased(|| not_found("pwd user info is missing")) } } diff --git a/gix-config-value/src/types.rs b/gix-config-value/src/types.rs index 4c2d1c399d6..99884f45aae 100644 --- a/gix-config-value/src/types.rs +++ b/gix-config-value/src/types.rs @@ -7,6 +7,7 @@ use crate::{color, integer}; /// Note that `git-config` allows color values to simply be a collection of /// [`color::Attribute`]s, and does not require a [`color::Name`] for either the /// foreground or background color. +/// Conversion errors expose invalid `input` bytes as [metadata](gix_error::Exn::metadata()). #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)] pub struct Color { /// A provided foreground color @@ -25,6 +26,7 @@ pub struct Color { /// suffix after fetching the value. [`integer::Suffix`] provides /// [`bitwise_offset()`][integer::Suffix::bitwise_offset] to help with the /// math, or [`to_decimal()`][Integer::to_decimal()] for obtaining a usable value in one step. +/// Conversion errors expose invalid `input` bytes as [metadata](gix_error::Exn::metadata()). #[derive(Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] pub struct Integer { /// The value, without any suffix modification @@ -34,6 +36,7 @@ pub struct Integer { } /// Any value that can be interpreted as a boolean. +/// Conversion errors expose invalid `input` bytes as [metadata](gix_error::Exn::metadata()). #[derive(Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] pub struct Boolean( /// The interpreted boolean value. diff --git a/gix-config-value/tests/value/boolean.rs b/gix-config-value/tests/value/boolean.rs index c5e845fd930..58799158b90 100644 --- a/gix-config-value/tests/value/boolean.rs +++ b/gix-config-value/tests/value/boolean.rs @@ -1,7 +1,8 @@ use gix_config_value::Boolean; +use gix_error::Result; #[test] -fn from_utf8_str() -> gix_error::Result { +fn from_utf8_str() -> Result { assert_eq!( Boolean::try_from("yes")?, Boolean(true), @@ -11,7 +12,7 @@ fn from_utf8_str() -> gix_error::Result { } #[test] -fn from_str_false() -> gix_error::Result { +fn from_str_false() -> Result { assert!(!Boolean::try_from("no")?.0); assert!(!Boolean::try_from("off")?.0); assert!(!Boolean::try_from("false")?.0); @@ -21,7 +22,7 @@ fn from_str_false() -> gix_error::Result { } #[test] -fn from_str_true() -> gix_error::Result { +fn from_str_true() -> Result { assert!(Boolean::try_from("yes")?.0); assert!(Boolean::try_from("on")?.0); assert!(Boolean::try_from("true")?.0); @@ -44,7 +45,7 @@ fn ignores_case() { } #[test] -fn numbers_are_parsed_as_integers() -> gix_error::Result { +fn numbers_are_parsed_as_integers() -> Result { // Use the same bases, suffixes, and full `i64` range as `Integer`. for (input, expected) in [ ("0x10", true), diff --git a/gix-config-value/tests/value/color.rs b/gix-config-value/tests/value/color.rs index 7f3a6b81b9a..112e4b261d7 100644 --- a/gix-config-value/tests/value/color.rs +++ b/gix-config-value/tests/value/color.rs @@ -1,7 +1,8 @@ use gix_config_value::Color; +use gix_error::Result; #[test] -fn from_utf8_str() -> gix_error::Result { +fn from_utf8_str() -> Result { assert_eq!( Color::try_from("red bold")?.to_string(), "red bold", @@ -201,6 +202,7 @@ mod attribute { mod from_git { use bstr::BStr; use gix_config_value::Color; + use gix_error::Result; #[test] fn reset() { @@ -288,7 +290,7 @@ mod from_git { try_color(name).expect("input color is expected to be valid") } - fn try_color<'a>(name: impl Into<&'a BStr>) -> gix_error::Result { + fn try_color<'a>(name: impl Into<&'a BStr>) -> Result { Ok(Color::try_from(name.into())?.to_string()) } } diff --git a/gix-config-value/tests/value/integer.rs b/gix-config-value/tests/value/integer.rs index ce3f76f7d88..7ed18d3c239 100644 --- a/gix-config-value/tests/value/integer.rs +++ b/gix-config-value/tests/value/integer.rs @@ -1,7 +1,8 @@ use gix_config_value::{Integer, integer::Suffix}; +use gix_error::Result; #[test] -fn from_utf8_str() -> gix_error::Result { +fn from_utf8_str() -> Result { assert_eq!( Integer::try_from("1k")?, Integer { diff --git a/gix-config-value/tests/value/path.rs b/gix-config-value/tests/value/path.rs index ee199d98894..69f267d3f04 100644 --- a/gix-config-value/tests/value/path.rs +++ b/gix-config-value/tests/value/path.rs @@ -1,12 +1,14 @@ mod interpolate { + use gix_error::Result; use std::path::{Path, PathBuf}; + use gix_error::ExnResult; + use bstr::BString; use gix_config_value::path; - use gix_error::NotFoundError; #[test] - fn backslash_is_not_special_and_they_are_not_escaping_anything() -> gix_error::Result { + fn backslash_is_not_special_and_they_are_not_escaping_anything() -> Result { for path in [r"C:\foo\bar", "/foo/bar"] { let actual = gix_config_value::Path::from(path).interpolate(Default::default())?; assert_eq!(actual, Path::new(path)); @@ -17,6 +19,7 @@ mod interpolate { #[test] fn empty_path_is_error() { let err = interpolate_without_context("").expect_err("empty paths are invalid"); + insta::assert_debug_snapshot!(err, "empty path is error", @"path is missing"); assert!(err.is_not_found()); } @@ -56,7 +59,7 @@ mod interpolate { } #[test] - fn tilde_alone_substitutes_current_user() -> gix_error::Result { + fn tilde_alone_substitutes_current_user() -> Result { let home = std::env::current_dir().expect("current directory is available"); assert_eq!( gix_config_value::Path::from("~").interpolate(path::interpolate::Context { @@ -66,18 +69,20 @@ mod interpolate { home ); let err = interpolate_without_context("~").expect_err("tilde expansion needs the current user's home"); - assert_eq!( - err.downcast_any_ref::() + insta::assert_debug_snapshot!(err.classify() + .find(|classification| classification.class() == gix_error::Class::NotFound) .expect("missing home directories are classified as not found") - .to_string(), - "home dir is missing", - "tilde expansion reports the missing home directory" - ); + .error(), "tilde expansion reports the missing home directory", @r#" + Message { + message: "home dir is missing", + class: NotFound, + } + "#); Ok(()) } #[test] - fn tilde_slash_substitutes_current_user() -> gix_error::Result { + fn tilde_slash_substitutes_current_user() -> Result { let home = std::env::current_dir().expect("current directory is available"); for suffix in ["", "user/bar", r"user\bar", "/user/bar"] { let actual = gix_config_value::Path::from(format!("~/{suffix}").as_str()).interpolate( @@ -97,7 +102,8 @@ mod interpolate { } #[test] - fn tilde_with_given_user() -> gix_error::Result { + fn tilde_with_given_user() -> Result { + let mut error_snapshots = Vec::new(); let home = std::env::current_dir().expect("current directory is available"); for path_suffix in &["foo/bar", r"foo\bar", ""] { @@ -118,14 +124,15 @@ mod interpolate { ); for path in ["~nonexistent", "~nonexistent/foo"] { let err = interpolate_without_context(path).expect_err("the named user does not exist"); - assert_eq!( - err.downcast_any_ref::() - .expect("missing users are classified as not found") - .to_string(), - "pwd user info is missing", - "named-user expansion reports the missing user" - ); + error_snapshots.push(gix_testtools::redact_debug_snapshot(&(err), &[])); + assert!(err.is_not_found(), "named-user expansion classifies missing users"); } + insta::assert_debug_snapshot!(error_snapshots, "tilde with given user", @" + [ + pwd user info is missing, + pwd user info is missing, + ] + "); Ok(()) } @@ -137,11 +144,16 @@ mod interpolate { ..Default::default() }) .expect_err("the username is not UTF-8"); + insta::assert_debug_snapshot!(err, "malformed usernames are validation errors with the utf8 cause", @r#" + Ill-formed UTF-8 in username, "input"="\xff" + | + └─ invalid utf-8 sequence of 1 bytes from index 0 + "#); assert!(err.is_validation()); assert!(err.downcast_any_ref::().is_some()); } - fn interpolate_without_context(path: impl AsRef) -> Result { + fn interpolate_without_context(path: impl AsRef) -> ExnResult { gix_config_value::Path::from(path.as_ref()).interpolate(path::interpolate::Context { home_for_user: Some(home_for_user), ..Default::default() diff --git a/gix-config/src/file/access/comfort.rs b/gix-config/src/file/access/comfort.rs index 59f09d690af..6d0ea859541 100644 --- a/gix-config/src/file/access/comfort.rs +++ b/gix-config/src/file/access/comfort.rs @@ -1,5 +1,6 @@ use bstr::{BStr, BString}; -use gix_error::{ErrorExt, ValidationError}; +use gix_error::ExnMessageResult; +use gix_error::{ErrorExt, validation}; use crate::{AsBStrOpt, AsKey, File, file::Metadata}; @@ -86,7 +87,7 @@ impl File { } /// Like [`boolean_by()`](File::boolean_by()), but suitable for statically known `key`s like `remote.origin.url`. - pub fn boolean(&self, key: impl AsKey) -> Result, gix_error::Exn> { + pub fn boolean(&self, key: impl AsKey) -> ExnMessageResult> { self.boolean_filter(key, |_| true) } @@ -96,7 +97,7 @@ impl File { section_name: impl AsRef, subsection_name: impl AsBStrOpt, value_name: impl AsRef, - ) -> Result, gix_error::Exn> { + ) -> ExnMessageResult> { self.boolean_filter_by(section_name, subsection_name, value_name, |_| true) } @@ -105,7 +106,7 @@ impl File { &self, key: impl AsKey, filter: impl FnMut(&Metadata) -> bool, - ) -> Result, gix_error::Exn> { + ) -> ExnMessageResult> { let Some(key) = key.try_as_key() else { return Ok(None); }; @@ -119,7 +120,7 @@ impl File { subsection_name: impl AsBStrOpt, value_name: impl AsRef, mut filter: impl FnMut(&Metadata) -> bool, - ) -> Result, gix_error::Exn> { + ) -> ExnMessageResult> { let section_name = section_name.as_ref(); let section_ids = self .section_ids_by_name_and_subname(section_name, subsection_name.as_bstr_opt()) @@ -143,7 +144,7 @@ impl File { } /// Like [`integer_by()`](File::integer_by()), but suitable for statically known `key`s like `remote.origin.url`. - pub fn integer(&self, key: impl AsKey) -> Result, gix_error::Exn> { + pub fn integer(&self, key: impl AsKey) -> ExnMessageResult> { self.integer_filter(key, |_| true) } @@ -153,7 +154,7 @@ impl File { section_name: impl AsRef, subsection_name: impl AsBStrOpt, value_name: impl AsRef, - ) -> Result, gix_error::Exn> { + ) -> ExnMessageResult> { self.integer_filter_by(section_name, subsection_name, value_name, |_| true) } @@ -162,7 +163,7 @@ impl File { &self, key: impl AsKey, filter: impl FnMut(&Metadata) -> bool, - ) -> Result, gix_error::Exn> { + ) -> ExnMessageResult> { let Some(key) = key.try_as_key() else { return Ok(None); }; @@ -170,13 +171,14 @@ impl File { } /// Like [`integer_by()`](File::integer_by()), but the section containing the returned value must pass `filter` as well. + /// Invalid or overflowing values include their bytes as `input` [metadata](gix_error::Exn::metadata()). pub fn integer_filter_by( &self, section_name: impl AsRef, subsection_name: impl AsBStrOpt, value_name: impl AsRef, filter: impl FnMut(&Metadata) -> bool, - ) -> Result, gix_error::Exn> { + ) -> ExnMessageResult> { let Some(int) = self .raw_value_filter_by(section_name, subsection_name, value_name, filter) .ok() @@ -186,7 +188,7 @@ impl File { crate::Integer::try_from(BStr::new(&int)) .and_then(|b| { b.to_decimal() - .ok_or_else(|| ValidationError::new_with_input("Integer overflow", int).raise()) + .ok_or_else(|| validation("Integer overflow").with("input", BStr::new(&int)).raise()) }) .map(Some) } @@ -226,7 +228,7 @@ impl File { } /// Like [`integers()`](File::integers()), but suitable for statically known `key`s like `remote.origin.url`. - pub fn integers(&self, key: impl AsKey) -> Result>, gix_error::Exn> { + pub fn integers(&self, key: impl AsKey) -> ExnMessageResult>> { self.integers_filter(key, |_| true) } @@ -237,7 +239,7 @@ impl File { section_name: impl AsRef, subsection_name: impl AsBStrOpt, value_name: impl AsRef, - ) -> Result>, gix_error::Exn> { + ) -> ExnMessageResult>> { self.integers_filter_by(section_name, subsection_name, value_name, |_| true) } @@ -246,7 +248,7 @@ impl File { &self, key: impl AsKey, filter: impl FnMut(&Metadata) -> bool, - ) -> Result>, gix_error::Exn> { + ) -> ExnMessageResult>> { let Some(key) = key.try_as_key() else { return Ok(None); }; @@ -255,13 +257,14 @@ impl File { /// Similar to [`integers_by(…)`](File::integers_by()) but all integers are in sections that passed `filter` /// and that are not overflowing. + /// Invalid or overflowing values include their bytes as `input` [metadata](gix_error::Exn::metadata()). pub fn integers_filter_by( &self, section_name: impl AsRef, subsection_name: impl AsBStrOpt, value_name: impl AsRef, filter: impl FnMut(&Metadata) -> bool, - ) -> Result>, gix_error::Exn> { + ) -> ExnMessageResult>> { let Some(values) = self .raw_values_filter_by(section_name, subsection_name, value_name, filter) .ok() @@ -273,7 +276,7 @@ impl File { .map(|v| { crate::Integer::try_from(BStr::new(&v)).and_then(|int| { int.to_decimal() - .ok_or_else(|| ValidationError::new_with_input("Integer overflow", v).raise()) + .ok_or_else(|| validation("Integer overflow").with("input", BStr::new(&v)).raise()) }) }) .collect::, _>>() diff --git a/gix-config/src/file/access/mutate.rs b/gix-config/src/file/access/mutate.rs index 3c9823202a5..5f2c6786bfe 100644 --- a/gix-config/src/file/access/mutate.rs +++ b/gix-config/src/file/access/mutate.rs @@ -1,4 +1,6 @@ use bstr::BStr; +use gix_error::ExnMessageResult; +use gix_error::ExnResult; use gix_features::threading::OwnShared; use crate::{ @@ -47,19 +49,11 @@ impl IntoBStringOpt for &T { /// Mutating low-level access methods. impl File { /// Returns the last mutable section with a given `name` and optional `subsection_name`, _if it exists_. - pub fn section_mut( - &mut self, - name: impl AsRef, - subsection_name: impl AsBStrOpt, - ) -> Result, gix_error::Exn> { + pub fn section_mut(&mut self, name: impl AsRef, subsection_name: impl AsBStrOpt) -> ExnResult> { self.section_mut_inner(name.as_ref(), subsection_name.as_bstr_opt()) } - fn section_mut_inner<'a>( - &'a mut self, - name: &str, - subsection_name: Option<&BStr>, - ) -> Result, gix_error::Exn> { + fn section_mut_inner<'a>(&'a mut self, name: &str, subsection_name: Option<&BStr>) -> ExnResult> { let id = self .section_ids_by_name_and_subname(name, subsection_name)? .next_back() @@ -71,7 +65,7 @@ impl File { } /// Returns the last found mutable section with a given `key`, identifying the name and subsection name like `core` or `remote.origin`. - pub fn section_mut_by_key(&mut self, key: impl crate::AsBStr) -> Result, gix_error::Exn> { + pub fn section_mut_by_key(&mut self, key: impl crate::AsBStr) -> ExnResult> { let key = section::unvalidated::KeyRef::parse(&key).ok_or_else(lookup::existing::key_missing)?; self.section_mut_inner(key.section_name, key.subsection_name) } @@ -89,7 +83,7 @@ impl File { &mut self, name: impl AsRef, subsection_name: impl AsBStrOpt, - ) -> Result, gix_error::ValidationError> { + ) -> ExnMessageResult> { self.section_mut_or_create_new_inner(name.as_ref(), subsection_name.as_bstr_opt()) } @@ -97,7 +91,7 @@ impl File { &'a mut self, name: &str, subsection_name: Option<&BStr>, - ) -> Result, gix_error::ValidationError> { + ) -> ExnMessageResult> { self.section_mut_or_create_new_filter_inner(name, subsection_name, |_| true) } @@ -108,7 +102,7 @@ impl File { name: impl AsRef, subsection_name: impl AsBStrOpt, filter: impl FnMut(&Metadata) -> bool, - ) -> Result, gix_error::ValidationError> { + ) -> ExnMessageResult> { self.section_mut_or_create_new_filter_inner(name.as_ref(), subsection_name.as_bstr_opt(), filter) } @@ -117,7 +111,7 @@ impl File { name: &str, subsection_name: Option<&BStr>, mut filter: impl FnMut(&Metadata) -> bool, - ) -> Result, gix_error::ValidationError> { + ) -> ExnMessageResult> { match self .section_ids_by_name_and_subname(name, subsection_name) .ok() @@ -144,7 +138,7 @@ impl File { name: impl AsRef, subsection_name: impl AsBStrOpt, filter: impl FnMut(&Metadata) -> bool, - ) -> Result>, gix_error::Exn> { + ) -> ExnResult>> { self.section_mut_filter_inner(name.as_ref(), subsection_name.as_bstr_opt(), filter) } @@ -153,7 +147,7 @@ impl File { name: &str, subsection_name: Option<&BStr>, mut filter: impl FnMut(&Metadata) -> bool, - ) -> Result>, gix_error::Exn> { + ) -> ExnResult>> { let id = self .section_ids_by_name_and_subname(name, subsection_name)? .rev() @@ -171,7 +165,7 @@ impl File { &mut self, key: impl crate::AsBStr, filter: impl FnMut(&Metadata) -> bool, - ) -> Result>, gix_error::Exn> { + ) -> ExnResult>> { let key = section::unvalidated::KeyRef::parse(&key).ok_or_else(lookup::existing::key_missing)?; self.section_mut_filter_inner(key.section_name, key.subsection_name, filter) } @@ -191,7 +185,7 @@ impl File { /// let section = git_config.new_section("hello", "world")?; /// let nl = section.newline().to_owned(); /// assert_eq!(git_config.to_string(), format!("[hello \"world\"]{nl}")); - /// # Ok::<(), Box>(()) + /// # Ok::<(), Box>(()) /// ``` /// /// Creating a new empty section and adding values to it: @@ -208,21 +202,17 @@ impl File { /// assert_eq!(git_config.to_string(), format!("[hello \"world\"]{nl}\ta = b{nl}")); /// let _section = git_config.new_section("core", None); /// assert_eq!(git_config.to_string(), format!("[hello \"world\"]{nl}\ta = b{nl}[core]{nl}")); - /// # Ok::<(), Box>(()) + /// # Ok::<(), Box>(()) /// ``` pub fn new_section( &mut self, name: impl AsRef, subsection: impl IntoBStringOpt, - ) -> Result, gix_error::ValidationError> { + ) -> ExnMessageResult> { self.new_section_inner(name.as_ref(), subsection.into_bstring_opt()) } - fn new_section_inner( - &mut self, - name: &str, - subsection: Option, - ) -> Result, gix_error::ValidationError> { + fn new_section_inner(&mut self, name: &str, subsection: Option) -> ExnMessageResult> { let section = file::SectionData::new(name, subsection, OwnShared::clone(&self.meta), &mut self.backing)?; let id = self.push_section_internal(section); let nl = self.detect_newline_style_smallvec(); @@ -250,7 +240,7 @@ impl File { /// let section = git_config.remove_section("hello", "world"); /// assert!(section.is_some()); /// assert_eq!(git_config.to_string(), ""); - /// # Ok::<(), Box>(()) + /// # Ok::<(), Box>(()) /// ``` /// /// Precedence example for removing sections with the same name: @@ -268,7 +258,7 @@ impl File { /// let section = git_config.remove_section("hello", "world"); /// assert!(section.is_some()); /// assert_eq!(git_config.to_string(), "[hello \"world\"]\n some-value = 4\n"); - /// # Ok::<(), Box>(()) + /// # Ok::<(), Box>(()) /// ``` pub fn remove_section(&mut self, name: impl AsRef, subsection_name: impl AsBStrOpt) -> Option { let id = self @@ -332,7 +322,7 @@ impl File { /// Adds the provided `section` to the config, returning a mutable reference to it for immediate editing. /// Note that its meta-data will remain as is. - pub fn push_section(&mut self, section: file::Section) -> Result, gix_error::ValidationError> { + pub fn push_section(&mut self, section: file::Section) -> ExnMessageResult> { let section = section.into_data(&mut self.backing)?; let id = self.push_section_internal(section); let nl = self.detect_newline_style_smallvec(); @@ -349,7 +339,7 @@ impl File { subsection_name: impl AsBStrOpt, new_name: impl AsRef, new_subsection_name: impl IntoBStringOpt, - ) -> Result<(), gix_error::Exn> { + ) -> ExnResult { self.rename_section_filter(name, subsection_name, new_name, new_subsection_name, |_| true) } @@ -366,7 +356,7 @@ impl File { new_name: impl AsRef, new_subsection_name: impl IntoBStringOpt, mut filter: impl FnMut(&Metadata) -> bool, - ) -> Result<(), gix_error::Exn> { + ) -> ExnResult { let ids: Vec<_> = self .section_ids_by_name_and_subname(name.as_ref(), subsection_name.as_bstr_opt())? .filter(|id| filter(&self.sections.get(id).expect("each id has a section").meta)) @@ -392,7 +382,7 @@ impl File { } /// Append another File to the end of ourselves, without losing any information. - pub fn append(&mut self, other: Self) -> Result<&mut Self, gix_error::ValidationError> { + pub fn append(&mut self, other: Self) -> ExnMessageResult<&mut Self> { self.append_or_insert(other, None) } @@ -401,7 +391,7 @@ impl File { &mut self, mut other: Self, mut insert_after: Option, - ) -> Result<&mut Self, gix_error::ValidationError> { + ) -> ExnMessageResult<&mut Self> { let nl = self.detect_newline_style_smallvec(); let our_last_section_before_append = insert_after.or_else(|| (self.next_section_id != 0).then(|| SectionId(self.next_section_id - 1))); @@ -465,7 +455,7 @@ impl File { Ok(self) } - fn rebase_events(&mut self, offset: usize) -> Result<(), gix_error::ValidationError> { + fn rebase_events(&mut self, offset: usize) -> ExnMessageResult { for event in &mut self.frontmatter_events { event.rebase(offset)?; } diff --git a/gix-config/src/file/access/raw.rs b/gix-config/src/file/access/raw.rs index 581aa34aeb1..a6dc076bf48 100644 --- a/gix-config/src/file/access/raw.rs +++ b/gix-config/src/file/access/raw.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; use bstr::{BStr, BString}; -use gix_error::ResultExt; +use gix_error::{ExnResult, ResultExt}; use smallvec::ToSmallVec; use crate::{ @@ -19,7 +19,7 @@ impl File { /// /// Consider [`Self::raw_values()`] if you want to get all values of /// a multivar instead. - pub fn raw_value(&self, key: impl AsKey) -> Result { + pub fn raw_value(&self, key: impl AsKey) -> ExnResult { let key = key.as_key(); self.raw_value_filter_by(key.section_name, key.subsection_name, key.value_name, |_| true) } @@ -34,7 +34,7 @@ impl File { section_name: impl AsRef, subsection_name: impl AsBStrOpt, value_name: impl AsRef, - ) -> Result { + ) -> ExnResult { self.raw_value_filter_by(section_name, subsection_name, value_name, |_| true) } @@ -42,7 +42,7 @@ impl File { /// /// Resolution is identical to [`raw_value()`][Self::raw_value()]: the last explicit value wins, even across /// multiple matching sections. - pub fn raw_value_with_section(&self, key: impl AsKey) -> Result<(BString, file::SectionRef<'_>), gix_error::Exn> { + pub fn raw_value_with_section(&self, key: impl AsKey) -> ExnResult<(BString, file::SectionRef<'_>)> { let key = key.as_key(); self.raw_value_with_section_by(key.section_name, key.subsection_name, key.value_name) } @@ -56,7 +56,7 @@ impl File { section_name: impl AsRef, subsection_name: impl AsBStrOpt, value_name: impl AsRef, - ) -> Result<(BString, file::SectionRef<'_>), gix_error::Exn> { + ) -> ExnResult<(BString, file::SectionRef<'_>)> { self.raw_value_with_section_filter_by(section_name, subsection_name, value_name, |_| true) } @@ -68,7 +68,7 @@ impl File { &self, key: impl AsKey, filter: impl FnMut(&Metadata) -> bool, - ) -> Result<(BString, file::SectionRef<'_>), gix_error::Exn> { + ) -> ExnResult<(BString, file::SectionRef<'_>)> { let key = key.as_key(); self.raw_value_with_section_filter_by(key.section_name, key.subsection_name, key.value_name, filter) } @@ -81,7 +81,7 @@ impl File { subsection_name: impl AsBStrOpt, value_name: impl AsRef, filter: impl FnMut(&Metadata) -> bool, - ) -> Result<(BString, file::SectionRef<'_>), gix_error::Exn> { + ) -> ExnResult<(BString, file::SectionRef<'_>)> { self.raw_value_with_section_filter_inner( section_name.as_ref(), subsection_name.as_bstr_opt(), @@ -94,11 +94,7 @@ impl File { /// /// Consider [`Self::raw_values()`] if you want to get all values of /// a multivar instead. - pub fn raw_value_filter( - &self, - key: impl AsKey, - filter: impl FnMut(&Metadata) -> bool, - ) -> Result { + pub fn raw_value_filter(&self, key: impl AsKey, filter: impl FnMut(&Metadata) -> bool) -> ExnResult { let key = key.as_key(); self.raw_value_filter_by(key.section_name, key.subsection_name, key.value_name, filter) } @@ -114,7 +110,7 @@ impl File { subsection_name: impl AsBStrOpt, value_name: impl AsRef, filter: impl FnMut(&Metadata) -> bool, - ) -> Result { + ) -> ExnResult { self.raw_value_filter_inner( section_name.as_ref(), subsection_name.as_bstr_opt(), @@ -129,7 +125,7 @@ impl File { subsection_name: Option<&BStr>, value_name: &str, filter: impl FnMut(&Metadata) -> bool, - ) -> Result { + ) -> ExnResult { self.raw_value_with_section_filter_inner(section_name, subsection_name, value_name, filter) .map(|(value, _section)| value) } @@ -140,7 +136,7 @@ impl File { subsection_name: Option<&BStr>, value_name: &str, mut filter: impl FnMut(&Metadata) -> bool, - ) -> Result<(BString, file::SectionRef<'_>), gix_error::Exn> { + ) -> ExnResult<(BString, file::SectionRef<'_>)> { let section_ids = self.section_ids_by_name_and_subname(section_name, subsection_name)?; for section_id in section_ids.rev() { let section = self.sections.get(§ion_id).expect("known section id"); @@ -159,7 +155,7 @@ impl File { /// /// Consider [`Self::raw_values_mut`] if you want to get mutable /// references to all values of a multivar instead. - pub fn raw_value_mut(&mut self, key: impl AsKey) -> Result, gix_error::Exn> { + pub fn raw_value_mut(&mut self, key: impl AsKey) -> ExnResult> { let key = key.as_key(); self.raw_value_mut_filter_inner(key.section_name, key.subsection_name, key.value_name, |_| true) } @@ -174,7 +170,7 @@ impl File { section_name: impl AsRef, subsection_name: impl AsBStrOpt, value_name: impl AsRef, - ) -> Result, gix_error::Exn> { + ) -> ExnResult> { self.raw_value_mut_filter_by(section_name, subsection_name, value_name, |_| true) } @@ -186,7 +182,7 @@ impl File { &mut self, key: impl AsKey, filter: impl FnMut(&Metadata) -> bool, - ) -> Result, gix_error::Exn> { + ) -> ExnResult> { let key = key.as_key(); self.raw_value_mut_filter_inner(key.section_name, key.subsection_name, key.value_name, filter) } @@ -201,7 +197,7 @@ impl File { subsection_name: impl AsBStrOpt, value_name: impl AsRef, filter: impl FnMut(&Metadata) -> bool, - ) -> Result, gix_error::Exn> { + ) -> ExnResult> { self.raw_value_mut_filter_inner( section_name.as_ref(), subsection_name.as_bstr_opt(), @@ -216,7 +212,7 @@ impl File { subsection_name: Option<&BStr>, value_name: &str, mut filter: impl FnMut(&Metadata) -> bool, - ) -> Result, gix_error::Exn> { + ) -> ExnResult> { let mut section_ids = self .section_ids_by_name_and_subname(section_name, subsection_name)? .rev(); @@ -307,7 +303,7 @@ impl File { /// /// Consider [`Self::raw_value`] if you want to get the resolved single /// value for a given key, if your value does not support multi-valued values. - pub fn raw_values(&self, key: impl AsKey) -> Result, gix_error::Exn> { + pub fn raw_values(&self, key: impl AsKey) -> ExnResult> { let key = key.as_key(); self.raw_values_by(key.section_name, key.subsection_name, key.value_name) } @@ -353,15 +349,12 @@ impl File { section_name: impl AsRef, subsection_name: impl AsBStrOpt, value_name: impl AsRef, - ) -> Result, gix_error::Exn> { + ) -> ExnResult> { self.raw_values_filter_by(section_name, subsection_name, value_name, |_| true) } /// Returns all uninterpreted values and their containing sections given a `key`, in order of occurrence. - pub fn raw_values_with_sections( - &self, - key: impl AsKey, - ) -> Result)>, gix_error::Exn> { + pub fn raw_values_with_sections(&self, key: impl AsKey) -> ExnResult)>> { let key = key.as_key(); self.raw_values_with_sections_by(key.section_name, key.subsection_name, key.value_name) } @@ -373,7 +366,7 @@ impl File { section_name: impl AsRef, subsection_name: impl AsBStrOpt, value_name: impl AsRef, - ) -> Result)>, gix_error::Exn> { + ) -> ExnResult)>> { self.raw_values_with_sections_filter_by(section_name, subsection_name, value_name, |_| true) } @@ -383,7 +376,7 @@ impl File { &self, key: impl AsKey, filter: impl FnMut(&Metadata) -> bool, - ) -> Result)>, gix_error::Exn> { + ) -> ExnResult)>> { let key = key.as_key(); self.raw_values_with_sections_filter_by(key.section_name, key.subsection_name, key.value_name, filter) } @@ -396,7 +389,7 @@ impl File { subsection_name: impl AsBStrOpt, value_name: impl AsRef, filter: impl FnMut(&Metadata) -> bool, - ) -> Result)>, gix_error::Exn> { + ) -> ExnResult)>> { self.raw_values_with_sections_filter_inner( section_name.as_ref(), subsection_name.as_bstr_opt(), @@ -409,11 +402,7 @@ impl File { /// /// The ordering means that the last of the returned values is the one that would be the /// value used in the single-value case. - pub fn raw_values_filter( - &self, - key: impl AsKey, - filter: impl FnMut(&Metadata) -> bool, - ) -> Result, gix_error::Exn> { + pub fn raw_values_filter(&self, key: impl AsKey, filter: impl FnMut(&Metadata) -> bool) -> ExnResult> { let key = key.as_key(); self.raw_values_filter_by(key.section_name, key.subsection_name, key.value_name, filter) } @@ -429,7 +418,7 @@ impl File { subsection_name: impl AsBStrOpt, value_name: impl AsRef, filter: impl FnMut(&Metadata) -> bool, - ) -> Result, gix_error::Exn> { + ) -> ExnResult> { self.raw_values_filter_inner( section_name.as_ref(), subsection_name.as_bstr_opt(), @@ -444,7 +433,7 @@ impl File { subsection_name: Option<&BStr>, value_name: &str, filter: impl FnMut(&Metadata) -> bool, - ) -> Result, gix_error::Exn> { + ) -> ExnResult> { self.raw_values_with_sections_filter_inner(section_name, subsection_name, value_name, filter) .map(|values| values.into_iter().map(|(value, _section)| value).collect()) } @@ -455,7 +444,7 @@ impl File { subsection_name: Option<&BStr>, value_name: &str, mut filter: impl FnMut(&Metadata) -> bool, - ) -> Result)>, gix_error::Exn> { + ) -> ExnResult)>> { let mut values = Vec::new(); let section_ids = self.section_ids_by_name_and_subname(section_name, subsection_name)?; for section_id in section_ids { @@ -527,7 +516,7 @@ impl File { /// /// Note that this operation is relatively expensive, requiring a full /// traversal of the config. - pub fn raw_values_mut(&mut self, key: impl AsKey) -> Result, gix_error::Exn> { + pub fn raw_values_mut(&mut self, key: impl AsKey) -> ExnResult> { let key = key.as_key(); self.raw_values_mut_filter_inner(key.section_name, key.subsection_name, key.value_name, |_| true) } @@ -585,7 +574,7 @@ impl File { section_name: impl AsRef, subsection_name: impl AsBStrOpt, value_name: impl AsRef, - ) -> Result, gix_error::Exn> { + ) -> ExnResult> { self.raw_values_mut_filter_by(section_name, subsection_name, value_name, |_| true) } @@ -595,7 +584,7 @@ impl File { &mut self, key: impl AsKey, filter: impl FnMut(&Metadata) -> bool, - ) -> Result, gix_error::Exn> { + ) -> ExnResult> { let key = key.as_key(); self.raw_values_mut_filter_inner(key.section_name, key.subsection_name, key.value_name, filter) } @@ -608,7 +597,7 @@ impl File { subsection_name: impl AsBStrOpt, value_name: impl AsRef, filter: impl FnMut(&Metadata) -> bool, - ) -> Result, gix_error::Exn> { + ) -> ExnResult> { self.raw_values_mut_filter_inner( section_name.as_ref(), subsection_name.as_bstr_opt(), @@ -623,7 +612,7 @@ impl File { subsection_name: Option<&BStr>, value_name: &str, mut filter: impl FnMut(&Metadata) -> bool, - ) -> Result, gix_error::Exn> { + ) -> ExnResult> { let section_ids = self.section_ids_by_name_and_subname(section_name, subsection_name)?; let key = section::ValueName::try_from(value_name).or_erased()?; @@ -715,11 +704,7 @@ impl File { /// ); /// # Ok::<(), Box>(()) /// ``` - pub fn set_existing_raw_value( - &mut self, - key: impl AsKey, - new_value: impl crate::AsBStr, - ) -> Result<(), gix_error::Exn> { + pub fn set_existing_raw_value(&mut self, key: impl AsKey, new_value: impl crate::AsBStr) -> ExnResult { let key = key.as_key(); self.raw_value_mut_filter_inner(key.section_name, key.subsection_name, key.value_name, |_| true)? .set(new_value) @@ -767,7 +752,7 @@ impl File { subsection_name: impl AsBStrOpt, value_name: impl AsRef, new_value: impl crate::AsBStr, - ) -> Result<(), gix_error::Exn> { + ) -> ExnResult { self.raw_value_mut_by(section_name, subsection_name, value_name)? .set(new_value) .or_erased()?; @@ -798,11 +783,7 @@ impl File { /// assert_eq!(git_config.raw_value("core.b").expect("value exists"), "f"); /// # Ok::<(), Box>(()) /// ``` - pub fn set_raw_value( - &mut self, - key: impl AsKey, - new_value: impl crate::AsBStr, - ) -> Result, gix_error::Exn> { + pub fn set_raw_value(&mut self, key: impl AsKey, new_value: impl crate::AsBStr) -> ExnResult> { self.set_raw_value_filter(key, new_value, |_| true) } @@ -836,7 +817,7 @@ impl File { subsection_name: impl AsBStrOpt, value_name: impl AsRef, new_value: impl crate::AsBStr, - ) -> Result, gix_error::Exn> { + ) -> ExnResult> { self.set_raw_value_filter_by(section_name, subsection_name, value_name, new_value, |_| true) } @@ -847,7 +828,7 @@ impl File { key: impl AsKey, new_value: impl crate::AsBStr, filter: impl FnMut(&Metadata) -> bool, - ) -> Result, gix_error::Exn> { + ) -> ExnResult> { let key = key.as_key(); self.set_raw_value_filter_by_inner(key.section_name, key.subsection_name, key.value_name, new_value, filter) } @@ -861,7 +842,7 @@ impl File { value_name: impl AsRef, new_value: impl crate::AsBStr, filter: impl FnMut(&Metadata) -> bool, - ) -> Result, gix_error::Exn> { + ) -> ExnResult> { self.set_raw_value_filter_by_inner( section_name.as_ref(), subsection_name.as_bstr_opt(), @@ -878,7 +859,7 @@ impl File { value_name: &str, new_value: impl crate::AsBStr, filter: impl FnMut(&Metadata) -> bool, - ) -> Result, gix_error::Exn> { + ) -> ExnResult> { let key = section::ValueName::try_from(value_name).or_erased()?; let mut section = self .section_mut_or_create_new_filter_inner(section_name, subsection_name, filter) @@ -964,11 +945,7 @@ impl File { /// assert!(!git_config.raw_values("core.a").expect("values exist").iter().any(|v| v == "discarded")); /// # Ok::<(), Box>(()) /// ``` - pub fn set_existing_raw_multi_value( - &mut self, - key: impl AsKey, - new_values: Iter, - ) -> Result<(), gix_error::Exn> + pub fn set_existing_raw_multi_value(&mut self, key: impl AsKey, new_values: Iter) -> ExnResult where Iter: IntoIterator, Item: crate::AsBStr, @@ -1064,7 +1041,7 @@ impl File { subsection_name: impl AsBStrOpt, value_name: impl AsRef, new_values: Iter, - ) -> Result<(), gix_error::Exn> + ) -> ExnResult where Iter: IntoIterator, Item: crate::AsBStr, diff --git a/gix-config/src/file/access/read_only.rs b/gix-config/src/file/access/read_only.rs index 55f4629cbce..ff225a5adc8 100644 --- a/gix-config/src/file/access/read_only.rs +++ b/gix-config/src/file/access/read_only.rs @@ -1,4 +1,5 @@ use bstr::{BStr, BString, ByteSlice}; +use gix_error::ExnResult; use gix_features::threading::OwnShared; use smallvec::SmallVec; @@ -176,7 +177,7 @@ impl File { /// // ... or explicitly declare the type to avoid the turbofish /// let c_value: Vec = git_config.values("core.c")?; /// assert_eq!(c_value, vec![Boolean(false)]); - /// # Ok::<(), gix_config::lookup::Error>>(()) + /// # Ok::<(), gix_config::lookup::Error>>(()) /// ``` /// /// [`value`]: crate::value @@ -230,7 +231,7 @@ impl File { /// // ... or explicitly declare the type to avoid the turbofish /// let c_value: Vec = git_config.values_by("core", None, "c")?; /// assert_eq!(c_value, vec![Boolean(false)]); - /// # Ok::<(), gix_config::lookup::Error>>(()) + /// # Ok::<(), gix_config::lookup::Error>>(()) /// ``` /// /// [`value`]: crate::value @@ -273,18 +274,14 @@ impl File { } /// Returns the last found immutable section with a given `name` and optional `subsection_name`. - pub fn section( - &self, - name: impl AsRef, - subsection_name: impl AsBStrOpt, - ) -> Result, gix_error::Exn> { + pub fn section(&self, name: impl AsRef, subsection_name: impl AsBStrOpt) -> ExnResult> { self.section_filter(name, subsection_name, |_| true)? .ok_or_else(lookup::existing::section_missing) } /// Returns the last found immutable section with a given `section_key`, identifying the name and subsection name like `core` /// or `remote.origin`. - pub fn section_by_key(&self, section_key: impl crate::AsBStr) -> Result, gix_error::Exn> { + pub fn section_by_key(&self, section_key: impl crate::AsBStr) -> ExnResult> { let key = crate::parse::section::unvalidated::KeyRef::parse(section_key.as_bstr()) .ok_or_else(lookup::existing::key_missing)?; self.section(key.section_name, key.subsection_name) @@ -299,7 +296,7 @@ impl File { name: impl AsRef, subsection_name: impl AsBStrOpt, mut filter: impl FnMut(&Metadata) -> bool, - ) -> Result>, gix_error::Exn> { + ) -> ExnResult>> { Ok(self .section_ids_by_name_and_subname(name.as_ref(), subsection_name.as_bstr_opt())? .rev() @@ -317,7 +314,7 @@ impl File { &self, section_key: impl crate::AsBStr, filter: impl FnMut(&Metadata) -> bool, - ) -> Result>, gix_error::Exn> { + ) -> ExnResult>> { let key = crate::parse::section::unvalidated::KeyRef::parse(section_key.as_bstr()) .ok_or_else(lookup::existing::key_missing)?; self.section_filter(key.section_name, key.subsection_name, filter) diff --git a/gix-config/src/file/includes/mod.rs b/gix-config/src/file/includes/mod.rs index 8ce312f0b7d..f89ed13d0f7 100644 --- a/gix-config/src/file/includes/mod.rs +++ b/gix-config/src/file/includes/mod.rs @@ -1,7 +1,7 @@ use std::path::{Path, PathBuf}; use bstr::{BStr, BString, ByteSlice, ByteVec}; -use gix_error::{ErrorExt, NotFoundError, OptionExt, ResultExt, ValidationError, message}; +use gix_error::{ErrorExt, ExnResult, OptionExt, ResultExt, message, not_found, validation}; use gix_features::threading::OwnShared; use gix_ref::Category; @@ -30,7 +30,7 @@ impl File { /// We can fix this by 'splitting' the include section if needed so the included sections are put into the right place. /// - `hasconfig:remote.*.url` will not prevent itself to include files with `[remote "name"]\nurl = x` values, but it also /// won't match them, i.e. one cannot include something that will cause the condition to match or to always be true. - pub fn resolve_includes(&mut self, options: init::Options<'_>) -> Result<(), gix_error::Exn> { + pub fn resolve_includes(&mut self, options: init::Options<'_>) -> ExnResult { if options.includes.max_depth == 0 { return Ok(()); } @@ -39,7 +39,7 @@ impl File { } } -pub(crate) fn resolve(config: &mut File, buf: &mut Vec, options: init::Options<'_>) -> Result<(), gix_error::Exn> { +pub(crate) fn resolve(config: &mut File, buf: &mut Vec, options: init::Options<'_>) -> ExnResult { resolve_includes_recursive(None, config, 0, buf, options) } @@ -49,10 +49,10 @@ fn resolve_includes_recursive( depth: u8, buf: &mut Vec, options: init::Options<'_>, -) -> Result<(), gix_error::Exn> { +) -> ExnResult { if depth == options.includes.max_depth { return if options.includes.err_on_max_depth_exceeded { - Err(ValidationError::new(format!( + Err(validation(format!( "The maximum allowed length {} of the file include chain built by following nested resolve_includes is exceeded", options.includes.max_depth )) @@ -96,7 +96,7 @@ fn insert_includes_recursively( depth: u8, options: init::Options<'_>, buf: &mut Vec, -) -> Result<(), gix_error::Exn> { +) -> ExnResult { for (section_id, config_path) in section_ids_and_include_paths { let meta = OwnShared::clone(&target_config.sections[§ion_id].meta); let target_config_path = meta.path.as_deref(); @@ -155,7 +155,7 @@ fn include_condition_match( target_config_path: Option<&Path>, search_config: &File, options: Options<'_>, -) -> Result { +) -> ExnResult { let mut tokens = condition.splitn(2, |b| *b == b':'); let (prefix, condition) = match (tokens.next(), tokens.next()) { (Some(a), Some(b)) => (a, b), @@ -242,12 +242,12 @@ fn gitdir_matches( .. }: Options<'_>, wildmatch_mode: gix_glob::wildmatch::Mode, -) -> Result { +) -> ExnResult { if !err_on_interpolation_failure && git_dir.is_none() { return Ok(false); } let git_dir = gix_path::to_unix_separators_on_windows(gix_path::into_bstr(git_dir.ok_or_raise_erased(|| { - NotFoundError::new("The git directory must be provided to support `gitdir:` conditional includes") + not_found("The git directory must be provided to support `gitdir:` conditional includes") })?)); let mut pattern_path = match check_interpolation_result( @@ -271,7 +271,7 @@ fn gitdir_matches( } let parent_dir = target_config_path .ok_or_raise_erased(|| { - NotFoundError::new( + not_found( "Include paths from environment variables must not be relative as no config file path exists as root", ) })? @@ -310,10 +310,7 @@ fn gitdir_matches( )) } -fn check_interpolation_result( - disable: bool, - res: Result, gix_error::Exn>, -) -> Result, gix_error::Exn> { +fn check_interpolation_result(disable: bool, res: ExnResult>) -> ExnResult> { if disable { return res.map(|path| Some(path.into())); } @@ -333,7 +330,7 @@ fn resolve_path( err_on_missing_config_path, .. }: includes::Options<'_>, -) -> Result, gix_error::Exn> { +) -> ExnResult> { let path = match check_interpolation_result(err_on_interpolation_failure, path.interpolate(context)) .or_raise_erased(|| message("Could not interpolate include path"))? { @@ -346,7 +343,7 @@ fn resolve_path( } target_config_path .ok_or_raise_erased(|| { - NotFoundError::new( + not_found( "Include paths from environment variables must not be relative as no config file path exists as root", ) })? diff --git a/gix-config/src/file/init/comfort.rs b/gix-config/src/file/init/comfort.rs index f98923568df..13e8eadad07 100644 --- a/gix-config/src/file/init/comfort.rs +++ b/gix-config/src/file/init/comfort.rs @@ -3,6 +3,7 @@ use crate::{ file::{Metadata, init}, path, source, }; +use gix_error::{ExnMessageResult, ExnResult}; /// Easy-instantiation of typical non-repository git configuration files with all configuration defaulting to typical values. /// @@ -23,7 +24,7 @@ impl File { /// which excludes repository local configuration, as well as override-configuration from environment variables. /// /// Note that the file might [be empty][File::is_void()] in case no configuration file was found. - pub fn from_globals() -> Result> { + pub fn from_globals() -> ExnMessageResult { let metas = [ source::Kind::GitInstallation, source::Kind::System, @@ -60,7 +61,7 @@ impl File { /// See [`git-config`'s documentation] for more information on the environment variables in question. /// /// [`git-config`'s documentation]: https://git-scm.com/docs/git-config#Documentation/git-config.txt-GITCONFIGCOUNT - pub fn from_environment_overrides() -> Result { + pub fn from_environment_overrides() -> ExnResult { let home = gix_path::env::home_dir(); let options = init::Options { includes: init::includes::Options::follow_without_conditional(home.as_deref()), @@ -84,7 +85,7 @@ impl File { /// /// Includes will be resolved within limits as some information like the git installation directory is missing to interpolate /// paths with as well as git repository information like the branch name. - pub fn from_git_dir(dir: std::path::PathBuf) -> Result> { + pub fn from_git_dir(dir: std::path::PathBuf) -> ExnMessageResult { use gix_error::{ResultExt, message}; let (mut local, git_dir) = { diff --git a/gix-config/src/file/init/from_env.rs b/gix-config/src/file/init/from_env.rs index d0610b8c2e6..1f1a547da30 100644 --- a/gix-config/src/file/init/from_env.rs +++ b/gix-config/src/file/init/from_env.rs @@ -1,4 +1,5 @@ use bstr::ByteSlice; +use gix_error::ExnResult; use crate::{File, KeyRef, file, file::init}; @@ -8,14 +9,17 @@ impl File { /// See [`git-config`'s documentation] for more information on the environment variables in question. /// /// With `options` configured, it's possible to resolve `include.path` or `includeIf..path` directives as well. + /// Integer parsing failures for `GIT_CONFIG_COUNT` and key parsing failures for `GIT_CONFIG_KEY_*` include their + /// bytes as `input` + /// [metadata](gix_error::Exn::metadata()). /// /// [`git-config`'s documentation]: https://git-scm.com/docs/git-config#Documentation/git-config.txt-GITCONFIGCOUNT - pub fn from_env(options: init::Options<'_>) -> Result, gix_error::Exn> { - use gix_error::{ErrorExt, NotFoundError, OptionExt, ResultExt, ValidationError, message}; + pub fn from_env(options: init::Options<'_>) -> ExnResult> { + use gix_error::{ErrorExt, OptionExt, ResultExt, message, not_found, validation}; use std::env; let count: usize = match env::var("GIT_CONFIG_COUNT") { Ok(v) => v.parse::().or_raise_erased(|| { - ValidationError::new_with_input("GIT_CONFIG_COUNT was not a positive integer", v) + validation("GIT_CONFIG_COUNT was not a positive integer").with("input", v.into_bytes()) })?, Err(_) => return Ok(None), }; @@ -34,15 +38,14 @@ impl File { for i in 0..count { let key = gix_path::os_string_into_bstring( env::var_os(format!("GIT_CONFIG_KEY_{i}")) - .ok_or_raise_erased(|| NotFoundError::new(format!("GIT_CONFIG_KEY_{i} was not set")))?, + .ok_or_raise_erased(|| not_found(format!("GIT_CONFIG_KEY_{i} was not set")))?, ) - .or_raise_erased(|| { - ValidationError::new(format!("Configuration key at index {i} contained illformed UTF-8")) - })?; + .or_raise_erased(|| validation(format!("Configuration key at index {i} contained illformed UTF-8")))?; let value = env::var_os(format!("GIT_CONFIG_VALUE_{i}")) - .ok_or_raise_erased(|| NotFoundError::new(format!("GIT_CONFIG_VALUE_{i} was not set")))?; + .ok_or_raise_erased(|| not_found(format!("GIT_CONFIG_VALUE_{i} was not set")))?; let key = KeyRef::parse_unvalidated(key.as_ref()).ok_or_else(|| { - ValidationError::new_with_input(format!("GIT_CONFIG_KEY_{i} was set to an invalid value"), key.clone()) + validation(format!("GIT_CONFIG_KEY_{i} was set to an invalid value")) + .with("input", key.as_bstr()) .raise_erased() })?; @@ -54,9 +57,7 @@ impl File { Some( gix_path::os_str_into_bstr(&value) .or_raise_erased(|| { - ValidationError::new(format!( - "Configuration value at index {i} contained illformed UTF-8" - )) + validation(format!("Configuration value at index {i} contained illformed UTF-8")) })? .as_bytes() .into(), diff --git a/gix-config/src/file/init/from_paths.rs b/gix-config/src/file/init/from_paths.rs index fa33f8c2a14..bcde512c817 100644 --- a/gix-config/src/file/init/from_paths.rs +++ b/gix-config/src/file/init/from_paths.rs @@ -1,5 +1,7 @@ use std::collections::BTreeSet; +use gix_error::ExnMessageResult; + use crate::{ File, file::{Metadata, init::Options}, @@ -10,10 +12,7 @@ impl File { /// Load the single file at `path` with `source` without following include directives. /// /// Note that the path will be checked for ownership to derive trust. - pub fn from_path_no_includes( - path: std::path::PathBuf, - source: crate::Source, - ) -> Result> { + pub fn from_path_no_includes(path: std::path::PathBuf, source: crate::Source) -> ExnMessageResult { use gix_error::{ResultExt, message}; let trust = gix_sec::Trust::from_path_ownership(&path).or_raise(|| { message!( @@ -44,7 +43,7 @@ impl File { pub fn from_paths_metadata( path_meta: impl IntoIterator>, options: Options<'_>, - ) -> Result, gix_error::Exn> { + ) -> ExnMessageResult> { let mut buf = Vec::with_capacity(512); let err_on_nonexisting_paths = true; Self::from_paths_metadata_buf( @@ -64,7 +63,7 @@ impl File { buf: &mut Vec, err_on_non_existing_paths: bool, options: Options<'_>, - ) -> Result, gix_error::Exn> { + ) -> ExnMessageResult> { use gix_error::{ErrorExt, ResultExt, message}; let mut target = None; let mut seen = BTreeSet::default(); diff --git a/gix-config/src/file/init/mod.rs b/gix-config/src/file/init/mod.rs index 26314660f53..46321604950 100644 --- a/gix-config/src/file/init/mod.rs +++ b/gix-config/src/file/init/mod.rs @@ -1,3 +1,4 @@ +use gix_error::ExnResult; use gix_features::threading::OwnShared; use crate::{ @@ -36,7 +37,7 @@ impl File { input: &[u8], meta: impl Into>, options: Options<'_>, - ) -> Result { + ) -> ExnResult { use gix_error::{ResultExt, message}; let meta = meta.into(); Ok(Self::from_parse_events_no_includes( @@ -84,7 +85,7 @@ impl File { input_and_buf: &mut Vec, meta: impl Into>, options: Options<'_>, - ) -> Result { + ) -> ExnResult { use gix_error::{ResultExt, message}; let mut config = Self::from_parse_events_no_includes( parse::Events::from_bytes(input_and_buf, options.to_event_filter()) diff --git a/gix-config/src/file/mutable/mod.rs b/gix-config/src/file/mutable/mod.rs index ae9b2861732..3011adf8030 100644 --- a/gix-config/src/file/mutable/mod.rs +++ b/gix-config/src/file/mutable/mod.rs @@ -1,4 +1,5 @@ use bstr::{BStr, BString, ByteSlice, ByteVec}; +use gix_error::ExnMessageResult; use crate::{file, parse::Event}; @@ -53,7 +54,7 @@ impl Default for Whitespace { } impl Whitespace { - fn key_value_separators(&self, backing: &mut Vec) -> Result, gix_error::ValidationError> { + fn key_value_separators(&self, backing: &mut Vec) -> ExnMessageResult> { let mut out = Vec::with_capacity(3); if let Some(ws) = &self.pre_sep { out.push(Event::Whitespace(crate::parse::Span::append(backing, ws)?)); diff --git a/gix-config/src/file/mutable/multi_value.rs b/gix-config/src/file/mutable/multi_value.rs index 72da048dc7f..50a3f785090 100644 --- a/gix-config/src/file/mutable/multi_value.rs +++ b/gix-config/src/file/mutable/multi_value.rs @@ -1,5 +1,8 @@ +use gix_error::ExnMessageResult; use std::{collections::HashMap, ops::DerefMut}; +use gix_error::ExnResult; + use bstr::{BStr, BString, ByteVec}; use crate::{ @@ -37,7 +40,7 @@ pub struct MultiValueMut<'borrow> { impl MultiValueMut<'_> { /// Returns the actual values. - pub fn get(&self) -> Result, gix_error::Exn> { + pub fn get(&self) -> ExnResult> { let mut expect_value = false; let mut values = Vec::new(); let mut concatenated_value = BString::default(); @@ -98,7 +101,7 @@ impl MultiValueMut<'_> { /// # Safety /// /// This will panic if the index is out of range. - pub fn set_string_at(&mut self, index: usize, value: impl AsRef) -> Result<(), gix_error::ValidationError> { + pub fn set_string_at(&mut self, index: usize, value: impl AsRef) -> ExnMessageResult { self.set_at(index, value.as_ref()) } @@ -107,7 +110,7 @@ impl MultiValueMut<'_> { /// # Safety /// /// This will panic if the index is out of range. - pub fn set_at(&mut self, index: usize, value: impl crate::AsBStr) -> Result<(), gix_error::ValidationError> { + pub fn set_at(&mut self, index: usize, value: impl crate::AsBStr) -> ExnMessageResult { let EntryData { section_id, offset_index, @@ -130,7 +133,7 @@ impl MultiValueMut<'_> { /// remaining values are ignored. /// /// [`zip`]: std::iter::Iterator::zip - pub fn set_values(&mut self, values: Iter) -> Result<(), gix_error::ValidationError> + pub fn set_values(&mut self, values: Iter) -> ExnMessageResult where Iter: IntoIterator, Item: crate::AsBStr, @@ -158,7 +161,7 @@ impl MultiValueMut<'_> { /// Sets all values in this multivar to the provided one without owning the /// provided input. - pub fn set_all(&mut self, input: impl crate::AsBStr) -> Result<(), gix_error::ValidationError> { + pub fn set_all(&mut self, input: impl crate::AsBStr) -> ExnMessageResult { let input = input.as_bstr(); for EntryData { section_id, @@ -186,7 +189,7 @@ impl MultiValueMut<'_> { section_id: SectionId, offset_index: usize, value: &BStr, - ) -> Result<(), gix_error::ValidationError> { + ) -> ExnMessageResult { let (offset, size) = MultiValueMut::index_and_size(offsets, section_id, offset_index); let whitespace = Whitespace::from_body(section, backing); let value = crate::parse::Span::append(backing, &escape_value(value))?; diff --git a/gix-config/src/file/mutable/section.rs b/gix-config/src/file/mutable/section.rs index eff9e60e304..a8f1fc87a2b 100644 --- a/gix-config/src/file/mutable/section.rs +++ b/gix-config/src/file/mutable/section.rs @@ -1,5 +1,8 @@ +use gix_error::ExnMessageResult; use std::{collections::HashMap, ops::Range}; +use gix_error::ExnResult; + use bstr::{BStr, BString, ByteSlice, ByteVec}; use gix_sec::Trust; use smallvec::SmallVec; @@ -42,7 +45,7 @@ impl SectionMut<'_> { &mut self, name: impl AsRef, subsection_name: impl IntoBStringOpt, - ) -> Result<&mut Self, gix_error::ValidationError> { + ) -> ExnMessageResult<&mut Self> { let header = parse::section::HeaderData::new_in(name, subsection_name.into_bstring_opt(), self.backing)?; self.set_header(header); Ok(self) @@ -50,11 +53,7 @@ impl SectionMut<'_> { /// Adds an entry to the end of this section name `value_name` and `value`. If `value` is `None`, no equal sign will be written leaving /// just the key. This is useful for boolean values which are true if merely the key exists. - pub fn push( - &mut self, - value_name: impl AsRef, - value: impl AsBStrOpt, - ) -> Result<&mut Self, gix_error::ValidationError> { + pub fn push(&mut self, value_name: impl AsRef, value: impl AsBStrOpt) -> ExnMessageResult<&mut Self> { let value_name = ValueName::try_from(value_name.as_ref())?; self.push_with_comment_inner(value_name, value.as_bstr_opt(), None)?; Ok(self) @@ -69,7 +68,7 @@ impl SectionMut<'_> { value_name: impl AsRef, value: impl AsBStrOpt, comment: impl crate::AsBStr, - ) -> Result<&mut Self, gix_error::ValidationError> { + ) -> ExnMessageResult<&mut Self> { let value_name = ValueName::try_from(value_name.as_ref())?; self.push_with_comment_inner(value_name, value.as_bstr_opt(), Some(comment.as_bstr()))?; Ok(self) @@ -80,7 +79,7 @@ impl SectionMut<'_> { value_name: ValueName, value: Option<&BStr>, comment: Option<&BStr>, - ) -> Result<(), gix_error::ValidationError> { + ) -> ExnMessageResult { let mut events = Vec::new(); if let Some(ws) = &self.whitespace.pre_key { events.push(Event::Whitespace(Span::append(self.backing, ws)?)); @@ -169,20 +168,12 @@ impl SectionMut<'_> { /// Sets the last key value pair if it exists, or adds the new value. /// Returns the previous value if it replaced a value, or None if it adds /// the value. - pub fn set( - &mut self, - value_name: impl AsRef, - value: impl crate::AsBStr, - ) -> Result, gix_error::ValidationError> { + pub fn set(&mut self, value_name: impl AsRef, value: impl crate::AsBStr) -> ExnMessageResult> { let value_name = ValueName::try_from(value_name.as_ref())?; self.set_inner(value_name, value.as_bstr()) } - pub(crate) fn set_inner( - &mut self, - value_name: ValueName, - value: &BStr, - ) -> Result, gix_error::ValidationError> { + pub(crate) fn set_inner(&mut self, value_name: ValueName, value: &BStr) -> ExnMessageResult> { match self.section.body.key_and_value_range_by_in(self.backing, &value_name) { None => { self.push_with_comment_inner(value_name, Some(value), None)?; @@ -216,7 +207,7 @@ impl SectionMut<'_> { /// Adds a new line event. Note that you don't need to call this unless /// you've disabled implicit newlines. - pub fn push_newline(&mut self) -> Result<&mut Self, gix_error::ValidationError> { + pub fn push_newline(&mut self) -> ExnMessageResult<&mut Self> { let newline = Span::append(self.backing, &self.newline)?; self.section.body.0.push(Event::Newline(newline)); Ok(self) @@ -394,7 +385,7 @@ impl<'a> SectionMut<'a> { } } - pub(crate) fn get(&self, key: &ValueName, start: Index, end: Index) -> Result { + pub(crate) fn get(&self, key: &ValueName, start: Index, end: Index) -> ExnResult { let mut expect_value = false; let mut concatenated_value = BString::default(); @@ -454,12 +445,7 @@ impl<'a> SectionMut<'a> { } } - pub(crate) fn set_internal( - &mut self, - index: Index, - key: ValueName, - value: &BStr, - ) -> Result { + pub(crate) fn set_internal(&mut self, index: Index, key: ValueName, value: &BStr) -> ExnMessageResult { let mut size = 0; let value = Span::append(self.backing, &escape_value(value))?; let sep_events = self.whitespace.key_value_separators(self.backing)?; diff --git a/gix-config/src/file/mutable/value.rs b/gix-config/src/file/mutable/value.rs index ab8076dc8a6..20b7e830642 100644 --- a/gix-config/src/file/mutable/value.rs +++ b/gix-config/src/file/mutable/value.rs @@ -1,4 +1,6 @@ use bstr::BString; +use gix_error::ExnMessageResult; +use gix_error::ExnResult; use crate::{ file, @@ -18,21 +20,21 @@ pub struct ValueMut<'borrow> { impl<'borrow> ValueMut<'borrow> { /// Returns the actual value. This is computed each time this is called /// requiring an allocation for multi-line values. - pub fn get(&self) -> Result { + pub fn get(&self) -> ExnResult { 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. - pub fn set_string(&mut self, input: impl AsRef) -> Result<(), gix_error::ValidationError> { + pub fn set_string(&mut self, input: impl AsRef) -> ExnMessageResult { self.set(input.as_ref()) } /// 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. - pub fn set(&mut self, input: impl crate::AsBStr) -> Result<(), gix_error::ValidationError> { + pub fn set(&mut self, input: impl crate::AsBStr) -> ExnMessageResult { let new_size = self .section .set_internal(self.index, self.key.to_owned(), input.as_bstr())?; diff --git a/gix-config/src/file/section/body.rs b/gix-config/src/file/section/body.rs index 3bbd2ed8731..01fa4f48981 100644 --- a/gix-config/src/file/section/body.rs +++ b/gix-config/src/file/section/body.rs @@ -1,3 +1,4 @@ +use gix_error::ExnMessageResult; use std::{borrow::Cow, iter::FusedIterator, ops::Range, slice}; use bstr::{BStr, BString, ByteSlice, ByteVec}; @@ -221,16 +222,12 @@ impl BodyData { }) } - pub(crate) fn copy_to_backing_in( - &self, - source: &[u8], - target: &mut Vec, - ) -> Result { + pub(crate) fn copy_to_backing_in(&self, source: &[u8], target: &mut Vec) -> ExnMessageResult { Ok(BodyData( self.0 .iter() .map(|event| event.copy_to_backing_in(source, target)) - .collect::>()?, + .collect::>()?, )) } } diff --git a/gix-config/src/file/section/mod.rs b/gix-config/src/file/section/mod.rs index 8a99a89212e..4dc63eacbe3 100644 --- a/gix-config/src/file/section/mod.rs +++ b/gix-config/src/file/section/mod.rs @@ -1,4 +1,5 @@ use bstr::{BStr, BString, ByteSlice}; +use gix_error::ExnMessageResult; use smallvec::SmallVec; use crate::{ @@ -72,11 +73,13 @@ impl<'file> SectionRef<'file> { impl Section { /// Create an owned section with an empty body. + /// Invalid section or subsection name bytes are stored as `input` in [`gix_error::Message::values`]. + /// After [wrapping](gix_error::Error::from_error()), inspect them with [metadata](gix_error::Error::metadata()). pub fn new( name: impl AsRef, subsection: impl IntoBStringOpt, meta: impl Into>, - ) -> Result { + ) -> ExnMessageResult { let mut backing = Vec::new(); let data = SectionData::new(name, subsection.into_bstring_opt(), meta, &mut backing)?; Ok(Section { backing, data }) @@ -107,7 +110,7 @@ impl Section { Section { backing, data } } - pub(crate) fn into_data(self, target: &mut Vec) -> Result { + pub(crate) fn into_data(self, target: &mut Vec) -> ExnMessageResult { self.data.copy_to_backing_in(&self.backing, target) } } @@ -119,7 +122,7 @@ impl SectionData { subsection: impl Into>, meta: impl Into>, backing: &mut Vec, - ) -> Result { + ) -> ExnMessageResult { Ok(SectionData { header: parse::section::HeaderData::new_in(name, subsection, backing)?, body: Default::default(), @@ -142,11 +145,7 @@ impl SectionData { &self.meta } - pub(crate) fn copy_to_backing_in( - &self, - source: &[u8], - target: &mut Vec, - ) -> Result { + pub(crate) fn copy_to_backing_in(&self, source: &[u8], target: &mut Vec) -> ExnMessageResult { Ok(SectionData { header: self.header.copy_to_backing_in(source, target)?, body: self.body.copy_to_backing_in(source, target)?, diff --git a/gix-config/src/file/util.rs b/gix-config/src/file/util.rs index bc30c2d40fc..4a234d16e0f 100644 --- a/gix-config/src/file/util.rs +++ b/gix-config/src/file/util.rs @@ -1,5 +1,7 @@ use std::collections::HashMap; +use gix_error::ExnResult; + use bstr::{BStr, BString, ByteSlice}; use crate::{ @@ -62,7 +64,7 @@ impl File { &'a self, section_name: &'a str, subsection_name: Option<&BStr>, - ) -> Result + DoubleEndedIterator + 'a, gix_error::Exn> { + ) -> ExnResult + DoubleEndedIterator + 'a> { let section_name = section::Name::from_str_unchecked(section_name); let lookup = self .section_lookup_tree @@ -79,7 +81,7 @@ impl File { pub(crate) fn section_ids_by_name<'a>( &'a self, section_name: &str, - ) -> Result + 'a + use<'a>, gix_error::Exn> { + ) -> ExnResult + 'a + use<'a>> { let lookup_name = section::Name::from_str_unchecked(section_name); let lookup = self .section_lookup_tree diff --git a/gix-config/src/lookup.rs b/gix-config/src/lookup.rs index 0535e780599..f2ba7c738e9 100644 --- a/gix-config/src/lookup.rs +++ b/gix-config/src/lookup.rs @@ -57,6 +57,6 @@ pub mod existing { fn not_found(message: &'static str) -> gix_error::Exn { use gix_error::ErrorExt; - gix_error::NotFoundError::new(message).raise_erased() + gix_error::not_found(message).raise_erased() } } diff --git a/gix-config/src/parse/comment.rs b/gix-config/src/parse/comment.rs index 4089fb9fab1..225f750fd0b 100644 --- a/gix-config/src/parse/comment.rs +++ b/gix-config/src/parse/comment.rs @@ -1,11 +1,8 @@ use crate::parse::Comment; +use gix_error::ExnMessageResult; impl Comment { - pub(crate) fn copy_to_backing_in( - &self, - source: &[u8], - target: &mut Vec, - ) -> Result { + pub(crate) fn copy_to_backing_in(&self, source: &[u8], target: &mut Vec) -> ExnMessageResult { Ok(Comment { tag: self.tag, text: self.text.copy_to_backing_in(source, target)?, diff --git a/gix-config/src/parse/error.rs b/gix-config/src/parse/error.rs index cc1a5ba5e82..dd2431d2815 100644 --- a/gix-config/src/parse/error.rs +++ b/gix-config/src/parse/error.rs @@ -111,10 +111,6 @@ impl Display for Error { impl std::error::Error for Error { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - static VALIDATION: gix_error::ValidationError = gix_error::ValidationError { - message: std::borrow::Cow::Borrowed("Invalid configuration input"), - input: None, - }; - Some(&VALIDATION) + Some(const { &gix_error::ClassificationMarker::VALIDATION }) } } diff --git a/gix-config/src/parse/event.rs b/gix-config/src/parse/event.rs index 0610b5ab6ea..4d6fb60e5dd 100644 --- a/gix-config/src/parse/event.rs +++ b/gix-config/src/parse/event.rs @@ -1,3 +1,4 @@ +use gix_error::ExnMessageResult; use std::fmt::Display; use bstr::{BStr, BString}; @@ -6,7 +7,7 @@ use crate::parse::{Event, EventRef}; impl Event { /// Shift all backing-buffer spans in this event forward by `offset` bytes. - pub(crate) fn rebase(&mut self, offset: usize) -> Result<(), gix_error::ValidationError> { + pub(crate) fn rebase(&mut self, offset: usize) -> ExnMessageResult { match self { Event::Comment(comment) => comment.text.rebase(offset), Event::SectionHeader(header) => header.rebase(offset), @@ -20,11 +21,7 @@ impl Event { } } - pub(crate) fn copy_to_backing_in( - &self, - source: &[u8], - target: &mut Vec, - ) -> Result { + pub(crate) fn copy_to_backing_in(&self, source: &[u8], target: &mut Vec) -> ExnMessageResult { Ok(match self { Event::Comment(comment) => Event::Comment(comment.copy_to_backing_in(source, target)?), Event::SectionHeader(header) => Event::SectionHeader(header.copy_to_backing_in(source, target)?), diff --git a/gix-config/src/parse/format.rs b/gix-config/src/parse/format.rs index 97cae17b8ea..a8a89010ca7 100644 --- a/gix-config/src/parse/format.rs +++ b/gix-config/src/parse/format.rs @@ -8,6 +8,7 @@ //! newlines and the `=` separator are rewritten according to [`Options`](crate::parse::format::Options). use bstr::BString; +use gix_error::ExnMessageResult; use crate::parse::{self, EventRef}; @@ -76,8 +77,8 @@ impl Default for Options { /// /// # Errors /// -/// Returns a [`gix_error::ValidationError`] if `input` is not a syntactically valid git-config file. -pub fn normalize(input: &[u8], options: &Options) -> Result { +/// Returns an error classified as [`gix_error::Class::Validation`] if `input` is not a syntactically valid git-config file. +pub fn normalize(input: &[u8], options: &Options) -> ExnMessageResult { let parsed = parse::Events::from_bytes(input, None)?; let events: Vec<_> = parsed.iter().collect(); Ok(normalize_events(&events, options)) diff --git a/gix-config/src/parse/from_bytes/tests.rs b/gix-config/src/parse/from_bytes/tests.rs index 7f230db2349..e43a1f506fb 100644 --- a/gix-config/src/parse/from_bytes/tests.rs +++ b/gix-config/src/parse/from_bytes/tests.rs @@ -8,13 +8,7 @@ fn input_size_is_limited_by_span_representation() { let err = ensure_supported_input_size(actual).expect_err("inputs above the span limit must be rejected"); assert_eq!(err.line_number(), 1); assert!(err.remaining_data().is_empty()); - assert_eq!( - err.to_string(), - format!( - "Configuration input is {actual} bytes large, but at most {} bytes are supported", - u32::MAX - ) - ); + insta::assert_debug_snapshot!(format_args!("{err}"), "oversized input reports the actual byte count and the supported limit", @"Configuration input is 4294967296 bytes large, but at most 4294967295 bytes are supported"); assert!( gix_error::Error::from_error(err).is_validation(), "input outside the parser's supported size range is invalid input" diff --git a/gix-config/src/parse/mod.rs b/gix-config/src/parse/mod.rs index b3e4bb9228f..0077f687987 100644 --- a/gix-config/src/parse/mod.rs +++ b/gix-config/src/parse/mod.rs @@ -11,6 +11,7 @@ //! [`File`]: crate::File use bstr::{BStr, BString, ByteSlice}; +use gix_error::ExnMessageResult; mod from_bytes; @@ -37,8 +38,8 @@ pub(crate) struct Span { /// Errors produced when a span cannot be represented. pub mod span { - pub(crate) fn error() -> gix_error::ValidationError { - gix_error::ValidationError::new(format!( + pub(crate) fn error() -> gix_error::Message { + gix_error::validation(format!( "configuration data exceeds the supported span size of {} bytes", u32::MAX )) @@ -77,15 +78,11 @@ impl MaybeDecoded { .map_or_else(|| self.raw.as_bstr_in(backing), |value| value.as_bstr()) } - pub(crate) fn rebase(&mut self, offset: usize) -> Result<(), gix_error::ValidationError> { + pub(crate) fn rebase(&mut self, offset: usize) -> ExnMessageResult { self.raw.rebase(offset) } - pub(crate) fn copy_to_backing_in( - &self, - source: &[u8], - target: &mut Vec, - ) -> Result { + pub(crate) fn copy_to_backing_in(&self, source: &[u8], target: &mut Vec) -> ExnMessageResult { Ok(Self { raw: self.raw.copy_to_backing_in(source, target)?, decoded: self.decoded.clone(), @@ -94,7 +91,7 @@ impl MaybeDecoded { } impl Span { - pub(crate) fn append(backing: &mut Vec, bytes: &[u8]) -> Result { + pub(crate) fn append(backing: &mut Vec, bytes: &[u8]) -> ExnMessageResult { let start = backing.len(); let span = Self::range(start, bytes.len())?; backing.len().checked_add(bytes.len()).ok_or_else(span::error)?; @@ -102,9 +99,9 @@ impl Span { Ok(span) } - pub(crate) fn range(start: usize, len: usize) -> Result { + pub(crate) fn range(start: usize, len: usize) -> ExnMessageResult { if start > u32::MAX as usize || len > u32::MAX as usize { - return Err(span::error()); + return Err(span::error().into()); } Ok(Span { start: start as u32, @@ -142,15 +139,11 @@ impl Span { self.as_slice_in(backing).into() } - pub(crate) fn copy_to_backing_in( - &self, - source: &[u8], - target: &mut Vec, - ) -> Result { + pub(crate) fn copy_to_backing_in(&self, source: &[u8], target: &mut Vec) -> ExnMessageResult { Span::append(target, self.as_slice_in(source)) } - pub(crate) fn rebase(&mut self, offset: usize) -> Result<(), gix_error::ValidationError> { + pub(crate) fn rebase(&mut self, offset: usize) -> ExnMessageResult { self.start = (self.start as usize) .checked_add(offset) .and_then(|start| start.try_into().ok()) @@ -269,7 +262,9 @@ pub(crate) struct Comment { /// A parser error reports the one-indexed line number where the parsing error /// occurred, as well as the last parser node and the remaining data to be /// parsed. -/// Its source preserves the validation classification when converted to [`gix_error::Error`]. +/// Its source is a classification-only [`gix_error::ClassificationMarker`]. +/// Use [`gix_error::classify()`] or `is_validation()` on [`gix_error::Exn`] and [`gix_error::Error`] to check the +/// classification. Downcast to this type for parser details. #[derive(PartialEq, Debug)] pub struct Error { kind: error::Kind, diff --git a/gix-config/src/parse/section/header.rs b/gix-config/src/parse/section/header.rs index 4916ac5d45d..ac5f7e6467b 100644 --- a/gix-config/src/parse/section/header.rs +++ b/gix-config/src/parse/section/header.rs @@ -1,13 +1,16 @@ use bstr::{BStr, BString, ByteSlice}; +use gix_error::ExnMessageResult; use crate::parse::{Span, section::HeaderData}; impl HeaderData { + /// Invalid section or subsection name bytes are stored as `input` in [`gix_error::Message::values`]. + /// After [wrapping](gix_error::Error::from_error()), inspect them with [metadata](gix_error::Error::metadata()). pub(crate) fn new_in( name: impl AsRef, subsection: impl Into>, backing: &mut Vec, - ) -> Result { + ) -> ExnMessageResult { let name = validated_name(name.as_ref().as_bytes().as_bstr())?; let name = Span::append(backing, &name)?; let (separator, subsection_name) = match subsection.into() { @@ -38,21 +41,27 @@ pub fn is_valid_subsection(name: impl crate::AsBStr) -> bool { name.as_bstr().find_byteset(b"\n\0").is_none() } -fn validated_subsection(name: &BStr) -> Result { +fn validated_subsection(name: &BStr) -> ExnMessageResult { is_valid_subsection(name).then(|| name.into()).ok_or_else(|| { - gix_error::ValidationError::new_with_input("sub-section names must not contain newlines or null bytes", name) + gix_error::validation("sub-section names must not contain newlines or null bytes") + .with("input", name) + .into() }) } -fn validated_name(name: &BStr) -> Result { +fn validated_name(name: &BStr) -> ExnMessageResult { name.iter() .all(|b| b.is_ascii_alphanumeric() || *b == b'-') .then(|| name.into()) - .ok_or_else(|| gix_error::ValidationError::new_with_input("section names can only be ascii, '-'", name)) + .ok_or_else(|| { + gix_error::validation("section names can only be ascii, '-'") + .with("input", name) + .into() + }) } impl HeaderData { - pub(crate) fn rebase(&mut self, offset: usize) -> Result<(), gix_error::ValidationError> { + pub(crate) fn rebase(&mut self, offset: usize) -> ExnMessageResult { self.name.rebase(offset)?; if let Some(separator) = &mut self.separator { separator.rebase(offset)?; @@ -63,11 +72,7 @@ impl HeaderData { Ok(()) } - pub(crate) fn copy_to_backing_in( - &self, - source: &[u8], - target: &mut Vec, - ) -> Result { + pub(crate) fn copy_to_backing_in(&self, source: &[u8], target: &mut Vec) -> ExnMessageResult { Ok(HeaderData { name: self.name.copy_to_backing_in(source, target)?, separator: self diff --git a/gix-config/src/parse/section/mod.rs b/gix-config/src/parse/section/mod.rs index af29b8812b6..c9d4222beb0 100644 --- a/gix-config/src/parse/section/mod.rs +++ b/gix-config/src/parse/section/mod.rs @@ -25,6 +25,10 @@ mod types { macro_rules! generate_case_insensitive { ($name:ident, $err_doc:literal, $validate:ident, $cow_inner_type:ty, $comment:literal) => { #[doc = $comment] + /// + /// Conversion errors store invalid name bytes as `input` in [`gix_error::Message::values`]. + /// After [wrapping](gix_error::Error::from_error()), inspect them with + /// [metadata](gix_error::Error::metadata()). #[derive(Clone, Eq, Debug, Default)] pub struct $name(pub(crate) bstr::BString); @@ -74,7 +78,7 @@ mod types { } impl std::convert::TryFrom<&str> for $name { - type Error = gix_error::ValidationError; + type Error = gix_error::Message; fn try_from(s: &str) -> Result { Self::try_from(bstr::ByteSlice::as_bstr(s.as_bytes())) @@ -82,7 +86,7 @@ mod types { } impl std::convert::TryFrom for $name { - type Error = gix_error::ValidationError; + type Error = gix_error::Message; fn try_from(s: String) -> Result { Self::try_from(bstr::BString::from(s)) @@ -90,25 +94,25 @@ mod types { } impl std::convert::TryFrom for $name { - type Error = gix_error::ValidationError; + type Error = gix_error::Message; fn try_from(s: bstr::BString) -> Result { if $validate(s.as_slice().as_bstr()) { Ok(Self(s.into())) } else { - Err(gix_error::ValidationError::new_with_input($err_doc, s)) + Err(gix_error::validation($err_doc).with("input", s)) } } } impl std::convert::TryFrom<&bstr::BStr> for $name { - type Error = gix_error::ValidationError; + type Error = gix_error::Message; fn try_from(s: &bstr::BStr) -> Result { if $validate(s) { Ok(Self(s.into())) } else { - Err(gix_error::ValidationError::new_with_input($err_doc, s)) + Err(gix_error::validation($err_doc).with("input", s)) } } } diff --git a/gix-config/src/parse/tests.rs b/gix-config/src/parse/tests.rs index c946c76dbe2..5f08a3abf08 100644 --- a/gix-config/src/parse/tests.rs +++ b/gix-config/src/parse/tests.rs @@ -67,13 +67,7 @@ mod span { assert!(Span::range(0, u32::MAX as usize + 1).is_err()); let mut span = Span::range(u32::MAX as usize, 0).expect("the maximum offset is representable"); - assert_eq!( - span.rebase(1).expect_err("rebasing beyond u32 must fail").message, - format!( - "configuration data exceeds the supported span size of {} bytes", - u32::MAX - ) - ); + insta::assert_debug_snapshot!(span.rebase(1).expect_err("rebasing beyond u32 must fail"), "reports range and rebase overflow", @"configuration data exceeds the supported span size of 4294967295 bytes"); } } diff --git a/gix-config/tests/config/file/access/mutate.rs b/gix-config/tests/config/file/access/mutate.rs index c6b2025dca8..c9ee4fe6d20 100644 --- a/gix-config/tests/config/file/access/mutate.rs +++ b/gix-config/tests/config/file/access/mutate.rs @@ -1,6 +1,8 @@ mod new_section { + use crate::Result; + #[test] - fn accepts_a_borrowed_subsection_name() -> crate::Result { + fn accepts_a_borrowed_subsection_name() -> Result { let mut file = gix_config::File::default(); file.new_section("remote", "origin")?; file.new_section("branch", "main")?; @@ -15,7 +17,7 @@ mod new_section { } #[test] - fn owned_sections_accept_a_borrowed_subsection_name() -> crate::Result { + fn owned_sections_accept_a_borrowed_subsection_name() -> Result { let section = gix_config::file::Section::new("remote", "origin", gix_config::file::Metadata::default())?; assert_eq!(section.to_ref().header().subsection_name(), Some("origin".into())); Ok(()) @@ -23,6 +25,8 @@ mod new_section { } mod remove_section { + use crate::Result; + #[test] fn removal_of_all_sections_programmatically_with_sections_and_ids_by_name() { let mut file = gix_config::File::try_from("[core] \na = b\nb=c\n\n[core \"name\"]\nd = 1\ne = 2").unwrap(); @@ -71,7 +75,7 @@ mod remove_section { } #[test] - fn removing_lookup_buckets_preserves_siblings_and_drops_the_final_name() -> crate::Result { + fn removing_lookup_buckets_preserves_siblings_and_drops_the_final_name() -> Result { let mut file = gix_config::File::try_from( "[core] key=plain\n\ [core \"a\"] key=a\n\ @@ -81,11 +85,7 @@ mod remove_section { file.remove_section("core", None).expect("plain section exists"); let err = file.section("core", None).unwrap_err(); assert!(err.is_not_found()); - assert_eq!( - err.to_string(), - "The requested subsection does not exist", - "the `core` section name still exists through its siblings, but its no-subsection bucket was removed" - ); + insta::assert_debug_snapshot!(err, "the `core` section name still exists through its siblings, but its no-subsection bucket was removed", @"The requested subsection does not exist"); assert_eq!(file.section("core", "a")?.value("key"), Some("a".into())); file.remove_section("core", "a").expect("first subsection exists"); @@ -94,12 +94,12 @@ mod remove_section { file.remove_section("core", "b").expect("final subsection exists"); let err = file.section("core", "b").unwrap_err(); assert!(err.is_not_found()); - assert_eq!(err.to_string(), "The requested section does not exist"); + insta::assert_debug_snapshot!(err, "removing lookup buckets preserves siblings and drops the final name", @"The requested section does not exist"); Ok(()) } #[test] - fn removed_sections_can_be_mutated_and_reinserted() -> crate::Result { + fn removed_sections_can_be_mutated_and_reinserted() -> Result { let mut file = gix_config::File::try_from("[core]\na = b\n")?; let mut section = file.remove_section("core", None).expect("section is present"); let removed_id = section.to_ref().id(); @@ -144,24 +144,22 @@ mod remove_section_filter { } mod rename_section { + use crate::Result; #[test] fn section_renaming_validates_new_name() { let mut file = gix_config::File::try_from("[core] a = b").unwrap(); let err = file.rename_section("core", None, "new_core", None).unwrap_err(); assert!(err.is_validation()); - assert_eq!(err.to_string(), "section names can only be ascii, '-': \"new_core\""); + insta::assert_debug_snapshot!(err, "section renaming validates new name", @r#"section names can only be ascii, '-', "input"="new_core""#); let err = file.rename_section("core", None, "new-core", "a\nb").unwrap_err(); assert!(err.is_validation()); - assert_eq!( - err.to_string(), - "sub-section names must not contain newlines or null bytes: \"a\\nb\"" - ); + insta::assert_debug_snapshot!(err, "section renaming validates new name", @r#"sub-section names must not contain newlines or null bytes, "input"="a\nb""#); } #[test] - fn accepts_borrowed_new_subsection_names() -> crate::Result { + fn accepts_borrowed_new_subsection_names() -> Result { let mut file = gix_config::File::try_from("[core] a = b")?; file.rename_section("core", None, "remote", "origin")?; assert_eq!( @@ -179,7 +177,7 @@ mod rename_section { } #[test] - fn all_matching_sections_are_renamed_and_target_collisions_are_preserved() -> crate::Result { + fn all_matching_sections_are_renamed_and_target_collisions_are_preserved() -> Result { let mut file = gix_config::File::try_from( "[branch \"source\"] key = one\n\ [some \"gar\"] key = unrelated\n\ @@ -203,7 +201,7 @@ mod rename_section { } #[test] - fn filter_renames_every_accepted_section() -> crate::Result { + fn filter_renames_every_accepted_section() -> Result { let mut file = gix_config::File::try_from( "[branch \"source\"] key = one\n\ [branch \"source\"] key = two\n\ @@ -239,11 +237,7 @@ mod rename_section { .rename_section_filter("branch", "source", "branch", "other", |_| false) .unwrap_err(); assert!(err.is_not_found()); - assert_eq!( - err.to_string(), - "The key does not exist in the requested section", - "matching nothing causes an error" - ); + insta::assert_debug_snapshot!(err, "matching nothing causes an error", @"The key does not exist in the requested section"); assert_eq!( file.to_string(), prev, @@ -253,7 +247,7 @@ mod rename_section { } #[test] - fn renaming_to_the_same_identity_updates_all_headers() -> crate::Result { + fn renaming_to_the_same_identity_updates_all_headers() -> Result { let mut file = gix_config::File::try_from( "[branch.source] one = 1\n\ [branch.source] two = 2\n", @@ -269,20 +263,21 @@ mod rename_section { } #[test] - fn an_empty_lookup_bucket_is_reported_as_missing() -> crate::Result { + fn an_empty_lookup_bucket_is_reported_as_missing() -> Result { let mut file = gix_config::File::try_from("[core] key = value\n")?; file.remove_section("core", None).expect("section exists"); let err = file.rename_section("core", None, "other", None).unwrap_err(); assert!(err.is_not_found()); - assert_eq!(err.to_string(), "The requested section does not exist"); + insta::assert_debug_snapshot!(err, "an empty lookup bucket is reported as missing", @"The requested section does not exist"); Ok(()) } } mod set_meta { + use crate::Result; use gix_config::file; #[test] - fn affects_newly_added_sections() -> crate::Result { + fn affects_newly_added_sections() -> Result { let mut file = gix_config::File::default(); let expected = &file::Metadata::api(); assert_eq!(file.meta(), expected); diff --git a/gix-config/tests/config/file/access/raw/raw_multi_value.rs b/gix-config/tests/config/file/access/raw/raw_multi_value.rs index 4ee7fac3a2c..dca64d1c664 100644 --- a/gix-config/tests/config/file/access/raw/raw_multi_value.rs +++ b/gix-config/tests/config/file/access/raw/raw_multi_value.rs @@ -1,23 +1,24 @@ +use crate::Result; use gix_config::File; use crate::file::bstring; #[test] -fn single_value_is_identical_to_single_value_query() -> crate::Result { +fn single_value_is_identical_to_single_value_query() -> Result { let config = File::try_from("[core]\na=b\nc=d")?; assert_eq!(vec![config.raw_value("core.a")?], config.raw_values("core.a")?); Ok(()) } #[test] -fn multi_value_in_section() -> crate::Result { +fn multi_value_in_section() -> Result { let config = File::try_from("[core]\na=b\na=c")?; assert_eq!(config.raw_values("core.a")?, vec![bstring("b"), bstring("c")]); Ok(()) } #[test] -fn multi_value_across_sections() -> crate::Result { +fn multi_value_across_sections() -> Result { let config = File::try_from( "[core]\n\ a=b\n\ @@ -32,7 +33,7 @@ fn multi_value_across_sections() -> crate::Result { } #[test] -fn values_with_sections_identify_each_values_section_in_file_order() -> crate::Result { +fn values_with_sections_identify_each_values_section_in_file_order() -> Result { let config = File::try_from( "[core]\n\ a=b\n\ @@ -61,7 +62,7 @@ fn values_with_sections_identify_each_values_section_in_file_order() -> crate::R } #[test] -fn values_with_sections_filter_returns_values_from_accepted_sections() -> crate::Result { +fn values_with_sections_filter_returns_values_from_accepted_sections() -> Result { let config = File::try_from( "[core]\n\ a=b\n\ @@ -92,34 +93,34 @@ fn values_with_sections_filter_returns_values_from_accepted_sections() -> crate: } #[test] -fn section_not_found() -> crate::Result { +fn section_not_found() -> Result { let config = File::try_from("[core]\na=b\nc=d")?; let err = config.raw_values("foo.a").unwrap_err(); assert!(err.is_not_found()); - assert_eq!(err.to_string(), "The requested section does not exist"); + insta::assert_debug_snapshot!(err, "section not found", @"The requested section does not exist"); Ok(()) } #[test] -fn subsection_not_found() -> crate::Result { +fn subsection_not_found() -> Result { let config = File::try_from("[core]\na=b\nc=d")?; let err = config.raw_values("core.a.a").unwrap_err(); assert!(err.is_not_found()); - assert_eq!(err.to_string(), "The requested subsection does not exist"); + insta::assert_debug_snapshot!(err, "subsection not found", @"The requested subsection does not exist"); Ok(()) } #[test] -fn key_not_found() -> crate::Result { +fn key_not_found() -> Result { let config = File::try_from("[core]\na=b\nc=d")?; let err = config.raw_values("core.aaaaaa").unwrap_err(); assert!(err.is_not_found()); - assert_eq!(err.to_string(), "The key does not exist in the requested section"); + insta::assert_debug_snapshot!(err, "key not found", @"The key does not exist in the requested section"); Ok(()) } #[test] -fn subsection_must_be_respected() -> crate::Result { +fn subsection_must_be_respected() -> Result { let config = File::try_from("[core]a=b\n[core.a]a=c")?; assert_eq!(config.raw_values("core.a")?, vec![bstring("b")]); assert_eq!(config.raw_values("core.a.a")?, vec![bstring("c")]); @@ -127,7 +128,7 @@ fn subsection_must_be_respected() -> crate::Result { } #[test] -fn non_relevant_subsection_is_ignored() -> crate::Result { +fn non_relevant_subsection_is_ignored() -> Result { let config = File::try_from("[core]\na=b\na=c\n[core]a=d\n[core]g=g")?; assert_eq!( config.raw_values("core.a")?, diff --git a/gix-config/tests/config/file/access/raw/raw_value.rs b/gix-config/tests/config/file/access/raw/raw_value.rs index c2363a6ef15..d3520902b3d 100644 --- a/gix-config/tests/config/file/access/raw/raw_value.rs +++ b/gix-config/tests/config/file/access/raw/raw_value.rs @@ -1,7 +1,8 @@ +use crate::Result; use gix_config::File; #[test] -fn single_section() -> crate::Result { +fn single_section() -> Result { let config = File::try_from("[core]\na=b\nc=d")?; assert_eq!(config.raw_value("core.a")?, "b"); assert_eq!(config.raw_value_by("core", None, "c")?, "d"); @@ -9,32 +10,28 @@ fn single_section() -> crate::Result { } #[test] -fn global_property_uses_empty_section_name() -> crate::Result { +fn global_property_uses_empty_section_name() -> Result { let config = File::try_from("a=b\n[core]\na=c")?; - assert_eq!( - config.raw_value_by("", None, "a").unwrap_err().to_string(), - "The requested section does not exist", - "these are not readable because the supporting this adds a lot of complexity" - ); + insta::assert_debug_snapshot!(config.raw_value_by("", None, "a").expect_err("these are not readable because the supporting this adds a lot of complexity"), "these are not readable because the supporting this adds a lot of complexity", @"The requested section does not exist"); Ok(()) } #[test] -fn last_one_wins_respected_in_section() -> crate::Result { +fn last_one_wins_respected_in_section() -> Result { let config = File::try_from("[core]\na=b\na=d")?; assert_eq!(config.raw_value("core.a")?, "d"); Ok(()) } #[test] -fn last_one_wins_respected_across_section() -> crate::Result { +fn last_one_wins_respected_across_section() -> Result { let config = File::try_from("[core]\na=b\n[core]\na=d")?; assert_eq!(config.raw_value("core.a")?, "d"); Ok(()) } #[test] -fn value_with_section_identifies_the_section_containing_the_resolved_value() -> crate::Result { +fn value_with_section_identifies_the_section_containing_the_resolved_value() -> Result { let config = File::try_from( "[core]\n\ a=first\n\ @@ -54,7 +51,7 @@ fn value_with_section_identifies_the_section_containing_the_resolved_value() -> } #[test] -fn value_with_section_filter_identifies_the_section_containing_the_resolved_value() -> crate::Result { +fn value_with_section_filter_identifies_the_section_containing_the_resolved_value() -> Result { let config = File::try_from( "[core]\n\ a=first\n\ @@ -76,7 +73,7 @@ fn value_with_section_filter_identifies_the_section_containing_the_resolved_valu } #[test] -fn mutable_value_filters_have_key_and_component_variants() -> crate::Result { +fn mutable_value_filters_have_key_and_component_variants() -> Result { let mut config = File::try_from( "[core]\n\ a=first\n\ @@ -113,52 +110,46 @@ fn mutable_value_filters_have_key_and_component_variants() -> crate::Result { } #[test] -fn section_not_found() -> crate::Result { +fn section_not_found() -> Result { let config = File::try_from("[core]\na=b\nc=d")?; let err = config.raw_value("foo.a").unwrap_err(); assert!(err.is_not_found()); - assert_eq!(err.to_string(), "The requested section does not exist"); + insta::assert_debug_snapshot!(err, "section not found", @"The requested section does not exist"); Ok(()) } #[test] -fn subsection_not_found() -> crate::Result { +fn subsection_not_found() -> Result { let config = File::try_from("[core]\na=b\nc=d")?; let err = config.raw_value("core.a.a").unwrap_err(); assert!(err.is_not_found()); - assert_eq!(err.to_string(), "The requested subsection does not exist"); + insta::assert_debug_snapshot!(err, "subsection not found", @"The requested subsection does not exist"); Ok(()) } #[test] -fn key_not_found() -> crate::Result { +fn key_not_found() -> Result { let config = File::try_from("[core]\na=b\nc=d")?; let err = config.raw_value("core.aaaaaa").unwrap_err(); assert!(err.is_not_found()); - assert_eq!(err.to_string(), "The key does not exist in the requested section"); + insta::assert_debug_snapshot!(err, "key not found", @"The key does not exist in the requested section"); Ok(()) } #[test] -fn invalid_value_names_are_reported_by_mutable_lookups() -> crate::Result { +fn invalid_value_names_are_reported_by_mutable_lookups() -> Result { let mut config = File::try_from("[core]\na=b")?; let err = config.raw_value_mut_by("core", None, "1invalid").unwrap_err(); assert!(err.is_validation()); - assert_eq!( - err.to_string(), - "Valid value names consist of alphanumeric characters or dashes, starting with an alphabetic character.: \"1invalid\"" - ); + insta::assert_debug_snapshot!(err, "invalid value names are reported by mutable lookups", @r#"Valid value names consist of alphanumeric characters or dashes, starting with an alphabetic character., "input"="1invalid""#); let err = config.raw_values_mut_by("core", None, "contains.dot").unwrap_err(); assert!(err.is_validation()); - assert_eq!( - err.to_string(), - "Valid value names consist of alphanumeric characters or dashes, starting with an alphabetic character.: \"contains.dot\"" - ); + insta::assert_debug_snapshot!(err, "invalid value names are reported by mutable lookups", @r#"Valid value names consist of alphanumeric characters or dashes, starting with an alphabetic character., "input"="contains.dot""#); Ok(()) } #[test] -fn subsection_must_be_respected() -> crate::Result { +fn subsection_must_be_respected() -> Result { let config = File::try_from("[core]a=b\n[core.a]a=c")?; assert_eq!(config.raw_value("core.a")?, "b"); assert_eq!(config.raw_value("core.a.a")?, "c"); diff --git a/gix-config/tests/config/file/access/raw/set_existing_raw_value.rs b/gix-config/tests/config/file/access/raw/set_existing_raw_value.rs index 02c53ebbea5..792cd2a1c2c 100644 --- a/gix-config/tests/config/file/access/raw/set_existing_raw_value.rs +++ b/gix-config/tests/config/file/access/raw/set_existing_raw_value.rs @@ -1,3 +1,5 @@ +use crate::Result; + fn file(input: &str) -> gix_config::File { input.parse().unwrap() } @@ -23,14 +25,10 @@ fn single_line() { } #[test] -fn global_property_uses_empty_section_name() -> crate::Result { +fn global_property_uses_empty_section_name() -> Result { let mut file = file("a=b\n[core]\na=c"); let err = file.set_existing_raw_value_by("", None, "a", "d").unwrap_err(); - assert_eq!( - err.to_string(), - "The requested section does not exist", - "cannot set global values" - ); + insta::assert_debug_snapshot!(err, "cannot set global values", @"The requested section does not exist"); Ok(()) } diff --git a/gix-config/tests/config/file/access/raw/set_raw_value.rs b/gix-config/tests/config/file/access/raw/set_raw_value.rs index 83d40c6576b..ba8b523eb84 100644 --- a/gix-config/tests/config/file/access/raw/set_raw_value.rs +++ b/gix-config/tests/config/file/access/raw/set_raw_value.rs @@ -1,3 +1,5 @@ +use crate::Result; + fn file(input: &str) -> gix_config::File { input.parse().unwrap() } @@ -51,7 +53,7 @@ fn comment_included() { } #[test] -fn non_existing_values_cannot_be_set() -> crate::Result { +fn non_existing_values_cannot_be_set() -> Result { let mut file = gix_config::File::default(); file.set_raw_value_by("new", None, "key", "value")?; file.set_raw_value_by("new", "subsection", "key", "subsection-value")?; @@ -66,7 +68,7 @@ fn non_existing_values_cannot_be_set() -> crate::Result { } #[test] -fn accepts_short_lived_keys() -> crate::Result { +fn accepts_short_lived_keys() -> Result { let mut file = gix_config::File::default(); let key = String::from("new.key"); @@ -81,9 +83,6 @@ fn invalid_value_names_fail_without_creating_a_section() { let mut file = gix_config::File::default(); let err = file.set_raw_value_by("new", None, "not.valid", "value").unwrap_err(); assert!(err.is_validation()); - assert_eq!( - err.to_string(), - "Valid value names consist of alphanumeric characters or dashes, starting with an alphabetic character.: \"not.valid\"" - ); + insta::assert_debug_snapshot!(err, "invalid value names fail without creating a section", @r#"Valid value names consist of alphanumeric characters or dashes, starting with an alphabetic character., "input"="not.valid""#); assert_eq!(file.sections().count(), 0, "validation precedes section creation"); } diff --git a/gix-config/tests/config/file/access/read_only.rs b/gix-config/tests/config/file/access/read_only.rs index 08eac67b1c8..ff81f13600e 100644 --- a/gix-config/tests/config/file/access/read_only.rs +++ b/gix-config/tests/config/file/access/read_only.rs @@ -1,3 +1,4 @@ +use crate::Result; use std::fs; use bstr::{BString, ByteSlice}; @@ -9,12 +10,13 @@ use gix_config::{ use crate::file::bstring; -fn lookup_error(err: gix_config::lookup::Error>) -> gix_error::Error { +fn lookup_error(err: gix_config::lookup::Error>) -> gix_error::Error { err.into_error() } #[test] -fn typed_lookup_errors_can_be_erased() -> crate::Result { +fn typed_lookup_errors_can_be_erased() -> Result { + let mut error_snapshots = Vec::new(); use gix_error::ResultExt; let config = File::try_from("[core]\nvalue = invalid\n")?; @@ -31,18 +33,30 @@ fn typed_lookup_errors_can_be_erased() -> crate::Result { .or_erased() .expect_err("invalid typed values must fail conversion") .into_error(); + error_snapshots.push(gix_testtools::redact_debug_snapshot(&(err), &[])); assert!(err.is_validation(), "erasure retains the conversion error"); } let err = config .value::("core.missing") .expect_err("the key does not exist") .into_error(); + insta::assert_debug_snapshot!(err, "erasure retains missing-value classification", @"The key does not exist in the requested section"); assert!(err.is_not_found(), "erasure retains missing-value classification"); + insta::assert_debug_snapshot!(error_snapshots, "typed lookup errors can be erased", @r#" + [ + Booleans need to be 'no', 'off', 'false', '' or 'yes', 'on', 'true' or any number, "input"="invalid", + Integers needs to be positive or negative numbers which may have a suffix like 1k, 42, or 50G, "input"="invalid", + Colors are specific color values and their attributes, like 'brightred', or 'blue', "input"="invalid", + Booleans need to be 'no', 'off', 'false', '' or 'yes', 'on', 'true' or any number, "input"="invalid", + Integers needs to be positive or negative numbers which may have a suffix like 1k, 42, or 50G, "input"="invalid", + Colors are specific color values and their attributes, like 'brightred', or 'blue', "input"="invalid", + ] + "#); Ok(()) } #[test] -fn parsed_section_header_legacy_check_uses_backing_buffer() -> crate::Result { +fn parsed_section_header_legacy_check_uses_backing_buffer() -> Result { let config = File::try_from( "[remote.origin]\n\turl = https://example.com\n[remote \"upstream\"]\n\turl = https://example.com\n", )?; @@ -56,7 +70,7 @@ fn parsed_section_header_legacy_check_uses_backing_buffer() -> crate::Result { /// Asserts we can cast into all variants of our type #[test] -fn get_value_for_all_provided_values() -> crate::Result { +fn get_value_for_all_provided_values() -> Result { let config = r#" [core] other-quoted = "hello" @@ -224,7 +238,7 @@ fn get_value_for_all_provided_values() -> crate::Result { } #[test] -fn get_value_looks_up_all_sections_before_failing() -> crate::Result { +fn get_value_looks_up_all_sections_before_failing() -> Result { let config = r#" [core] bool-explicit = false @@ -254,7 +268,7 @@ fn get_value_looks_up_all_sections_before_failing() -> crate::Result { } #[test] -fn interpreted_values_can_be_returned_with_their_sections() -> crate::Result { +fn interpreted_values_can_be_returned_with_their_sections() -> Result { let file = File::try_from( "[core]\n\ a=1\n\ @@ -289,7 +303,7 @@ fn interpreted_values_can_be_returned_with_their_sections() -> crate::Result { } #[test] -fn section_names_are_case_insensitive() -> crate::Result { +fn section_names_are_case_insensitive() -> Result { let config = "[core] a=true"; let file = File::try_from(config)?; assert_eq!( @@ -301,7 +315,7 @@ fn section_names_are_case_insensitive() -> crate::Result { } #[test] -fn value_names_are_case_insensitive() -> crate::Result { +fn value_names_are_case_insensitive() -> Result { let config = "[core] a = true A = false"; @@ -316,7 +330,7 @@ fn value_names_are_case_insensitive() -> crate::Result { } #[test] -fn section_value_access_is_case_insensitive() -> crate::Result { +fn section_value_access_is_case_insensitive() -> Result { let file = File::try_from("[core]\nMixedCase = one\nMIXEDCASE = two")?; let section = file.section("core", None)?; @@ -344,7 +358,7 @@ fn single_section() { } #[test] -fn sections_by_name() -> crate::Result { +fn sections_by_name() -> Result { let config = r#" [core] repositoryformatversion = 0 @@ -363,7 +377,7 @@ fn sections_by_name() -> crate::Result { } #[test] -fn sections_by_name_ignores_subsections_and_preserves_file_order() -> crate::Result { +fn sections_by_name_ignores_subsections_and_preserves_file_order() -> Result { let config = File::try_from( "[remote] marker=plain\n\ [other] marker=unrelated\n\ @@ -391,11 +405,11 @@ fn sections_by_name_ignores_subsections_and_preserves_file_order() -> crate::Res } #[test] -fn unknown_section() -> crate::Result { +fn unknown_section() -> Result { let config = File::default(); let err = config.section("missing", None).unwrap_err(); assert!(err.is_not_found()); - assert_eq!(err.to_string(), "The requested section does not exist"); + insta::assert_debug_snapshot!(err, "unknown section", @"The requested section does not exist"); let config = r#" [present] @@ -404,7 +418,7 @@ fn unknown_section() -> crate::Result { let mut config = File::try_from(config)?; let err = config.section("present", Some("subsection".into())).unwrap_err(); assert!(err.is_not_found()); - assert_eq!(err.to_string(), "The requested subsection does not exist"); + insta::assert_debug_snapshot!(err, "unknown section", @"The requested subsection does not exist"); config.set_raw_value_by("present", "subsection", "key", "value")?; assert!(config.section("present", Some("subsection".into())).is_ok()); @@ -417,7 +431,7 @@ fn unknown_section() -> crate::Result { } let err = config.section("present", None).unwrap_err(); assert!(err.is_not_found()); - assert_eq!(err.to_string(), "The requested section does not exist"); + insta::assert_debug_snapshot!(err, "unknown section", @"The requested section does not exist"); Ok(()) } @@ -527,7 +541,7 @@ fn multi_line_value_with_empty_continuation_line() { } #[test] -fn multi_line_value_starting_on_a_continuation_line_is_not_indented() -> crate::Result { +fn multi_line_value_starting_on_a_continuation_line_is_not_indented() -> Result { let baseline = crate::scripted_fixture_read_only("make_value_whitespace_baseline.sh")?; let baseline = fs::read(baseline.join("baseline.git"))?; let baseline = baseline @@ -571,7 +585,7 @@ fn overrides_with_implicit_booleans_work_in_single_section() { } #[test] -fn implicit_booleans_may_be_followed_by_whitespace() -> crate::Result { +fn implicit_booleans_may_be_followed_by_whitespace() -> Result { for config in [ "[a]\n\tb \n", "[a]\n\tb\t\n", diff --git a/gix-config/tests/config/file/impls.rs b/gix-config/tests/config/file/impls.rs index dddc5ba9d2d..fdc101e46bd 100644 --- a/gix-config/tests/config/file/impls.rs +++ b/gix-config/tests/config/file/impls.rs @@ -1,3 +1,4 @@ +use crate::Result; use gix_config::File; #[test] @@ -70,7 +71,7 @@ fn can_reconstruct_configs_without_whitespace_in_middle() { } #[test] -fn equality_ignores_section_and_value_name_case_but_not_subsection_case() -> crate::Result { +fn equality_ignores_section_and_value_name_case_but_not_subsection_case() -> Result { let mixed_case = File::try_from("[Core]\nMixedCase = value\n[Remote \"Origin\"]\nURL = location\n")?; let equivalent = File::try_from("[core]\nmixedcase = value\n[remote \"Origin\"]\nurl = location\n")?; assert_eq!(mixed_case, equivalent, "section and value names are case-insensitive"); diff --git a/gix-config/tests/config/file/init/comfort.rs b/gix-config/tests/config/file/init/comfort.rs index 740f209eb3f..a7a5f4cd8f6 100644 --- a/gix-config/tests/config/file/init/comfort.rs +++ b/gix-config/tests/config/file/init/comfort.rs @@ -1,10 +1,11 @@ +use crate::Result; use gix_config::source; use serial_test::serial; #[test] #[serial] -fn from_globals() -> crate::Result { +fn from_globals() -> Result { let _environment = gix_testtools::isolate_git_environment()?; let worktree_dir = crate::scripted_fixture_read_only("make_config_repo.sh")?.canonicalize()?; let _environment = _environment.set( @@ -22,7 +23,7 @@ fn from_globals() -> crate::Result { #[test] #[serial] -fn from_environment_overrides() -> crate::Result { +fn from_environment_overrides() -> Result { let _environment = gix_testtools::isolate_git_environment()?.set("GIT_CONFIG_COUNT", "0"); let config = gix_config::File::from_environment_overrides()?; assert!(config.is_void()); @@ -31,7 +32,7 @@ fn from_environment_overrides() -> crate::Result { #[test] #[serial] -fn from_git_dir() -> crate::Result { +fn from_git_dir() -> Result { let _environment = gix_testtools::isolate_git_environment()?; let worktree_dir = crate::scripted_fixture_read_only("make_config_repo.sh")?; let git_dir = worktree_dir.join(".git"); @@ -96,7 +97,7 @@ fn from_git_dir() -> crate::Result { #[test] #[serial] -fn from_git_dir_with_worktree_extension() -> crate::Result { +fn from_git_dir_with_worktree_extension() -> Result { let _environment = gix_testtools::isolate_git_environment()?; let git_dir = crate::scripted_fixture_read_only("config_with_worktree_extension.sh")? .join("main-worktree") diff --git a/gix-config/tests/config/file/init/from_env.rs b/gix-config/tests/config/file/init/from_env.rs index fa8f5566c0a..830ad573156 100644 --- a/gix-config/tests/config/file/init/from_env.rs +++ b/gix-config/tests/config/file/init/from_env.rs @@ -1,3 +1,4 @@ +use crate::Result; use std::fs; use gix_config::{ @@ -11,7 +12,7 @@ use crate::file::init::from_paths::escape_backslashes; #[test] #[serial] -fn empty_without_relevant_environment() -> crate::Result { +fn empty_without_relevant_environment() -> Result { let _environment = gix_testtools::isolate_git_environment()?.unset("GIT_CONFIG_COUNT"); let config = File::from_env(Default::default())?; assert!(config.is_none()); @@ -20,7 +21,7 @@ fn empty_without_relevant_environment() -> crate::Result { #[test] #[serial] -fn empty_with_zero_count() -> crate::Result { +fn empty_with_zero_count() -> Result { let _environment = gix_testtools::isolate_git_environment()?.set("GIT_CONFIG_COUNT", "0"); let config = File::from_env(Default::default())?; assert!(config.is_none()); @@ -29,19 +30,21 @@ fn empty_with_zero_count() -> crate::Result { #[test] #[serial] -fn parse_error_with_invalid_count() -> crate::Result { +fn parse_error_with_invalid_count() -> Result { let _environment = gix_testtools::isolate_git_environment()?.set("GIT_CONFIG_COUNT", "invalid"); let err = File::from_env(Default::default()).expect_err("the configuration count is not an integer"); - let err = err - .downcast_any_ref::() - .expect("invalid counts are validation errors"); - assert_eq!(err.message, "GIT_CONFIG_COUNT was not a positive integer"); + assert!(err.is_validation(), "invalid counts are validation errors"); + insta::assert_debug_snapshot!(err, "parse error with invalid count", @r#" + GIT_CONFIG_COUNT was not a positive integer, "input"="invalid" + | + └─ invalid digit found in string + "#); Ok(()) } #[test] #[serial] -fn single_key_value_pair() -> crate::Result { +fn single_key_value_pair() -> Result { let _environment = gix_testtools::isolate_git_environment()? .set("GIT_CONFIG_COUNT", "1") .set("GIT_CONFIG_KEY_0", "core.key") @@ -60,7 +63,7 @@ fn single_key_value_pair() -> crate::Result { #[test] #[serial] -fn multiple_key_value_pairs() -> crate::Result { +fn multiple_key_value_pairs() -> Result { let _environment = gix_testtools::isolate_git_environment()? .set("GIT_CONFIG_COUNT", "3") .set("GIT_CONFIG_KEY_0", "core.a") @@ -81,7 +84,7 @@ fn multiple_key_value_pairs() -> crate::Result { #[test] #[serial] -fn error_on_relative_paths_in_include_paths() -> crate::Result { +fn error_on_relative_paths_in_include_paths() -> Result { let _environment = gix_testtools::isolate_git_environment()? .set("GIT_CONFIG_COUNT", "1") .set("GIT_CONFIG_KEY_0", "include.path") @@ -96,18 +99,21 @@ fn error_on_relative_paths_in_include_paths() -> crate::Result { ..Default::default() }); let err = res.expect_err("relative includes without a configuration path must fail"); - assert_eq!( - err.downcast_any_ref::() + insta::assert_debug_snapshot!(err.classify() + .find(|classification| classification.class() == gix_error::Class::NotFound) .expect("the missing configuration path is retained") - .message, - "Include paths from environment variables must not be relative as no config file path exists as root" - ); + .error(), "error on relative paths in include paths", @r#" + Message { + message: "Include paths from environment variables must not be relative as no config file path exists as root", + class: NotFound, + } + "#); Ok(()) } #[test] #[serial] -fn follow_include_paths() -> crate::Result { +fn follow_include_paths() -> Result { let _environment = gix_testtools::isolate_git_environment()?; let dir = tempdir().unwrap(); let a_path = dir.path().join("a"); diff --git a/gix-config/tests/config/file/init/from_paths/includes/conditional/gitdir/mod.rs b/gix-config/tests/config/file/init/from_paths/includes/conditional/gitdir/mod.rs index 4896ad8d13b..77178610acb 100644 --- a/gix-config/tests/config/file/init/from_paths/includes/conditional/gitdir/mod.rs +++ b/gix-config/tests/config/file/init/from_paths/includes/conditional/gitdir/mod.rs @@ -1,5 +1,6 @@ mod util; +use crate::Result; use gix_testtools::Env; use serial_test::serial; use util::{Condition, GitEnv, assert_section_value}; @@ -7,12 +8,12 @@ use util::{Condition, GitEnv, assert_section_value}; use crate::file::init::from_paths::escape_backslashes; #[test] -fn relative_path_with_trailing_slash_matches_like_star_star() -> crate::Result { +fn relative_path_with_trailing_slash_matches_like_star_star() -> Result { assert_section_value(Condition::new("gitdir:worktree/"), GitEnv::repo_name("worktree")?) } #[test] -fn relative_path_without_trailing_slash_does_not_match() -> crate::Result { +fn relative_path_without_trailing_slash_does_not_match() -> Result { assert_section_value( Condition::new("gitdir:worktree").expect_original_value(), GitEnv::repo_name("worktree")?, @@ -20,18 +21,18 @@ fn relative_path_without_trailing_slash_does_not_match() -> crate::Result { } #[test] -fn relative_path_without_trailing_slash_and_dot_git_suffix_matches() -> crate::Result { +fn relative_path_without_trailing_slash_and_dot_git_suffix_matches() -> Result { assert_section_value(Condition::new("gitdir:worktree/.git"), GitEnv::repo_name("worktree")?) } #[test] -fn tilde_slash_expands_the_current_user_home() -> crate::Result { +fn tilde_slash_expands_the_current_user_home() -> Result { let env = GitEnv::repo_name(std::path::Path::new("subdir").join("worktree"))?; assert_section_value(Condition::new("gitdir:~/subdir/worktree/"), env) } #[test] -fn failed_user_expansion_matches_the_literal_pattern() -> crate::Result { +fn failed_user_expansion_matches_the_literal_pattern() -> Result { let temp = gix_testtools::tempfile::tempdir()?; let name = format!( "~gix-config-{}", @@ -86,18 +87,18 @@ path = included.config } #[test] -fn tilde_alone_does_not_match_even_if_home_is_git_directory() -> crate::Result { +fn tilde_alone_does_not_match_even_if_home_is_git_directory() -> Result { let env = GitEnv::repo_in_home()?; assert_section_value(Condition::new("gitdir:~").expect_original_value(), env) } #[test] -fn explicit_star_star_prefix_and_suffix_match_zero_or_more_path_components() -> crate::Result { +fn explicit_star_star_prefix_and_suffix_match_zero_or_more_path_components() -> Result { assert_section_value(Condition::new("gitdir:**/worktree/**"), GitEnv::repo_name("worktree")?) } #[test] -fn double_slash_does_not_match() -> crate::Result { +fn double_slash_does_not_match() -> Result { assert_section_value( Condition::new("gitdir://worktree").expect_original_value(), GitEnv::repo_name("worktree")?, @@ -105,7 +106,7 @@ fn double_slash_does_not_match() -> crate::Result { } #[test] -fn absolute_git_dir_with_os_separators_match() -> crate::Result { +fn absolute_git_dir_with_os_separators_match() -> Result { assert_section_value( original_value_on_windows(Condition::new("gitdir:$gitdir")), GitEnv::repo_name("worktree")?, @@ -113,7 +114,7 @@ fn absolute_git_dir_with_os_separators_match() -> crate::Result { } #[test] -fn absolute_worktree_dir_with_os_separators_does_not_match_if_trailing_slash_is_missing() -> crate::Result { +fn absolute_worktree_dir_with_os_separators_does_not_match_if_trailing_slash_is_missing() -> Result { assert_section_value( Condition::new("gitdir:$worktree").expect_original_value(), GitEnv::repo_name("worktree")?, @@ -121,7 +122,7 @@ fn absolute_worktree_dir_with_os_separators_does_not_match_if_trailing_slash_is_ } #[test] -fn absolute_worktree_dir_with_os_separators_matches_with_trailing_glob() -> crate::Result { +fn absolute_worktree_dir_with_os_separators_matches_with_trailing_glob() -> Result { assert_section_value( original_value_on_windows(Condition::new(format!( "gitdir:$worktree{}**", @@ -132,7 +133,7 @@ fn absolute_worktree_dir_with_os_separators_matches_with_trailing_glob() -> crat } #[test] -fn dot_slash_path_is_replaced_with_directory_containing_the_including_config_file() -> crate::Result { +fn dot_slash_path_is_replaced_with_directory_containing_the_including_config_file() -> Result { assert_section_value( Condition::new("gitdir:./").set_user_config_instead_of_repo_config(), GitEnv::repo_name("worktree")?, @@ -142,7 +143,7 @@ fn dot_slash_path_is_replaced_with_directory_containing_the_including_config_fil #[test] #[serial] -fn dot_slash_from_environment_causes_error() -> crate::Result { +fn dot_slash_from_environment_causes_error() -> Result { let _isolated_environment = gix_testtools::isolate_git_environment()?; let env = GitEnv::repo_name("worktree")?; // Only slashes can be used as matches, even on Windows. @@ -155,13 +156,16 @@ fn dot_slash_from_environment_causes_error() -> crate::Result { .set("GIT_CONFIG_VALUE_0", "./include.path"); let res = gix_config::File::from_env(env.to_init_options()); - assert_eq!( - res.expect_err("resolving the relative include path must fail") - .downcast_any_ref::() - .expect("the missing configuration path is retained") - .message, - "Include paths from environment variables must not be relative as no config file path exists as root" + let err = res.expect_err("resolving the relative include path must fail"); + assert!( + err.is_not_found(), + "relative environment includes have no configuration-file base" ); + insta::assert_debug_snapshot!(err, "relative environment includes and patterns require a containing configuration file", @" + Could not resolve includes in environment configuration + | + └─ Include paths from environment variables must not be relative as no config file path exists as root + "); } let absolute_path = escape_backslashes(env.home_dir().join("include.config")); @@ -172,13 +176,16 @@ fn dot_slash_from_environment_causes_error() -> crate::Result { .set("GIT_CONFIG_VALUE_0", &absolute_path); let res = gix_config::File::from_env(env.to_init_options()); - assert_eq!( - res.expect_err("resolving the relative pattern must fail") - .downcast_any_ref::() - .expect("the missing configuration path is retained") - .message, - "Include paths from environment variables must not be relative as no config file path exists as root" + let err = res.expect_err("resolving the relative pattern must fail"); + assert!( + err.is_not_found(), + "relative environment includes have no configuration-file base" ); + insta::assert_debug_snapshot!(err, "relative environment includes and patterns require a containing configuration file", @" + Could not resolve includes in environment configuration + | + └─ Include paths from environment variables must not be relative as no config file path exists as root + "); } { @@ -195,7 +202,7 @@ fn dot_slash_from_environment_causes_error() -> crate::Result { } #[test] -fn dot_dot_slash_prefixes_are_not_special_and_are_not_what_you_want() -> crate::Result { +fn dot_dot_slash_prefixes_are_not_special_and_are_not_what_you_want() -> Result { assert_section_value( Condition::new("gitdir:../") .set_user_config_instead_of_repo_config() @@ -205,12 +212,12 @@ fn dot_dot_slash_prefixes_are_not_special_and_are_not_what_you_want() -> crate:: } #[test] -fn leading_dots_are_not_special() -> crate::Result { +fn leading_dots_are_not_special() -> Result { assert_section_value(Condition::new("gitdir:.hidden/"), GitEnv::repo_name(".hidden")?) } #[test] -fn dot_slash_path_with_dot_git_suffix_matches() -> crate::Result { +fn dot_slash_path_with_dot_git_suffix_matches() -> Result { assert_section_value( Condition::new("gitdir:./worktree/.git").set_user_config_instead_of_repo_config(), GitEnv::repo_name("worktree")?, @@ -218,7 +225,7 @@ fn dot_slash_path_with_dot_git_suffix_matches() -> crate::Result { } #[test] -fn globbing_and_wildcards() -> crate::Result { +fn globbing_and_wildcards() -> Result { assert_section_value( Condition::new("gitdir:stan?ard/glo*ng/[xwz]ildcards/.git").set_user_config_instead_of_repo_config(), GitEnv::repo_name("standard/globbing/wildcards")?, @@ -226,7 +233,7 @@ fn globbing_and_wildcards() -> crate::Result { } #[test] -fn case_insensitive_matches_any_case() -> crate::Result { +fn case_insensitive_matches_any_case() -> Result { assert_section_value(Condition::new("gitdir/i:WORKTREE/"), GitEnv::repo_name("worktree")?)?; assert_section_value( Condition::new("gitdir:WORKTREE/").expect_original_value(), @@ -235,7 +242,7 @@ fn case_insensitive_matches_any_case() -> crate::Result { } #[test] -fn pattern_with_escaped_backslash() -> crate::Result { +fn pattern_with_escaped_backslash() -> Result { assert_section_value( original_value_on_windows(Condition::new(r"gitdir:\\work\\tree\\/")), GitEnv::repo_name("worktree")?, @@ -243,12 +250,12 @@ fn pattern_with_escaped_backslash() -> crate::Result { } #[test] -fn pattern_with_backslash() -> crate::Result { +fn pattern_with_backslash() -> Result { assert_section_value(Condition::new(r"gitdir:work\tree/"), GitEnv::repo_name("worktree")?) } #[test] -fn star_star_in_the_middle() -> crate::Result { +fn star_star_in_the_middle() -> Result { assert_section_value( Condition::new("gitdir:**/dir/**/worktree/**"), GitEnv::repo_name("dir/worktree")?, @@ -257,14 +264,14 @@ fn star_star_in_the_middle() -> crate::Result { #[test] #[cfg(not(windows))] -fn tilde_expansion_with_symlink() -> crate::Result { +fn tilde_expansion_with_symlink() -> Result { let env = util::git_env_with_symlinked_repo()?; assert_section_value(Condition::new("gitdir:~/worktree/"), env) } #[test] #[cfg(not(windows))] -fn dot_path_with_symlink() -> crate::Result { +fn dot_path_with_symlink() -> Result { let env = util::git_env_with_symlinked_repo()?; assert_section_value( Condition::new("gitdir:./symlink-worktree/.git").set_user_config_instead_of_repo_config(), @@ -274,7 +281,7 @@ fn dot_path_with_symlink() -> crate::Result { #[test] #[cfg(not(windows))] -fn relative_path_matching_symlink() -> crate::Result { +fn relative_path_matching_symlink() -> Result { let env = util::git_env_with_symlinked_repo()?; assert_section_value( Condition::new("gitdir:symlink-worktree/").set_user_config_instead_of_repo_config(), @@ -284,7 +291,7 @@ fn relative_path_matching_symlink() -> crate::Result { #[test] #[cfg(not(windows))] -fn dot_path_matching_symlink_with_icase() -> crate::Result { +fn dot_path_matching_symlink_with_icase() -> Result { let env = util::git_env_with_symlinked_repo()?; assert_section_value( Condition::new("gitdir/i:SYMLINK-WORKTREE/").set_user_config_instead_of_repo_config(), diff --git a/gix-config/tests/config/file/init/from_paths/includes/conditional/gitdir/util.rs b/gix-config/tests/config/file/init/from_paths/includes/conditional/gitdir/util.rs index 8d49ad8d9bf..41c9fd95719 100644 --- a/gix-config/tests/config/file/init/from_paths/includes/conditional/gitdir/util.rs +++ b/gix-config/tests/config/file/init/from_paths/includes/conditional/gitdir/util.rs @@ -1,5 +1,6 @@ #![cfg_attr(windows, allow(dead_code))] +use crate::Result; use std::{ io::Write, path::{Path, PathBuf}, @@ -66,7 +67,7 @@ impl Condition { } impl GitEnv { - pub fn repo_name(repo_name: impl AsRef) -> crate::Result { + pub fn repo_name(repo_name: impl AsRef) -> Result { let tempdir = gix_testtools::tempfile::tempdir()?; let root_dir = gix_path::realpath(tempdir.path())?; let worktree_dir = root_dir.join(repo_name); @@ -80,7 +81,7 @@ impl GitEnv { }) } - pub fn repo_in_home() -> crate::Result { + pub fn repo_in_home() -> Result { Self::repo_name("") } } @@ -118,7 +119,7 @@ pub fn assert_section_value( config_location, }: Condition, env: GitEnv, -) -> crate::Result { +) -> Result { write_config(condition, &env, config_location)?; let mut paths = vec![env.git_dir().join("config")]; @@ -147,7 +148,7 @@ pub fn assert_section_value( assure_git_agrees(expected, env) } -pub fn git_env_with_symlinked_repo() -> crate::Result { +pub fn git_env_with_symlinked_repo() -> Result { let mut env = GitEnv::repo_name("worktree")?; let link_destination = env.root_dir().join("symlink-worktree"); crate::file::init::from_paths::includes::conditional::create_symlink(&link_destination, env.worktree_dir()); @@ -157,7 +158,7 @@ pub fn git_env_with_symlinked_repo() -> crate::Result { Ok(env) } -fn assure_git_agrees(expected: Option, env: GitEnv) -> crate::Result { +fn assure_git_agrees(expected: Option, env: GitEnv) -> Result { let output = gix_testtools::git_command(env.worktree_dir()) .args(["config", "--get", "section.value"]) .env("HOME", env.home_dir()) @@ -187,18 +188,18 @@ fn assure_git_agrees(expected: Option, env: GitEnv) -> crate::Result { Ok(()) } -fn write_config(condition: impl AsRef, env: &GitEnv, overwrite_config_location: ConfigLocation) -> crate::Result { +fn write_config(condition: impl AsRef, env: &GitEnv, overwrite_config_location: ConfigLocation) -> Result { let include_config = write_included_config(env)?; write_main_config(condition, include_config, env, overwrite_config_location) } -fn write_included_config(env: &GitEnv) -> crate::Result { +fn write_included_config(env: &GitEnv) -> Result { let include_path = env.worktree_dir().join("include.path"); write_append_config_value(&include_path, "override-value")?; Ok(include_path) } -fn write_append_config_value(path: impl AsRef, value: &str) -> crate::Result { +fn write_append_config_value(path: impl AsRef, value: &str) -> Result { let mut file = std::fs::OpenOptions::new().append(true).create(true).open(path)?; file.write_all( format!( @@ -216,7 +217,7 @@ fn write_main_config( include_file_path: PathBuf, env: &GitEnv, overwrite_config_location: ConfigLocation, -) -> crate::Result { +) -> Result { git_init(env.worktree_dir(), false)?; if overwrite_config_location == ConfigLocation::Repo { diff --git a/gix-config/tests/config/file/init/from_paths/includes/conditional/hasconfig.rs b/gix-config/tests/config/file/init/from_paths/includes/conditional/hasconfig.rs index 1e6844d344c..527fe1bd82a 100644 --- a/gix-config/tests/config/file/init/from_paths/includes/conditional/hasconfig.rs +++ b/gix-config/tests/config/file/init/from_paths/includes/conditional/hasconfig.rs @@ -1,9 +1,10 @@ +use crate::Result; use std::path::{Path, PathBuf}; use gix_config::file::{includes, init}; #[test] -fn simple() -> crate::Result { +fn simple() -> Result { let (config, root) = config_with_includes("basic")?; compare_baseline(&config, "user.this", root.join("expected")); assert_eq!(config.string("user.that"), None); @@ -11,7 +12,7 @@ fn simple() -> crate::Result { } #[test] -fn inclusion_order() -> crate::Result { +fn inclusion_order() -> Result { let (config, root) = config_with_includes("inclusion-order")?; for key in ["one", "two", "three"] { compare_baseline(&config, format!("user.{key}"), root.join(format!("expected.{key}"))); @@ -20,7 +21,7 @@ fn inclusion_order() -> crate::Result { } #[test] -fn globs() -> crate::Result { +fn globs() -> Result { let (config, root) = config_with_includes("globs")?; for key in ["dss", "dse", "dsm", "ssm"] { compare_baseline(&config, format!("user.{key}"), root.join(format!("expected.{key}"))); @@ -30,7 +31,7 @@ fn globs() -> crate::Result { } #[test] -fn cycle_breaker() -> crate::Result { +fn cycle_breaker() -> Result { for name in ["cycle-breaker-direct", "cycle-breaker-indirect"] { let (_config, _root) = config_with_includes(name)?; } @@ -39,7 +40,7 @@ fn cycle_breaker() -> crate::Result { } #[test] -fn no_cycle() -> crate::Result { +fn no_cycle() -> Result { let (config, root) = config_with_includes("no-cycle")?; compare_baseline(&config, "user.name", root.join("expected")); Ok(()) @@ -59,7 +60,7 @@ fn compare_baseline(config: &gix_config::File, key: impl AsRef, expected: i ); } -fn config_with_includes(name: &str) -> crate::Result<(gix_config::File, PathBuf)> { +fn config_with_includes(name: &str) -> Result<(gix_config::File, PathBuf)> { let root = crate::scripted_fixture_read_only("hasconfig.sh")?.join(name); let options = init::Options { includes: includes::Options::follow(Default::default(), Default::default()), diff --git a/gix-config/tests/config/file/init/from_paths/includes/conditional/mod.rs b/gix-config/tests/config/file/init/from_paths/includes/conditional/mod.rs index a21beb98d18..4b63be11267 100644 --- a/gix-config/tests/config/file/init/from_paths/includes/conditional/mod.rs +++ b/gix-config/tests/config/file/init/from_paths/includes/conditional/mod.rs @@ -1,3 +1,4 @@ +use crate::Result; use std::{fs, path::Path, str::FromStr}; use gix_config::{ @@ -14,7 +15,7 @@ mod hasconfig; mod onbranch; #[test] -fn include_and_includeif_correct_inclusion_order_and_delayed_resolve_include() -> crate::Result { +fn include_and_includeif_correct_inclusion_order_and_delayed_resolve_include() -> Result { let dir = tempdir()?; let config_path = dir.path().join("root"); let first_include_path = dir.path().join("first-incl"); @@ -139,7 +140,7 @@ fn options_with_git_dir(git_dir: &Path) -> init::Options<'_> { } } -fn git_init(dir: impl AsRef, bare: bool) -> crate::Result { +fn git_init(dir: impl AsRef, bare: bool) -> Result { let dir = dir.as_ref(); let mut args = vec!["init"]; if bare { diff --git a/gix-config/tests/config/file/init/from_paths/includes/conditional/onbranch.rs b/gix-config/tests/config/file/init/from_paths/includes/conditional/onbranch.rs index 3c3fb09a169..694e822aee0 100644 --- a/gix-config/tests/config/file/init/from_paths/includes/conditional/onbranch.rs +++ b/gix-config/tests/config/file/init/from_paths/includes/conditional/onbranch.rs @@ -1,3 +1,4 @@ +use crate::Result; use std::fs; use bstr::{BString, ByteSlice}; @@ -11,8 +12,6 @@ use gix_testtools::tempfile::tempdir; use crate::file::{bstring, init::from_paths::includes::conditional::git_init}; -type Result = crate::Result; - #[test] fn literal_branch_names_match() -> Result { assert_section_value( @@ -178,7 +177,7 @@ struct GitEnv { } impl GitEnv { - fn new() -> crate::Result { + fn new() -> Result { let dir = tempdir()?; git_init(dir.path(), true)?; Ok(GitEnv { dir }) @@ -191,7 +190,7 @@ struct Options<'a> { expect: Value, } -fn assert_section_value(opts: Options, env: &mut GitEnv) -> crate::Result { +fn assert_section_value(opts: Options, env: &mut GitEnv) -> Result { assert_section_value_msg(opts, env, None) } @@ -203,7 +202,7 @@ fn assert_section_value_msg( }: Options, GitEnv { dir }: &mut GitEnv, message: Option<&str>, -) -> crate::Result<()> { +) -> Result<()> { let root_config = dir.path().join("config"); let included_config = dir.path().join("include.config"); @@ -274,7 +273,7 @@ value = branch-override-by-include Ok(()) } -fn assure_git_agrees(expected: Value, dir: &mut gix_testtools::tempfile::TempDir) -> crate::Result { +fn assure_git_agrees(expected: Value, dir: &mut gix_testtools::tempfile::TempDir) -> Result { let git_dir = dir.path(); let output = gix_testtools::git_command(git_dir) .args(["config", "--get", "section.value"]) diff --git a/gix-config/tests/config/file/init/from_paths/includes/unconditional.rs b/gix-config/tests/config/file/init/from_paths/includes/unconditional.rs index 1814c4fb199..7a75811b9bb 100644 --- a/gix-config/tests/config/file/init/from_paths/includes/unconditional.rs +++ b/gix-config/tests/config/file/init/from_paths/includes/unconditional.rs @@ -1,3 +1,4 @@ +use crate::Result; use std::fs; use gix_config::{ @@ -18,20 +19,16 @@ fn follow_options() -> init::Options<'static> { } } -fn assert_include_depth(err: gix_error::Exn, max_depth: u8) { - let err = err - .downcast_any_ref::() - .expect("exceeding the include depth is a validation error"); - assert_eq!( - err.to_string(), - format!( - "The maximum allowed length {max_depth} of the file include chain built by following nested resolve_includes is exceeded" - ) +fn assert_include_depth(err: gix_error::Exn) -> gix_error::Exn { + assert!( + err.is_validation(), + "exceeding the include depth is invalid configuration" ); + err } #[test] -fn multiple() -> crate::Result { +fn multiple() -> Result { let dir = tempdir()?; let a_path = dir.path().join("a"); @@ -98,7 +95,7 @@ fn multiple() -> crate::Result { } #[test] -fn respect_max_depth() -> crate::Result { +fn respect_max_depth() -> Result { let dir = tempdir()?; // 0 includes 1 - base level @@ -170,7 +167,13 @@ fn respect_max_depth() -> crate::Result { // with max_allowed_depth of 2 and 4 levels of includes, max_allowed_depth is exceeded and error is returned let options = make_options(2, true); let config = File::from_paths_metadata(into_meta(vec![dir.path().join("0")]), options); - assert_include_depth(config.expect_err("the configured include depth must be enforced"), 2); + insta::assert_debug_snapshot!(assert_include_depth(config.expect_err("the configured include depth must be enforced")), "include-depth limits report the configured maximum", @" + Could not initialize configuration from a path + | + └─ Could not resolve configuration includes + | + └─ The maximum allowed length 2 of the file include chain built by following nested resolve_includes is exceeded + "); // with max_allowed_depth of 2 and 4 levels of includes and error_on_max_depth_exceeded: false , max_allowed_depth is exceeded and the value of level 2 is returned let options = make_options(2, false); @@ -180,12 +183,18 @@ fn respect_max_depth() -> crate::Result { // with max_allowed_depth of 0 and 4 levels of includes, max_allowed_depth is exceeded and error is returned let options = make_options(0, true); let config = File::from_paths_metadata(into_meta(vec![dir.path().join("0")]), options); - assert_include_depth(config.expect_err("the configured include depth must be enforced"), 0); + insta::assert_debug_snapshot!(assert_include_depth(config.expect_err("the configured include depth must be enforced")), "include-depth limits report the configured maximum", @" + Could not initialize configuration from a path + | + └─ Could not resolve configuration includes + | + └─ The maximum allowed length 0 of the file include chain built by following nested resolve_includes is exceeded + "); Ok(()) } #[test] -fn simple() -> crate::Result { +fn simple() -> Result { let dir = tempdir()?; let a_path = dir.path().join("a"); @@ -221,7 +230,7 @@ fn simple() -> crate::Result { } #[test] -fn cycle_detection() -> crate::Result { +fn cycle_detection() -> Result { let dir = tempdir()?; let a_path = dir.path().join("a"); @@ -260,7 +269,13 @@ fn cycle_detection() -> crate::Result { ..Default::default() }; let config = File::from_paths_metadata(into_meta(vec![a_path.clone()]), options); - assert_include_depth(config.expect_err("the configured include depth must be enforced"), 4); + insta::assert_debug_snapshot!(assert_include_depth(config.expect_err("the configured include depth must be enforced")), "include-depth limits report the configured maximum", @" + Could not initialize configuration from a path + | + └─ Could not resolve configuration includes + | + └─ The maximum allowed length 4 of the file include chain built by following nested resolve_includes is exceeded + "); let options = init::Options { includes: includes::Options { @@ -276,7 +291,7 @@ fn cycle_detection() -> crate::Result { } #[test] -fn nested() -> crate::Result { +fn nested() -> Result { let dir = tempdir()?; let a_path = dir.path().join("a"); diff --git a/gix-config/tests/config/file/init/from_paths/mod.rs b/gix-config/tests/config/file/init/from_paths/mod.rs index 74c16cc55e2..9f8eea45fce 100644 --- a/gix-config/tests/config/file/init/from_paths/mod.rs +++ b/gix-config/tests/config/file/init/from_paths/mod.rs @@ -1,3 +1,4 @@ +use crate::Result; use std::{fs, path::PathBuf}; use gix_config::{File, Source}; @@ -17,6 +18,20 @@ mod from_path_no_includes { let config_path = dir.path().join("config"); let err = gix_config::File::from_path_no_includes(config_path, gix_config::Source::Local).unwrap_err(); + #[cfg(not(windows))] + insta::assert_debug_snapshot!(gix_testtools::redact_debug_snapshot(&(err), &[(&(dir.path()).to_string_lossy(), "")]), "file not found", @r#" + The configuration file at "/config" could not be inspected + | + └─ NotFound + "#); + #[cfg(windows)] + insta::assert_debug_snapshot!(gix_testtools::redact_debug_snapshot(&(err), &[(&(dir.path()).to_string_lossy(), "")]), "file not found", @r#" + The configuration file at "/config" could not be inspected + | + └─ I/O error (NotFound) + | + └─ "/config" does not exist. + "#); assert_eq!( err.downcast_any_ref::() .expect("the I/O source is retained") @@ -39,7 +54,7 @@ mod from_path_no_includes { } #[test] -fn multiple_paths_single_value() -> crate::Result { +fn multiple_paths_single_value() -> Result { let dir = tempdir()?; let a_path = dir.path().join("a"); @@ -67,7 +82,7 @@ fn multiple_paths_single_value() -> crate::Result { } #[test] -fn frontmatter_is_maintained_in_multiple_files() -> crate::Result { +fn frontmatter_is_maintained_in_multiple_files() -> Result { let dir = tempdir()?; let a_path = dir.path().join("a"); @@ -128,7 +143,7 @@ fn frontmatter_is_maintained_in_multiple_files() -> crate::Result { } #[test] -fn multiple_paths_multi_value_and_filter() -> crate::Result { +fn multiple_paths_multi_value_and_filter() -> Result { let dir = tempdir()?; let a_path = dir.path().join("a"); diff --git a/gix-config/tests/config/file/init/from_str.rs b/gix-config/tests/config/file/init/from_str.rs index da15368d7ab..6ceb2464e22 100644 --- a/gix-config/tests/config/file/init/from_str.rs +++ b/gix-config/tests/config/file/init/from_str.rs @@ -1,5 +1,7 @@ +use crate::Result; + #[test] -fn empty_yields_default_file() -> crate::Result { +fn empty_yields_default_file() -> Result { let a: gix_config::File = "".parse()?; assert_eq!(a, gix_config::File::default()); assert_eq!(a.to_string(), ""); @@ -7,7 +9,7 @@ fn empty_yields_default_file() -> crate::Result { } #[test] -fn whitespace_without_section_contains_front_matter() -> crate::Result { +fn whitespace_without_section_contains_front_matter() -> Result { let input = " \t"; let a: gix_config::File = input.parse()?; assert_eq!(a.to_string(), input); diff --git a/gix-config/tests/config/file/mod.rs b/gix-config/tests/config/file/mod.rs index 47ca46a0df8..1c017039269 100644 --- a/gix-config/tests/config/file/mod.rs +++ b/gix-config/tests/config/file/mod.rs @@ -1,3 +1,4 @@ +use crate::Result; use std::path::PathBuf; use bstr::BString; @@ -73,7 +74,7 @@ fn fuzzed_stackoverflow() { } #[test] -fn fuzzed_long_runtime() -> crate::Result { +fn fuzzed_long_runtime() -> Result { let config = std::fs::read(fixture_path("fuzzed/long-parsetime.config"))?; let file = File::from_bytes_no_includes(&config, gix_config::file::Metadata::default(), Default::default())?; assert_eq!(file.sections().count(), 52); diff --git a/gix-config/tests/config/file/mutable/multi_value.rs b/gix-config/tests/config/file/mutable/multi_value.rs index 6a9cbc2fd48..e221602e87f 100644 --- a/gix-config/tests/config/file/mutable/multi_value.rs +++ b/gix-config/tests/config/file/mutable/multi_value.rs @@ -1,8 +1,9 @@ mod get { + use crate::Result; use crate::file::{bstring, mutable::multi_value::init_config}; #[test] - fn single_lines() -> crate::Result { + fn single_lines() -> Result { let mut config = init_config(); let value = config.raw_values_mut_by("core", None, "a")?; @@ -11,7 +12,7 @@ mod get { } #[test] - fn multi_line() -> crate::Result { + fn multi_line() -> Result { let mut config: gix_config::File = r#"[core] a=b\ "100" @@ -36,7 +37,7 @@ c } #[test] - fn value_names_are_case_insensitive() -> crate::Result { + fn value_names_are_case_insensitive() -> Result { let mut config: gix_config::File = "[core]\nMixedCase = one\nMIXEDCASE = two".parse()?; assert_eq!( config.raw_values_mut_by("core", None, "mixedcase")?.get()?, @@ -47,10 +48,11 @@ c } mod access { + use crate::Result; use crate::file::mutable::multi_value::init_config; #[test] - fn non_empty_sizes() -> crate::Result { + fn non_empty_sizes() -> Result { let mut config = init_config(); assert_eq!(config.raw_values_mut_by("core", None, "a")?.len(), 3); assert!(!config.raw_values_mut_by("core", None, "a")?.is_empty()); @@ -59,10 +61,11 @@ mod access { } mod set { + use crate::Result; use crate::file::{bstring, mutable::multi_value::init_config}; #[test] - fn values_are_escaped() -> crate::Result { + fn values_are_escaped() -> Result { for value in ["a b", " a b", "a b\t", ";c", "#c", "a\nb\n\tc"] { let mut config = init_config(); let mut values = config.raw_values_mut_by("core", None, "a")?; @@ -80,7 +83,7 @@ mod set { } #[test] - fn single_at_start() -> crate::Result { + fn single_at_start() -> Result { let mut config = init_config(); let mut values = config.raw_values_mut_by("core", None, "a")?; values.set_string_at(0, "Hello")?; @@ -92,7 +95,7 @@ mod set { } #[test] - fn single_at_end() -> crate::Result { + fn single_at_end() -> Result { let mut config = init_config(); let mut values = config.raw_values_mut_by("core", None, "a")?; values.set_string_at(2, "Hello")?; @@ -104,7 +107,7 @@ mod set { } #[test] - fn all() -> crate::Result { + fn all() -> Result { let mut config = init_config(); let mut values = config.raw_values_mut_by("core", None, "a")?; values.set_all("Hello")?; @@ -116,7 +119,7 @@ mod set { } #[test] - fn all_empty() -> crate::Result { + fn all_empty() -> Result { let mut config = init_config(); let mut values = config.raw_values_mut_by("core", None, "a")?; values.set_all("")?; @@ -129,10 +132,11 @@ mod set { } mod delete { + use crate::Result; use crate::file::mutable::multi_value::init_config; #[test] - fn single_at_start_and_end() -> crate::Result { + fn single_at_start_and_end() -> Result { let mut config = init_config(); { let mut values = config.raw_values_mut_by("core", None, "a")?; @@ -150,7 +154,7 @@ mod delete { } #[test] - fn all() -> crate::Result { + fn all() -> Result { let mut config = init_config(); let mut values = config.raw_values_mut_by("core", None, "a")?; values.delete_all(); diff --git a/gix-config/tests/config/file/mutable/section.rs b/gix-config/tests/config/file/mutable/section.rs index bd7fe8ae673..2f461be38ac 100644 --- a/gix-config/tests/config/file/mutable/section.rs +++ b/gix-config/tests/config/file/mutable/section.rs @@ -1,3 +1,5 @@ +use crate::Result; + #[test] fn section_mut_must_exist_as_section_is_not_created_automatically() { let mut config = multi_value_section(); @@ -5,7 +7,7 @@ fn section_mut_must_exist_as_section_is_not_created_automatically() { } #[test] -fn section_mut_or_create_new_is_infallible() -> crate::Result { +fn section_mut_or_create_new_is_infallible() -> Result { let mut config = multi_value_section(); let section = config.section_mut_or_create_new("name", "subsection")?; assert_eq!(section.header().name(), "name"); @@ -14,7 +16,7 @@ fn section_mut_or_create_new_is_infallible() -> crate::Result { } #[test] -fn section_mut_or_create_new_filter_may_reject_existing_sections() -> crate::Result { +fn section_mut_or_create_new_filter_may_reject_existing_sections() -> Result { let mut config = multi_value_section(); let section = config.section_mut_or_create_new_filter("a", None, |_| false)?; assert_eq!(section.header().name(), "a"); @@ -38,10 +40,11 @@ fn section_mut_by_id() { } mod rename { + use crate::Result; use bstr::ByteSlice; #[test] - fn detached_sections_can_be_renamed() -> crate::Result { + fn detached_sections_can_be_renamed() -> Result { let mut section = gix_config::file::Section::new("remote", "origin", gix_config::file::Metadata::default())?; section.to_mut().rename("branch", "main")?; @@ -52,7 +55,7 @@ mod rename { } #[test] - fn attached_sections_are_renamed_unambiguously_and_update_lookups() -> crate::Result { + fn attached_sections_are_renamed_unambiguously_and_update_lookups() -> Result { let mut file = gix_config::File::try_from( "[target \"same\"] key = first\n\ [source \"old\"] key = selected\n\ @@ -90,7 +93,7 @@ mod rename { } #[test] - fn invalid_names_leave_attached_sections_unchanged() -> crate::Result { + fn invalid_names_leave_attached_sections_unchanged() -> Result { let mut file = gix_config::File::try_from("[core] key = value\n")?; assert!(file.section_mut("core", None)?.rename("not_valid", None).is_err()); assert_eq!( @@ -108,9 +111,10 @@ mod rename { mod remove { use super::multi_value_section; + use crate::Result; #[test] - fn all() -> crate::Result { + fn all() -> Result { let mut config = multi_value_section(); let mut section = config.section_mut("a", None)?; @@ -134,9 +138,10 @@ mod remove { mod pop { use super::multi_value_section; + use crate::Result; #[test] - fn all() -> crate::Result { + fn all() -> Result { let mut config = multi_value_section(); let mut section = config.section_mut_by_key("a")?; @@ -160,9 +165,10 @@ mod pop { mod set { use super::multi_value_section; + use crate::Result; #[test] - fn various_escapes_onto_various_kinds_of_values() -> crate::Result { + fn various_escapes_onto_various_kinds_of_values() -> Result { let mut config = multi_value_section(); let mut section = config.section_mut("a", None)?; let values = vec!["", " a", "b\t", "; comment", "a\n\tc d\\ \"x\""]; @@ -189,34 +195,49 @@ mod set { } mod value_name_validation { + use crate::Result; + #[test] - fn mutations_validate_names_and_leave_the_section_unchanged_on_error() -> crate::Result { + fn mutations_validate_names_and_leave_the_section_unchanged_on_error() -> Result { let mut config = gix_config::File::default(); let mut section = config.new_section("core", None)?; - let err: gix_error::ValidationError = section.push("not.valid", Some("value".into())).unwrap_err(); - assert_eq!( - err.to_string(), - "Valid value names consist of alphanumeric characters or dashes, starting with an alphabetic character.: \"not.valid\"" - ); - let err: gix_error::ValidationError = section + let err: gix_error::Message = section + .push("not.valid", Some("value".into())) + .unwrap_err() + .into_inner(); + insta::assert_debug_snapshot!(err, "mutations validate names and leave the section unchanged on error", @r#" + Message { + message: "Valid value names consist of alphanumeric characters or dashes, starting with an alphabetic character.", + class: Validation, + values: {"input": Bytes("not.valid")}, + } + "#); + let err: gix_error::Message = section .push_with_comment("1invalid", Some("value".into()), "comment") - .unwrap_err(); - assert_eq!( - err.to_string(), - "Valid value names consist of alphanumeric characters or dashes, starting with an alphabetic character.: \"1invalid\"" - ); - let err: gix_error::ValidationError = section.set("also invalid", "value").unwrap_err(); - assert_eq!( - err.to_string(), - "Valid value names consist of alphanumeric characters or dashes, starting with an alphabetic character.: \"also invalid\"" - ); + .unwrap_err() + .into_inner(); + insta::assert_debug_snapshot!(err, "mutations validate names and leave the section unchanged on error", @r#" + Message { + message: "Valid value names consist of alphanumeric characters or dashes, starting with an alphabetic character.", + class: Validation, + values: {"input": Bytes("1invalid")}, + } + "#); + let err: gix_error::Message = section.set("also invalid", "value").unwrap_err().into_inner(); + insta::assert_debug_snapshot!(err, "mutations validate names and leave the section unchanged on error", @r#" + Message { + message: "Valid value names consist of alphanumeric characters or dashes, starting with an alphabetic character.", + class: Validation, + values: {"input": Bytes("also invalid")}, + } + "#); assert_eq!(section.num_values(), 0, "validation happens before mutation"); Ok(()) } #[test] - fn names_returned_by_public_apis_are_strings() -> crate::Result { + fn names_returned_by_public_apis_are_strings() -> Result { let mut config = super::multi_value_section(); let mut section = config.section_mut("a", None)?; let names: Vec = section.value_names().collect(); @@ -229,10 +250,11 @@ mod value_name_validation { } mod push { + use crate::Result; use crate::file::bstring; #[test] - fn none_as_value_omits_the_key_value_separator() -> crate::Result { + fn none_as_value_omits_the_key_value_separator() -> Result { let mut file = gix_config::File::default(); let mut section = file.section_mut_or_create_new("a", "sub")?; section.push("key", None)?; @@ -248,7 +270,7 @@ mod push { } #[test] - fn whitespace_is_derived_from_whitespace_before_first_value() -> crate::Result { + fn whitespace_is_derived_from_whitespace_before_first_value() -> Result { for (input, expected_pre_key, expected_sep) in [ ("[a]\n\t\tb=c", Some("\t\t".into()), (None, None)), ("[a]\nb= c", None, (None, Some(" "))), @@ -336,8 +358,10 @@ mod push_with_comment { } mod set_leading_whitespace { + use crate::Result; + #[test] - fn any_whitespace_is_ok() -> crate::Result { + fn any_whitespace_is_ok() -> Result { let mut config = gix_config::File::default(); let mut section = config.new_section("core", None)?; diff --git a/gix-config/tests/config/file/mutable/value.rs b/gix-config/tests/config/file/mutable/value.rs index d2668dc5a0c..8b6569c1b29 100644 --- a/gix-config/tests/config/file/mutable/value.rs +++ b/gix-config/tests/config/file/mutable/value.rs @@ -1,4 +1,5 @@ mod get { + use crate::Result; use bstr::BString; use crate::file::mutable::value::init_config; @@ -29,7 +30,7 @@ mod get { } #[test] - fn value_is_correct() -> crate::Result { + fn value_is_correct() -> Result { let mut config = init_config(); let value = config.raw_value_mut_by("core", None, "a")?; @@ -38,7 +39,7 @@ mod get { } #[test] - fn value_names_are_case_insensitive() -> crate::Result { + fn value_names_are_case_insensitive() -> Result { let mut config: gix_config::File = "[core]\nMixedCase = value".parse()?; assert_eq!(config.raw_value_mut_by("core", None, "mIxEdCaSe")?.get()?, "value"); Ok(()) @@ -46,6 +47,7 @@ mod get { } mod set_string { + use crate::Result; use crate::file::mutable::value::init_config; fn assert_set_string(expected: &str) { @@ -125,7 +127,7 @@ mod set_string { } #[test] - fn unquoted_comments_end_continued_values_and_survive_replacement() -> crate::Result { + fn unquoted_comments_end_continued_values_and_survive_replacement() -> Result { for newline in ["\n", "\r\n"] { for comment in ["# comment", "; comment"] { let mut config: gix_config::File = @@ -144,7 +146,7 @@ mod set_string { } #[test] - fn quoted_comment_markers_in_continued_values_are_value_content() -> crate::Result { + fn quoted_comment_markers_in_continued_values_are_value_content() -> Result { let mut config: gix_config::File = r#"[a] k="one\ #not;comments" @@ -166,7 +168,7 @@ next=value"# } #[test] - fn simple_value_and_empty_string() -> crate::Result { + fn simple_value_and_empty_string() -> Result { let mut config = init_config(); let mut value = config.raw_value_mut_by("core", None, "a")?; @@ -198,9 +200,10 @@ next=value"# mod delete { use super::init_config; + use crate::Result; #[test] - fn single_line_value() -> crate::Result { + fn single_line_value() -> Result { let mut config = init_config(); let mut value = config.raw_value_mut_by("core", None, "a")?; @@ -220,7 +223,7 @@ mod delete { } #[test] - fn get_value_after_deleted() -> crate::Result { + fn get_value_after_deleted() -> Result { let mut config = init_config(); let mut value = config.raw_value_mut_by("core", None, "a")?; @@ -230,7 +233,7 @@ mod delete { } #[test] - fn set_string_after_deleted() -> crate::Result { + fn set_string_after_deleted() -> Result { let mut config = init_config(); let mut value = config.raw_value_mut_by("core", None, "a")?; @@ -249,7 +252,7 @@ mod delete { } #[test] - fn idempotency() -> crate::Result { + fn idempotency() -> Result { let mut config = init_config(); let mut value = config.raw_value_mut_by("core", None, "a")?; @@ -264,7 +267,7 @@ mod delete { } #[test] - fn multi_line_value() -> crate::Result { + fn multi_line_value() -> Result { let mut config: gix_config::File = r#"[core] a=b"100"\ c\ diff --git a/gix-config/tests/config/file/resolve_includes.rs b/gix-config/tests/config/file/resolve_includes.rs index 4493d2e8b69..a91ec08a5c2 100644 --- a/gix-config/tests/config/file/resolve_includes.rs +++ b/gix-config/tests/config/file/resolve_includes.rs @@ -1,7 +1,8 @@ +use crate::Result; use gix_config::{file, file::init}; #[test] -fn missing_includes_are_ignored_by_default() -> crate::Result { +fn missing_includes_are_ignored_by_default() -> Result { let input = r#" [include] path = /etc/absolute/missing.config diff --git a/gix-config/tests/config/file/write.rs b/gix-config/tests/config/file/write.rs index 223ce1089f2..5355c47257b 100644 --- a/gix-config/tests/config/file/write.rs +++ b/gix-config/tests/config/file/write.rs @@ -1,3 +1,4 @@ +use crate::Result; use bstr::ByteVec; use gix_config::file::{Metadata, init}; @@ -77,7 +78,7 @@ fn inserted_newlines_use_each_sections_newline_style() { } #[test] -fn crlf_after_a_comment_is_detected_and_used_for_insertions() -> crate::Result { +fn crlf_after_a_comment_is_detected_and_used_for_insertions() -> Result { let input = "; root\r\n[core]\nkey=value\n"; let mut config = gix_config::File::try_from(input)?; assert_eq!( @@ -163,11 +164,12 @@ fn complex_lossless_roundtrip() { } mod to_filter { + use crate::Result; use bstr::ByteSlice; use gix_config::file::Metadata; #[test] - fn allows_only_selected_sections() -> crate::Result { + fn allows_only_selected_sections() -> Result { let mut config = gix_config::File::new(Metadata::api()); config.set_raw_value_by("a", None, "b", "c")?; diff --git a/gix-config/tests/config/parse/error.rs b/gix-config/tests/config/parse/error.rs index f51fee88eca..8ea9539984a 100644 --- a/gix-config/tests/config/parse/error.rs +++ b/gix-config/tests/config/parse/error.rs @@ -2,72 +2,50 @@ use crate::parse::Events; #[test] fn line_no_is_one_indexed() { - assert_eq!(Events::from_str("[hello").unwrap_err().line_number(), 1); + let err = Events::from_str("[hello").expect_err("the section header is unterminated"); + assert_eq!(err.line_number(), 1); + insta::assert_debug_snapshot!(format_args!("{err}"), "parser diagnostics use one-based line numbers", @"Got an unexpected token on line 1 while trying to parse a section header: '[hello'"); } #[test] -fn conversion_retains_validation_and_bad_tokens() { +fn malformed_input_retains_validation_and_bad_tokens() { use gix_error::ErrorExt; let err = Events::from_str("[hello") .expect_err("the section header is unterminated") .raise(); + insta::assert_debug_snapshot!(err, "malformed configuration retains the offending input", @"Got an unexpected token on line 1 while trying to parse a section header: '[hello'"); assert!(err.is_validation(), "malformed configuration is invalid input"); - let err = err.into_error(); - assert!(err.is_validation(), "conversion preserves the classification"); - assert_eq!( - err.downcast_any_ref::() - .expect("retain the original parse error") - .remaining_data(), - b"[hello" + assert!( + err.probable_cause().is::(), + "the parser error, not its classification marker, is the probable cause" ); + assert_eq!(err.remaining_data(), b"[hello", "the parser retains the unparsed input"); } #[test] fn to_string_truncates_extra_values() { - assert_eq!( - Events::from_str("[1234567890").unwrap_err().to_string(), - "Got an unexpected token on line 1 while trying to parse a section header: '[123456789' ... (1 characters omitted)" - ); + let err = Events::from_str("[1234567890").expect_err("the section header is unterminated"); + insta::assert_debug_snapshot!(format_args!("{err}"), "long invalid tokens show ten characters and the omitted length", @"Got an unexpected token on line 1 while trying to parse a section header: '[123456789' ... (1 characters omitted)"); } #[test] fn to_string() { - let input = "[a_b]\n c=d"; - assert_eq!( - Events::from_str(input).unwrap_err().to_string(), - "Got an unexpected token on line 1 while trying to parse a section header: '[a_b]\n c=d'", - "underscores in section names aren't allowed and will be rejected by git" - ); - let input = "[core] a=b\\\n cd\n[core]\n\n 4a=3"; - assert_eq!( - Events::from_str(input).unwrap_err().to_string(), - "Got an unexpected token on line 5 while trying to parse a name: '4a=3'" - ); - let input = "[core] a=b\\\n cd\n 4a=3"; - assert_eq!( - Events::from_str(input).unwrap_err().to_string(), - "Got an unexpected token on line 3 while trying to parse a name: '4a=3'" - ); - let input = "[core] a=b\n 4a=3"; - assert_eq!( - Events::from_str(input).unwrap_err().to_string(), - "Got an unexpected token on line 2 while trying to parse a name: '4a=3'" - ); - let input = "[core] a=b\n =3"; - assert_eq!( - Events::from_str(input).unwrap_err().to_string(), - "Got an unexpected token on line 2 while trying to parse a name: '=3'" - ); - let input = "[core"; - assert_eq!( - Events::from_str(input).unwrap_err().to_string(), - "Got an unexpected token on line 1 while trying to parse a section header: '[core'" - ); - let input = "[a]\n\tb \u{8}\n"; - assert_eq!( - Events::from_str(input).unwrap_err().to_string(), - "Got an unexpected token on line 2 while trying to parse a name: '\u{8}\n'", - "Git rejects backspace as trailing whitespace after an implicit boolean" - ); + let err = Events::from_str("[a_b]\n c=d").expect_err("underscores are invalid in section names"); + insta::assert_debug_snapshot!(format_args!("{err}"), "underscores in section names are rejected by Git", @" + Got an unexpected token on line 1 while trying to parse a section header: '[a_b] + c=d' + "); + let err = Events::from_str("[core] a=b\\\n cd\n[core]\n\n 4a=3").expect_err("names cannot start with a digit"); + insta::assert_debug_snapshot!(format_args!("{err}"), "line numbers include continuations, section headers, and blank lines", @"Got an unexpected token on line 5 while trying to parse a name: '4a=3'"); + let err = Events::from_str("[core] a=b\\\n cd\n 4a=3").expect_err("names cannot start with a digit"); + insta::assert_debug_snapshot!(format_args!("{err}"), "line numbers include continued values", @"Got an unexpected token on line 3 while trying to parse a name: '4a=3'"); + let err = Events::from_str("[core] a=b\n 4a=3").expect_err("names cannot start with a digit"); + insta::assert_debug_snapshot!(format_args!("{err}"), "an invalid name is reported with its line and unparsed input", @"Got an unexpected token on line 2 while trying to parse a name: '4a=3'"); + let err = Events::from_str("[core] a=b\n =3").expect_err("names cannot be empty"); + insta::assert_debug_snapshot!(format_args!("{err}"), "a missing name reports the remaining assignment", @"Got an unexpected token on line 2 while trying to parse a name: '=3'"); + let err = Events::from_str("[core").expect_err("the section header is unterminated"); + insta::assert_debug_snapshot!(format_args!("{err}"), "an unterminated header identifies the section parser", @"Got an unexpected token on line 1 while trying to parse a section header: '[core'"); + let err = Events::from_str("[a]\n\tb \u{8}\n").expect_err("backspace is not trailing whitespace"); + insta::assert_debug_snapshot!(format_args!("{err}"), "Git rejects backspace as trailing whitespace after an implicit boolean", @"Got an unexpected token on line 2 while trying to parse a name: '\u{8}\n'"); } diff --git a/gix-config/tests/config/parse/section.rs b/gix-config/tests/config/parse/section.rs index d98a7cc2735..977bb6c550f 100644 --- a/gix-config/tests/config/parse/section.rs +++ b/gix-config/tests/config/parse/section.rs @@ -11,25 +11,27 @@ pub fn header_event(name: &'static str, subsection: impl Into Result { + fn serialized(name: &str, subsection: impl IntoBStringOpt) -> ExnMessageResult { let mut config = gix_config::File::default(); let section = config.new_section(name, subsection.into_bstring_opt())?; Ok(section.header().to_bstring()) } mod write_to { + use crate::Result; use crate::parse::section::header::serialized; #[test] - fn subsection_backslashes_and_quotes_are_escaped() -> crate::Result { + fn subsection_backslashes_and_quotes_are_escaped() -> Result { assert_eq!(serialized("core", r"a\b")?, r#"[core "a\\b"]"#); assert_eq!(serialized("core", r#"a:"b""#)?, r#"[core "a:\"b\""]"#); Ok(()) } #[test] - fn everything_is_allowed() -> crate::Result { + fn everything_is_allowed() -> Result { assert_eq!(serialized("core", "a/b \t\t a\\b")?, "[core \"a/b \t\t a\\\\b\"]"); Ok(()) } @@ -39,24 +41,38 @@ mod header { #[test] fn names_must_be_mostly_ascii() { + let mut message_diagnostics = Vec::new(); for name in ["🤗", "x.y", "x y", "x\ny"] { - assert_eq!( - serialized(name, None).expect_err("name must be rejected").message, - "section names can only be ascii, '-'" - ); + message_diagnostics.push(gix_testtools::redact_debug_snapshot( + &(serialized(name, None).expect_err("name must be rejected")), + &[], + )); } + insta::assert_debug_snapshot!(message_diagnostics, "names must be mostly ascii", @r#" + [ + section names can only be ascii, '-', "input"="🤗", + section names can only be ascii, '-', "input"="x.y", + section names can only be ascii, '-', "input"="x y", + section names can only be ascii, '-', "input"="x\ny", + ] + "#); } #[test] fn subsections_with_newlines_and_null_bytes_are_rejected() { + let mut message_diagnostics = Vec::new(); for subsection in ["a\nb", "a\0b"] { - assert_eq!( - serialized("a", subsection) - .expect_err("subsection must be rejected") - .message, - "sub-section names must not contain newlines or null bytes" - ); + message_diagnostics.push(gix_testtools::redact_debug_snapshot( + &(serialized("a", subsection).expect_err("subsection must be rejected")), + &[], + )); } + insta::assert_debug_snapshot!(message_diagnostics, "subsections with newlines and null bytes are rejected", @r#" + [ + sub-section names must not contain newlines or null bytes, "input"="a\nb", + sub-section names must not contain newlines or null bytes, "input"="a\0b", + ] + "#); } } } diff --git a/gix-credentials/Cargo.toml b/gix-credentials/Cargo.toml index 700e51662c2..5a30219d164 100644 --- a/gix-credentials/Cargo.toml +++ b/gix-credentials/Cargo.toml @@ -38,6 +38,7 @@ bstr = { version = "1.12.0", default-features = false, features = ["std"] } document-features = { version = "0.2.1", optional = true } [dev-dependencies] +insta = "1.46.3" gix-sec = { path = "../gix-sec" } gix-testtools = { path = "../tests/tools", features = ["sha1"] } diff --git a/gix-credentials/examples/custom-helper.rs b/gix-credentials/examples/custom-helper.rs index 16c0f6b6ca7..ea830f8ed30 100644 --- a/gix-credentials/examples/custom-helper.rs +++ b/gix-credentials/examples/custom-helper.rs @@ -1,14 +1,15 @@ use gix_credentials::{program, protocol}; use gix_error::ErrorExt; +use gix_error::ExnResult; /// Run like this `echo url=https://example.com | cargo run --example custom-helper -- get` -pub fn main() -> Result<(), gix_error::Exn> { +pub fn main() -> ExnResult { gix_credentials::program::main( std::env::args_os().skip(1), std::io::stdin(), std::io::stdout(), protocol::ContextOptions::default(), - |action, context| -> Result<_, gix_error::Exn> { + |action, context| -> ExnResult<_> { match action { program::main::Action::Get => Ok(Some(protocol::Context { username: Some("user".into()), diff --git a/gix-credentials/examples/git-credential-lite.rs b/gix-credentials/examples/git-credential-lite.rs index 613743cc586..c53a7a62c98 100644 --- a/gix-credentials/examples/git-credential-lite.rs +++ b/gix-credentials/examples/git-credential-lite.rs @@ -1,5 +1,6 @@ +use gix_error::ExnResult; /// Run like this `echo url=https://example.com | cargo run --example git-credential-light -- fill` -pub fn main() -> Result<(), gix_error::Exn> { +pub fn main() -> ExnResult { gix_credentials::program::main( std::env::args_os().skip(1), std::io::stdin(), diff --git a/gix-credentials/src/helper/invoke.rs b/gix-credentials/src/helper/invoke.rs index ff41ec0cb86..1b019709ebf 100644 --- a/gix-credentials/src/helper/invoke.rs +++ b/gix-credentials/src/helper/invoke.rs @@ -1,6 +1,6 @@ use std::io::Read; -use gix_error::{ErrorExt, ResultExt, RetryableError, message}; +use gix_error::{Class, ClassificationMarker, ErrorExt, ExnResult, ResultExt, message}; use crate::helper::{Action, Context, NextAction, Outcome, Result}; @@ -44,10 +44,7 @@ pub fn invoke(helper: &mut crate::Program, action: &Action) -> Result { } } -pub(crate) fn raw( - helper: &mut crate::Program, - action: &Action, -) -> std::result::Result>, gix_error::Exn> { +pub(crate) fn raw(helper: &mut crate::Program, action: &Action) -> ExnResult>> { let communication_error = || message("An IO error occurred while communicating to the credentials helper"); let (mut stdin, stdout) = helper.start(action).or_raise_erased(communication_error)?; if let (Action::Get(_), None) = (&action, &stdout) { @@ -61,10 +58,10 @@ pub(crate) fn raw( stdout.read_to_end(&mut buf).map(|_| buf) }) .transpose() - .map_err(|err| RetryableError::new(err).raise_erased())?; + .map_err(|err| ClassificationMarker::with_source(Class::Retryable, err).raise_erased())?; helper.finish().map_err(|err| { if err.kind() == std::io::ErrorKind::Other { - RetryableError::new(err).raise_erased() + ClassificationMarker::with_source(Class::Retryable, err).raise_erased() } else { err.and_raise(communication_error()).erased() } diff --git a/gix-credentials/src/helper/mod.rs b/gix-credentials/src/helper/mod.rs index 32e2ead5e59..73c7b0668df 100644 --- a/gix-credentials/src/helper/mod.rs +++ b/gix-credentials/src/helper/mod.rs @@ -1,4 +1,5 @@ use bstr::{BStr, BString}; +use gix_error::{Exn, ExnResult, Message}; use crate::{Program, protocol, protocol::Context}; @@ -54,7 +55,7 @@ impl Outcome { } /// The Result type used in [`invoke()`][crate::helper::invoke()]. -pub type Result = std::result::Result, gix_error::Exn>; +pub type Result = ExnResult>; /// The action to perform by the credentials [helper][`crate::helper::invoke()`]. #[derive(Clone, Debug)] @@ -135,7 +136,7 @@ pub struct NextAction { } impl TryFrom<&NextAction> for Context { - type Error = gix_error::ValidationError; + type Error = Exn; fn try_from(value: &NextAction) -> std::result::Result { Context::from_bytes(value.previous_output.as_ref(), value.options) diff --git a/gix-credentials/src/program/main.rs b/gix-credentials/src/program/main.rs index d3b625f9db5..4a99104b81a 100644 --- a/gix-credentials/src/program/main.rs +++ b/gix-credentials/src/program/main.rs @@ -1,6 +1,6 @@ use std::ffi::OsString; -use gix_error::ValidationError; +use gix_error::{Message, validation}; /// The action passed to the credential helper implementation in [`main()`][crate::program::main()]. #[derive(Debug, Copy, Clone)] @@ -14,18 +14,20 @@ pub enum Action { } impl TryFrom for Action { - type Error = ValidationError; + type Error = Message; + /// Invalid action bytes are stored as `input` in [`Message::values`]. + /// After [wrapping](gix_error::Error::from_error()), inspect them with [metadata](gix_error::Error::metadata()). fn try_from(value: OsString) -> Result { Ok(match value.to_str() { Some("fill" | "get") => Action::Get, Some("approve" | "store") => Action::Store, Some("reject" | "erase") => Action::Erase, _ => { - return Err(ValidationError::new_with_input( + return Err(validation( "Action is invalid, need 'get', 'store', 'erase' or 'fill', 'approve', 'reject'", - value.as_encoded_bytes().to_owned(), - )); + ) + .with("input", value.as_encoded_bytes())); } }) } @@ -45,7 +47,7 @@ impl Action { pub(crate) mod function { use std::ffi::OsString; - use gix_error::{ErrorExt, ResultExt, ValidationError}; + use gix_error::{ErrorExt, ExnResult, ResultExt, validation}; use crate::{ program::main::Action, @@ -66,23 +68,22 @@ pub(crate) mod function { stdout: impl std::io::Write, options: ContextOptions, credentials: CredentialsFn, - ) -> Result<(), gix_error::Exn> + ) -> ExnResult where - CredentialsFn: FnOnce(Action, Context) -> Result, gix_error::Exn>, + CredentialsFn: FnOnce(Action, Context) -> ExnResult>, { let action = args .into_iter() .next() - .ok_or_else(|| ValidationError::new("The first argument must be the action to perform").raise_erased())?; + .ok_or_else(|| validation("The first argument must be the action to perform").raise_erased())?; let action = Action::try_from(action).or_erased()?; let mut buf = Vec::::with_capacity(512); stdin.read_to_end(&mut buf).or_erased()?; let ctx = Context::from_bytes(&buf, options).or_erased()?; if ctx.url.is_none() && (ctx.protocol.is_none() || ctx.host.is_none()) { - return Err(ValidationError::new( - "Either 'url' field or both 'protocol' and 'host' fields must be provided", - ) - .raise_erased()); + return Err( + validation("Either 'url' field or both 'protocol' and 'host' fields must be provided").raise_erased(), + ); } let res = credentials(action, ctx.clone())?; match (action, res) { @@ -94,8 +95,7 @@ pub(crate) mod function { .or_else(|| ctx_for_error.to_url()) .expect("URL is available either directly or via protocol+host which we checked for"); return Err( - gix_error::NotFoundError::new(format!("Credentials for {url:?} could not be obtained")) - .raise_erased(), + gix_error::not_found(format!("Credentials for {url:?} could not be obtained")).raise_erased(), ); } (Action::Get, Some(mut ctx)) => { diff --git a/gix-credentials/src/protocol/context/mod.rs b/gix-credentials/src/protocol/context/mod.rs index e076acfcd5c..dae6e3029fe 100644 --- a/gix-credentials/src/protocol/context/mod.rs +++ b/gix-credentials/src/protocol/context/mod.rs @@ -91,7 +91,8 @@ mod access { mod mutate { use bstr::ByteSlice; - use gix_error::{OptionExt, ResultExt, ValidationError}; + use gix_error::ExnResult; + use gix_error::{OptionExt, ResultExt, validation}; use crate::protocol::Context; @@ -100,10 +101,10 @@ mod mutate { /// Destructure the url at our `url` field into parts like protocol, host, username and path and store /// them in our respective fields. If `use_http_path` is set, http paths are significant even though /// normally this isn't the case. - pub fn destructure_url_in_place(&mut self, use_http_path: bool) -> Result<&mut Self, gix_error::Exn> { + pub fn destructure_url_in_place(&mut self, use_http_path: bool) -> ExnResult<&mut Self> { if self.url.is_none() { self.url = Some(self.to_url().ok_or_raise_erased(|| { - ValidationError::new("Either 'url' field or both 'protocol' and 'host' fields must be provided") + validation("Either 'url' field or both 'protocol' and 'host' fields must be provided") })?); } diff --git a/gix-credentials/src/protocol/context/serde.rs b/gix-credentials/src/protocol/context/serde.rs index 494c897645a..f543f0816fa 100644 --- a/gix-credentials/src/protocol/context/serde.rs +++ b/gix-credentials/src/protocol/context/serde.rs @@ -1,4 +1,5 @@ use bstr::BStr; +use gix_error::ExnMessageResult; mod write { use bstr::{BStr, BString}; @@ -7,6 +8,9 @@ mod write { impl Context { /// Write ourselves to `out` such that [`from_bytes()`][Self::from_bytes()] can decode it losslessly. + /// Invalid field values are retained as `input` bytes in the I/O error. + /// After [wrapping](gix_error::Error::from_error()), inspect them with + /// [metadata](gix_error::Error::metadata()). pub fn write_to(&self, mut out: impl std::io::Write) -> std::io::Result<()> { use bstr::ByteSlice; fn write_key(out: &mut impl std::io::Write, key: &str, value: &BStr) -> std::io::Result<()> { @@ -74,14 +78,18 @@ mod write { /// pub mod decode { use bstr::ByteSlice; - use gix_error::ValidationError; + use gix_error::ExnMessageResult; + use gix_error::validation; use crate::protocol::{Context, ContextOptions, context::serde::validate}; impl Context { /// Decode ourselves from `input` which is the format written by [`write_to()`][Self::write_to()]. /// `options` control what to support during deserialization. - pub fn from_bytes(input: &[u8], options: ContextOptions) -> Result { + /// Invalid line or value bytes are stored as `input` in [`gix_error::Message::values`]. + /// After [wrapping](gix_error::Error::from_error()), inspect them with + /// [metadata](gix_error::Error::metadata()). + pub fn from_bytes(input: &[u8], options: ContextOptions) -> ExnMessageResult { let mut ctx = Context { options, ..Context::default() @@ -108,20 +116,18 @@ pub mod decode { (Some(key), Some(value)) => { validate(key, value, options.protect_protocol).map(|_| (key, value.to_owned())) } - _ => Err(ValidationError::new_with_input( - "Invalid format, expecting key=value", - line, - )), + _ => Err(validation("Invalid format, expecting key=value") + .with("input", line) + .into()), } }) { let (key, value) = res?; match key { "protocol" | "host" | "username" | "password" | "oauth_refresh_token" => { if !value.is_utf8() { - return Err(ValidationError::new_with_input( - format!("Illformed UTF-8 in value of key {key:?}"), - value, - )); + return Err(validation(format!("Illformed UTF-8 in value of key {key:?}")) + .with("input", value) + .into()); } let value = value.to_string(); *match key { @@ -152,7 +158,7 @@ pub mod decode { } } -fn validate(key: &str, value: &BStr, protect_protocol: bool) -> Result<(), gix_error::ValidationError> { +fn validate(key: &str, value: &BStr, protect_protocol: bool) -> ExnMessageResult { if key.contains('\0') || key.contains('\n') || key.contains('\r') @@ -160,10 +166,11 @@ fn validate(key: &str, value: &BStr, protect_protocol: bool) -> Result<(), gix_e || value.contains(&b'\n') || (protect_protocol && value.contains(&b'\r')) { - return Err(gix_error::ValidationError::new_with_input( - format!("{key:?}={value:?} must not contain null bytes or newlines neither in key nor in value."), - value, - )); + return Err(gix_error::validation(format!( + "{key:?}={value:?} must not contain null bytes or newlines neither in key nor in value." + )) + .with("input", value) + .into()); } Ok(()) } diff --git a/gix-credentials/src/protocol/mod.rs b/gix-credentials/src/protocol/mod.rs index 397ea574c44..f1c84419b15 100644 --- a/gix-credentials/src/protocol/mod.rs +++ b/gix-credentials/src/protocol/mod.rs @@ -1,5 +1,6 @@ use bstr::BString; use gix_error::ErrorExt; +use gix_error::ExnResult; use crate::helper; @@ -13,7 +14,7 @@ pub struct Outcome { } /// The Result type used in credentials top-level functions to obtain a complete identity. -pub type Result = std::result::Result, gix_error::Exn>; +pub type Result = ExnResult>; /// Additional context to be passed to the credentials helper. #[derive(Debug, Default, Clone, Eq, PartialEq)] @@ -86,7 +87,7 @@ fn identity_missing(context: Context) -> gix_error::Exn { let mut buf = Vec::new(); // Invalid protocol values must not prevent reporting the missing identity. context.redacted().write_to(&mut buf).ok(); - gix_error::NotFoundError::new(format!( + gix_error::not_found(format!( "Could not obtain identity for context: {}", String::from_utf8_lossy(&buf) )) diff --git a/gix-credentials/tests/helper/cascade.rs b/gix-credentials/tests/helper/cascade.rs index 09c272af347..de37ff3998c 100644 --- a/gix-credentials/tests/helper/cascade.rs +++ b/gix-credentials/tests/helper/cascade.rs @@ -1,4 +1,5 @@ mod invoke { + use crate::Result; use bstr::ByteSlice; use gix_credentials::{ Program, @@ -10,6 +11,7 @@ mod invoke { #[test] fn invalid_authentication_challenges_fail_without_helpers() { + let mut error_snapshots = Vec::new(); for value in [ b"Basic realm=\"a\rb\"".as_slice(), b"Basic\nusername=other", @@ -24,15 +26,29 @@ mod invoke { }), ) .expect_err("malformed authentication challenges must fail without panicking"); + error_snapshots.push(gix_testtools::redact_debug_snapshot(&(err), &[])); assert!( err.downcast_any_ref::().is_some(), "protocol validation must run even when no helper is configured and prompting is disabled" ); } + insta::assert_debug_snapshot!(error_snapshots, "invalid authentication challenges fail without helpers", @r#" + [ + I/O error (Other) + | + └─ "wwwauth[]"="Basic realm=\"a\rb\"" must not contain null bytes or newlines neither in key nor in value., "input"="Basic realm=\"a\rb\"", + I/O error (Other) + | + └─ "wwwauth[]"="Basic\nusername=other" must not contain null bytes or newlines neither in key nor in value., "input"="Basic\nusername=other", + I/O error (Other) + | + └─ "wwwauth[]"="Basic\0realm=example" must not contain null bytes or newlines neither in key nor in value., "input"="Basic\0realm=example", + ] + "#); } #[test] - fn a_helper_closing_its_input_does_not_prevent_fallback_with_challenges() -> crate::Result { + fn a_helper_closing_its_input_does_not_prevent_fallback_with_challenges() -> Result { let outcome = Cascade::default() .extend([ Program::from_custom_definition("!f() { exit 1; }; f"), @@ -63,7 +79,7 @@ mod invoke { } #[test] - fn authentication_challenges_reach_all_helpers_until_credentials_are_complete() -> crate::Result { + fn authentication_challenges_reach_all_helpers_until_credentials_are_complete() -> Result { let outcome = Cascade::default() .extend([ Program::from_custom_definition("!f() { cat >/dev/null; echo username=user; }; f"), diff --git a/gix-credentials/tests/helper/context.rs b/gix-credentials/tests/helper/context.rs index 70d6b1ce789..98ac8a8593d 100644 --- a/gix-credentials/tests/helper/context.rs +++ b/gix-credentials/tests/helper/context.rs @@ -1,8 +1,9 @@ +use crate::Result; use bstr::ByteSlice; use gix_credentials::protocol::{Context, ContextOptions}; #[test] -fn authentication_challenges_survive_a_protocol_roundtrip() -> crate::Result { +fn authentication_challenges_survive_a_protocol_roundtrip() -> Result { let input = b"protocol=https host=github.com wwwauth[]=Basic realm=\"GitHub\" domain_hint=\"example\" @@ -86,6 +87,7 @@ mod write_to { #[test] fn record_delimiters_are_invalid() { + let mut error_snapshots = Vec::new(); for input in [&b"foo\0"[..], b"foo\n", b"foo\r"] { let ctx = Context { url: Some(input.into()), @@ -93,8 +95,25 @@ mod write_to { }; let mut buf = Vec::::new(); let err = ctx.write_to(&mut buf).unwrap_err(); + error_snapshots.push(gix_testtools::redact_debug_snapshot(&(err), &[])); assert_eq!(err.kind(), std::io::ErrorKind::Other); } + insta::assert_debug_snapshot!(error_snapshots, "record delimiters are invalid", @r#" + [ + Custom { + kind: Other, + error: "url"="foo\0" must not contain null bytes or newlines neither in key nor in value., "input"="foo\0", + }, + Custom { + kind: Other, + error: "url"="foo\n" must not contain null bytes or newlines neither in key nor in value., "input"="foo\n", + }, + Custom { + kind: Other, + error: "url"="foo\r" must not contain null bytes or newlines neither in key nor in value., "input"="foo\r", + }, + ] + "#); } #[test] @@ -173,7 +192,7 @@ username=bob"; #[test] fn null_bytes_when_decoding() { let err = Context::from_bytes(b"url=https://foo\0", ContextOptions::default()).unwrap_err(); - assert!(err.message.contains("must not contain null bytes or newlines")); + insta::assert_debug_snapshot!(err, "null bytes when decoding", @r#""url"="https://foo\0" must not contain null bytes or newlines neither in key nor in value., "input"="https://foo\0""#); } #[test] diff --git a/gix-credentials/tests/helper/invoke.rs b/gix-credentials/tests/helper/invoke.rs index b92f8f96896..c76031560c6 100644 --- a/gix-credentials/tests/helper/invoke.rs +++ b/gix-credentials/tests/helper/invoke.rs @@ -67,12 +67,13 @@ fn store_and_reject() { } mod program { + use crate::Result; use gix_credentials::{Program, helper, program::Kind}; use crate::helper::script_helper; #[test] - fn builtin() -> crate::Result { + fn builtin() -> Result { // Other tests resolve fixture paths relative to the working directory, so change it only in a child. if gix_testtools::run_in_isolated_process()? { return Ok(()); @@ -84,6 +85,11 @@ mod program { &helper::Action::get_for_url("/path/without/scheme/fails/with/error"), ) .expect_err("the builtin helper rejects a URL without a scheme"); + insta::assert_debug_snapshot!(err, "this failure indicates we could launch the helper, even though it wasn't happy which is fine. It doesn't like the URL", @" + I/O error (Other) + | + └─ Credentials helper program failed with status code Some(128) + "); assert!( err.is_retryable(), "this failure indicates we could launch the helper, even though it wasn't happy which is fine. It doesn't like the URL" @@ -136,7 +142,7 @@ mod program { } #[test] - fn path_to_helper_as_script_to_workaround_executable_bits() -> crate::Result { + fn path_to_helper_as_script_to_workaround_executable_bits() -> Result { assert_eq!( gix_credentials::helper::invoke( &mut script_helper("custom-helper"), diff --git a/gix-credentials/tests/helper/mod.rs b/gix-credentials/tests/helper/mod.rs index efbafc2d8ab..e3db117bd83 100644 --- a/gix-credentials/tests/helper/mod.rs +++ b/gix-credentials/tests/helper/mod.rs @@ -19,11 +19,13 @@ mod invoke_outcome_to_helper_result { action, ) .unwrap_err(); + insta::assert_debug_snapshot!(err, "missing username or password causes failure with get action", @"Could not obtain identity for context: url=does/not/matter"); assert!(err.is_not_found()); } #[test] fn invalid_context_still_reports_missing_identity() { + let mut error_snapshots = Vec::new(); for value in ["invalid\nvalue", "invalid\0value", "invalid\rvalue"] { for context in [ protocol::Context::from_url(value, Default::default()), @@ -34,12 +36,23 @@ mod invoke_outcome_to_helper_result { ] { let err = helper_outcome_to_result(None, helper::Action::Get(context)) .expect_err("Missing credentials must return an error even when the context is invalid"); + error_snapshots.push(gix_testtools::redact_debug_snapshot(&(err), &[])); assert!( err.is_not_found(), "Invalid context must not replace the missing-credentials classification" ); } } + insta::assert_debug_snapshot!(error_snapshots, "invalid context still reports missing identity", @" + [ + Could not obtain identity for context: , + Could not obtain identity for context: , + Could not obtain identity for context: , + Could not obtain identity for context: , + Could not obtain identity for context: , + Could not obtain identity for context: , + ] + "); } #[test] @@ -56,10 +69,7 @@ mod invoke_outcome_to_helper_result { action, ) .unwrap_err(); - assert_eq!( - err.to_string(), - "The handler asked to stop trying to obtain credentials" - ); + insta::assert_debug_snapshot!(err, "quit message in context causes special error ignoring missing identity", @"The handler asked to stop trying to obtain credentials"); } } diff --git a/gix-credentials/tests/program/main.rs b/gix-credentials/tests/program/main.rs index dd5b4f513d9..00596aca511 100644 --- a/gix-credentials/tests/program/main.rs +++ b/gix-credentials/tests/program/main.rs @@ -1,4 +1,5 @@ use gix_credentials::program::main; +use gix_error::ExnResult; use std::io::Cursor; #[test] @@ -7,7 +8,11 @@ fn invalid_non_utf8_action_is_preserved() { use std::{ffi::OsString, os::unix::ffi::OsStringExt}; let err = main::Action::try_from(OsString::from_vec(vec![0xff])).expect_err("the action is invalid"); - assert_eq!(err.input.expect("the invalid action is retained").as_slice(), &[0xff]); + assert_eq!( + err.values.get("input"), + Some(&gix_error::MetadataValue::Bytes(vec![0xff].into())), + "the invalid action is retained" + ); } #[test] @@ -23,7 +28,7 @@ fn context_options_apply_to_input_and_output() { Cursor::new(input), &mut output, options, - |_action, context| -> Result, gix_error::Exn> { + |_action, context| -> ExnResult> { assert_eq!( context.url.as_ref().map(|url| url.as_slice()), Some(&input[4..input.len() - 1]) @@ -50,7 +55,7 @@ fn protocol_and_host_without_url_is_valid() { Cursor::new(input), &mut output, gix_credentials::protocol::ContextOptions::default(), - |_action, context| -> Result, gix_error::Exn> { + |_action, context| -> ExnResult> { assert_eq!(context.protocol.as_deref(), Some("https")); assert_eq!(context.host.as_deref(), Some("github.com")); assert_eq!(context.url, None, "the URL isn't automatically populated"); @@ -63,6 +68,7 @@ fn protocol_and_host_without_url_is_valid() { // This should fail because our mock helper returned None (no credentials found) // but it should NOT fail because of missing URL let err = result.expect_err("missing credentials must fail"); + insta::assert_debug_snapshot!(err, "protocol and host without url is valid", @r#"Credentials for "https://github.com" could not be obtained"#); assert!(err.is_not_found()); assert!( called, @@ -72,6 +78,7 @@ fn protocol_and_host_without_url_is_valid() { #[test] fn missing_protocol_with_only_host_or_protocol_fails() { + let mut error_snapshots = Vec::new(); for input in ["host=github.com\n", "protocol=https\n"] { let mut output = Vec::new(); @@ -81,16 +88,23 @@ fn missing_protocol_with_only_host_or_protocol_fails() { Cursor::new(input), &mut output, gix_credentials::protocol::ContextOptions::default(), - |_action, _context| -> Result, gix_error::Exn> { + |_action, _context| -> ExnResult> { called = true; Ok(None) }, ); let err = result.expect_err("incomplete URL must fail validation"); + error_snapshots.push(gix_testtools::redact_debug_snapshot(&(err), &[])); assert!(err.is_validation()); assert!(!called, "the context is lacking, hence nothing gets called"); } + insta::assert_debug_snapshot!(error_snapshots, "missing protocol with only host or protocol fails", @" + [ + Either 'url' field or both 'protocol' and 'host' fields must be provided, + Either 'url' field or both 'protocol' and 'host' fields must be provided, + ] + "); } #[test] @@ -104,7 +118,7 @@ fn url_alone_is_valid() { Cursor::new(input), &mut output, gix_credentials::protocol::ContextOptions::default(), - |_action, context| -> Result, gix_error::Exn> { + |_action, context| -> ExnResult> { called = true; assert_eq!(context.url.unwrap(), "https://github.com"); assert_eq!(context.host, None, "not auto-populated"); @@ -117,6 +131,7 @@ fn url_alone_is_valid() { // This should fail because our mock helper returned None (no credentials found) // but it should NOT fail because of missing URL let err = result.expect_err("missing credentials must fail"); + insta::assert_debug_snapshot!(err, "url alone is valid", @r#"Credentials for "https://github.com" could not be obtained"#); assert!(err.is_not_found()); assert!(called); } diff --git a/gix-credentials/tests/protocol/context.rs b/gix-credentials/tests/protocol/context.rs index 8a021f1ef6b..ad76fc7ecee 100644 --- a/gix-credentials/tests/protocol/context.rs +++ b/gix-credentials/tests/protocol/context.rs @@ -1,4 +1,5 @@ mod destructure_url_in_place { + use crate::Result; use gix_credentials::protocol::Context; fn url_ctx(url: &str) -> Context { @@ -38,7 +39,7 @@ mod destructure_url_in_place { } #[test] - fn passwords_are_placed_in_context_too() -> crate::Result { + fn passwords_are_placed_in_context_too() -> Result { let mut ctx = url_ctx("http://user:password@host/path"); ctx.destructure_url_in_place(false)?; assert_eq!(ctx.password.as_deref(), Some("password")); @@ -108,10 +109,7 @@ mod destructure_url_in_place { host: Some("github.com".into()), ..Default::default() }; - assert_eq!( - ctx_no_protocol.destructure_url_in_place(false).unwrap_err().to_string(), - "Either 'url' field or both 'protocol' and 'host' fields must be provided" - ); + insta::assert_debug_snapshot!(ctx_no_protocol.destructure_url_in_place(false).expect_err("missing protocol or host without url fails"), "missing protocol or host without url fails", @"Either 'url' field or both 'protocol' and 'host' fields must be provided"); let mut ctx_no_host = Context { protocol: Some("https".into()), diff --git a/gix-date/Cargo.toml b/gix-date/Cargo.toml index 27611d0aa3e..383cbc147c6 100644 --- a/gix-date/Cargo.toml +++ b/gix-date/Cargo.toml @@ -28,6 +28,7 @@ jiff = "0.2.25" document-features = { version = "0.2.0", optional = true } [dev-dependencies] +insta = "1.46.3" gix-hash = { path = "../gix-hash" } gix-testtools = { path = "../tests/tools", default-features = false, features = ["sha1"] } pretty_assertions = "1.4.1" diff --git a/gix-date/src/parse/function.rs b/gix-date/src/parse/function.rs index c4dd8a56f3d..54dac5c32ee 100644 --- a/gix-date/src/parse/function.rs +++ b/gix-date/src/parse/function.rs @@ -9,13 +9,14 @@ use crate::{ parse::relative, time::format::{DEFAULT, GITOXIDE, ISO8601, ISO8601_STRICT, SHORT}, }; -use gix_error::{Exn, ResultExt}; +use gix_error::{ExnMessageResult, ResultExt}; /// The widest timezone offset git reads, as `match_tz()` in `date.c` takes the four digits as a /// clock time: hours below 24 and minutes below 60, so `+2359` is the last offset it accepts. const MAX_OFFSET_IN_SECONDS: i32 = 23 * 3600 + 59 * 60; /// Parse `input` as any time that Git can parse when inputting a date. +/// Unknown formats and timezone conversion failures include `input` bytes as [metadata](gix_error::Exn::metadata()). /// /// ## Examples /// @@ -101,7 +102,7 @@ const MAX_OFFSET_IN_SECONDS: i32 = 23 * 3600 + 59 * 60; /// /// In any of these formats, a timezone offset wider than `±23:59` is not a timezone to Git, so it /// is not accepted here either. -pub fn parse(input: &str, now: Option) -> Result> { +pub fn parse(input: &str, now: Option) -> ExnMessageResult