diff --git a/Cargo.lock b/Cargo.lock index 2f744a169a8..5555ac6ca99 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1523,7 +1523,6 @@ dependencies = [ "smallvec", "sysinfo", "tempfile", - "thiserror 2.0.18", "tracing", "tracing-forest 0.2.0", "tracing-subscriber", @@ -1657,7 +1656,6 @@ dependencies = [ "gix-trace", "serde", "smallvec", - "thiserror 2.0.18", "unicode-bom", ] @@ -1691,7 +1689,6 @@ dependencies = [ "gix-worktree", "pretty_assertions", "smallvec", - "thiserror 2.0.18", ] [[package]] @@ -1751,7 +1748,6 @@ dependencies = [ "serde", "serial_test", "smallvec", - "thiserror 2.0.18", "unicode-bom", ] @@ -1765,7 +1761,6 @@ dependencies = [ "gix-path", "libc", "serde", - "thiserror 2.0.18", ] [[package]] @@ -1785,7 +1780,6 @@ dependencies = [ "gix-trace", "gix-url", "serde", - "thiserror 2.0.18", ] [[package]] @@ -1831,7 +1825,6 @@ dependencies = [ "pretty_assertions", "serde", "shell-words", - "thiserror 2.0.18", ] [[package]] @@ -1851,7 +1844,6 @@ dependencies = [ "gix-utils", "gix-worktree", "pretty_assertions", - "thiserror 2.0.18", ] [[package]] @@ -1869,7 +1861,6 @@ dependencies = [ "is_ci", "serial_test", "tempfile", - "thiserror 2.0.18", ] [[package]] @@ -1925,7 +1916,6 @@ dependencies = [ "gix-utils", "gix-worktree", "smallvec", - "thiserror 2.0.18", ] [[package]] @@ -1940,7 +1930,6 @@ dependencies = [ "is_ci", "serde", "tempfile", - "thiserror 2.0.18", ] [[package]] @@ -1979,7 +1968,6 @@ dependencies = [ "serde", "sha1-checked", "sha2", - "thiserror 2.0.18", ] [[package]] @@ -2050,7 +2038,6 @@ dependencies = [ "rustix", "serde", "smallvec", - "thiserror 2.0.18", ] [[package]] @@ -2064,7 +2051,6 @@ dependencies = [ "gix-tempfile", "gix-utils", "tempfile", - "thiserror 2.0.18", ] [[package]] @@ -2119,7 +2105,6 @@ dependencies = [ "pretty_assertions", "serde", "termtree", - "thiserror 2.0.18", ] [[package]] @@ -2163,7 +2148,6 @@ dependencies = [ "serde", "smallvec", "termtree", - "thiserror 2.0.18", ] [[package]] @@ -2193,7 +2177,6 @@ dependencies = [ "pretty_assertions", "serde", "tempfile", - "thiserror 2.0.18", ] [[package]] @@ -2224,7 +2207,6 @@ dependencies = [ "parking_lot", "serde", "smallvec", - "thiserror 2.0.18", "uluru", ] @@ -2245,7 +2227,6 @@ dependencies = [ "gix-trace", "pin-project-lite", "serde", - "thiserror 2.0.18", ] [[package]] @@ -2257,7 +2238,6 @@ dependencies = [ "gix-trace", "gix-validate", "serial_test", - "thiserror 2.0.18", "windows 0.62.2", "winreg", ] @@ -2274,7 +2254,6 @@ dependencies = [ "gix-path", "gix-testtools", "serial_test", - "thiserror 2.0.18", ] [[package]] @@ -2288,7 +2267,6 @@ dependencies = [ "parking_lot", "rustix", "serial_test", - "thiserror 2.0.18", ] [[package]] @@ -2320,7 +2298,6 @@ dependencies = [ "gix-utils", "nonempty", "serde", - "thiserror 2.0.18", ] [[package]] @@ -2359,7 +2336,6 @@ dependencies = [ "libc", "memmap2", "serde", - "thiserror 2.0.18", ] [[package]] @@ -2375,7 +2351,6 @@ dependencies = [ "gix-validate", "insta", "smallvec", - "thiserror 2.0.18", ] [[package]] @@ -2413,7 +2388,6 @@ dependencies = [ "gix-object", "gix-testtools", "smallvec", - "thiserror 2.0.18", ] [[package]] @@ -2443,7 +2417,6 @@ dependencies = [ "nonempty", "serde", "tempfile", - "thiserror 2.0.18", ] [[package]] @@ -2470,7 +2443,6 @@ dependencies = [ "hashbrown 0.16.1", "portable-atomic", "pretty_assertions", - "thiserror 2.0.18", "windows-sys 0.61.2", ] @@ -2486,7 +2458,6 @@ dependencies = [ "gix-refspec", "gix-testtools", "gix-url", - "thiserror 2.0.18", ] [[package]] @@ -2575,7 +2546,6 @@ dependencies = [ "pin-project-lite", "reqwest", "serde", - "thiserror 2.0.18", ] [[package]] @@ -2594,7 +2564,6 @@ dependencies = [ "gix-testtools", "insta", "smallvec", - "thiserror 2.0.18", ] [[package]] @@ -2613,7 +2582,6 @@ dependencies = [ "gix-utils", "percent-encoding", "serde", - "thiserror 2.0.18", ] [[package]] @@ -2675,7 +2643,6 @@ dependencies = [ "gix-worktree", "gix-worktree-state", "io-close", - "thiserror 2.0.18", "walkdir", ] @@ -2705,7 +2672,6 @@ version = "0.1.0" dependencies = [ "bstr", "serde", - "thiserror 2.0.18", "zlib-rs", ] diff --git a/etc/plan/gix-error.md b/etc/plan/gix-error.md index 59344f6b0bb..05358b650c2 100644 --- a/etc/plan/gix-error.md +++ b/etc/plan/gix-error.md @@ -22,16 +22,16 @@ Finish the migration from `thiserror`-based error enums to `gix-error` / `Exn`, - [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`. +- [ ] Make `cargo nextest run --workspace` complete without `--exclude gix-error`. + Evidence: `.github/workflows/ci.yml` still excludes `gix-error`, in three places (`ci.yml:304,364,450`). The adjacent comment claims `gix-error` "is tested individually," but no dedicated job for it was found in any `.github/workflows/*.yml` file at this commit — worth confirming with Byron whether the exclusion is a migration artifact or a deliberate, permanent split. - [ ] Replace `thiserror` with `gix-error` everywhere. - Evidence: 33 crates in this branch still carry a `thiserror` dependency and/or `thiserror::Error` usage. + Evidence: no longer the actual target. Only `gix` still depends on `thiserror` (42 `thiserror::Error` derives across 27 files); of the other crates, 14 adopt `gix-error`'s types in their own public error surface (`gix-date` most completely — its entire public error is `pub use gix_error::ValidationError as Error;`, `gix-date/src/lib.rs:36`), while the plumbing crates covered by the maintainer's revert kept hand-written concrete `Display`/`Error` impls with no `gix-error` dependency at all. (A raw `gix-error` dependency-key count across the workspace runs to 16; that also counts `gix` itself and `gix-error`'s own self-referential dev-dependency, neither of which is an adopter.) See "Migration Rules" for the three-tier strategy this reflects. - [x] Keep `NotARepository` distinct from generic open failures. - Evidence: `gix::open::Error::NotARepository` exists and is asserted in tests. + Evidence: `gix::open::Error::NotARepository` exists (`gix/src/open/mod.rs`) and is constructed in `gix/src/open/repository.rs`. - [ ] 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. + Evidence: only 3 files workspace-wide use `gix_error::Error` under a `tests/` path. Not moot: `Exn` is not rare in `gix` — it appears 33 times across 7 files, concentrated in the revision-spec parsing delegate layer (see "Migration Rules" for the breakdown) — so real `Exn`-heavy production surface exists; simplifying test paths this way remains open. +- [x] Make validation failures identifiable as `gix_error::ValidationError` in crates that adopt `gix-error`. + Evidence: `gix-error` exports `ValidationError`; adopted directly by `gix-actor`, `gix-bitmap`, `gix-chunk`, `gix-date`, `gix-mailmap`, `gix-pack`, `gix-quote` and `gix-revision`, plus used at the `gix` boundary via `or_raise`/`message`. Note `gix-validate` itself is a plumbing crate with hand-written concrete errors (no `gix-error` dependency) — its own failures propagate as their own concrete types (e.g. `gix_validate::reference::name::Error`, re-exported verbatim by `gix-ref`), not literally as `ValidationError`. ## Current Snapshot @@ -39,11 +39,14 @@ Workspace scan basis: - `thiserror` dependency present in `Cargo.toml` - `thiserror::Error` mentions under `src/**/*.rs` +- 68 top-level workspace member crates (the 70 entries in root `Cargo.toml`'s `members`, minus the two nested harness crates `tests/tools` and `tests/it`) -Result on 2026-04-22: +Result on 2026-07-27, on the `gix-error-batch1` branch: -- 32 crates are done -- 33 crates are still pending +- 67 crates are done +- 1 crate is still pending: `gix` — 42 `thiserror::Error` derives across 27 files, every one carrying a `TODO(review)` note explaining why it stays concrete (see "Migration Rules") + +Diff size, measured against this branch's merge-base (the "Merge pull request #2738 from GitoxideLabs/improvements" merge, 2026-07-22): the branch adds roughly 6,000 lines net — the line count went up, not down. Restricted to `gix/src` and `gitoxide-core/src`, the two directories the maintainer's "redeemed on the caller side" question is actually about, it is net −318. The growth is concentrated in the plumbing crates, where dropping `thiserror` meant hand-writing the `Display` and `Error` impls it used to generate: `gix-pack` +770, `gix-ref` +669, `gix-filter` +494, `gix-odb` +435, `gix-config` +412 and `gix-protocol` +406 net. The 42 `TODO(review)` notes add roughly 260 comment lines to `gix/src`, about 4% of the branch total — moving them into this file would take the caller-side figure to about −580 but would not materially change the branch as a whole. ## Linked Upstream PRs @@ -59,83 +62,110 @@ 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. -- 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. +**Decision, acted on 2026-07-22:** the maintainer overruled the original "erase to `Exn` everywhere" approach, in review feedback on [GitoxideLabs/gitoxide#2716](https://github.com/GitoxideLabs/gitoxide/pull/2716) — summarized: in the plumbing crates, keep the original expanded, hand-implemented error types for now instead of bringing in `gix-error::Exn`, and focus the erasure effort on `gix` itself and its usage of `gix::Error` with direct error forwarding. + +Acting on that, `gix-fs`, `gix-attributes`, `gix-pathspec`, `gix-lock`, `gix-shallow`, `gix-prompt`, `gix-url` and `gix-path` had their `Exn` conversions reverted back to hand-written error types (the `revert!: keep the plumbing crates' error types concrete` commit, 2026-07-22). The migration is now three-tier: + +- **Plumbing crates** — drop `thiserror`, keep concrete enums with hand-written `Display`/`Error` impls. No `gix-error` dependency at all. +- **`gix-error` adopters** — take a `gix-error` dependency directly and use its types (e.g. `gix_error::ValidationError`) in their own public error surface, short of the full erasure below. 14 crates do this (`gix-date` most completely — its entire public error is `pub use gix_error::ValidationError as Error;`, `gix-date/src/lib.rs:36`): `gix-actor`, `gix-archive`, `gix-bitmap`, `gix-blame`, `gix-chunk`, `gix-commitgraph`, `gix-date`, `gix-mailmap`, `gix-pack`, `gix-quote`, `gix-refspec`, `gix-revision`, `gix-revwalk` and `gix-worktree-stream`. (A raw dependency-key count of `gix-error` across the workspace runs to 16 — the other two are `gix-error` itself, which defines these types rather than adopting them, and `gix`, which anchors the boundary tier next.) +- **The `gix` boundary** — erase to `pub type Error = gix_error::Error;` where callers don't need to match variants. `Exn` itself is not rare in `gix` — it appears 33 times across 7 files (`lib.rs`, `repository/reference.rs`, `config/tree/keys.rs`, `revision/spec/parse/mod.rs`, and `revision/spec/parse/delegate/{mod,navigate,revision}.rs`), with the revision-spec delegate layer alone holding roughly 14 function signatures returning `Result<_, Exn>` — core parsing plumbing, not config/test downcasting. + +Rules: + +- In plumbing crates: remove `thiserror` from `Cargo.toml`; replace `#[derive(thiserror::Error)]` enums with hand-written `Display` + `std::error::Error` impls on the same concrete enum shape. Do not add a `gix-error` dependency. +- In `gix`: replace `#[derive(thiserror::Error)]` types with `pub type Error = gix_error::Error;`, and convert call sites with `.map_err(gix_error::Error::from_error)`, `.or_raise(...)` or `.ok_or_raise(...)` — unless the type is blocked (see below). +- Convert validation/parsing-only paths to `gix_error::ValidationError` where a crate does adopt `gix-error` (unaffected by the plumbing-crate reversal above). - When migrating a crate, run its local checks and at least one downstream compile pass. +### Why a type stays concrete in `gix` + +All 42 types still concrete in `gix` (2026-07-27) are covered by four buckets, each recorded in that type's own `TODO(review)` comment; most cite exactly one, but a type can be blocked more than one independent way at once: + +1. **Callers match variants.** Code matches on variants or reads fields directly; erasing breaks the call site. +2. **E0119, spent slot.** A parent enum already embeds a different erased type via one `#[from]`; erasing this type too would give the parent a second `From` impl. Order-dependent, not permanent — erasing the *hub* enum that's holding the slot deletes it and frees everything it was pinning, so a blocker here can evaporate later in the campaign. +3. **Generic.** The type carries a type parameter (e.g. a caller-supplied source error `E`) that a `pub type` alias can't carry. +4. **E0117, orphan rule.** A local `impl for Error` (e.g. `gix_transport::IsSpuriousError`, re-exported through `gix_protocol::transport`) becomes foreign-trait-on-foreign-type once `Error` is an alias to the equally-foreign `gix_error::Error`. + +Thirteen of the 42 cite more than one bucket, and the reasons stack rather than replace each other. Five cite three: `gix/src/remote/connect.rs:16`, `gix/src/remote/connection/ref_map.rs:12`, `gix/src/remote/connection/fetch/error.rs:3` and `gix/src/remote/connection/fetch/mod.rs:104` all open "kept concrete, blocked three independent ways" over buckets 4, 2 and 1; `gix/src/env.rs:54` (`env::collate::fetch::Error`) makes the same three-way case in prose rather than a numbered list, over buckets 3, 4 and 1. Eight more cite two, each signaled by a `Separately, …` clause except one argued in two unconnected sentences: `gix/src/config/mod.rs:62` and `gix/src/config/mod.rs:268` are two different types in the same file, over buckets 1+2 and 3+4 respectively; `gix/src/open/mod.rs:43`, `gix/src/reference/errors.rs:95`, `gix/src/remote/errors.rs:10` and `gix/src/status/iter/mod.rs:240` each cite buckets 1+2, as do `gix/src/submodule/errors.rs:27` and `gix/src/submodule/errors.rs:74`, two different types in the same file. The remaining 29 cite exactly one. + +### The double-wrap trap + +`.map_err(gix_error::Error::from_error)?` (equally `.or_raise(...)` / `.ok_or_raise(...)`) applied to a callee that *already* returns `gix_error::Error` nests one erased error inside another. It compiles and passes the full test suite — the compiler can't see it structurally, and tests don't catch it either, since the nested error still renders and downcasts fine one level down. Eight such sites are recorded fixed on this branch, in the `fix: collapse double-wrapped erased errors and correct a stale note` commit (`status::iter`; three in `repository::merge`; `repository::blame`; `commit`; `pathspec`; `filter`) — three of them inside `gix/src/repository/merge.rs`, which a bare `cargo check -p gix` does not compile at all, since `merge` is not a default feature (verified: absent from `default`, `basic`, `extras` and `comfort`; only pulled in by the non-default `need-more-recent-msrv` bundle). The campaign-wide count may be higher; eight is what this branch evidences. + +Detection method: for each `from_error` / `or_raise` / `ok_or_raise` call site, resolve the callee's return type through its alias chain and check whether it's already `gix_error::Error`. A plain grep won't find this — the call site reads identically whether the callee's error is concrete or already erased. Watch the feature-gating blind spot specifically: any module behind a non-default feature is invisible to a bare `cargo check -p gix` — but not to `cargo check --workspace`, since `gitoxide-core` depends on `gix` non-optionally with `features = ["merge", ...]` (`gitoxide-core/Cargo.toml:52`), so workspace-wide checks and tests compile it via feature unification. + ## 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-hash` +- [x] `gix-url` +- [x] `gix-packetline` +- [x] `gix-features` (already converted when its zlib module moved to `gix-zlib`) +- [x] `gix-path` +- [x] `gix-attributes` - [x] `gix-quote` -- [ ] `gix-lock` - 1 -- [x] `gix-fs` +- [x] `gix-lock` +- [x] `gix-fs` (`thiserror` removed; kept concrete per the 2026-07-22 plumbing-crate decision — see "Migration Rules") - [x] `gix-bitmap` - [x] `gix-mailmap` +- [x] `gix-zlib` (not originally listed; extracted from `gix-features` after this plan was written) ### Batch 2: simple dependents -- [ ] `gix-object` - 11 -- [ ] `gix-config-value` - 2 -- [ ] `gix-shallow` - 2 -- [ ] `gix-refspec` - 1 +- [x] `gix-object` +- [x] `gix-config-value` +- [x] `gix-shallow` +- [x] `gix-refspec` ### Batch 3: ref / filter layer -- [ ] `gix-ref` - 22 -- [ ] `gix-filter` - 18 -- [ ] `gix-revwalk` - 4 -- [ ] `gix-pathspec` - 3 -- [ ] `gix-prompt` - 1 +- [x] `gix-ref` +- [x] `gix-filter` +- [x] `gix-revwalk` +- [x] `gix-pathspec` +- [x] `gix-prompt` ### Batch 4: config and discovery -- [ ] `gix-traverse` - 3 -- [ ] `gix-config` - 11 -- [ ] `gix-credentials` - 5 -- [ ] `gix-discover` - 4 +- [x] `gix-traverse` +- [x] `gix-config` (`thiserror` removed; hand-written concrete errors, no `gix-error` dependency) +- [x] `gix-credentials` +- [x] `gix-discover` ### Batch 5: transport and index-adjacent -- [ ] `gix-index` - 11 -- [ ] `gix-transport` - 10 +- [x] `gix-index` +- [x] `gix-transport` - [x] `gix-worktree-stream` -- [ ] `gix-submodule` - 6 +- [x] `gix-submodule` (`thiserror` removed; hand-written concrete errors, no `gix-error` dependency) ### Batch 6: diff / protocol tier -- [ ] `gix-diff` - 8 -- [ ] `gix-protocol` - 8 -- [ ] `gix-dir` - 1 -- [ ] `gix-worktree-state` - 1 +- [x] `gix-diff` +- [x] `gix-protocol` (`thiserror` removed; hand-written concrete errors, no `gix-error` dependency) +- [x] `gix-dir` +- [x] `gix-worktree-state` - [x] `gix-archive` ### Batch 7: heavier consumers -- [ ] `gix-pack` - 23 -- [ ] `gix-merge` - 8 -- [ ] `gix-status` - 3 -- [ ] `gix-blame` - 1 +- [x] `gix-pack` +- [x] `gix-merge` +- [x] `gix-status` +- [x] `gix-blame` ### Batch 8: object database -- [ ] `gix-odb` - 11 +- [x] `gix-odb` ### Batch 9: top-level API -- [ ] `gix` - 138 +- [ ] `gix` — 42 `thiserror::Error` derives across 27 files remain, all documented (`TODO(review)`) against the four blockers in "Migration Rules" ## Already Done Outside The Active Queue +- [x] `gitoxide-core` (no `thiserror`; carries a configuration-only `gix-error` dependency to pin feature resolution workspace-wide, but no actual error-handling usage — was never tracked anywhere in this file until now) - [x] `gix-actor` - [x] `gix-chunk` - [x] `gix-command` @@ -147,6 +177,7 @@ Result on 2026-04-22: - [x] `gix-glob` - [x] `gix-hashtable` - [x] `gix-ignore` +- [x] `gix-imara-diff` (no `thiserror`, no `gix-error` — was never tracked anywhere in this file until now) - [x] `gix-lfs` - [x] `gix-macros` - [x] `gix-negotiate` @@ -165,15 +196,21 @@ Result on 2026-04-22: ## 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. +- [ ] Convert the remaining 42 `thiserror::Error` types in `gix` to `pub type Error = gix_error::Error;` wherever none of the four blockers apply — re-check after each hub-enum erasure, since freeing an E0119 slot can unblock types that looked permanently stuck. +- [ ] For each blocked type, decide case-by-case whether the blocker is worth engineering around (e.g. restructuring a hub enum to free its one `From` slot) or should stay concrete for good; record the call in its `TODO(review)` note. +- [ ] After every new erasure or `.or_raise`/`.ok_or_raise`/`from_error` call, check the callee isn't already returning `gix_error::Error` — see "The double-wrap trap." Remember non-default features (`merge`, and worth auditing similarly) are invisible to a bare `cargo check -p gix`. +- [ ] Confirm with Byron whether `.github/workflows/ci.yml`'s `--exclude gix-error` is a migration artifact or a deliberate permanent split — the adjacent comment claims individual testing that no workflow file in this repo currently shows. +- [ ] Once `gix` no longer depends on `thiserror`, drop it from `gix/Cargo.toml` and close out "Exit Criteria." ## Exit Criteria - [ ] No crate in this workspace depends on `thiserror`. + Still open: `gix` does (`gix/Cargo.toml:400`). - [ ] 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. + Still open: 42 derives across 27 files in `gix/src`. +- [ ] `cargo nextest run --workspace` no longer excludes `gix-error`. + Still open: three `--exclude gix-error` invocations remain in `.github/workflows/ci.yml` as of 2026-07-27, on the `gix-error-batch1` branch. +- [x] The `gix` boundary still returns `gix_error::Error` where type erasure is desired. + 101 `pub type Error = gix_error::Error;` aliases in `gix/src` at this commit. +- [x] Validation-heavy crates still expose typed validation failures where callers need them. + Holds under the three-tier strategy: plumbing crates (e.g. `gix-validate`) keep their own typed concrete errors; crates that do adopt `gix-error` use `gix_error::ValidationError` for validation-only paths. diff --git a/gitoxide-core/Cargo.toml b/gitoxide-core/Cargo.toml index f367ceffd39..fafc43b2812 100644 --- a/gitoxide-core/Cargo.toml +++ b/gitoxide-core/Cargo.toml @@ -58,7 +58,6 @@ gix-fsck = { version = "^0.24.0", path = "../gix-fsck" } gix-error-for-configuration-only = { package = "gix-error", version = "^0.2.5", 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 631db5376dc..4e472790585 100644 --- a/gitoxide-core/src/pack/create.rs +++ b/gitoxide-core/src/pack/create.rs @@ -360,14 +360,5 @@ struct Statistics { } pub mod input_iteration { - use gix::{hash, traverse}; - #[derive(Debug, thiserror::Error)] - pub enum Error { - #[error("input objects couldn't be iterated completely")] - Iteration(#[from] traverse::commit::simple::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), - } + pub type Error = gix::Error; } diff --git a/gitoxide-core/src/pack/explode.rs b/gitoxide-core/src/pack/explode.rs index 878f000c3b5..31e4c3f4f24 100644 --- a/gitoxide-core/src/pack/explode.rs +++ b/gitoxide-core/src/pack/explode.rs @@ -8,8 +8,9 @@ use std::{ use anyhow::{Result, anyhow}; use gix::{ NestedProgress, + error::{ErrorExt, OptionExt, ResultExt}, hash::ObjectId, - object, objs, odb, + object, odb, odb::{loose, pack}, prelude::Write, }; @@ -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(#[from] objs::data::verify::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" @@ -242,11 +217,12 @@ 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, - kind: object_kind, - id: index_entry.oid, - })?; + let written_id = out + .write_buf(object_kind, buf) + .map_err(std::io::Error::other) + .or_raise(|| { + gix::error::message!("Failed to write {kind} object {id}", kind = object_kind, id = index_entry.oid) + })?; if let Err(err) = written_id.verify(&index_entry.oid) { if let object::Kind::Tree = object_kind { progress.info(format!( @@ -254,21 +230,31 @@ 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(gix::error::message!( + "{kind} object wasn't re-encoded without change", + kind = object_kind + )) + .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, + .or_raise(|| { + gix::error::message!( + "The recently written file for loose object {id} cold not be read", + id = written_id + ) })? - .ok_or(Error::WrittenFileMissing { id: written_id })?; - obj.verify_checksum(&written_id)?; + .ok_or_raise(|| { + gix::error::message!( + "The recently written file for loose object {id} could not be found", + id = written_id + ) + })?; + obj.verify_checksum(&written_id) + .or_raise(|| gix::error::message("Object didn't verify after right after writing it"))?; } Ok(()) } diff --git a/gitoxide-core/src/repository/credential.rs b/gitoxide-core/src/repository/credential.rs index 26a9c2608f1..3291d43fbba 100644 --- a/gitoxide-core/src/repository/credential.rs +++ b/gitoxide-core/src/repository/credential.rs @@ -1,15 +1,3 @@ -#[derive(Debug, thiserror::Error)] -enum Error { - #[error(transparent)] - UrlParse(#[from] gix::url::parse::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::*; gix::credentials::program::main( @@ -17,26 +5,30 @@ pub fn function(repo: Option, action: gix::credentials::program std::io::stdin(), std::io::stdout(), gix::credentials::protocol::ContextOptions::default(), - |action, context| -> Result<_, Error> { + |action, context| -> Result<_, gix::Error> { let url = context .url .clone() .or_else(|| context.to_url()) - .ok_or(Error::Protocol(gix::credentials::protocol::Error::UrlMissing))?; + .ok_or(gix::Error::from_error(gix::credentials::protocol::Error::UrlMissing))?; let (mut cascade, _action, prompt_options) = match repo { - Some(ref repo) => repo.config_snapshot().credential_helpers(gix::url::parse(&url)?)?, + Some(ref repo) => repo + .config_snapshot() + .credential_helpers(gix::url::parse(&url).map_err(gix::Error::from_error)?) + .map_err(gix::Error::from_error)?, None => { - let config = gix::config::File::from_globals()?; + let config = gix::config::File::from_globals().map_err(gix::Error::from_error)?; let environment = gix::open::permissions::Environment::all(); gix::config::credential_helpers( - gix::url::parse(&url)?, + gix::url::parse(&url).map_err(gix::Error::from_error)?, &config, false, /* lenient config */ |_| true, /* section filter */ environment, false, /* use http path (override, uses configuration now)*/ - )? + ) + .map_err(gix::Error::from_error)? } }; cascade @@ -49,7 +41,7 @@ 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(gix::Error::from_error) }, ) .map_err(Into::into) diff --git a/gix-attributes/Cargo.toml b/gix-attributes/Cargo.toml index 35a92ca4454..ba8684c37f4 100644 --- a/gix-attributes/Cargo.toml +++ b/gix-attributes/Cargo.toml @@ -35,7 +35,6 @@ gix-trace = { version = "^0.1.21", path = "../gix-trace" } bstr = { version = "1.12.0", default-features = false, features = ["std", "unicode"] } smallvec = "1.15.1" unicode-bom = { version = "2.0.3" } -thiserror = "2.0.18" serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"] } document-features = { version = "0.2.1", optional = true } diff --git a/gix-attributes/src/name.rs b/gix-attributes/src/name.rs index f1cde9cf43a..3e706a176a2 100644 --- a/gix-attributes/src/name.rs +++ b/gix-attributes/src/name.rs @@ -92,9 +92,20 @@ impl<'de> serde::Deserialize<'de> for Name { } /// The error returned by [`parse::Iter`][crate::parse::Iter]. -#[derive(Debug, thiserror::Error)] -#[error("Attribute has non-ascii characters or starts with '-': {attribute}")] +#[derive(Debug)] pub struct Error { /// The attribute that failed to parse. pub attribute: BString, } + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "Attribute has non-ascii characters or starts with '-': {}", + self.attribute + ) + } +} + +impl std::error::Error for Error {} diff --git a/gix-attributes/src/parse.rs b/gix-attributes/src/parse.rs index cce9680f1c9..e6391b755a5 100644 --- a/gix-attributes/src/parse.rs +++ b/gix-attributes/src/parse.rs @@ -17,17 +17,52 @@ pub enum Kind { mod error { use bstr::BString; /// The error returned by [`parse::Lines`][crate::parse::Lines]. - #[derive(thiserror::Error, Debug)] + #[derive(Debug)] #[expect(missing_docs)] pub enum Error { - #[error(r"Line {line_number} has a negative pattern, for literal characters use \!: {line}")] PatternNegation { line_number: usize, line: BString }, - #[error("Attribute in line {line_number} has non-ascii characters or starts with '-': {attribute}")] AttributeName { line_number: usize, attribute: BString }, - #[error("Macro in line {line_number} has non-ascii characters or starts with '-': {macro_name}")] MacroName { line_number: usize, macro_name: BString }, - #[error("Could not unquote attributes line")] - Unquote(#[from] gix_quote::ansi_c::undo::Error), + Unquote(gix_quote::ansi_c::undo::Error), + } + + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::PatternNegation { line_number, line } => write!( + f, + r"Line {line_number} has a negative pattern, for literal characters use \!: {line}" + ), + Error::AttributeName { line_number, attribute } => write!( + f, + "Attribute in line {line_number} has non-ascii characters or starts with '-': {attribute}" + ), + Error::MacroName { + line_number, + macro_name, + } => write!( + f, + "Macro in line {line_number} has non-ascii characters or starts with '-': {macro_name}" + ), + Error::Unquote(_) => f.write_str("Could not unquote attributes line"), + } + } + } + + impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::PatternNegation { .. } | Error::AttributeName { .. } | Error::MacroName { .. } => None, + // `Exn` does not implement `std::error::Error` itself, so expose the error it carries. + Error::Unquote(err) => Some(&**err), + } + } + } + + impl From for Error { + fn from(err: gix_quote::ansi_c::undo::Error) -> Self { + Error::Unquote(err) + } } } pub use error::Error; diff --git a/gix-blame/Cargo.toml b/gix-blame/Cargo.toml index 7a03b3ed3cf..82ffcb51a36 100644 --- a/gix-blame/Cargo.toml +++ b/gix-blame/Cargo.toml @@ -30,7 +30,6 @@ gix-worktree = { version = "^0.55.0", path = "../gix-worktree", default-features gix-traverse = { version = "^0.60.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 9cedec7b600..71ed0525fd4 100644 --- a/gix-blame/src/error.rs +++ b/gix-blame/src/error.rs @@ -1,40 +1,128 @@ use gix_object::bstr::BString; /// The error returned by [file()](crate::file()). -#[derive(Debug, thiserror::Error)] +// TODO(review): the `BlobDiff*`/`DiffTree*` variants hand-preserve `#[error(transparent)]` +// semantics (Display and `source()` pass through to the inner error), and +// `source()` for the `Box`-carrying `FindObject`/`Traverse` variants +// yields the boxed error via `&**err` — matching the previous thiserror output. +#[derive(Debug)] #[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}")] + BlobDiffSetResource(gix_diff::blob::platform::set_resource::Error), + BlobDiffPrepare(gix_diff::blob::platform::prepare_diff::Error), 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(#[from] gix_object::find::existing_object::Error), - #[error("Could not find existing iterator over a tree")] - FindExistingIter(#[from] gix_object::find::existing_iter::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 ','" - )] + FindObject(gix_object::find::Error), + FindExistingObject(gix_object::find::existing_object::Error), + FindExistingIter(gix_object::find::existing_iter::Error), + Traverse(Box), + DiffTree(gix_diff::tree::Error), + DiffTreeWithRewrites(gix_diff::tree_with_rewrites::Error), 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), + DecodeCommit(gix_object::decode::Error), + GetParentFromCommitGraph(gix_error::Message), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::EmptyTraversal => f.write_str("No commit was given"), + Error::BlobDiffSetResource(err) => std::fmt::Display::fmt(err, f), + Error::BlobDiffPrepare(err) => std::fmt::Display::fmt(err, f), + Error::FileMissing { file_path, commit_id } => write!( + f, + "The file to blame at '{file_path}' wasn't found in the first commit at {commit_id}" + ), + Error::FindObject(_) => f.write_str("Couldn't find commit or tree in the object database"), + Error::FindExistingObject(_) => f.write_str("Could not find existing blob or commit"), + Error::FindExistingIter(_) => f.write_str("Could not find existing iterator over a tree"), + Error::Traverse(_) => f.write_str("Failed to obtain the next commit in the commit-graph traversal"), + Error::DiffTree(err) => std::fmt::Display::fmt(err, f), + Error::DiffTreeWithRewrites(err) => std::fmt::Display::fmt(err, f), + Error::InvalidOneBasedLineRange => f.write_str( + "Invalid line range was given, line range is expected to be a 1-based inclusive range in the format ','" + ), + Error::DecodeCommit(_) => f.write_str("Failure to decode commit during traversal"), + Error::GetParentFromCommitGraph(_) => { + f.write_str("Failed to get parent from commitgraph during traversal") + } + } + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::EmptyTraversal | Error::FileMissing { .. } | Error::InvalidOneBasedLineRange => None, + Error::BlobDiffSetResource(err) => err.source(), + Error::BlobDiffPrepare(err) => err.source(), + Error::FindObject(err) => Some(&**err), + Error::FindExistingObject(err) => Some(err), + Error::FindExistingIter(err) => Some(err), + Error::Traverse(err) => Some(&**err), + Error::DiffTree(err) => err.source(), + Error::DiffTreeWithRewrites(err) => err.source(), + Error::DecodeCommit(err) => Some(err), + Error::GetParentFromCommitGraph(err) => Some(err), + } + } +} + +impl From for Error { + fn from(err: gix_diff::blob::platform::set_resource::Error) -> Self { + Error::BlobDiffSetResource(err) + } +} + +impl From for Error { + fn from(err: gix_diff::blob::platform::prepare_diff::Error) -> Self { + Error::BlobDiffPrepare(err) + } +} + +impl From for Error { + fn from(err: gix_object::find::Error) -> Self { + Error::FindObject(err) + } +} + +impl From for Error { + fn from(err: gix_object::find::existing_object::Error) -> Self { + Error::FindExistingObject(err) + } +} + +impl From for Error { + fn from(err: gix_object::find::existing_iter::Error) -> Self { + Error::FindExistingIter(err) + } +} + +impl From for Error { + fn from(err: gix_diff::tree::Error) -> Self { + Error::DiffTree(err) + } +} + +impl From for Error { + fn from(err: gix_diff::tree_with_rewrites::Error) -> Self { + Error::DiffTreeWithRewrites(err) + } +} + +impl From for Error { + fn from(err: gix_object::decode::Error) -> Self { + Error::DecodeCommit(err) + } +} + +impl From for Error { + fn from(err: gix_error::Message) -> Self { + Error::GetParentFromCommitGraph(err) + } } diff --git a/gix-config-value/Cargo.toml b/gix-config-value/Cargo.toml index 49214a33a7e..b2eb4a6a517 100644 --- a/gix-config-value/Cargo.toml +++ b/gix-config-value/Cargo.toml @@ -21,7 +21,6 @@ serde = ["dep:serde", "bstr/serde"] [dependencies] gix-path = { version = "^0.12.3", path = "../gix-path" } -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/src/lib.rs b/gix-config-value/src/lib.rs index 763661a7e55..fcc6736b73f 100644 --- a/gix-config-value/src/lib.rs +++ b/gix-config-value/src/lib.rs @@ -26,16 +26,28 @@ #![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)] +#[derive(Debug, 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 std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Could not decode '{}': {}", self.input, self.message) + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + self.utf8_err + .as_ref() + .map(|err| err as &(dyn std::error::Error + 'static)) + } +} + 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 { diff --git a/gix-config-value/src/path.rs b/gix-config-value/src/path.rs index e2eb345f9c7..0cc9f47541e 100644 --- a/gix-config-value/src/path.rs +++ b/gix-config-value/src/path.rs @@ -30,23 +30,49 @@ pub mod interpolate { } /// The error returned by [`Path::interpolate()`][crate::Path::interpolate()]. - #[derive(Debug, thiserror::Error)] + #[derive(Debug)] #[expect(missing_docs)] pub enum Error { - #[error("{} is missing", .what)] - Missing { what: &'static str }, - #[error("Ill-formed UTF-8 in {}", .what)] + Missing { + what: &'static str, + }, Utf8Conversion { what: &'static str, - #[source] err: gix_path::Utf8Error, }, - #[error("Ill-formed UTF-8 in username")] - UsernameConversion(#[from] std::str::Utf8Error), - #[error("User interpolation is not available on this platform")] + UsernameConversion(std::str::Utf8Error), UserInterpolationUnsupported, } + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Missing { what } => write!(f, "{what} is missing"), + Error::Utf8Conversion { what, .. } => write!(f, "Ill-formed UTF-8 in {what}"), + Error::UsernameConversion(_) => f.write_str("Ill-formed UTF-8 in username"), + Error::UserInterpolationUnsupported => { + f.write_str("User interpolation is not available on this platform") + } + } + } + } + + impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::Utf8Conversion { err, .. } => Some(err), + Error::UsernameConversion(err) => Some(err), + Error::Missing { .. } | Error::UserInterpolationUnsupported => None, + } + } + } + + impl From for Error { + fn from(err: std::str::Utf8Error) -> Self { + Error::UsernameConversion(err) + } + } + /// 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()]. diff --git a/gix-config/Cargo.toml b/gix-config/Cargo.toml index ca6d6c4f35c..1609716310d 100644 --- a/gix-config/Cargo.toml +++ b/gix-config/Cargo.toml @@ -30,7 +30,6 @@ gix-ref = { version = "^0.66.0", path = "../gix-ref" } gix-glob = { version = "^0.27.0", path = "../gix-glob" } gix-utils = { version = "^0.3.5", 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/src/file/includes/types.rs b/gix-config/src/file/includes/types.rs index 7aa25c0a873..fe6c6c76966 100644 --- a/gix-config/src/file/includes/types.rs +++ b/gix-config/src/file/includes/types.rs @@ -3,27 +3,83 @@ use std::path::PathBuf; use crate::{parse, path::interpolate}; /// The error returned when following includes. -#[derive(Debug, thiserror::Error)] +// The hand-written impls below preserve the `thiserror` semantics they replace: the formerly +// transparent variants (`Parse`, `Span`, `Interpolate` and `Realpath`) forward both `Display` and +// `source()` to the error they carry, while the `#[source]`-only `CopyBuffer` and the +// `source`-named field of `Io` keep their own message and surface the cause via `source()`. +#[derive(Debug)] #[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())] + CopyBuffer(std::io::Error), Io { path: PathBuf, source: std::io::Error }, - #[error(transparent)] - Parse(#[from] parse::Error), - #[error(transparent)] - Span(#[from] parse::span::Error), - #[error(transparent)] - Interpolate(#[from] interpolate::Error), - #[error("The maximum allowed length {} of the file include chain built by following nested resolve_includes is exceeded", .max_depth)] + Parse(parse::Error), + Span(parse::span::Error), + Interpolate(interpolate::Error), 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(#[from] gix_path::realpath::Error), + Realpath(gix_path::realpath::Error), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::CopyBuffer(_) => f.write_str("Failed to copy configuration file into buffer"), + Error::Io { path, .. } => write!(f, "Could not read included configuration file at '{}'", path.display()), + Error::Parse(err) => std::fmt::Display::fmt(err, f), + Error::Span(err) => std::fmt::Display::fmt(err, f), + Error::Interpolate(err) => std::fmt::Display::fmt(err, f), + Error::IncludeDepthExceeded { max_depth } => write!( + f, + "The maximum allowed length {max_depth} of the file include chain built by following nested resolve_includes is exceeded" + ), + Error::MissingConfigPath => f.write_str( + "Include paths from environment variables must not be relative as no config file paths exists as root", + ), + Error::MissingGitDir => { + f.write_str("The git directory must be provided to support `gitdir:` conditional includes") + } + Error::Realpath(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::CopyBuffer(err) => Some(err), + Error::Io { source, .. } => Some(source), + Error::Parse(err) => err.source(), + Error::Span(err) => err.source(), + Error::Interpolate(err) => err.source(), + Error::IncludeDepthExceeded { .. } | Error::MissingConfigPath | Error::MissingGitDir => None, + Error::Realpath(err) => err.source(), + } + } +} + +impl From for Error { + fn from(err: parse::Error) -> Self { + Error::Parse(err) + } +} + +impl From for Error { + fn from(err: parse::span::Error) -> Self { + Error::Span(err) + } +} + +impl From for Error { + fn from(err: interpolate::Error) -> Self { + Error::Interpolate(err) + } +} + +impl From for Error { + fn from(err: gix_path::realpath::Error) -> Self { + Error::Realpath(err) + } } /// Options to handle includes, like `include.path` or `includeIf..path`, diff --git a/gix-config/src/file/init/comfort.rs b/gix-config/src/file/init/comfort.rs index 275a7b0e53c..f070b1ff979 100644 --- a/gix-config/src/file/init/comfort.rs +++ b/gix-config/src/file/init/comfort.rs @@ -147,17 +147,66 @@ 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)] + #[derive(Debug)] 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), + FromPaths(init::from_paths::Error), + FromEnv(init::from_env::Error), + Init(init::Error), + Includes(init::includes::Error), + Span(crate::parse::span::Error), + } + + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::FromPaths(err) => std::fmt::Display::fmt(err, f), + Error::FromEnv(err) => std::fmt::Display::fmt(err, f), + Error::Init(err) => std::fmt::Display::fmt(err, f), + Error::Includes(err) => std::fmt::Display::fmt(err, f), + Error::Span(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::FromPaths(err) => err.source(), + Error::FromEnv(err) => err.source(), + Error::Init(err) => err.source(), + Error::Includes(err) => err.source(), + Error::Span(err) => err.source(), + } + } + } + + impl From for Error { + fn from(err: init::from_paths::Error) -> Self { + Error::FromPaths(err) + } + } + + impl From for Error { + fn from(err: init::from_env::Error) -> Self { + Error::FromEnv(err) + } + } + + impl From for Error { + fn from(err: init::Error) -> Self { + Error::Init(err) + } + } + + impl From for Error { + fn from(err: init::includes::Error) -> Self { + Error::Includes(err) + } + } + + impl From for Error { + fn from(err: crate::parse::span::Error) -> Self { + Error::Span(err) + } } } diff --git a/gix-config/src/file/init/from_env.rs b/gix-config/src/file/init/from_env.rs index 32effaa2418..ed38727ba2f 100644 --- a/gix-config/src/file/init/from_env.rs +++ b/gix-config/src/file/init/from_env.rs @@ -3,27 +3,80 @@ use bstr::ByteSlice; use crate::{File, KeyRef, file, file::init, parse::section, path::interpolate}; /// Represents the errors that may occur when calling [`File::from_env()`]. -#[derive(Debug, thiserror::Error)] +#[derive(Debug)] #[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(#[from] interpolate::Error), - #[error(transparent)] - Includes(#[from] init::includes::Error), - #[error(transparent)] - Section(#[from] section::header::Error), - #[error(transparent)] - SectionValue(#[from] file::section::value::Error), + PathInterpolationError(interpolate::Error), + Includes(init::includes::Error), + Section(section::header::Error), + SectionValue(file::section::value::Error), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::IllformedUtf8 { index, kind } => { + write!(f, "Configuration {kind} at index {index} contained illformed UTF-8") + } + Error::InvalidConfigCount { input } => { + write!(f, "GIT_CONFIG_COUNT was not a positive integer: {input}") + } + Error::InvalidKeyId { key_id } => write!(f, "GIT_CONFIG_KEY_{key_id} was not set"), + Error::InvalidKeyValue { key_id, key_val } => { + write!(f, "GIT_CONFIG_KEY_{key_id} was set to an invalid value: {key_val}") + } + Error::InvalidValueId { value_id } => write!(f, "GIT_CONFIG_VALUE_{value_id} was not set"), + Error::PathInterpolationError(err) => std::fmt::Display::fmt(err, f), + Error::Includes(err) => std::fmt::Display::fmt(err, f), + Error::Section(err) => std::fmt::Display::fmt(err, f), + Error::SectionValue(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::IllformedUtf8 { .. } + | Error::InvalidConfigCount { .. } + | Error::InvalidKeyId { .. } + | Error::InvalidKeyValue { .. } + | Error::InvalidValueId { .. } => None, + Error::PathInterpolationError(err) => err.source(), + Error::Includes(err) => err.source(), + Error::Section(err) => err.source(), + Error::SectionValue(err) => err.source(), + } + } +} + +impl From for Error { + fn from(err: interpolate::Error) -> Self { + Error::PathInterpolationError(err) + } +} + +impl From for Error { + fn from(err: init::includes::Error) -> Self { + Error::Includes(err) + } +} + +impl From for Error { + fn from(err: section::header::Error) -> Self { + Error::Section(err) + } +} + +impl From for Error { + fn from(err: file::section::value::Error) -> Self { + Error::SectionValue(err) + } } /// Instantiation from environment variables diff --git a/gix-config/src/file/init/from_paths.rs b/gix-config/src/file/init/from_paths.rs index 63227112aa9..80b5777445d 100644 --- a/gix-config/src/file/init/from_paths.rs +++ b/gix-config/src/file/init/from_paths.rs @@ -6,16 +6,40 @@ use crate::{ }; /// The error returned by [`File::from_paths_metadata()`] and [`File::from_path_no_includes()`]. -#[derive(Debug, thiserror::Error)] +#[derive(Debug)] #[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), + Init(init::Error), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Io { path, .. } => { + write!(f, "The configuration file at \"{}\" could not be read", path.display()) + } + Error::Init(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::Io { source, .. } => Some(source), + Error::Init(err) => err.source(), + } + } +} + +impl From for Error { + fn from(err: init::Error) -> Self { + Error::Init(err) + } } /// Instantiation from one or more paths diff --git a/gix-config/src/file/init/types.rs b/gix-config/src/file/init/types.rs index c1b4f32e518..d45728035d1 100644 --- a/gix-config/src/file/init/types.rs +++ b/gix-config/src/file/init/types.rs @@ -1,17 +1,61 @@ use crate::{file::init, parse, parse::EventRef, path::interpolate}; /// The error returned by [`File::from_bytes_no_includes()`][crate::File::from_bytes_no_includes()]. -#[derive(Debug, thiserror::Error)] +// TODO(review): all variants were `#[error(transparent)]`: `Display` and `source()` forward to the +// wrapped error, exactly like the `thiserror`-generated code did. +#[derive(Debug)] #[expect(missing_docs)] pub enum Error { - #[error(transparent)] - Parse(#[from] parse::Error), - #[error(transparent)] - Interpolate(#[from] interpolate::Error), - #[error(transparent)] - Includes(#[from] init::includes::Error), - #[error(transparent)] - Span(#[from] parse::span::Error), + Parse(parse::Error), + Interpolate(interpolate::Error), + Includes(init::includes::Error), + Span(parse::span::Error), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Parse(err) => std::fmt::Display::fmt(err, f), + Error::Interpolate(err) => std::fmt::Display::fmt(err, f), + Error::Includes(err) => std::fmt::Display::fmt(err, f), + Error::Span(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::Parse(err) => err.source(), + Error::Interpolate(err) => err.source(), + Error::Includes(err) => err.source(), + Error::Span(err) => err.source(), + } + } +} + +impl From for Error { + fn from(err: parse::Error) -> Self { + Error::Parse(err) + } +} + +impl From for Error { + fn from(err: interpolate::Error) -> Self { + Error::Interpolate(err) + } +} + +impl From for Error { + fn from(err: init::includes::Error) -> Self { + Error::Includes(err) + } +} + +impl From for Error { + fn from(err: parse::span::Error) -> Self { + Error::Span(err) + } } /// Options when loading git config using [`File::from_paths_metadata()`][crate::File::from_paths_metadata()]. diff --git a/gix-config/src/file/mod.rs b/gix-config/src/file/mod.rs index 5f4c4350906..6d0187624cd 100644 --- a/gix-config/src/file/mod.rs +++ b/gix-config/src/file/mod.rs @@ -27,30 +27,100 @@ pub mod section; /// pub mod rename_section { /// The error returned by [`File::rename_section(…)`][crate::File::rename_section()]. - #[derive(Debug, thiserror::Error)] + #[derive(Debug)] #[expect(missing_docs)] pub enum Error { - #[error(transparent)] - Lookup(#[from] crate::lookup::existing::Error), - #[error(transparent)] - Section(#[from] crate::parse::section::header::Error), + Lookup(crate::lookup::existing::Error), + Section(crate::parse::section::header::Error), + } + + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Lookup(err) => std::fmt::Display::fmt(err, f), + Error::Section(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::Lookup(err) => err.source(), + Error::Section(err) => err.source(), + } + } + } + + impl From for Error { + fn from(err: crate::lookup::existing::Error) -> Self { + Error::Lookup(err) + } + } + + impl From for Error { + fn from(err: crate::parse::section::header::Error) -> Self { + Error::Section(err) + } } } /// pub mod set_raw_value { /// The error returned by [`File::set_raw_value(…)`][crate::File::set_raw_value()]. - #[derive(Debug, thiserror::Error)] + #[derive(Debug)] #[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), + Lookup(crate::lookup::existing::Error), + Header(crate::parse::section::header::Error), + ValueName(crate::parse::section::value_name::Error), + Span(crate::parse::span::Error), + } + + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Lookup(err) => std::fmt::Display::fmt(err, f), + Error::Header(err) => std::fmt::Display::fmt(err, f), + Error::ValueName(err) => std::fmt::Display::fmt(err, f), + Error::Span(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::Lookup(err) => err.source(), + Error::Header(err) => err.source(), + Error::ValueName(err) => err.source(), + Error::Span(err) => err.source(), + } + } + } + + impl From for Error { + fn from(err: crate::lookup::existing::Error) -> Self { + Error::Lookup(err) + } + } + + impl From for Error { + fn from(err: crate::parse::section::header::Error) -> Self { + Error::Header(err) + } + } + + impl From for Error { + fn from(err: crate::parse::section::value_name::Error) -> Self { + Error::ValueName(err) + } + } + + impl From for Error { + fn from(err: crate::parse::span::Error) -> Self { + Error::Span(err) + } } } diff --git a/gix-config/src/file/section/mod.rs b/gix-config/src/file/section/mod.rs index 2376ec39042..623df9d7ab8 100644 --- a/gix-config/src/file/section/mod.rs +++ b/gix-config/src/file/section/mod.rs @@ -18,13 +18,41 @@ 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)] + #[derive(Debug)] #[allow(missing_docs)] pub enum Error { - #[error(transparent)] - ValueName(#[from] crate::parse::section::value_name::Error), - #[error(transparent)] - Span(#[from] crate::parse::span::Error), + ValueName(crate::parse::section::value_name::Error), + Span(crate::parse::span::Error), + } + + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::ValueName(err) => std::fmt::Display::fmt(err, f), + Error::Span(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::ValueName(err) => err.source(), + Error::Span(err) => err.source(), + } + } + } + + impl From for Error { + fn from(err: crate::parse::section::value_name::Error) -> Self { + Error::ValueName(err) + } + } + + impl From for Error { + fn from(err: crate::parse::span::Error) -> Self { + Error::Span(err) + } } } diff --git a/gix-config/src/lookup.rs b/gix-config/src/lookup.rs index f16d073a39e..3f330c59511 100644 --- a/gix-config/src/lookup.rs +++ b/gix-config/src/lookup.rs @@ -1,26 +1,75 @@ /// The error when looking up a value, for example via [`File::try_value()`][crate::File::try_value()]. -#[derive(Debug, thiserror::Error)] +// TODO(review): these implementations hand-preserve `#[error(transparent)]` semantics: `Display` +// passes the formatter through and `source()` forwards to the inner error's source, +// exactly like the `thiserror`-generated code did — here on a generic enum, with the +// bounds `thiserror` would have inferred. The same pattern is used in the other +// error types of this crate. +#[derive(Debug)] #[expect(missing_docs)] pub enum Error { - #[error(transparent)] - ValueMissing(#[from] existing::Error), - #[error(transparent)] + ValueMissing(existing::Error), FailedConversion(E), } +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) => err.source(), + Error::FailedConversion(err) => err.source(), + } + } +} + +impl From for Error { + fn from(err: existing::Error) -> Self { + Error::ValueMissing(err) + } +} + /// 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)] + #[derive(Debug)] #[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), + ValueName(crate::parse::section::value_name::Error), + } + + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::SectionMissing => f.write_str("The requested section does not exist"), + Error::SubSectionMissing => f.write_str("The requested subsection does not exist"), + Error::KeyMissing => f.write_str("The key does not exist in the requested section"), + Error::ValueName(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::SectionMissing | Error::SubSectionMissing | Error::KeyMissing => None, + Error::ValueName(err) => err.source(), + } + } + } + + impl From for Error { + fn from(err: crate::parse::section::value_name::Error) -> Self { + Error::ValueName(err) + } } } diff --git a/gix-config/src/parse/mod.rs b/gix-config/src/parse/mod.rs index c6717261de0..d6f0573c9a3 100644 --- a/gix-config/src/parse/mod.rs +++ b/gix-config/src/parse/mod.rs @@ -37,9 +37,20 @@ 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)] + #[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] pub struct Error; + + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "configuration data exceeds the supported span size of {} bytes", + u32::MAX + ) + } + } + + impl std::error::Error for Error {} } /// A raw span whose semantic value may have required decoding while parsing. diff --git a/gix-config/src/parse/section/header.rs b/gix-config/src/parse/section/header.rs index e6629898500..268042aadaf 100644 --- a/gix-config/src/parse/section/header.rs +++ b/gix-config/src/parse/section/header.rs @@ -3,15 +3,37 @@ 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)] +#[derive(Debug, PartialOrd, PartialEq, Eq)] #[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), + Span(crate::parse::span::Error), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::InvalidName => f.write_str("section names can only be ascii, '-'"), + Error::InvalidSubSection => f.write_str("sub-section names must not contain newlines or null bytes"), + Error::Span(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::InvalidName | Error::InvalidSubSection => None, + Error::Span(err) => err.source(), + } + } +} + +impl From for Error { + fn from(err: crate::parse::span::Error) -> Self { + Error::Span(err) + } } impl HeaderData { diff --git a/gix-config/src/parse/section/mod.rs b/gix-config/src/parse/section/mod.rs index 0e1d0f68d2f..8c2893b3748 100644 --- a/gix-config/src/parse/section/mod.rs +++ b/gix-config/src/parse/section/mod.rs @@ -27,9 +27,16 @@ 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)] + #[derive(Debug, Copy, Clone)] pub struct Error; + + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str($err_doc) + } + } + + impl std::error::Error for Error {} } #[doc = $comment] diff --git a/gix-credentials/Cargo.toml b/gix-credentials/Cargo.toml index cc6d8dd87dc..4f264512b34 100644 --- a/gix-credentials/Cargo.toml +++ b/gix-credentials/Cargo.toml @@ -29,7 +29,6 @@ gix-prompt = { version = "^0.16.0", path = "../gix-prompt" } gix-date = { version = "^0.15.6", 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/src/helper/mod.rs b/gix-credentials/src/helper/mod.rs index 49641470606..bca4d40c026 100644 --- a/gix-credentials/src/helper/mod.rs +++ b/gix-credentials/src/helper/mod.rs @@ -57,17 +57,46 @@ impl Outcome { pub type Result = std::result::Result, Error>; /// The error used in the [credentials helper invocation][crate::helper::invoke()]. -#[derive(Debug, thiserror::Error)] +#[derive(Debug)] #[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)] + ContextDecode(protocol::context::decode::Error), + Io(std::io::Error), CredentialsHelperFailed { source: std::io::Error }, } +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::ContextDecode(err) => std::fmt::Display::fmt(err, f), + Error::Io(_) => f.write_str("An IO error occurred while communicating to the credentials helper"), + Error::CredentialsHelperFailed { source } => std::fmt::Display::fmt(source, f), + } + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::ContextDecode(err) => err.source(), + Error::Io(err) => Some(err), + Error::CredentialsHelperFailed { source } => source.source(), + } + } +} + +impl From for Error { + fn from(err: protocol::context::decode::Error) -> Self { + Error::ContextDecode(err) + } +} + +impl From for Error { + fn from(err: std::io::Error) -> Self { + Error::Io(err) + } +} + /// The action to perform by the credentials [helper][`crate::helper::invoke()`]. #[derive(Clone, Debug)] pub enum Action { diff --git a/gix-credentials/src/program/main.rs b/gix-credentials/src/program/main.rs index e7ef34cfc0a..a98e95c89cb 100644 --- a/gix-credentials/src/program/main.rs +++ b/gix-credentials/src/program/main.rs @@ -38,27 +38,69 @@ impl Action { } /// The error of [`main()`][crate::program::main()]. -#[derive(Debug, thiserror::Error)] +#[derive(Debug)] #[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")] + ActionInvalid { + name: OsString, + }, 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")] + Io(std::io::Error), + Context(crate::protocol::context::decode::Error), + CredentialsMissing { + url: BString, + }, UrlMissing, } +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::ActionInvalid { name } => write!( + f, + "Action named {name:?} is invalid, need 'get', 'store', 'erase' or 'fill', 'approve', 'reject'" + ), + Error::ActionMissing => f.write_str("The first argument must be the action to perform"), + Error::Helper { source } => std::fmt::Display::fmt(source, f), + Error::Io(err) => std::fmt::Display::fmt(err, f), + Error::Context(err) => std::fmt::Display::fmt(err, f), + Error::CredentialsMissing { url } => write!(f, "Credentials for {url:?} could not be obtained"), + Error::UrlMissing => { + f.write_str("Either 'url' field or both 'protocol' and 'host' fields must be provided") + } + } + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::Helper { source } => source.source(), + Error::Io(err) => err.source(), + Error::Context(err) => err.source(), + Error::ActionInvalid { .. } + | Error::ActionMissing + | Error::CredentialsMissing { .. } + | Error::UrlMissing => None, + } + } +} + +impl From for Error { + fn from(err: std::io::Error) -> Self { + Error::Io(err) + } +} + +impl From for Error { + fn from(err: crate::protocol::context::decode::Error) -> Self { + Error::Context(err) + } +} + pub(crate) mod function { use std::ffi::OsString; diff --git a/gix-credentials/src/protocol/context/mod.rs b/gix-credentials/src/protocol/context/mod.rs index 8b36ab03d7b..88333fde2b3 100644 --- a/gix-credentials/src/protocol/context/mod.rs +++ b/gix-credentials/src/protocol/context/mod.rs @@ -3,13 +3,25 @@ use bstr::BString; use crate::protocol::{Context, ContextOptions}; /// Indicates key or values contain errors that can't be encoded. -#[derive(Debug, thiserror::Error)] +#[derive(Debug)] #[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 }, } +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Encoding { key, value } => write!( + f, + "{key:?}={value:?} must not contain null bytes or newlines neither in key nor in value." + ), + } + } +} + +impl std::error::Error for Error {} + impl Context { /// Create a context containing `url`, encoded and decoded according to `options`. pub fn from_url(url: impl Into, options: ContextOptions) -> Self { diff --git a/gix-credentials/src/protocol/context/serde.rs b/gix-credentials/src/protocol/context/serde.rs index 22e0dc5a0ab..9a03ec42e36 100644 --- a/gix-credentials/src/protocol/context/serde.rs +++ b/gix-credentials/src/protocol/context/serde.rs @@ -74,17 +74,41 @@ pub mod decode { use crate::protocol::{Context, ContextOptions, context, context::serde::validate}; /// The error returned by [`from_bytes()`][Context::from_bytes()]. - #[derive(Debug, thiserror::Error)] + #[derive(Debug)] #[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")] + Encoding(context::Error), Syntax { line: BString }, } + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::IllformedUtf8InValue { key, value } => { + write!(f, "Illformed UTF-8 in value of key {key:?}: {value:?}") + } + Error::Encoding(err) => std::fmt::Display::fmt(err, f), + Error::Syntax { line } => write!(f, "Invalid format in line {line:?}, expecting key=value"), + } + } + } + + impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::Encoding(err) => err.source(), + Error::IllformedUtf8InValue { .. } | Error::Syntax { .. } => None, + } + } + } + + impl From for Error { + fn from(err: context::Error) -> Self { + Error::Encoding(err) + } + } + impl Context { /// Decode ourselves from `input` which is the format written by [`write_to()`][Self::write_to()]. /// `options` control what to support during deserialization. diff --git a/gix-credentials/src/protocol/mod.rs b/gix-credentials/src/protocol/mod.rs index 88cb989b306..b03108fa194 100644 --- a/gix-credentials/src/protocol/mod.rs +++ b/gix-credentials/src/protocol/mod.rs @@ -15,28 +15,83 @@ pub struct Outcome { pub type Result = std::result::Result, Error>; /// The error returned top-level credential functions. -#[derive(Debug, thiserror::Error)] +// TODO(review): these implementations hand-preserve `#[error(transparent)]` semantics for +// `UrlParse`, `ContextDecode` and `InvokeHelper`: `Display` passes the formatter +// through and `source()` forwards to the inner error's source, exactly like the +// `thiserror`-generated code did. The same pattern is used in the other error +// types of this crate, including the transparent struct-variants +// `helper::Error::CredentialsHelperFailed` and `program::main::Error::Helper`, +// which forward to their `source` field. +#[derive(Debug)] #[expect(missing_docs)] pub enum Error { - #[error(transparent)] - UrlParse(#[from] gix_url::parse::Error), - #[error("Either 'url' field or both 'protocol' and 'host' fields must be provided")] + UrlParse(gix_url::parse::Error), UrlMissing, - #[error(transparent)] - ContextDecode(#[from] context::decode::Error), - #[error(transparent)] - InvokeHelper(#[from] helper::Error), - #[error("Could not configure credential helpers")] + ContextDecode(context::decode::Error), + InvokeHelper(helper::Error), 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")] + IdentityMissing { + context: Context, + }, Quit, - #[error("Couldn't obtain {prompt}")] - Prompt { prompt: String, source: gix_prompt::Error }, + Prompt { + prompt: String, + source: gix_prompt::Error, + }, +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::UrlParse(err) => std::fmt::Display::fmt(err, f), + Error::UrlMissing => { + f.write_str("Either 'url' field or both 'protocol' and 'host' fields must be provided") + } + Error::ContextDecode(err) => std::fmt::Display::fmt(err, f), + Error::InvokeHelper(err) => std::fmt::Display::fmt(err, f), + Error::ConfigureCredentialHelpers { .. } => f.write_str("Could not configure credential helpers"), + Error::IdentityMissing { context } => write!(f, "Could not obtain identity for context: {}", { + let mut buf = Vec::::new(); + context.write_to(&mut buf).ok(); + String::from_utf8_lossy(&buf).into_owned() + }), + Error::Quit => f.write_str("The handler asked to stop trying to obtain credentials"), + Error::Prompt { prompt, .. } => write!(f, "Couldn't obtain {prompt}"), + } + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::UrlParse(err) => err.source(), + Error::ContextDecode(err) => err.source(), + Error::InvokeHelper(err) => err.source(), + Error::ConfigureCredentialHelpers { source } => Some(&**source), + Error::Prompt { source, .. } => Some(source), + Error::UrlMissing | Error::IdentityMissing { .. } | Error::Quit => None, + } + } +} + +impl From for Error { + fn from(err: gix_url::parse::Error) -> Self { + Error::UrlParse(err) + } +} + +impl From for Error { + fn from(err: context::decode::Error) -> Self { + Error::ContextDecode(err) + } +} + +impl From for Error { + fn from(err: helper::Error) -> Self { + Error::InvokeHelper(err) + } } /// Additional context to be passed to the credentials helper. diff --git a/gix-credentials/tests/program/main.rs b/gix-credentials/tests/program/main.rs index 3b45dc8edfc..72bd47fcc67 100644 --- a/gix-credentials/tests/program/main.rs +++ b/gix-credentials/tests/program/main.rs @@ -1,10 +1,17 @@ use gix_credentials::program::main; use std::io::Cursor; -#[derive(Debug, thiserror::Error)] -#[error("Test error")] +#[derive(Debug)] struct TestError; +impl std::fmt::Display for TestError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("Test error") + } +} + +impl std::error::Error for TestError {} + #[test] fn context_options_apply_to_input_and_output() { let input = b"url=https://github.com/with\rreturn\n"; diff --git a/gix-diff/Cargo.toml b/gix-diff/Cargo.toml index e7fd0d695ab..941fbf28fe7 100644 --- a/gix-diff/Cargo.toml +++ b/gix-diff/Cargo.toml @@ -67,7 +67,6 @@ gix-trace = { version = "^0.1.21", path = "../gix-trace", optional = true } gix-traverse = { version = "^0.60.0", path = "../gix-traverse", optional = true } imara-diff = { package = "gix-imara-diff", version = "^0.2.4", 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 c5614d864e5..9618c85eb1c 100644 --- a/gix-diff/src/blob/pipeline.rs +++ b/gix-diff/src/blob/pipeline.rs @@ -130,41 +130,134 @@ pub mod convert_to_diffable { 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)] + #[derive(Debug)] + #[allow(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}")] + InvalidEntryKind { + rela_path: BString, + actual: EntryKind, + }, + SymlinkDisabled { + rela_path: BString, + }, + ReadLink { + rela_path: BString, + source: std::io::Error, + }, + OpenOrRead { + rela_path: BString, + source: std::io::Error, + }, + StreamCopy { + rela_path: BString, + source: std::io::Error, + }, 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}")] + CreateTempfile { + rela_path: BString, + source: std::io::Error, + }, TextConvFilterFailed { rela_path: BString, cmd: String, stderr: BString, }, - #[error(transparent)] - FindObject(#[from] gix_object::find::existing_object::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), + FindObject(gix_object::find::existing_object::Error), + ConvertToWorktree(gix_filter::pipeline::convert::to_worktree::Error), + ConvertToGit(gix_filter::pipeline::convert::to_git::Error), + OutOfMemory(TryReserveError), + } + + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::InvalidEntryKind { rela_path, actual } => { + write!( + f, + "Entry at '{rela_path}' must be regular file or symlink, but was {actual:?}" + ) + } + Error::SymlinkDisabled { rela_path } => write!( + f, + "Entry at '{rela_path}' is declared as symlink but symlinks are disabled via core.symlinks" + ), + Error::ReadLink { rela_path, .. } => { + write!(f, "Entry at '{rela_path}' could not be read as symbolic link") + } + Error::OpenOrRead { rela_path, .. } => { + write!(f, "Entry at '{rela_path}' could not be opened for reading or read from") + } + Error::StreamCopy { rela_path, .. } => write!( + f, + "Entry at '{rela_path}' could not be copied from a filter process to a memory buffer" + ), + Error::RunTextConvFilter { rela_path, cmd, .. } => { + write!( + f, + "Failed to run '{cmd}' for binary-to-text conversion of entry at {rela_path}" + ) + } + Error::CreateTempfile { rela_path, .. } => write!( + f, + "Tempfile for binary-to-text conversion for entry at {rela_path} could not be created" + ), + Error::TextConvFilterFailed { rela_path, cmd, stderr } => write!( + f, + "Binary-to-text conversion '{cmd}' for entry at {rela_path} failed with: {stderr}" + ), + Error::FindObject(err) => std::fmt::Display::fmt(err, f), + Error::ConvertToWorktree(err) => std::fmt::Display::fmt(err, f), + Error::ConvertToGit(err) => std::fmt::Display::fmt(err, f), + Error::OutOfMemory(_) => f.write_str("Memory allocation failed"), + } + } + } + + impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::ReadLink { source, .. } + | Error::OpenOrRead { source, .. } + | Error::StreamCopy { source, .. } + | Error::RunTextConvFilter { source, .. } + | Error::CreateTempfile { source, .. } => Some(source), + Error::FindObject(err) => err.source(), + Error::ConvertToWorktree(err) => err.source(), + Error::ConvertToGit(err) => err.source(), + Error::OutOfMemory(err) => Some(err), + Error::InvalidEntryKind { .. } | Error::SymlinkDisabled { .. } | Error::TextConvFilterFailed { .. } => { + None + } + } + } + } + + impl From for Error { + fn from(err: gix_object::find::existing_object::Error) -> Self { + Error::FindObject(err) + } + } + + impl From for Error { + fn from(err: gix_filter::pipeline::convert::to_worktree::Error) -> Self { + Error::ConvertToWorktree(err) + } + } + + impl From for Error { + fn from(err: gix_filter::pipeline::convert::to_git::Error) -> Self { + Error::ConvertToGit(err) + } + } + + impl From for Error { + fn from(err: TryReserveError) -> Self { + Error::OutOfMemory(err) + } } } @@ -229,7 +322,7 @@ impl Pipeline { /// /// As these files are ultimately named tempfiles, they will be leaked unless the [gix_tempfile] is configured with /// a signal handler. If they leak, they would remain in the system's `$TMP` directory. - #[expect(clippy::too_many_arguments)] + #[allow(clippy::too_many_arguments)] pub fn convert_to_diffable( &mut self, id: &gix_hash::oid, diff --git a/gix-diff/src/blob/platform.rs b/gix-diff/src/blob/platform.rs index 4f2f4eda474..f6b29b94579 100644 --- a/gix-diff/src/blob/platform.rs +++ b/gix-diff/src/blob/platform.rs @@ -245,25 +245,54 @@ pub mod set_resource { use crate::blob::{ResourceKind, pipeline}; /// The error returned by [Platform::set_resource](super::Platform::set_resource). - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] + #[derive(Debug)] + #[allow(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}'")] + InvalidMode { + mode: gix_object::tree::EntryKind, + }, 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), + ConvertToDiffable(pipeline::convert_to_diffable::Error), + } + + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::InvalidMode { mode } => write!(f, "Can only diff blobs and links, not {mode:?}"), + Error::Io { rela_path, kind, .. } => { + write!(f, "Failed to read {kind} worktree data from '{rela_path}'") + } + Error::Attributes { rela_path, kind, .. } => { + write!(f, "Failed to obtain attributes for {kind} resource at '{rela_path}'") + } + Error::ConvertToDiffable(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::Io { source, .. } | Error::Attributes { source, .. } => Some(source), + Error::ConvertToDiffable(err) => err.source(), + Error::InvalidMode { .. } => None, + } + } + } + + impl From for Error { + fn from(err: pipeline::convert_to_diffable::Error) -> Self { + Error::ConvertToDiffable(err) + } } } @@ -335,14 +364,27 @@ pub mod prepare_diff { } /// The error returned by [Platform::prepare_diff()](super::Platform::prepare_diff()). - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] + #[derive(Debug)] + #[allow(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, } + + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::SourceOrDestinationUnset => { + f.write_str("Either the source or the destination of the diff operation were not set") + } + Error::SourceAndDestinationRemoved => { + f.write_str("Tried to diff resources that are both considered removed") + } + } + } + } + + impl std::error::Error for Error {} } /// @@ -352,19 +394,45 @@ pub mod prepare_diff_command { use bstr::BString; /// The error returned by [Platform::prepare_diff_command()](super::Platform::prepare_diff_command()). - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] + #[derive(Debug)] + #[allow(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 }, } + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::SourceOrDestinationUnset => { + f.write_str("Either the source or the destination of the diff operation were not set") + } + Error::SourceOrDestinationBinary => f.write_str( + "Binary resources can't be diffed with an external command (as we don't have the data anymore)", + ), + Error::CreateTempfile { rela_path, .. } => write!( + f, + "Tempfile to store content of '{rela_path}' for passing to external diff command could not be created" + ), + Error::WriteTempfile { rela_path, .. } => write!( + f, + "Could not write content of '{rela_path}' to tempfile for passing to external diff command" + ), + } + } + } + + impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::CreateTempfile { source, .. } | Error::WriteTempfile { source, .. } => Some(source), + Error::SourceOrDestinationUnset | Error::SourceOrDestinationBinary => None, + } + } + } + /// 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* diff --git a/gix-diff/src/index/mod.rs b/gix-diff/src/index/mod.rs index 1a380ac8dee..970b3921f6a 100644 --- a/gix-diff/src/index/mod.rs +++ b/gix-diff/src/index/mod.rs @@ -3,17 +3,42 @@ use std::borrow::Cow; use bstr::BStr; /// The error returned by [`index()`](crate::index()). -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] +#[derive(Debug)] +#[allow(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), + Callback(Box), + RenameTracking(crate::rewrites::tracker::emit::Error), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::IsSparse => f.write_str("Cannot diff indices that contain sparse entries"), + Error::LhsHasUnmerged => { + f.write_str("Unmerged entries aren't allowed in the left-hand index, only in the right-hand index") + } + Error::Callback(_) => f.write_str("The callback indicated failure"), + Error::RenameTracking(_) => f.write_str("Failure during rename tracking"), + } + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::Callback(err) => Some(&**err), + Error::RenameTracking(err) => Some(err), + Error::IsSparse | Error::LhsHasUnmerged => None, + } + } +} + +impl From for Error { + fn from(err: crate::rewrites::tracker::emit::Error) -> Self { + Error::RenameTracking(err) + } } /// 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 4de51e07988..17240b0a1d8 100644 --- a/gix-diff/src/rewrites/tracker.rs +++ b/gix-diff/src/rewrites/tracker.rs @@ -136,17 +136,55 @@ pub mod visit { /// pub mod emit { /// The error returned by [Tracker::emit()](super::Tracker::emit()). - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] + #[derive(Debug)] + #[allow(missing_docs)] pub enum Error { - #[error("Could not find blob for similarity checking")] - FindExistingBlob(#[from] gix_object::find::existing_object::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), + FindExistingBlob(gix_object::find::existing_object::Error), + GetItemsForExhaustiveCopyDetection(Box), + SetResource(crate::blob::platform::set_resource::Error), + PrepareDiff(crate::blob::platform::prepare_diff::Error), + } + + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::FindExistingBlob(_) => f.write_str("Could not find blob for similarity checking"), + Error::GetItemsForExhaustiveCopyDetection(_) => { + f.write_str("Could not obtain exhaustive item set to use as possible sources for copy detection") + } + Error::SetResource(err) => std::fmt::Display::fmt(err, f), + Error::PrepareDiff(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::FindExistingBlob(err) => Some(err), + Error::GetItemsForExhaustiveCopyDetection(err) => Some(&**err), + Error::SetResource(err) => err.source(), + Error::PrepareDiff(err) => err.source(), + } + } + } + + impl From for Error { + fn from(err: gix_object::find::existing_object::Error) -> Self { + Error::FindExistingBlob(err) + } + } + + impl From for Error { + fn from(err: crate::blob::platform::set_resource::Error) -> Self { + Error::SetResource(err) + } + } + + impl From for Error { + fn from(err: crate::blob::platform::prepare_diff::Error) -> Self { + Error::PrepareDiff(err) + } } } @@ -366,7 +404,7 @@ impl Tracker { }); } - #[expect(clippy::too_many_arguments)] + #[allow(clippy::too_many_arguments)] fn match_pairs_of_kind( &mut self, kind: visit::SourceKind, @@ -417,7 +455,7 @@ impl Tracker { Ok(()) } - #[expect(clippy::too_many_arguments)] + #[allow(clippy::too_many_arguments)] fn match_pairs( &mut self, cb: &mut impl FnMut(visit::Destination<'_, T>, Option>) -> Action, @@ -695,7 +733,7 @@ type SourceTuple<'a, T> = (usize, &'a Item, Option); /// any non-deletion otherwise. /// Note that we always try to find by identity first even if a percentage is given as it's much faster and may reduce the set /// of items to be searched. -#[expect(clippy::too_many_arguments)] +#[allow(clippy::too_many_arguments)] fn find_match<'a, T: Change>( items: &'a [Item], item: &Item, diff --git a/gix-diff/src/tree/mod.rs b/gix-diff/src/tree/mod.rs index 52064b1c309..0ca5bc411d6 100644 --- a/gix-diff/src/tree/mod.rs +++ b/gix-diff/src/tree/mod.rs @@ -7,15 +7,48 @@ use gix_object::bstr::BString; use crate::tree::visit::Relation; /// The error returned by [`tree()`](super::tree()). -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] +// TODO(review): these implementations hand-preserve `#[error(transparent)]` semantics for `Find` +// and `EntriesDecode`: `Display` passes the formatter through and `source()` forwards +// to the inner error's source, exactly like the `thiserror`-generated code did. The +// same pattern is used across the other error types of this crate. +#[derive(Debug)] +#[allow(missing_docs)] pub enum Error { - #[error(transparent)] - Find(#[from] gix_object::find::existing_iter::Error), - #[error("The delegate cancelled the operation")] + Find(gix_object::find::existing_iter::Error), Cancelled, - #[error(transparent)] - EntriesDecode(#[from] gix_object::decode::Error), + EntriesDecode(gix_object::decode::Error), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Find(err) => std::fmt::Display::fmt(err, f), + Error::Cancelled => f.write_str("The delegate cancelled the operation"), + Error::EntriesDecode(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::Find(err) => err.source(), + Error::Cancelled => None, + Error::EntriesDecode(err) => err.source(), + } + } +} + +impl From for Error { + fn from(err: gix_object::find::existing_iter::Error) -> Self { + Error::Find(err) + } +} + +impl From for Error { + fn from(err: gix_object::decode::Error) -> Self { + Error::EntriesDecode(err) + } } /// A trait to allow responding to a traversal designed to figure out the [changes](visit::Change) diff --git a/gix-diff/src/tree_with_rewrites/mod.rs b/gix-diff/src/tree_with_rewrites/mod.rs index 28d608f5017..ccf257e1528 100644 --- a/gix-diff/src/tree_with_rewrites/mod.rs +++ b/gix-diff/src/tree_with_rewrites/mod.rs @@ -4,15 +4,44 @@ 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)] +#[derive(Debug)] +#[allow(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), + Diff(crate::tree::Error), + ForEach(Box), + RenameTracking(crate::rewrites::tracker::emit::Error), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Diff(err) => std::fmt::Display::fmt(err, f), + Error::ForEach(_) => f.write_str("The user-provided callback failed"), + Error::RenameTracking(_) => f.write_str("Failure during rename tracking"), + } + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::Diff(err) => err.source(), + Error::ForEach(err) => Some(&**err), + Error::RenameTracking(err) => Some(err), + } + } +} + +impl From for Error { + fn from(err: crate::tree::Error) -> Self { + Error::Diff(err) + } +} + +impl From for Error { + fn from(err: crate::rewrites::tracker::emit::Error) -> Self { + Error::RenameTracking(err) + } } /// Returned by the [`tree_with_rewrites()`](super::tree_with_rewrites()) function to control flow. diff --git a/gix-dir/Cargo.toml b/gix-dir/Cargo.toml index 7b69120cff1..7c9cfb2bc3e 100644 --- a/gix-dir/Cargo.toml +++ b/gix-dir/Cargo.toml @@ -34,7 +34,6 @@ gix-ignore = { version = "^0.22.0", path = "../gix-ignore" } gix-utils = { version = "^0.3.5", path = "../gix-utils", features = ["bstr"] } bstr = { version = "1.12.0", default-features = false } -thiserror = "2.0.18" [dev-dependencies] gix-testtools = { path = "../tests/tools" } diff --git a/gix-dir/src/walk/mod.rs b/gix-dir/src/walk/mod.rs index 0529f097cca..47e5c5b0619 100644 --- a/gix-dir/src/walk/mod.rs +++ b/gix-dir/src/walk/mod.rs @@ -274,35 +274,99 @@ pub struct Outcome { } /// The error returned by [`walk()`](function::walk()). -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] +// TODO(review): `ExcludesAccess`'s single `std::io::Error` field is an unnamed tuple field with no +// `#[source]`, so `thiserror` did not treat it as a source; `source()` returning `None` +// for it is preserved behavior (unlike the `source`-named fields of the other io variants). +#[derive(Debug)] +#[allow(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())] + WorktreeRootIsFile { + root: PathBuf, + }, + NormalizeRoot { + root: PathBuf, + }, 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())] + ReadDir { + path: PathBuf, + source: std::io::Error, + }, 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 }, + DirEntryFileType { + path: PathBuf, + source: std::io::Error, + }, + SymlinkMetadata { + path: PathBuf, + source: std::io::Error, + }, +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Interrupted => f.write_str("Interrupted"), + Error::WorktreeRootIsFile { root } => { + write!(f, "Worktree root at '{}' is not a directory", root.display()) + } + Error::NormalizeRoot { root } => write!( + f, + "Traversal root '{}' contains relative path components and could not be normalized", + root.display() + ), + Error::SymlinkInRoot { + root, + worktree_root, + component_index, + } => write!( + f, + "A symlink was found at component {component_index} of traversal root '{}' as seen from worktree root '{}'", + root.display(), + worktree_root.display() + ), + Error::ExcludesAccess(_) => f.write_str("Failed to update the excludes stack to see if a path is excluded"), + Error::ReadDir { path, .. } => write!(f, "Failed to read the directory at '{}'", path.display()), + Error::DirEntry { parent_directory, .. } => { + write!( + f, + "Could not obtain directory entry in root of '{}'", + parent_directory.display() + ) + } + Error::DirEntryFileType { path, .. } => { + write!(f, "Could not obtain filetype of directory entry '{}'", path.display()) + } + Error::SymlinkMetadata { path, .. } => { + write!(f, "Could not obtain symlink metadata on '{}'", path.display()) + } + } + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::ReadDir { source, .. } + | Error::DirEntry { source, .. } + | Error::DirEntryFileType { source, .. } + | Error::SymlinkMetadata { source, .. } => Some(source), + Error::Interrupted + | Error::WorktreeRootIsFile { .. } + | Error::NormalizeRoot { .. } + | Error::SymlinkInRoot { .. } + | Error::ExcludesAccess(_) => None, + } + } } mod classify; diff --git a/gix-discover/Cargo.toml b/gix-discover/Cargo.toml index b58dce8057f..03b8d8e271c 100644 --- a/gix-discover/Cargo.toml +++ b/gix-discover/Cargo.toml @@ -27,7 +27,6 @@ gix-ref = { version = "^0.66.0", path = "../gix-ref" } gix-fs = { version = "^0.22.0", path = "../gix-fs" } 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/lib.rs b/gix-discover/src/lib.rs index bc64e8da35a..96d2ce9a801 100644 --- a/gix-discover/src/lib.rs +++ b/gix-discover/src/lib.rs @@ -42,31 +42,89 @@ pub mod is_git { use std::path::PathBuf; /// The error returned by [`crate::is_git()`]. - #[derive(Debug, thiserror::Error)] + // TODO(review): this implementation hand-preserves `#[error(transparent)]` semantics for + // `GitFile`: `Display` passes the formatter through and `source()` forwards to + // the inner error's source, exactly like the `thiserror`-generated code did. + // The same pattern is used in `path::from_gitdir_file::Error`. + #[derive(Debug)] #[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'")] + FindHeadRef(gix_ref::file::find::existing::Error), 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}\"")] + GitFile(crate::path::from_gitdir_file::Error), 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), + CurrentDir(std::io::Error), + } + + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::FindHeadRef(_) => f.write_str("Could not find a valid HEAD reference"), + Error::MissingHead => f.write_str("Missing HEAD at '.git/HEAD'"), + Error::MisplacedHead { name } => write!(f, "Expected HEAD at '.git/HEAD', got '.git/{name}'"), + Error::MissingObjectsDirectory { missing } => { + write!(f, "Expected an objects directory at '{}'", missing.display()) + } + Error::MissingCommonDir { missing, .. } => write!( + f, + "The worktree's private repo's commondir file at '{}' or it could not be read", + missing.display() + ), + Error::MissingRefsDirectory { missing } => { + write!(f, "Expected a refs directory at '{}'", missing.display()) + } + Error::GitFile(err) => std::fmt::Display::fmt(err, f), + Error::Metadata { path, .. } => { + write!(f, "Could not retrieve metadata of \"{}\"", path.display()) + } + Error::Inconclusive => f.write_str( + "The repository's config file doesn't exist or didn't have a 'bare' configuration or contained core.worktree without value", + ), + Error::CurrentDir(_) => { + f.write_str("Could not obtain current directory for resolving the '.' repository path") + } + } + } + } + + impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::FindHeadRef(err) => Some(err), + Error::MissingCommonDir { source, .. } => Some(source), + Error::GitFile(err) => err.source(), + Error::Metadata { source, .. } => Some(source), + Error::CurrentDir(err) => Some(err), + Error::MissingHead + | Error::MisplacedHead { .. } + | Error::MissingObjectsDirectory { .. } + | Error::MissingRefsDirectory { .. } + | Error::Inconclusive => None, + } + } + } + + impl From for Error { + fn from(err: gix_ref::file::find::existing::Error) -> Self { + Error::FindHeadRef(err) + } + } + + impl From for Error { + fn from(err: crate::path::from_gitdir_file::Error) -> Self { + Error::GitFile(err) + } + } + + impl From for Error { + fn from(err: std::io::Error) -> Self { + Error::CurrentDir(err) + } } } diff --git a/gix-discover/src/parse.rs b/gix-discover/src/parse.rs index 38c6fd4b17d..163f18e03cb 100644 --- a/gix-discover/src/parse.rs +++ b/gix-discover/src/parse.rs @@ -7,14 +7,25 @@ pub mod gitdir { use bstr::BString; /// The error returned by [`parse::gitdir()`][super::gitdir()]. - #[derive(Debug, thiserror::Error)] + #[derive(Debug)] #[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 }, } + + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::InvalidFormat { input } => { + write!(f, "Format should be 'gitdir: ', but got: {input:?}") + } + Error::IllformedUtf8 { input } => write!(f, "Couldn't decode {input:?} as UTF8"), + } + } + } + + impl std::error::Error for Error {} } /// Parse typical `gitdir` files as seen in worktrees and submodules. diff --git a/gix-discover/src/path.rs b/gix-discover/src/path.rs index 76e5c75cb19..df7f4284693 100644 --- a/gix-discover/src/path.rs +++ b/gix-discover/src/path.rs @@ -17,13 +17,41 @@ pub enum RepositoryKind { /// pub mod from_gitdir_file { /// The error returned by [`from_gitdir_file()`][crate::path::from_gitdir_file()]. - #[derive(Debug, thiserror::Error)] + #[derive(Debug)] #[expect(missing_docs)] pub enum Error { - #[error(transparent)] - Io(#[from] std::io::Error), - #[error(transparent)] - Parse(#[from] crate::parse::gitdir::Error), + Io(std::io::Error), + Parse(crate::parse::gitdir::Error), + } + + 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::Parse(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::Io(err) => err.source(), + Error::Parse(err) => err.source(), + } + } + } + + impl From for Error { + fn from(err: std::io::Error) -> Self { + Error::Io(err) + } + } + + impl From for Error { + fn from(err: crate::parse::gitdir::Error) -> Self { + Error::Parse(err) + } } } diff --git a/gix-discover/src/upwards/types.rs b/gix-discover/src/upwards/types.rs index e5e8de8ddf4..70f17f1733e 100644 --- a/gix-discover/src/upwards/types.rs +++ b/gix-discover/src/upwards/types.rs @@ -1,37 +1,110 @@ use std::{env, ffi::OsStr, path::PathBuf}; /// The error returned by [`gix_discover::upwards()`][crate::upwards()]. -#[derive(Debug, thiserror::Error)] +// TODO(review): the missing space in the `InvalidInput` message ("…\"tries…") replicates the +// `thiserror` attribute text byte-for-byte. +#[derive(Debug)] #[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.")] + CurrentDir(std::io::Error), + InvalidInput { + directory: PathBuf, + }, + InaccessibleDirectory { + path: PathBuf, + }, + NoGitRepository { + path: PathBuf, + }, + NoGitRepositoryWithinCeiling { + path: PathBuf, + ceiling_height: usize, + }, + NoGitRepositoryWithinFs { + path: PathBuf, + limit: PathBuf, + }, 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, }, } +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::CurrentDir(_) => f.write_str("Could not obtain the current working directory"), + Error::InvalidInput { directory } => write!( + f, + "Relative path \"{}\"tries to reach beyond root filesystem", + directory.display() + ), + Error::InaccessibleDirectory { path } => write!( + f, + "Failed to access a directory, or path is not a directory: '{}'", + path.display() + ), + Error::NoGitRepository { path } => write!( + f, + "Could not find a git repository in '{}' or in any of its parents", + path.display() + ), + Error::NoGitRepositoryWithinCeiling { path, ceiling_height } => write!( + f, + "Could not find a git repository in '{}' or in any of its parents within ceiling height of {}", + path.display(), + ceiling_height + ), + Error::NoGitRepositoryWithinFs { path, limit } => write!( + f, + "Could not find a git repository in '{}' or in any of its parents within device limits below '{}'", + path.display(), + limit.display() + ), + Error::NoMatchingCeilingDir => f.write_str( + "None of the passed ceiling directories prefixed the git-dir candidate, making them ineffective.", + ), + Error::NoTrustedGitRepository { path, candidate, .. } => write!( + f, + "Could not find a trusted git repository in '{}' or in any of its parents, candidate at '{}' discarded", + path.display(), + candidate.display() + ), + Error::CheckTrust { path, .. } => { + write!(f, "Could not determine trust level for path '{}'.", path.display()) + } + } + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::CurrentDir(err) => Some(err), + Error::CheckTrust { err, .. } => Some(err), + Error::InvalidInput { .. } + | Error::InaccessibleDirectory { .. } + | Error::NoGitRepository { .. } + | Error::NoGitRepositoryWithinCeiling { .. } + | Error::NoGitRepositoryWithinFs { .. } + | Error::NoMatchingCeilingDir + | Error::NoTrustedGitRepository { .. } => None, + } + } +} + +impl From for Error { + fn from(err: std::io::Error) -> Self { + Error::CurrentDir(err) + } +} + /// 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-filter/Cargo.toml b/gix-filter/Cargo.toml index abc2099f18c..76cb2a2c40b 100644 --- a/gix-filter/Cargo.toml +++ b/gix-filter/Cargo.toml @@ -52,7 +52,6 @@ gix-attributes = { version = "^0.34.0", path = "../gix-attributes" } encoding_rs = "0.8.32" bstr = { version = "1.12.0", default-features = false, features = ["std"] } -thiserror = "2.0.18" smallvec = "1.15.1" diff --git a/gix-filter/src/driver/apply.rs b/gix-filter/src/driver/apply.rs index 931e5bf30c1..e53378fa94a 100644 --- a/gix-filter/src/driver/apply.rs +++ b/gix-filter/src/driver/apply.rs @@ -24,27 +24,63 @@ pub enum Delay { } /// The error returned by [State::apply()][super::State::apply()]. -#[derive(Debug, thiserror::Error)] +// TODO(review): this implementation hand-preserves `#[error(transparent)]` semantics for `Init`: +// `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)] - 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")] + Init(driver::init::Error), + WriteSource(std::io::Error), 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, }, } +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Init(err) => std::fmt::Display::fmt(err, f), + Error::WriteSource(_) => f.write_str("Could not write entire object to driver"), + Error::DelayNotAllowed => f.write_str("Filter process delayed an entry even though that was not requested"), + Error::ProcessInvoke { command, .. } => write!(f, "Failed to invoke '{command}' command"), + Error::ProcessStatus { status, command } => write!( + f, + "The invoked command '{command}' in process indicated an error: {status:?}" + ), + } + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::Init(err) => err.source(), + Error::WriteSource(err) => Some(err), + Error::DelayNotAllowed | Error::ProcessStatus { .. } => None, + Error::ProcessInvoke { source, .. } => Some(source), + } + } +} + +impl From for Error { + fn from(err: driver::init::Error) -> Self { + Error::Init(err) + } +} + +impl From for Error { + fn from(err: std::io::Error) -> Self { + Error::WriteSource(err) + } +} + /// Additional information for use in the [`State::apply()`] method. #[derive(Debug, Copy, Clone)] pub struct Context<'a, 'b> { diff --git a/gix-filter/src/driver/delayed.rs b/gix-filter/src/driver/delayed.rs index c33e1eee114..cda893d3961 100644 --- a/gix-filter/src/driver/delayed.rs +++ b/gix-filter/src/driver/delayed.rs @@ -10,16 +10,45 @@ pub mod list { use crate::driver; /// The error returned by [State::list_delayed_paths()][super::State::list_delayed_paths()]. - #[derive(Debug, thiserror::Error)] + #[derive(Debug)] #[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:?}")] + ProcessInvoke(driver::process::client::invoke::without_content::Error), ProcessStatus { status: driver::process::Status }, } + + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::ProcessMissing { wanted } => write!( + f, + "Could not get process named '{}' which should be running and tracked", + wanted.0 + ), + Error::ProcessInvoke(_) => f.write_str("Failed to run 'list_available_blobs' command"), + Error::ProcessStatus { status } => write!( + f, + "The invoked command 'list_available_blobs' in process indicated an error: {status:?}" + ), + } + } + } + + impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::ProcessMissing { .. } | Error::ProcessStatus { .. } => None, + Error::ProcessInvoke(err) => Some(err), + } + } + } + + impl From for Error { + fn from(err: driver::process::client::invoke::without_content::Error) -> Self { + Error::ProcessInvoke(err) + } + } } /// @@ -27,22 +56,47 @@ pub mod fetch { use crate::driver; /// The error returned by [State::fetch_delayed()][super::State::fetch_delayed()]. - #[derive(Debug, thiserror::Error)] + #[derive(Debug)] #[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")] + ProcessMissing { + wanted: driver::Key, + }, 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, }, } + + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::ProcessMissing { wanted } => write!( + f, + "Could not get process named '{}' which should be running and tracked", + wanted.0 + ), + Error::ProcessInvoke { command, .. } => write!(f, "Failed to run '{command}' command"), + Error::ProcessStatus { status, command } => write!( + f, + "The invoked command '{command}' in process indicated an error: {status:?}" + ), + } + } + } + + impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::ProcessMissing { .. } | Error::ProcessStatus { .. } => None, + Error::ProcessInvoke { source, .. } => Some(source), + } + } + } } /// Operations related to delayed filtering. diff --git a/gix-filter/src/driver/init.rs b/gix-filter/src/driver/init.rs index eaa11ad5a3b..ea2e525035f 100644 --- a/gix-filter/src/driver/init.rs +++ b/gix-filter/src/driver/init.rs @@ -8,21 +8,37 @@ use crate::{ }; /// The error returned by [State::maybe_launch_process()][super::State::maybe_launch_process()]. -#[derive(Debug, thiserror::Error)] +#[derive(Debug)] #[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, }, } +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::SpawnCommand { command, .. } => write!(f, "Failed to spawn driver: {command:?}"), + Error::ProcessHandshake { command, .. } => write!(f, "Process handshake with command {command:?} failed"), + } + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::SpawnCommand { source, .. } => Some(source), + Error::ProcessHandshake { source, .. } => Some(source), + } + } +} + /// Lifecycle impl State { /// Obtain a process as defined in `driver` suitable for a given `operation. `rela_path` may be used to substitute the current diff --git a/gix-filter/src/driver/process/client.rs b/gix-filter/src/driver/process/client.rs index 40594770753..f3fcf46586d 100644 --- a/gix-filter/src/driver/process/client.rs +++ b/gix-filter/src/driver/process/client.rs @@ -11,38 +11,112 @@ use crate::driver::{ /// pub mod handshake { /// The error returned by [Client::handshake()][super::Client::handshake()]. - #[derive(Debug, thiserror::Error)] + #[derive(Debug)] #[expect(missing_docs)] pub enum Error { - #[error("Failed to read or write to the process")] - Io(#[from] std::io::Error), - #[error("{msg} '{actual}'")] + Io(std::io::Error), 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 }, } + + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Io(_) => f.write_str("Failed to read or write to the process"), + Error::Protocol { msg, actual } => write!(f, "{msg} '{actual}'"), + Error::UnsupportedCapability { name } => write!( + f, + "The server sent the '{name}' capability which isn't among the ones we desire can support" + ), + } + } + } + + impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::Io(err) => Some(err), + Error::Protocol { .. } | Error::UnsupportedCapability { .. } => None, + } + } + } + + impl From for Error { + fn from(err: std::io::Error) -> Self { + Error::Io(err) + } + } } /// pub mod invoke { /// The error returned by [Client::invoke()][super::Client::invoke()]. - #[derive(Debug, thiserror::Error)] + #[derive(Debug)] #[expect(missing_docs)] pub enum Error { - #[error("Failed to read or write to the process")] - 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("Failed to read or write to the process"), + } + } + } + + 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) + } } /// pub mod without_content { /// The error returned by [Client::invoke_without_content()][super::super::Client::invoke_without_content()]. - #[derive(Debug, thiserror::Error)] + #[derive(Debug)] #[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), + Io(std::io::Error), + PacketlineDecode(gix_packetline::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("Failed to read or write to the process"), + Error::PacketlineDecode(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::Io(err) => Some(err), + Error::PacketlineDecode(err) => err.source(), + } + } + } + + impl From for Error { + fn from(err: std::io::Error) -> Self { + Error::Io(err) + } + } + + impl From for Error { + fn from(err: gix_packetline::decode::Error) -> Self { + Error::PacketlineDecode(err) + } } impl From for Error { diff --git a/gix-filter/src/driver/process/server.rs b/gix-filter/src/driver/process/server.rs index 1ce68521d6f..8e803039ebf 100644 --- a/gix-filter/src/driver/process/server.rs +++ b/gix-filter/src/driver/process/server.rs @@ -19,31 +19,86 @@ pub mod next_request { use bstr::BString; /// The error returned by [Server::next_request()][super::Server::next_request()]. - #[derive(Debug, thiserror::Error)] + #[derive(Debug)] #[expect(missing_docs)] pub enum Error { - #[error("Failed to read from the client")] - Io(#[from] std::io::Error), - #[error("{msg} '{actual}'")] + Io(std::io::Error), Protocol { msg: String, actual: BString }, - #[error(transparent)] - PacketlineDecode(#[from] gix_packetline::decode::Error), + PacketlineDecode(gix_packetline::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("Failed to read from the client"), + Error::Protocol { msg, actual } => write!(f, "{msg} '{actual}'"), + Error::PacketlineDecode(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::Io(err) => Some(err), + Error::Protocol { .. } => None, + Error::PacketlineDecode(err) => err.source(), + } + } + } + + impl From for Error { + fn from(err: std::io::Error) -> Self { + Error::Io(err) + } + } + + impl From for Error { + fn from(err: gix_packetline::decode::Error) -> Self { + Error::PacketlineDecode(err) + } } } /// pub mod handshake { /// The error returned by [Server::handshake()][super::Server::handshake()]. - #[derive(Debug, thiserror::Error)] + #[derive(Debug)] #[expect(missing_docs)] pub enum Error { - #[error("Failed to read or write to the client")] - Io(#[from] std::io::Error), - #[error("{msg} '{actual}'")] + Io(std::io::Error), 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 }, } + + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Io(_) => f.write_str("Failed to read or write to the client"), + Error::Protocol { msg, actual } => write!(f, "{msg} '{actual}'"), + Error::VersionMismatch { actual } => write!( + f, + "Could not select supported version from the one sent by the client: {}", + actual.iter().map(ToString::to_string).collect::>().join(", ") + ), + } + } + } + + impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::Io(err) => Some(err), + Error::Protocol { .. } | Error::VersionMismatch { .. } => None, + } + } + } + + impl From for Error { + fn from(err: std::io::Error) -> Self { + Error::Io(err) + } + } } impl Server { diff --git a/gix-filter/src/eol/convert_to_git.rs b/gix-filter/src/eol/convert_to_git.rs index eb50924a709..e1afc55ae5d 100644 --- a/gix-filter/src/eol/convert_to_git.rs +++ b/gix-filter/src/eol/convert_to_git.rs @@ -27,15 +27,38 @@ pub enum RoundTripCheck<'a> { } /// The error returned by [convert_to_git()][super::convert_to_git()]. -#[derive(Debug, thiserror::Error)] +#[derive(Debug)] #[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), + FetchObjectFromIndex(Box), + OutOfMemory(std::collections::TryReserveError), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::RoundTrip { msg, path } => write!(f, "{msg} in '{}'", path.display()), + Error::FetchObjectFromIndex(_) => f.write_str("Could not obtain index object to check line endings for"), + Error::OutOfMemory(_) => f.write_str("Could not allocate buffer"), + } + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::RoundTrip { .. } => None, + Error::FetchObjectFromIndex(err) => Some(&**err), + Error::OutOfMemory(err) => Some(err), + } + } +} + +impl From for Error { + fn from(err: std::collections::TryReserveError) -> Self { + Error::OutOfMemory(err) + } } /// 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. diff --git a/gix-filter/src/eol/convert_to_worktree.rs b/gix-filter/src/eol/convert_to_worktree.rs index 206a4673572..b8c4f38a316 100644 --- a/gix-filter/src/eol/convert_to_worktree.rs +++ b/gix-filter/src/eol/convert_to_worktree.rs @@ -6,11 +6,32 @@ use crate::{ }; /// The error produced by [`convert_to_worktree()`]. -#[derive(Debug, thiserror::Error)] +#[derive(Debug)] #[expect(missing_docs)] pub enum Error { - #[error("Could not allocate buffer")] - OutOfMemory(#[from] std::collections::TryReserveError), + OutOfMemory(std::collections::TryReserveError), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::OutOfMemory(_) => f.write_str("Could not allocate buffer"), + } + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::OutOfMemory(err) => Some(err), + } + } +} + +impl From for Error { + fn from(err: std::collections::TryReserveError) -> Self { + Error::OutOfMemory(err) + } } /// Convert all `\n` in `src` to `crlf` if `digest` and `config` indicate it, returning `true` if `buf` holds the result, or `false` diff --git a/gix-filter/src/ident.rs b/gix-filter/src/ident.rs index 4e0d8573fd9..93c3c7eaef7 100644 --- a/gix-filter/src/ident.rs +++ b/gix-filter/src/ident.rs @@ -43,13 +43,41 @@ pub fn undo(src: &[u8], buf: &mut Vec) -> Result) -> std::fmt::Result { + match self { + Error::OutOfMemory(_) => f.write_str("Could not allocate buffer"), + Error::Hasher(_) => f.write_str("Could not hash blob"), + } + } + } + + impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::OutOfMemory(err) => Some(err), + Error::Hasher(err) => Some(err), + } + } + } + + impl From for Error { + fn from(err: std::collections::TryReserveError) -> Self { + Error::OutOfMemory(err) + } + } + + impl From for Error { + fn from(err: gix_hash::hasher::Error) -> Self { + Error::Hasher(err) + } } } diff --git a/gix-filter/src/pipeline/convert.rs b/gix-filter/src/pipeline/convert.rs index 759fa61133b..aa469be1bb4 100644 --- a/gix-filter/src/pipeline/convert.rs +++ b/gix-filter/src/pipeline/convert.rs @@ -9,14 +9,23 @@ pub mod configuration { use bstr::BString; /// Errors related to the configuration of filter attributes. - #[derive(Debug, thiserror::Error)] + #[derive(Debug)] #[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, } + + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::UnknownEncoding { name } => write!(f, "The encoding named '{name}' isn't available"), + Error::InvalidEncoding => f.write_str("Encodings must be names, like UTF-16, and cannot be booleans."), + } + } + } + + impl std::error::Error for Error {} } /// @@ -25,21 +34,83 @@ 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)] + // TODO(review): these implementations hand-preserve `#[error(transparent)]` semantics for the + // first four variants: `Display` passes the formatter through and `source()` + // forwards to the inner error's source, exactly like the `thiserror`-generated + // code did. The same pattern is used in `to_worktree::Error` below, for + // `driver::apply::Error::Init`, and for the `PacketlineDecode` variants in + // `driver::process::client` and `driver::process::server`. + #[derive(Debug)] #[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), + Eol(crate::eol::convert_to_git::Error), + Worktree(crate::worktree::encode_to_git::Error), + Driver(crate::driver::apply::Error), + Configuration(super::configuration::Error), + ReadProcessOutputToBuffer(std::io::Error), + OutOfMemory(std::collections::TryReserveError), + } + + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Eol(err) => std::fmt::Display::fmt(err, f), + Error::Worktree(err) => std::fmt::Display::fmt(err, f), + Error::Driver(err) => std::fmt::Display::fmt(err, f), + Error::Configuration(err) => std::fmt::Display::fmt(err, f), + Error::ReadProcessOutputToBuffer(_) => f.write_str("Copy of driver process output to memory failed"), + Error::OutOfMemory(_) => f.write_str("Could not allocate buffer"), + } + } + } + + impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::Eol(err) => err.source(), + Error::Worktree(err) => err.source(), + Error::Driver(err) => err.source(), + Error::Configuration(err) => err.source(), + Error::ReadProcessOutputToBuffer(err) => Some(err), + Error::OutOfMemory(err) => Some(err), + } + } + } + + impl From for Error { + fn from(err: crate::eol::convert_to_git::Error) -> Self { + Error::Eol(err) + } + } + + impl From for Error { + fn from(err: crate::worktree::encode_to_git::Error) -> Self { + Error::Worktree(err) + } + } + + impl From for Error { + fn from(err: crate::driver::apply::Error) -> Self { + Error::Driver(err) + } + } + + impl From for Error { + fn from(err: super::configuration::Error) -> Self { + Error::Configuration(err) + } + } + + impl From for Error { + fn from(err: std::io::Error) -> Self { + Error::ReadProcessOutputToBuffer(err) + } + } + + impl From for Error { + fn from(err: std::collections::TryReserveError) -> Self { + Error::OutOfMemory(err) + } } } @@ -67,19 +138,68 @@ pub mod to_worktree { } /// The error returned by [Pipeline::convert_to_worktree()][super::Pipeline::convert_to_worktree()]. - #[derive(Debug, thiserror::Error)] + #[derive(Debug)] #[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), + Ident(crate::ident::apply::Error), + Eol(crate::eol::convert_to_worktree::Error), + Worktree(crate::worktree::encode_to_worktree::Error), + Driver(crate::driver::apply::Error), + Configuration(super::configuration::Error), + } + + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Ident(err) => std::fmt::Display::fmt(err, f), + Error::Eol(err) => std::fmt::Display::fmt(err, f), + Error::Worktree(err) => std::fmt::Display::fmt(err, f), + Error::Driver(err) => std::fmt::Display::fmt(err, f), + Error::Configuration(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::Ident(err) => err.source(), + Error::Eol(err) => err.source(), + Error::Worktree(err) => err.source(), + Error::Driver(err) => err.source(), + Error::Configuration(err) => err.source(), + } + } + } + + impl From for Error { + fn from(err: crate::ident::apply::Error) -> Self { + Error::Ident(err) + } + } + + impl From for Error { + fn from(err: crate::eol::convert_to_worktree::Error) -> Self { + Error::Eol(err) + } + } + + impl From for Error { + fn from(err: crate::worktree::encode_to_worktree::Error) -> Self { + Error::Worktree(err) + } + } + + impl From for Error { + fn from(err: crate::driver::apply::Error) -> Self { + Error::Driver(err) + } + } + + impl From for Error { + fn from(err: super::configuration::Error) -> Self { + Error::Configuration(err) + } } } diff --git a/gix-filter/src/worktree/encode_to_git.rs b/gix-filter/src/worktree/encode_to_git.rs index 3937cbeb373..bad151752d4 100644 --- a/gix-filter/src/worktree/encode_to_git.rs +++ b/gix-filter/src/worktree/encode_to_git.rs @@ -8,20 +8,46 @@ pub enum RoundTripCheck { } /// The error returned by [`encode_to_git()][super::encode_to_git()]. -#[derive(Debug, thiserror::Error)] +#[derive(Debug)] #[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")] + Overflow { + input_len: usize, + }, + Malformed { + encoding: &'static str, + }, RoundTrip { src_encoding: &'static str, dest_encoding: &'static str, }, } +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Overflow { input_len } => { + write!( + f, + "Cannot convert input of {input_len} bytes to UTF-8 without overflowing" + ) + } + Error::Malformed { encoding } => { + write!(f, "The input was malformed and could not be decoded as '{encoding}'") + } + Error::RoundTrip { + src_encoding, + dest_encoding, + } => write!( + f, + "Encoding from '{src_encoding}' to '{dest_encoding}' and back is not the same" + ), + } + } +} + +impl std::error::Error for Error {} + pub(crate) mod function { use encoding_rs::DecoderResult; diff --git a/gix-filter/src/worktree/encode_to_worktree.rs b/gix-filter/src/worktree/encode_to_worktree.rs index cd5915c44e1..45b1334f3e8 100644 --- a/gix-filter/src/worktree/encode_to_worktree.rs +++ b/gix-filter/src/worktree/encode_to_worktree.rs @@ -1,18 +1,51 @@ /// The error returned by [`encode_to_worktree()][super::encode_to_worktree()]. -#[derive(Debug, thiserror::Error)] +#[derive(Debug)] #[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}")] + Overflow { + input_len: usize, + }, + InputAsUtf8(std::str::Utf8Error), Unmappable { character: char, worktree_encoding: &'static str, }, } +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Overflow { input_len } => write!( + f, + "Cannot convert input of {input_len} UTF-8 bytes to target encoding without overflowing" + ), + Error::InputAsUtf8(_) => f.write_str("Input was not UTF-8 encoded"), + Error::Unmappable { + character, + worktree_encoding, + } => write!( + f, + "The character '{character}' could not be mapped to the {worktree_encoding}" + ), + } + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::Overflow { .. } | Error::Unmappable { .. } => None, + Error::InputAsUtf8(err) => Some(err), + } + } +} + +impl From for Error { + fn from(err: std::str::Utf8Error) -> Self { + Error::InputAsUtf8(err) + } +} + pub(crate) mod function { use encoding_rs::EncoderResult; diff --git a/gix-filter/src/worktree/encoding.rs b/gix-filter/src/worktree/encoding.rs index ae1dbc2062a..fc4886289d6 100644 --- a/gix-filter/src/worktree/encoding.rs +++ b/gix-filter/src/worktree/encoding.rs @@ -6,12 +6,21 @@ pub mod for_label { use bstr::BString; /// The error returned by [for_label()][super::for_label()]. - #[derive(Debug, thiserror::Error)] + #[derive(Debug)] #[expect(missing_docs)] pub enum Error { - #[error("An encoding named '{name}' is not known")] Unknown { 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, "An encoding named '{name}' is not known"), + } + } + } + + impl std::error::Error for Error {} } /// Try to produce a new `Encoding` for `label` or report an error if it is not known. diff --git a/gix-fs/Cargo.toml b/gix-fs/Cargo.toml index c8dbe7274e6..37f57035315 100644 --- a/gix-fs/Cargo.toml +++ b/gix-fs/Cargo.toml @@ -23,7 +23,6 @@ bstr = "1.12.0" gix-path = { version = "^0.12.3", path = "../gix-path" } gix-features = { version = "^0.49.0", path = "../gix-features", features = ["fs-read-dir"] } gix-utils = { version = "^0.3.5", path = "../gix-utils" } -thiserror = "2.0.18" serde = { version = "1.0.114", optional = true, default-features = false, features = ["std", "derive"] } [dev-dependencies] diff --git a/gix-fs/src/stack.rs b/gix-fs/src/stack.rs index 771a5e83dd2..c68ca9b25db 100644 --- a/gix-fs/src/stack.rs +++ b/gix-fs/src/stack.rs @@ -12,14 +12,27 @@ pub mod to_normal_path_components { use std::path::PathBuf; /// The error used in [`ToNormalPathComponents::to_normal_path_components()`](super::ToNormalPathComponents::to_normal_path_components()). - #[derive(Debug, thiserror::Error)] + #[derive(Debug)] #[expect(missing_docs)] pub enum Error { - #[error("Input path \"{path}\" contains relative or absolute components", path = .0.display())] NotANormalComponent(PathBuf), - #[error("Could not convert to UTF8 or from UTF8 due to ill-formed input")] IllegalUtf8, } + + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::NotANormalComponent(path) => write!( + f, + "Input path \"{path}\" contains relative or absolute components", + path = path.display() + ), + Error::IllegalUtf8 => f.write_str("Could not convert to UTF8 or from UTF8 due to ill-formed input"), + } + } + } + + impl std::error::Error for Error {} } /// Obtain an iterator over `OsStr`-components which are normal, none-relative and not absolute. diff --git a/gix-hash/Cargo.toml b/gix-hash/Cargo.toml index b7dae20b000..4a82d9f7286 100644 --- a/gix-hash/Cargo.toml +++ b/gix-hash/Cargo.toml @@ -27,7 +27,6 @@ serde = ["dep:serde", "faster-hex/serde"] [dependencies] gix-features = { version = "^0.49.0", path = "../gix-features", features = ["progress"] } -thiserror = "2.0.18" faster-hex = { version = "0.10.0", default-features = false, features = ["std"] } serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"] } sha1-checked = { version = "0.10.0", optional = true, default-features = false } diff --git a/gix-hash/src/hasher.rs b/gix-hash/src/hasher.rs index e577256f834..4725b29e9ad 100644 --- a/gix-hash/src/hasher.rs +++ b/gix-hash/src/hasher.rs @@ -1,11 +1,20 @@ /// The error returned by [`Hasher::try_finalize()`](crate::Hasher::try_finalize()). -#[derive(Debug, thiserror::Error)] +#[derive(Debug)] #[expect(missing_docs)] pub enum Error { - #[error("Detected SHA-1 collision attack with digest {digest}")] CollisionAttack { digest: crate::ObjectId }, } +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::CollisionAttack { digest } => write!(f, "Detected SHA-1 collision attack with digest {digest}"), + } + } +} + +impl std::error::Error for Error {} + pub(super) mod _impl { #[cfg(feature = "sha1")] use sha1_checked::{CollisionResult, Digest}; @@ -75,7 +84,7 @@ pub(super) mod _impl { // // As of Rust 1.84.1, the compiler can’t figure out // this function cannot panic without this. - #[expect(unsafe_code)] + #[allow(unsafe_code)] unsafe { std::hint::unreachable_unchecked() } diff --git a/gix-hash/src/io.rs b/gix-hash/src/io.rs index dd534d067d8..895a297d0fe 100644 --- a/gix-hash/src/io.rs +++ b/gix-hash/src/io.rs @@ -1,13 +1,44 @@ use crate::hasher; /// The error type for I/O operations that compute hashes. -#[derive(Debug, thiserror::Error)] +#[derive(Debug)] #[expect(missing_docs)] pub enum Error { - #[error(transparent)] - Io(#[from] std::io::Error), - #[error("Failed to hash data")] - Hasher(#[from] hasher::Error), + Io(std::io::Error), + Hasher(hasher::Error), +} + +// TODO(review): these implementations hand-preserve `#[error(transparent)]` semantics for `Io`: +// `Display` passes the formatter through and `source()` forwards to the inner +// error's source, exactly like the `thiserror`-generated code did. +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::Hasher(_) => f.write_str("Failed to hash data"), + } + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::Io(err) => err.source(), + Error::Hasher(err) => Some(err), + } + } +} + +impl From for Error { + fn from(err: std::io::Error) -> Self { + Error::Io(err) + } +} + +impl From for Error { + fn from(err: hasher::Error) -> Self { + Error::Hasher(err) + } } pub(super) mod _impl { diff --git a/gix-hash/src/object_id.rs b/gix-hash/src/object_id.rs index c81ba2cacd4..28ff1886bdf 100644 --- a/gix-hash/src/object_id.rs +++ b/gix-hash/src/object_id.rs @@ -31,6 +31,7 @@ pub enum ObjectId { // extremely unlikely to begin with so it doesn't matter. // This implementation matches the `Hash` implementation for `oid` // and allows the usage of custom Hashers that only copy a truncated ShaHash +#[allow(clippy::derived_hash_with_manual_eq)] impl Hash for ObjectId { fn hash(&self, state: &mut H) { state.write(self.as_slice()); @@ -50,15 +51,26 @@ pub mod decode { use crate::{SIZE_OF_SHA256_DIGEST, SIZE_OF_SHA256_HEX_DIGEST}; /// An error returned by [`ObjectId::from_hex()`][crate::ObjectId::from_hex()] - #[derive(Debug, thiserror::Error)] + #[derive(Debug)] #[expect(missing_docs)] pub enum Error { - #[error("A hash sized {0} hexadecimal characters is invalid")] InvalidHexEncodingLength(usize), - #[error("Invalid character encountered")] Invalid, } + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::InvalidHexEncodingLength(len) => { + write!(f, "A hash sized {len} hexadecimal characters is invalid") + } + Error::Invalid => f.write_str("Invalid character encountered"), + } + } + } + + impl std::error::Error for Error {} + /// Hash decoding impl ObjectId { /// Create an instance from a `buffer` of 40 bytes or 64 bytes encoded with hexadecimal diff --git a/gix-hash/src/oid.rs b/gix-hash/src/oid.rs index de048e67568..ca63192d09a 100644 --- a/gix-hash/src/oid.rs +++ b/gix-hash/src/oid.rs @@ -22,7 +22,7 @@ use crate::{EMPTY_BLOB_SHA256, EMPTY_TREE_SHA256, SIZE_OF_SHA256_DIGEST}; /// than 64 bytes. #[derive(PartialEq, Eq, Ord, PartialOrd)] #[repr(transparent)] -#[expect(non_camel_case_types, reason = "the name mirrors 'str'")] +#[allow(non_camel_case_types)] #[cfg_attr(feature = "serde", derive(serde::Serialize))] pub struct oid { bytes: [u8], @@ -33,6 +33,7 @@ pub struct oid { // it attempting to hash the length of the slice first. On 32 bit systems // this can lead to issues with the custom `gix_hashtable` `Hasher` implementation, // and it currently ends up being discarded there anyway. +#[allow(clippy::derived_hash_with_manual_eq)] impl hash::Hash for oid { fn hash(&self, state: &mut H) { state.write(self.as_bytes()); @@ -73,12 +74,23 @@ impl std::fmt::Debug for oid { /// The error returned when trying to convert a byte slice to an [`oid`] or [`ObjectId`] #[expect(missing_docs)] -#[derive(Debug, thiserror::Error)] +#[derive(Debug)] pub enum Error { - #[error("Cannot instantiate git hash from a digest of length {0}")] InvalidByteSliceLength(usize), } +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::InvalidByteSliceLength(len) => { + write!(f, "Cannot instantiate git hash from a digest of length {len}") + } + } + } +} + +impl std::error::Error for Error {} + /// Conversion impl oid { /// Try to create a shared object id from a slice of bytes representing a hash `digest` @@ -87,14 +99,14 @@ impl oid { match digest.len() { #[cfg(feature = "sha1")] SIZE_OF_SHA1_DIGEST => Ok( - #[expect(unsafe_code)] + #[allow(unsafe_code)] unsafe { &*(std::ptr::from_ref::<[u8]>(digest) as *const oid) }, ), #[cfg(feature = "sha256")] SIZE_OF_SHA256_DIGEST => Ok( - #[expect(unsafe_code)] + #[allow(unsafe_code)] unsafe { &*(std::ptr::from_ref::<[u8]>(digest) as *const oid) }, @@ -111,7 +123,7 @@ impl oid { /// Only from code that statically assures correct sizes using array conversions. pub(crate) fn from_bytes(value: &[u8]) -> &Self { - #[expect(unsafe_code)] + #[allow(unsafe_code)] unsafe { &*(std::ptr::from_ref::<[u8]>(value) as *const oid) } diff --git a/gix-hash/src/prefix.rs b/gix-hash/src/prefix.rs index cff37e7df3b..56f4a1ff581 100644 --- a/gix-hash/src/prefix.rs +++ b/gix-hash/src/prefix.rs @@ -3,34 +3,62 @@ use std::cmp::Ordering; use crate::{ObjectId, Prefix, oid}; /// The error returned by [`Prefix::new()`]. -#[derive(Debug, thiserror::Error)] +#[derive(Debug)] #[expect(missing_docs)] pub enum Error { - #[error( - "The minimum hex length of a short object id is {}, got {hex_len}", - Prefix::MIN_HEX_LEN - )] TooShort { hex_len: usize }, - #[error("An object of kind {object_kind} cannot be larger than {} in hex, but {hex_len} was requested", object_kind.len_in_hex())] TooLong { object_kind: crate::Kind, hex_len: usize }, } +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::TooShort { hex_len } => write!( + f, + "The minimum hex length of a short object id is {}, got {hex_len}", + Prefix::MIN_HEX_LEN + ), + Error::TooLong { object_kind, hex_len } => write!( + f, + "An object of kind {object_kind} cannot be larger than {} in hex, but {hex_len} was requested", + object_kind.len_in_hex() + ), + } + } +} + +impl std::error::Error for Error {} + /// pub mod from_hex { /// The error returned by [`Prefix::from_hex`][super::Prefix::from_hex()]. - #[derive(Debug, Eq, PartialEq, thiserror::Error)] + #[derive(Debug, Eq, PartialEq)] #[expect(missing_docs)] pub enum Error { - #[error( - "The minimum hex length of a short object id is {}, got {hex_len}", - super::Prefix::MIN_HEX_LEN - )] TooShort { hex_len: usize }, - #[error("An id cannot be larger than {} chars in hex, but {hex_len} was requested", crate::Kind::longest().len_in_hex())] TooLong { hex_len: usize }, - #[error("Invalid hex character")] Invalid, } + + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::TooShort { hex_len } => write!( + f, + "The minimum hex length of a short object id is {}, got {hex_len}", + super::Prefix::MIN_HEX_LEN + ), + Error::TooLong { hex_len } => write!( + f, + "An id cannot be larger than {} chars in hex, but {hex_len} was requested", + crate::Kind::longest().len_in_hex() + ), + Error::Invalid => f.write_str("Invalid hex character"), + } + } + } + + impl std::error::Error for Error {} } impl Prefix { diff --git a/gix-hash/src/verify.rs b/gix-hash/src/verify.rs index 24f33d39277..3bb2bc42880 100644 --- a/gix-hash/src/verify.rs +++ b/gix-hash/src/verify.rs @@ -1,14 +1,21 @@ use crate::{ObjectId, oid}; /// The error returned by [`oid::verify()`]. -#[derive(Debug, thiserror::Error)] +#[derive(Debug)] #[expect(missing_docs)] -#[error("Hash was {actual}, but should have been {expected}")] pub struct Error { pub actual: ObjectId, pub expected: ObjectId, } +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Hash was {}, but should have been {}", self.actual, self.expected) + } +} + +impl std::error::Error for Error {} + impl oid { /// Verify that `self` matches the `expected` object ID. /// diff --git a/gix-index/Cargo.toml b/gix-index/Cargo.toml index 6001b9a8e64..c5142471c98 100644 --- a/gix-index/Cargo.toml +++ b/gix-index/Cargo.toml @@ -47,7 +47,6 @@ gix-utils = { version = "^0.3.5", 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/header.rs b/gix-index/src/decode/header.rs index 8de157edb01..22d60c14863 100644 --- a/gix-index/src/decode/header.rs +++ b/gix-index/src/decode/header.rs @@ -7,14 +7,23 @@ 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)] + #[derive(Debug)] + #[allow(missing_docs)] pub enum Error { - #[error("{0}")] Corrupt(&'static str), - #[error("Index version {0} is not supported")] UnsupportedVersion(u32), } + + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Corrupt(msg) => f.write_str(msg), + Error::UnsupportedVersion(version) => write!(f, "Index version {version} is not supported"), + } + } + } + + impl std::error::Error for Error {} } pub use error::Error; diff --git a/gix-index/src/decode/mod.rs b/gix-index/src/decode/mod.rs index 8c2c23b22b1..a92520ef17b 100644 --- a/gix-index/src/decode/mod.rs +++ b/gix-index/src/decode/mod.rs @@ -11,23 +11,76 @@ mod error { use std::collections::TryReserveError; /// The error returned by [`State::from_bytes()`][crate::State::from_bytes()]. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] + // TODO(review): this implementation hand-preserves `#[error(transparent)]` semantics for `Header`: + // `Display` passes the formatter through and `source()` forwards to the inner error's + // source, exactly like the `thiserror`-generated code did. The same pattern is used + // for the transparent variants in `file::init`, `file::write`, `verify::extensions`, + // `init::from_tree`, and `extension::tree::verify`. + #[derive(Debug)] + #[allow(missing_docs)] pub enum Error { - #[error(transparent)] - Header(#[from] decode::header::Error), - #[error("Could not hash index data")] - Hasher(#[from] gix_hash::hasher::Error), - #[error("Index data would require more memory than can be reserved")] + Header(decode::header::Error), + Hasher(gix_hash::hasher::Error), 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}")] + Extension(extension::decode::Error), UnexpectedTrailerLength { expected: usize, actual: usize }, - #[error("Shared index checksum mismatch")] - Verify(#[from] gix_hash::verify::Error), + Verify(gix_hash::verify::Error), + } + + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Header(err) => std::fmt::Display::fmt(err, f), + Error::Hasher(_) => f.write_str("Could not hash index data"), + Error::OutOfMemory => f.write_str("Index data would require more memory than can be reserved"), + Error::Entry { index } => write!(f, "Could not parse entry at index {index}"), + Error::Extension(_) => f.write_str("Mandatory extension wasn't implemented or malformed."), + Error::UnexpectedTrailerLength { expected, actual } => { + write!( + f, + "Index trailer should have been {expected} bytes long, but was {actual}" + ) + } + Error::Verify(_) => f.write_str("Shared index checksum mismatch"), + } + } + } + + impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::Header(err) => err.source(), + Error::Hasher(err) => Some(err), + Error::Extension(err) => Some(err), + Error::Verify(err) => Some(err), + Error::OutOfMemory | Error::Entry { .. } | Error::UnexpectedTrailerLength { .. } => None, + } + } + } + + impl From for Error { + fn from(err: decode::header::Error) -> Self { + Error::Header(err) + } + } + + impl From for Error { + fn from(err: gix_hash::hasher::Error) -> Self { + Error::Hasher(err) + } + } + + impl From for Error { + fn from(err: extension::decode::Error) -> Self { + Error::Extension(err) + } + } + + impl From for Error { + fn from(err: gix_hash::verify::Error) -> Self { + Error::Verify(err) + } } impl From for Error { diff --git a/gix-index/src/extension/decode.rs b/gix-index/src/extension/decode.rs index 5367373bc30..f46f95c0878 100644 --- a/gix-index/src/extension/decode.rs +++ b/gix-index/src/extension/decode.rs @@ -10,16 +10,39 @@ mod error { use crate::extension; /// The error returned when decoding extensions. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] + #[derive(Debug)] + #[allow(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), + Link(extension::link::decode::Error), + } + + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::MandatoryUnimplemented { signature } => write!( + f, + "Encountered mandatory extension '{}' which isn't implemented yet", + String::from_utf8_lossy(signature) + ), + Error::Link(_) => f.write_str("Could not parse mandatory link extension"), + } + } + } + + impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::MandatoryUnimplemented { .. } => None, + Error::Link(err) => Some(err), + } + } + } + + impl From for Error { + fn from(err: extension::link::decode::Error) -> Self { + Error::Link(err) + } } } pub use error::Error; diff --git a/gix-index/src/extension/link.rs b/gix-index/src/extension/link.rs index 463e37e9f04..3921a350eb1 100644 --- a/gix-index/src/extension/link.rs +++ b/gix-index/src/extension/link.rs @@ -16,18 +16,30 @@ pub struct Bitmaps { pub mod decode { /// The error returned when decoding link extensions. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] + // TODO(review): `BitmapDecode`'s `err` field is not named `source` and carries no `#[source]`, + // so `thiserror` did not treat it as a source; `source()` returning `None` for it + // is preserved behavior. + #[derive(Debug)] + #[allow(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 std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Corrupt(msg) => f.write_str(msg), + Error::BitmapDecode { kind, .. } => write!(f, "{kind} bitmap corrupt"), + } + } + } + + impl std::error::Error for Error {} + impl From for Error { fn from(_: std::num::TryFromIntError) -> Self { Self::Corrupt("error in bitmap iteration trying to convert from u64 to usize") diff --git a/gix-index/src/extension/tree/verify.rs b/gix-index/src/extension/tree/verify.rs index 46285d83b7e..0cd33de8813 100644 --- a/gix-index/src/extension/tree/verify.rs +++ b/gix-index/src/extension/tree/verify.rs @@ -6,42 +6,32 @@ 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)] +#[derive(Debug)] +#[allow(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(#[from] gix_object::find::existing_iter::Error), - #[error( - "The tree with id {oid} should have {expected_childcount} children, but its cached representation had {actual_childcount} of them" - )] + TreeNodeNotFound(gix_object::find::existing_iter::Error), 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("TREE entry '{name}' declared {actual} entries, but the index only contains {expected} entries")] + RootWithName { + name: BString, + }, + EntriesCount { + actual: u32, + expected: u32, + }, 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, @@ -49,6 +39,69 @@ pub enum Error { }, } +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::MissingTreeDirectory { + parent_id, + entry_id, + name, + } => write!( + f, + "The entry {entry_id} at path '{name}' in parent tree {parent_id} wasn't found in the nodes children, making it incomplete" + ), + Error::TreeNodeNotFound(err) => std::fmt::Display::fmt(err, f), + Error::TreeNodeChildcountMismatch { + oid, + expected_childcount, + actual_childcount, + } => write!( + f, + "The tree with id {oid} should have {expected_childcount} children, but its cached representation had {actual_childcount} of them" + ), + Error::RootWithName { name } => { + write!(f, "The root tree was named '{name}', even though it should be empty") + } + Error::EntriesCount { actual, expected } => write!( + f, + "Expected not more than {expected} entries to be reachable from the top-level, but actual count was {actual}" + ), + Error::EntriesCountExceedsIndex { name, actual, expected } => write!( + f, + "TREE entry '{name}' declared {actual} entries, but the index only contains {expected} entries" + ), + Error::OutOfOrder { + parent_id, + current_path, + previous_path, + } => write!( + f, + "Parent tree '{parent_id}' contained out-of order trees prev = '{previous_path}' and next = '{current_path}'" + ), + } + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::TreeNodeNotFound(err) => err.source(), + Error::MissingTreeDirectory { .. } + | Error::TreeNodeChildcountMismatch { .. } + | Error::RootWithName { .. } + | Error::EntriesCount { .. } + | Error::EntriesCountExceedsIndex { .. } + | Error::OutOfOrder { .. } => None, + } + } +} + +impl From for Error { + fn from(err: gix_object::find::existing_iter::Error) -> Self { + Error::TreeNodeNotFound(err) + } +} + 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> { @@ -100,6 +153,7 @@ impl Tree { } for child in children { // This is actually needed here as it's a mut ref, which isn't copy. We do a re-borrow here. + #[allow(clippy::needless_option_as_deref)] let actual_num_entries = verify_recursive(child.id, &child.children, object_buf.as_deref_mut(), objects)?; if let Some((actual, num_entries)) = actual_num_entries.zip(child.num_entries) { diff --git a/gix-index/src/file/init.rs b/gix-index/src/file/init.rs index 9409b85d783..ce7a813f06e 100644 --- a/gix-index/src/file/init.rs +++ b/gix-index/src/file/init.rs @@ -7,15 +7,50 @@ use crate::{File, State, decode, extension}; mod error { /// The error returned by [File::at()][super::File::at()]. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] + #[derive(Debug)] + #[allow(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), + Io(std::io::Error), + Decode(crate::decode::Error), + LinkExtension(crate::extension::link::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("An IO error occurred while opening the index"), + Error::Decode(err) => std::fmt::Display::fmt(err, f), + Error::LinkExtension(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::Io(err) => Some(err), + Error::Decode(err) => err.source(), + Error::LinkExtension(err) => err.source(), + } + } + } + + impl From for Error { + fn from(err: std::io::Error) -> Self { + Error::Io(err) + } + } + + impl From for Error { + fn from(err: crate::decode::Error) -> Self { + Error::Decode(err) + } + } + + impl From for Error { + fn from(err: crate::extension::link::decode::Error) -> Self { + Error::LinkExtension(err) + } } } @@ -61,7 +96,7 @@ impl File { let (data, mtime) = { let mut 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)] let data = unsafe { memmap2::MmapOptions::new().map_copy_read_only(&file)? }; if !skip_hash { diff --git a/gix-index/src/file/verify.rs b/gix-index/src/file/verify.rs index 2ed9ac49c1f..edfcac13fb6 100644 --- a/gix-index/src/file/verify.rs +++ b/gix-index/src/file/verify.rs @@ -4,13 +4,41 @@ use crate::File; mod error { /// The error returned by [File::verify_integrity()][super::File::verify_integrity()]. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] + #[derive(Debug)] + #[allow(missing_docs)] pub enum Error { - #[error("Could not read index file to generate hash")] - Io(#[from] gix_hash::io::Error), - #[error("Index checksum mismatch")] - Verify(#[from] gix_hash::verify::Error), + Io(gix_hash::io::Error), + Verify(gix_hash::verify::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("Could not read index file to generate hash"), + Error::Verify(_) => f.write_str("Index checksum mismatch"), + } + } + } + + impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::Io(err) => Some(err), + Error::Verify(err) => Some(err), + } + } + } + + impl From for Error { + fn from(err: gix_hash::io::Error) -> Self { + Error::Io(err) + } + } + + impl From for Error { + fn from(err: gix_hash::verify::Error) -> Self { + Error::Verify(err) + } } } pub use error::Error; diff --git a/gix-index/src/file/write.rs b/gix-index/src/file/write.rs index 01f9c247462..fd7d74be609 100644 --- a/gix-index/src/file/write.rs +++ b/gix-index/src/file/write.rs @@ -1,15 +1,50 @@ use crate::{File, Version, write}; /// The error produced by [`File::write()`]. -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] +#[derive(Debug)] +#[allow(missing_docs)] pub enum Error { - #[error(transparent)] - Io(#[from] gix_hash::io::Error), - #[error("Could not acquire lock for index file")] - AcquireLock(#[from] gix_lock::acquire::Error), - #[error("Could not commit lock for index file")] - CommitLock(#[from] gix_lock::commit::Error), + Io(gix_hash::io::Error), + AcquireLock(gix_lock::acquire::Error), + CommitLock(gix_lock::commit::Error), +} + +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::AcquireLock(_) => f.write_str("Could not acquire lock for index file"), + Error::CommitLock(_) => f.write_str("Could not commit lock for index file"), + } + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::Io(err) => err.source(), + Error::AcquireLock(err) => Some(err), + Error::CommitLock(err) => Some(err), + } + } +} + +impl From for Error { + fn from(err: gix_hash::io::Error) -> Self { + Error::Io(err) + } +} + +impl From for Error { + fn from(err: gix_lock::acquire::Error) -> Self { + Error::AcquireLock(err) + } +} + +impl From> for Error { + fn from(err: gix_lock::commit::Error) -> Self { + Error::CommitLock(err) + } } impl File { diff --git a/gix-index/src/init.rs b/gix-index/src/init.rs index b1d6519cd18..0478693d167 100644 --- a/gix-index/src/init.rs +++ b/gix-index/src/init.rs @@ -12,16 +12,38 @@ pub mod from_tree { }; /// The error returned by [State::from_tree()]. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] + #[derive(Debug)] + #[allow(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_traverse::tree::depthfirst::Error), + Traversal(gix_traverse::tree::depthfirst::Error), + } + + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::InvalidComponent { path, .. } => write!(f, "The path \"{path}\" is invalid"), + Error::Traversal(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::InvalidComponent { source, .. } => Some(source), + Error::Traversal(err) => err.source(), + } + } + } + + impl From for Error { + fn from(err: gix_traverse::tree::depthfirst::Error) -> Self { + Error::Traversal(err) + } } /// Initialization diff --git a/gix-index/src/verify.rs b/gix-index/src/verify.rs index 84f52a0f91d..dfb6e92b372 100644 --- a/gix-index/src/verify.rs +++ b/gix-index/src/verify.rs @@ -7,12 +7,9 @@ pub mod entries { use bstr::BString; /// The error returned by [`State::verify_entries()`][crate::State::verify_entries()]. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] + #[derive(Debug)] + #[allow(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, @@ -21,6 +18,25 @@ pub mod entries { previous_stage: u8, }, } + + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::OutOfOrder { + current_index, + current_path, + current_stage, + previous_path, + previous_stage, + } => write!( + f, + "Entry '{current_path}' (stage = {current_stage}) at index {current_index} should order after prior entry '{previous_path}' (stage = {previous_stage})" + ), + } + } + } + + impl std::error::Error for Error {} } /// @@ -28,11 +44,32 @@ pub mod extensions { use crate::extension; /// The error returned by [`State::verify_extensions()`][crate::State::verify_extensions()]. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] + #[derive(Debug)] + #[allow(missing_docs)] pub enum Error { - #[error(transparent)] - Tree(#[from] extension::tree::verify::Error), + Tree(extension::tree::verify::Error), + } + + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Tree(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::Tree(err) => err.source(), + } + } + } + + impl From for Error { + fn from(err: extension::tree::verify::Error) -> Self { + Error::Tree(err) + } } } diff --git a/gix-lock/Cargo.toml b/gix-lock/Cargo.toml index eb42581771f..9e1965255d2 100644 --- a/gix-lock/Cargo.toml +++ b/gix-lock/Cargo.toml @@ -18,7 +18,6 @@ test = true [dependencies] gix-utils = { version = "^0.3.5", default-features = false, path = "../gix-utils" } gix-tempfile = { version = "^24.0.0", default-features = false, path = "../gix-tempfile" } -thiserror = "2.0.18" [dev-dependencies] tempfile = "3.26.0" diff --git a/gix-lock/src/acquire.rs b/gix-lock/src/acquire.rs index 4e87cf83434..9a48b5342b1 100644 --- a/gix-lock/src/acquire.rs +++ b/gix-lock/src/acquire.rs @@ -41,15 +41,10 @@ impl From for Fail { } /// The error returned when acquiring a [`File`] or [`Marker`]. -#[derive(Debug, thiserror::Error)] +#[derive(Debug)] #[expect(missing_docs)] pub enum Error { - #[error("Another IO error occurred while obtaining the lock")] - Io(#[from] std::io::Error), - #[error( - "The lock for resource '{resource_path}' could not be obtained {mode} after {attempts} attempt(s). The lockfile at '{resource_path}{}' might need manual deletion.", - super::DOT_LOCK_SUFFIX - )] + Io(std::io::Error), PermanentlyLocked { resource_path: PathBuf, mode: Fail, @@ -57,6 +52,39 @@ pub enum Error { }, } +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Error::Io(_) => f.write_str("Another IO error occurred while obtaining the lock"), + Error::PermanentlyLocked { + resource_path, + mode, + attempts, + } => write!( + f, + "The lock for resource '{resource_path}' could not be obtained {mode} after {attempts} attempt(s). The lockfile at '{resource_path}{suffix}' might need manual deletion.", + resource_path = resource_path.display(), + suffix = DOT_LOCK_SUFFIX + ), + } + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::Io(err) => Some(err), + Error::PermanentlyLocked { .. } => None, + } + } +} + +impl From for Error { + fn from(err: std::io::Error) -> Self { + Error::Io(err) + } +} + impl File { /// Create a writable lock file with failure `mode` whose content will eventually overwrite the given resource `at_path`. /// diff --git a/gix-merge/Cargo.toml b/gix-merge/Cargo.toml index 4751d44ec9a..a30700412a7 100644 --- a/gix-merge/Cargo.toml +++ b/gix-merge/Cargo.toml @@ -40,7 +40,6 @@ gix-diff = { version = "^0.66.0", path = "../gix-diff", default-features = false gix-index = { version = "^0.54.0", path = "../gix-index" } imara-diff = { package = "gix-imara-diff", version = "^0.2.4", 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/src/blob/pipeline.rs b/gix-merge/src/blob/pipeline.rs index 77244309041..dc4de11d501 100644 --- a/gix-merge/src/blob/pipeline.rs +++ b/gix-merge/src/blob/pipeline.rs @@ -118,25 +118,86 @@ pub mod convert_to_mergeable { 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)] + // TODO(review): hand-written impls preserve the `thiserror` semantics. `FindObject`, + // `ConvertToWorktree` and `ConvertToGit` are `#[error(transparent)]`: `Display` and + // `source()` forward to the wrapped error. `ReadLink`/`OpenOrRead`/`StreamCopy` + // expose their named `source` field; `OutOfMemory` exposes its `#[from]` error; + // `InvalidEntryKind` has no source. + #[derive(Debug)] + #[allow(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_object::find::existing_object::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), + FindObject(gix_object::find::existing_object::Error), + ConvertToWorktree(gix_filter::pipeline::convert::to_worktree::Error), + ConvertToGit(gix_filter::pipeline::convert::to_git::Error), + OutOfMemory(TryReserveError), + } + + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::InvalidEntryKind { rela_path, actual } => write!( + f, + "Entry at '{rela_path}' must be regular file or symlink, but was {actual:?}" + ), + Error::ReadLink { rela_path, .. } => { + write!(f, "Entry at '{rela_path}' could not be read as symbolic link") + } + Error::OpenOrRead { rela_path, .. } => { + write!(f, "Entry at '{rela_path}' could not be opened for reading or read from") + } + Error::StreamCopy { rela_path, .. } => write!( + f, + "Entry at '{rela_path}' could not be copied from a filter process to a memory buffer" + ), + Error::FindObject(err) => std::fmt::Display::fmt(err, f), + Error::ConvertToWorktree(err) => std::fmt::Display::fmt(err, f), + Error::ConvertToGit(err) => std::fmt::Display::fmt(err, f), + Error::OutOfMemory(_) => f.write_str("Memory allocation failed"), + } + } + } + + impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::ReadLink { source, .. } => Some(source), + Error::OpenOrRead { source, .. } => Some(source), + Error::StreamCopy { source, .. } => Some(source), + Error::FindObject(err) => err.source(), + Error::ConvertToWorktree(err) => err.source(), + Error::ConvertToGit(err) => err.source(), + Error::OutOfMemory(err) => Some(err), + Error::InvalidEntryKind { .. } => None, + } + } + } + + impl From for Error { + fn from(err: gix_object::find::existing_object::Error) -> Self { + Error::FindObject(err) + } + } + + impl From for Error { + fn from(err: gix_filter::pipeline::convert::to_worktree::Error) -> Self { + Error::ConvertToWorktree(err) + } + } + + impl From for Error { + fn from(err: gix_filter::pipeline::convert::to_git::Error) -> Self { + Error::ConvertToGit(err) + } + } + + impl From for Error { + fn from(err: TryReserveError) -> Self { + Error::OutOfMemory(err) + } } } @@ -161,7 +222,7 @@ impl Pipeline { /// Only blobs are allowed. /// /// Use `convert` to control what kind of the resource will be produced. - #[expect(clippy::too_many_arguments)] + #[allow(clippy::too_many_arguments)] pub fn convert_to_mergeable( &mut self, id: &gix_hash::oid, diff --git a/gix-merge/src/blob/platform/merge.rs b/gix-merge/src/blob/platform/merge.rs index 2769962c825..792d88a7fc0 100644 --- a/gix-merge/src/blob/platform/merge.rs +++ b/gix-merge/src/blob/platform/merge.rs @@ -16,20 +16,64 @@ pub struct Options { } /// The error returned by [`PlatformRef::merge()`]. -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] +// TODO(review): hand-written impls preserve the `thiserror` semantics. `PrepareExternalDriver` is +// `#[error(transparent)]`: `Display` and `source()` forward to the wrapped error. +// `SpawnExternalDriver` (named `source`) and `ExternalDriverIO` (`#[from]`) expose their +// error as `source()`; `ExternalDriverFailure` has no source. +#[derive(Debug)] +#[allow(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}")] + PrepareExternalDriver(inner::prepare_external_driver::Error), + SpawnExternalDriver { + cmd: String, + source: std::io::Error, + }, ExternalDriverFailure { status: std::process::ExitStatus, cmd: String, }, - #[error("IO failed when dealing with merge-driver output")] - ExternalDriverIO(#[from] std::io::Error), + ExternalDriverIO(std::io::Error), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::PrepareExternalDriver(err) => std::fmt::Display::fmt(err, f), + Error::SpawnExternalDriver { cmd, .. } => { + write!(f, "Failed to launch external merge driver: {cmd}") + } + Error::ExternalDriverFailure { status, cmd } => { + write!( + f, + "External merge driver failed with non-zero exit status {status:?}: {cmd}" + ) + } + Error::ExternalDriverIO(_) => f.write_str("IO failed when dealing with merge-driver output"), + } + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::PrepareExternalDriver(err) => err.source(), + Error::SpawnExternalDriver { source, .. } => Some(source), + Error::ExternalDriverIO(err) => Some(err), + Error::ExternalDriverFailure { .. } => None, + } + } +} + +impl From for Error { + fn from(err: inner::prepare_external_driver::Error) -> Self { + Error::PrepareExternalDriver(err) + } +} + +impl From for Error { + fn from(err: std::io::Error) -> Self { + Error::ExternalDriverIO(err) + } } /// The product of a [`PlatformRef::prepare_external_driver()`] operation. @@ -76,22 +120,17 @@ pub(super) mod inner { }; /// The error returned by [PlatformRef::prepare_external_driver()](PlatformRef::prepare_external_driver()). - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] + #[derive(Debug)] + #[allow(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" - )] + ResourceTooLarge { + kind: ResourceKind, + }, 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, @@ -99,6 +138,34 @@ pub(super) mod inner { }, } + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::ResourceTooLarge { kind } => { + write!(f, "The resource of kind {kind:?} was too large to be processed") + } + Error::CreateTempfile { rela_path, kind, .. } => write!( + f, + "Tempfile to store content of '{rela_path}' ({kind:?}) for passing to external merge command could not be created" + ), + Error::WriteTempfile { rela_path, kind, .. } => write!( + f, + "Could not write content of '{rela_path}' ({kind:?}) to tempfile for passing to external merge command" + ), + } + } + } + + impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::CreateTempfile { source, .. } => Some(source), + Error::WriteTempfile { source, .. } => Some(source), + Error::ResourceTooLarge { .. } => None, + } + } + } + /// Plumbing impl<'parent> PlatformRef<'parent> { /// Given `merge_command` and `context`, typically obtained from git-configuration, and the currently set merge-resources, diff --git a/gix-merge/src/blob/platform/prepare_merge.rs b/gix-merge/src/blob/platform/prepare_merge.rs index 78681df1794..02440aef0a9 100644 --- a/gix-merge/src/blob/platform/prepare_merge.rs +++ b/gix-merge/src/blob/platform/prepare_merge.rs @@ -10,12 +10,10 @@ use crate::blob::{ }; /// The error returned by [Platform::prepare_merge_state()](Platform::prepare_merge()). -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] +#[derive(Debug)] +#[allow(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, @@ -23,6 +21,28 @@ pub enum Error { }, } +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::UnsetResource => { + f.write_str("The 'current', 'ancestor' or 'other' resource for the merge operation were not set") + } + Error::Attributes { rela_path, kind, .. } => { + write!(f, "Failed to obtain attributes for {kind:?} resource at '{rela_path}'") + } + } + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::Attributes { source, .. } => Some(source), + Error::UnsetResource => None, + } + } +} + /// Preparation impl Platform { /// Prepare all state needed for performing a merge, using all [previously set](Self::set_resource()) resources. diff --git a/gix-merge/src/blob/platform/set_resource.rs b/gix-merge/src/blob/platform/set_resource.rs index 35706837040..d1f4e473f5f 100644 --- a/gix-merge/src/blob/platform/set_resource.rs +++ b/gix-merge/src/blob/platform/set_resource.rs @@ -3,25 +3,58 @@ use bstr::{BStr, BString}; use crate::blob::{Platform, ResourceKind, pipeline, platform::Resource}; /// The error returned by [Platform::set_resource](Platform::set_resource). -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] +// TODO(review): hand-written impls preserve the `thiserror` semantics. `ConvertToMergeable` is +// `#[error(transparent)]`: `Display` and `source()` forward to the wrapped error. `Io` +// and `Attributes` expose their named `source` field; `InvalidMode` has no source. +#[derive(Debug)] +#[allow(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}'")] + InvalidMode { + mode: gix_object::tree::EntryKind, + }, 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), + ConvertToMergeable(pipeline::convert_to_mergeable::Error), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::InvalidMode { mode } => write!(f, "Can only diff blobs, not {mode:?}"), + Error::Io { rela_path, kind, .. } => { + write!(f, "Failed to read {kind:?} worktree data from '{rela_path}'") + } + Error::Attributes { rela_path, kind, .. } => { + write!(f, "Failed to obtain attributes for {kind:?} resource at '{rela_path}'") + } + Error::ConvertToMergeable(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::Io { source, .. } => Some(source), + Error::Attributes { source, .. } => Some(source), + Error::ConvertToMergeable(err) => err.source(), + Error::InvalidMode { .. } => None, + } + } +} + +impl From for Error { + fn from(err: pipeline::convert_to_mergeable::Error) -> Self { + Error::ConvertToMergeable(err) + } } /// Preparation diff --git a/gix-merge/src/commit/mod.rs b/gix-merge/src/commit/mod.rs index 0a32c818ac8..ee6cdcad110 100644 --- a/gix-merge/src/commit/mod.rs +++ b/gix-merge/src/commit/mod.rs @@ -1,22 +1,72 @@ /// The error returned by [`commit()`](crate::commit()). -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] +// TODO(review): hand-written impls preserve the `thiserror` semantics. `VirtualMergeBase` and +// `MergeTree` are `#[error(transparent)]`: `Display` and `source()` forward to the +// wrapped error. The other `#[from]` variants render their own message and expose the +// wrapped error as `source()`; `NoMergeBase` has no source. +#[derive(Debug)] +#[allow(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}")] + MergeBase(gix_revision::merge_base::Error), + VirtualMergeBase(virtual_merge_base::Error), + MergeTree(crate::tree::Error), 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(#[from] gix_object::find::existing_object::Error), + FindCommit(gix_object::find::existing_object::Error), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::MergeBase(_) => f.write_str("Failed to obtain the merge base between the two commits to be merged"), + Error::VirtualMergeBase(err) => std::fmt::Display::fmt(err, f), + Error::MergeTree(err) => std::fmt::Display::fmt(err, f), + Error::NoMergeBase { + our_commit_id, + their_commit_id, + } => write!(f, "No common ancestor between {our_commit_id} and {their_commit_id}"), + Error::FindCommit(_) => f.write_str("Could not find ancestor, our or their commit to extract tree from"), + } + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::MergeBase(err) => Some(err), + Error::VirtualMergeBase(err) => err.source(), + Error::MergeTree(err) => err.source(), + Error::FindCommit(err) => Some(err), + Error::NoMergeBase { .. } => None, + } + } +} + +impl From for Error { + fn from(err: gix_revision::merge_base::Error) -> Self { + Error::MergeBase(err) + } +} + +impl From for Error { + fn from(err: virtual_merge_base::Error) -> Self { + Error::VirtualMergeBase(err) + } +} + +impl From for Error { + fn from(err: crate::tree::Error) -> Self { + Error::MergeTree(err) + } +} + +impl From for Error { + fn from(err: gix_object::find::existing_object::Error) -> Self { + Error::FindCommit(err) + } } /// 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..7ca698a6dc6 100644 --- a/gix-merge/src/commit/virtual_merge_base.rs +++ b/gix-merge/src/commit/virtual_merge_base.rs @@ -12,21 +12,61 @@ pub struct Outcome { } /// The error returned by [`commit::merge_base()`](crate::commit::virtual_merge_base()). -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] +// TODO(review): hand-written impls preserve the `thiserror` semantics. `MergeTree` is +// `#[error(transparent)]`: `Display` and `source()` forward to the wrapped error. +// `WriteObject` wraps an unnamed, unannotated error and so has no `source()` — matching +// `thiserror`, which never treats such a field as the source. +#[derive(Debug)] +#[allow(missing_docs)] pub enum Error { - #[error(transparent)] - MergeTree(#[from] crate::tree::Error), - #[error("Failed to write tree for merged merge-base or virtual commit")] + MergeTree(crate::tree::Error), 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." - )] + DecodeCommit(gix_object::decode::Error), VirtualMergeBaseConflict, - #[error("Could not find commit to use as basis for a virtual commit")] - FindCommit(#[from] gix_object::find::existing_object::Error), + FindCommit(gix_object::find::existing_object::Error), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::MergeTree(err) => std::fmt::Display::fmt(err, f), + Error::WriteObject(_) => f.write_str("Failed to write tree for merged merge-base or virtual commit"), + Error::DecodeCommit(_) => f.write_str("Failed to decode a commit needed to build a virtual merge-base"), + Error::VirtualMergeBaseConflict => f.write_str( + "Conflicts occurred when trying to resolve multiple merge-bases by merging them. This is most certainly a bug.", + ), + Error::FindCommit(_) => f.write_str("Could not find commit to use as basis for a virtual commit"), + } + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::MergeTree(err) => err.source(), + Error::DecodeCommit(err) => Some(err), + Error::FindCommit(err) => Some(err), + Error::WriteObject(_) | Error::VirtualMergeBaseConflict => None, + } + } +} + +impl From for Error { + fn from(err: crate::tree::Error) -> Self { + Error::MergeTree(err) + } +} + +impl From for Error { + fn from(err: gix_object::decode::Error) -> Self { + Error::DecodeCommit(err) + } +} + +impl From for Error { + fn from(err: gix_object::find::existing_object::Error) -> Self { + Error::FindCommit(err) + } } pub(super) mod function { @@ -45,7 +85,7 @@ pub(super) mod function { /// The parameters `graph`, `diff_resource_cache`, `blob_merge`, `objects`, `abbreviate_hash` and `options` are passed /// directly to [`tree()`](crate::tree()) for merging the trees of two merge-bases at a time. /// Note that most of `options` are overwritten to match the requirements of a merge-base merge. - #[expect(clippy::too_many_arguments)] + #[allow(clippy::too_many_arguments)] pub fn virtual_merge_base<'objects>( first_commit: gix_hash::ObjectId, second_commit: gix_hash::ObjectId, diff --git a/gix-merge/src/tree/mod.rs b/gix-merge/src/tree/mod.rs index 633bb4472bb..e996c53caa3 100644 --- a/gix-merge/src/tree/mod.rs +++ b/gix-merge/src/tree/mod.rs @@ -2,29 +2,103 @@ 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)] +// TODO(review): hand-written impls preserve the `thiserror` semantics. `BlobMergePrepare` and +// `BlobMerge` are `#[error(transparent)]`: `Display` and `source()` forward to the +// wrapped error. `WriteBlobToOdb` wraps an unnamed, unannotated `Box` and so +// has no `source()` — matching `thiserror`, which never treats such a field as the +// source. The remaining `#[from]` variants render their own message and expose the +// wrapped error as `source()`. +#[derive(Debug)] +#[allow(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), - #[error("Could not find ancestor, our or their tree iterator to get started")] - FindTreeIter(#[from] gix_object::find::existing_iter::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), - #[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")] + FindTree(gix_object::find::existing_object::Error), + FindTreeIter(gix_object::find::existing_iter::Error), + DiffTree(gix_diff::tree_with_rewrites::Error), + TreeEdit(gix_object::tree::editor::Error), + BlobMergeSetResource(crate::blob::platform::set_resource::Error), + BlobMergePrepare(crate::blob::platform::prepare_merge::Error), + BlobMerge(crate::blob::platform::merge::Error), WriteBlobToOdb(Box), - #[error("The merge was performed, but the binary merge result couldn't be selected as it wasn't found")] MergeResourceNotFound, } +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::FindTree(_) => f.write_str("Could not find ancestor, our or their tree to get started"), + Error::FindTreeIter(_) => f.write_str("Could not find ancestor, our or their tree iterator to get started"), + Error::DiffTree(_) => f.write_str("Failed to diff our side or their side"), + Error::TreeEdit(_) => f.write_str("Could not apply merge result to base tree"), + Error::BlobMergeSetResource(_) => f.write_str("Failed to load resource to prepare for blob merge"), + Error::BlobMergePrepare(err) => std::fmt::Display::fmt(err, f), + Error::BlobMerge(err) => std::fmt::Display::fmt(err, f), + Error::WriteBlobToOdb(_) => { + f.write_str("Failed to write merged blob content as blob to the object database") + } + Error::MergeResourceNotFound => f.write_str( + "The merge was performed, but the binary merge result couldn't be selected as it wasn't found", + ), + } + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::FindTree(err) => Some(err), + Error::FindTreeIter(err) => Some(err), + Error::DiffTree(err) => Some(err), + Error::TreeEdit(err) => Some(err), + Error::BlobMergeSetResource(err) => Some(err), + Error::BlobMergePrepare(err) => err.source(), + Error::BlobMerge(err) => err.source(), + Error::WriteBlobToOdb(_) | Error::MergeResourceNotFound => None, + } + } +} + +impl From for Error { + fn from(err: gix_object::find::existing_object::Error) -> Self { + Error::FindTree(err) + } +} + +impl From for Error { + fn from(err: gix_object::find::existing_iter::Error) -> Self { + Error::FindTreeIter(err) + } +} + +impl From for Error { + fn from(err: gix_diff::tree_with_rewrites::Error) -> Self { + Error::DiffTree(err) + } +} + +impl From for Error { + fn from(err: gix_object::tree::editor::Error) -> Self { + Error::TreeEdit(err) + } +} + +impl From for Error { + fn from(err: crate::blob::platform::set_resource::Error) -> Self { + Error::BlobMergeSetResource(err) + } +} + +impl From for Error { + fn from(err: crate::blob::platform::prepare_merge::Error) -> Self { + Error::BlobMergePrepare(err) + } +} + +impl From for Error { + fn from(err: crate::blob::platform::merge::Error) -> Self { + Error::BlobMerge(err) + } +} + /// The outcome produced by [`tree()`](crate::tree()). #[derive(Clone)] pub struct Outcome<'a> { diff --git a/gix-object/Cargo.toml b/gix-object/Cargo.toml index b22b1a23e2d..a12083874c2 100644 --- a/gix-object/Cargo.toml +++ b/gix-object/Cargo.toml @@ -50,7 +50,6 @@ gix-date = { version = "^0.15.6", path = "../gix-date" } gix-utils = { version = "^0.3.5", path = "../gix-utils" } 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/data.rs b/gix-object/src/data.rs index 9c8be80e46f..7d1c844fd04 100644 --- a/gix-object/src/data.rs +++ b/gix-object/src/data.rs @@ -56,13 +56,41 @@ impl<'a> Data<'a> { /// Types supporting object hash verification pub mod verify { /// Returned by [`crate::Data::verify_checksum()`] - #[derive(Debug, thiserror::Error)] + #[derive(Debug)] #[expect(missing_docs)] pub enum Error { - #[error("Failed to hash object")] - Hasher(#[from] gix_hash::hasher::Error), - #[error(transparent)] - Verify(#[from] gix_hash::verify::Error), + Hasher(gix_hash::hasher::Error), + Verify(gix_hash::verify::Error), + } + + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Hasher(_) => f.write_str("Failed to hash object"), + Error::Verify(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::Hasher(err) => Some(err), + Error::Verify(err) => err.source(), + } + } + } + + impl From for Error { + fn from(err: gix_hash::hasher::Error) -> Self { + Error::Hasher(err) + } + } + + impl From for Error { + fn from(err: gix_hash::verify::Error) -> Self { + Error::Verify(err) + } } impl crate::Data<'_> { diff --git a/gix-object/src/encode.rs b/gix-object/src/encode.rs index 842fbe87696..795103d14cb 100644 --- a/gix-object/src/encode.rs +++ b/gix-object/src/encode.rs @@ -4,15 +4,24 @@ use std::io::{self, Write}; use bstr::{BString, ByteSlice}; /// An error returned when object encoding fails. -#[derive(Debug, thiserror::Error)] +#[derive(Debug)] #[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, } +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::NewlineInHeaderValue { value } => write!(f, "Newlines are not allowed in header values: {value:?}"), + Error::EmptyValue => f.write_str("Header values must not be empty"), + } + } +} + +impl std::error::Error for Error {} + macro_rules! check { ($e: expr) => { $e.expect("Writing to a Vec should never fail.") diff --git a/gix-object/src/find.rs b/gix-object/src/find.rs index c24dced7f57..4a05745fe30 100644 --- a/gix-object/src/find.rs +++ b/gix-object/src/find.rs @@ -5,14 +5,36 @@ pub mod existing { use gix_hash::ObjectId; /// The error returned by the [`find(…)`][crate::FindExt::find()] trait methods. - #[derive(Debug, thiserror::Error)] + #[derive(Debug)] #[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 }, } + + // TODO(review): these implementations hand-preserve `#[error(transparent)]` semantics for the + // `Find` variants of all three error types in this module: `Display` passes the + // formatter through and `source()` forwards to the inner error's source, exactly + // like the `thiserror`-generated code did — here through a `Box`. + // The same pattern is used for `verify::Error`, `tree::editor::Error` and + // `LooseDecodeError` in this crate. + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Find(err) => std::fmt::Display::fmt(err, f), + Error::NotFound { oid } => write!(f, "An object with id {oid} could not be found"), + } + } + } + + impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::Find(err) => err.source(), + Error::NotFound { .. } => None, + } + } + } } /// @@ -20,25 +42,46 @@ 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)] + #[derive(Debug)] #[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}")] + NotFound { + oid: ObjectId, + }, ObjectKind { oid: ObjectId, actual: crate::Kind, expected: crate::Kind, }, } + + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Find(err) => std::fmt::Display::fmt(err, f), + Error::Decode { oid, .. } => write!(f, "Could not decode object at {oid}"), + Error::NotFound { oid } => write!(f, "An object with id {oid} could not be found"), + Error::ObjectKind { oid, actual, expected } => { + write!(f, "Expected object of kind {expected} but got {actual} at {oid}") + } + } + } + } + + impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::Find(err) => err.source(), + Error::Decode { source, .. } => Some(source), + Error::NotFound { .. } | Error::ObjectKind { .. } => None, + } + } + } } /// @@ -46,20 +89,40 @@ 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)] + #[derive(Debug)] #[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}")] + NotFound { + oid: ObjectId, + }, ObjectKind { oid: ObjectId, actual: crate::Kind, expected: crate::Kind, }, } + + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Find(err) => std::fmt::Display::fmt(err, f), + Error::NotFound { oid } => write!(f, "An object with id {oid} could not be found"), + Error::ObjectKind { oid, actual, expected } => { + write!(f, "Expected object of kind {expected} but got {actual} at {oid}") + } + } + } + } + + impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::Find(err) => err.source(), + Error::NotFound { .. } | Error::ObjectKind { .. } => None, + } + } + } } /// An implementation of object access traits that stores nothing and finds nothing. diff --git a/gix-object/src/kind.rs b/gix-object/src/kind.rs index aeef9c4d72a..a6d6a4f849a 100644 --- a/gix-object/src/kind.rs +++ b/gix-object/src/kind.rs @@ -3,13 +3,22 @@ use std::fmt; use crate::Kind; /// The Error used in [`Kind::from_bytes()`]. -#[derive(Debug, Clone, thiserror::Error)] +#[derive(Debug, Clone)] #[expect(missing_docs)] pub enum Error { - #[error("Unknown object kind: {kind:?}")] InvalidObjectKind { kind: bstr::BString }, } +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Error::InvalidObjectKind { kind } => write!(f, "Unknown object kind: {kind:?}"), + } + } +} + +impl std::error::Error for Error {} + /// Initialization impl Kind { /// Parse a `Kind` from its serialized loose git objects. diff --git a/gix-object/src/lib.rs b/gix-object/src/lib.rs index 47d19eb51e7..f5897ef9d41 100644 --- a/gix-object/src/lib.rs +++ b/gix-object/src/lib.rs @@ -331,19 +331,48 @@ pub mod decode { pub(crate) use error::empty_error; /// Returned by [`loose_header()`] - #[derive(Debug, thiserror::Error)] + #[derive(Debug)] #[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), + InvalidHeader { + message: &'static str, + }, + ObjectHeader(super::kind::Error), + } + + impl std::fmt::Display for LooseHeaderDecodeError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + LooseHeaderDecodeError::ParseIntegerError { message, number, .. } => { + write!(f, "{message}: {number:?}") + } + LooseHeaderDecodeError::InvalidHeader { message } => f.write_str(message), + LooseHeaderDecodeError::ObjectHeader(_) => { + f.write_str("The object header contained an unknown object kind.") + } + } + } + } + + impl std::error::Error for LooseHeaderDecodeError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + LooseHeaderDecodeError::ParseIntegerError { source, .. } => Some(source), + LooseHeaderDecodeError::InvalidHeader { .. } => None, + LooseHeaderDecodeError::ObjectHeader(err) => Some(err), + } + } + } + + impl From for LooseHeaderDecodeError { + fn from(err: super::kind::Error) -> Self { + LooseHeaderDecodeError::ObjectHeader(err) + } } use bstr::ByteSlice; diff --git a/gix-object/src/object/mod.rs b/gix-object/src/object/mod.rs index cac46323056..b585827420a 100644 --- a/gix-object/src/object/mod.rs +++ b/gix-object/src/object/mod.rs @@ -190,16 +190,50 @@ use crate::{ decode::{Error as DecodeError, LooseHeaderDecodeError, loose_header}, }; -#[derive(Debug, thiserror::Error)] +#[derive(Debug)] 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")] + InvalidHeader(LooseHeaderDecodeError), + InvalidContent(DecodeError), OutOfMemory { size: u64 }, } +impl std::fmt::Display for LooseDecodeError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + LooseDecodeError::InvalidHeader(err) => std::fmt::Display::fmt(err, f), + LooseDecodeError::InvalidContent(err) => std::fmt::Display::fmt(err, f), + LooseDecodeError::OutOfMemory { size } => { + write!( + f, + "Object sized {size} does not fit into memory - this can happen on 32 bit systems" + ) + } + } + } +} + +impl std::error::Error for LooseDecodeError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + LooseDecodeError::InvalidHeader(err) => err.source(), + LooseDecodeError::InvalidContent(err) => err.source(), + LooseDecodeError::OutOfMemory { .. } => None, + } + } +} + +impl From for LooseDecodeError { + fn from(err: LooseHeaderDecodeError) -> Self { + LooseDecodeError::InvalidHeader(err) + } +} + +impl From for LooseDecodeError { + fn from(err: DecodeError) -> Self { + LooseDecodeError::InvalidContent(err) + } +} + 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> { diff --git a/gix-object/src/tag/write.rs b/gix-object/src/tag/write.rs index 5367c96083e..09b58fcf9fd 100644 --- a/gix-object/src/tag/write.rs +++ b/gix-object/src/tag/write.rs @@ -6,13 +6,35 @@ use gix_date::parse::TimeBuf; use crate::{Kind, Tag, TagRef, encode, encode::NL}; /// An Error used in [`Tag::write_to()`][crate::WriteTo::write_to()]. -#[derive(Debug, thiserror::Error)] +#[derive(Debug)] #[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), + InvalidRefName(gix_validate::tag::name::Error), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::StartsWithDash => f.write_str("Tags must not start with a dash: '-'"), + Error::InvalidRefName(_) => f.write_str("The tag name was no valid reference name"), + } + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::StartsWithDash => None, + Error::InvalidRefName(err) => Some(err), + } + } +} + +impl From for Error { + fn from(err: gix_validate::tag::name::Error) -> Self { + Error::InvalidRefName(err) + } } impl From for io::Error { diff --git a/gix-object/src/tree/editor.rs b/gix-object/src/tree/editor.rs index ca90c4d587d..2e7abfb9a61 100644 --- a/gix-object/src/tree/editor.rs +++ b/gix-object/src/tree/editor.rs @@ -32,17 +32,41 @@ impl std::fmt::Debug for Editor<'_> { } /// The error returned by [Editor] or [Cursor] edit operation. -#[derive(Debug, thiserror::Error)] +#[derive(Debug)] #[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")] + FindExistingObject(crate::find::existing_object::Error), CannotRemoveNonLeaf { rela_path: BString }, } +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::EmptyPathComponent => f.write_str("Empty path components are not allowed"), + Error::FindExistingObject(err) => std::fmt::Display::fmt(err, f), + Error::CannotRemoveNonLeaf { rela_path } => { + write!(f, "Cannot remove '{rela_path}' as leaf entry because it is a tree") + } + } + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::EmptyPathComponent | Error::CannotRemoveNonLeaf { .. } => None, + Error::FindExistingObject(err) => err.source(), + } + } +} + +impl From for Error { + fn from(err: crate::find::existing_object::Error) -> Self { + Error::FindExistingObject(err) + } +} + /// Lifecycle impl<'a> Editor<'a> { /// Create a new editor that uses `root` as base for all edits. Use `find` to lookup existing diff --git a/gix-object/src/tree/write.rs b/gix-object/src/tree/write.rs index 540eaa856ef..16c7dd223cd 100644 --- a/gix-object/src/tree/write.rs +++ b/gix-object/src/tree/write.rs @@ -9,13 +9,27 @@ use crate::{ }; /// The Error used in [`Tree::write_to()`][crate::WriteTo::write_to()]. -#[derive(Debug, thiserror::Error)] +#[derive(Debug)] #[expect(missing_docs)] pub enum Error { - #[error("Nullbytes are invalid in file paths as they are separators: {name:?}")] NullbyteInFilename { name: BString }, } +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::NullbyteInFilename { name } => { + write!( + f, + "Nullbytes are invalid in file paths as they are separators: {name:?}" + ) + } + } + } +} + +impl std::error::Error for Error {} + impl From for io::Error { fn from(err: Error) -> Self { io::Error::other(err) diff --git a/gix-object/tests/object/encode.rs b/gix-object/tests/object/encode.rs index c4b534e6ff1..d8d537376fe 100644 --- a/gix-object/tests/object/encode.rs +++ b/gix-object/tests/object/encode.rs @@ -1,11 +1,20 @@ /// Because the `TryFrom` implementations don't return proper errors /// on failure -#[derive(Debug, thiserror::Error)] +#[derive(Debug)] enum Error { - #[error("")] TryFromError, } +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::TryFromError => f.write_str(""), + } + } +} + +impl std::error::Error for Error {} + /// Needed for roundtripping object types that take a `object_hash` parameter. /// This is the same as `round_trip`, but for types that have `from_bytes()` with `object_hash`. macro_rules! round_trip_with_hash_kind { diff --git a/gix-odb/Cargo.toml b/gix-odb/Cargo.toml index 6c09459689f..81ef1be1395 100644 --- a/gix-odb/Cargo.toml +++ b/gix-odb/Cargo.toml @@ -38,7 +38,6 @@ gix-fs = { version = "^0.22.0", 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 6bc1e2403f0..8bff5c94a23 100644 --- a/gix-odb/src/alternate/mod.rs +++ b/gix-odb/src/alternate/mod.rs @@ -24,19 +24,64 @@ use gix_path::realpath::MAX_SYMLINKS; pub mod 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_path::realpath::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) => err.source(), + Error::Realpath(err) => err.source(), + Error::Parse(err) => err.source(), + Error::Cycle(_) => None, + } + } +} + +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) + } +} + +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 121ba27e820..c5abad0959f 100644 --- a/gix-odb/src/alternate/parse.rs +++ b/gix-odb/src/alternate/parse.rs @@ -3,13 +3,42 @@ use std::{borrow::Cow, path::PathBuf}; use gix_object::bstr::ByteSlice; /// Returned as part of [`crate::alternate::Error::Parse`] -#[derive(thiserror::Error, Debug)] -#[expect(missing_docs)] +// TODO(review): `Unquote` wraps `gix_quote::ansi_c::undo::Error`, which is a `gix_error::Exn` and does +// not implement `std::error::Error`; `source()` dereferences it to the inner error, just +// as the `thiserror` `#[from]` did via `as_dyn_error()` (which auto-derefs the `Exn`). +#[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), - #[error("Could not unquote alternate path")] - Unquote(#[from] gix_quote::ansi_c::undo::Error), + Unquote(gix_quote::ansi_c::undo::Error), +} + +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) + ), + Error::Unquote(_) => f.write_str("Could not unquote alternate path"), + } + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::Unquote(err) => Some(&**err), + Error::PathConversion(_) => None, + } + } +} + +impl From for Error { + fn from(err: gix_quote::ansi_c::undo::Error) -> Self { + Error::Unquote(err) + } } pub(crate) fn content(input: &[u8]) -> Result, Error> { diff --git a/gix-odb/src/store_impls/dynamic/find.rs b/gix-odb/src/store_impls/dynamic/find.rs index c612a76d527..9cafc467313 100644 --- a/gix-odb/src/store_impls/dynamic/find.rs +++ b/gix-odb/src/store_impls/dynamic/find.rs @@ -8,36 +8,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] pack::data::decode::Error), - #[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(pack::data::decode::Error), + 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, @@ -46,6 +37,82 @@ 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) => err.source(), + Error::LoadPack(err) => err.source(), + Error::EntryType(err) => err.source(), + Error::DeltaBaseLookup { err, .. } => Some(&**err), + Error::DeltaBaseRecursionLimit { .. } | Error::DeltaBaseMissing { .. } => 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) + } + } + + 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 ae52d30cd01..39ce9e0c8f8 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) => err.source(), + Error::Alternate(err) => err.source(), + 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..fed9f33b525 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) => err.source(), + } + } + } + + 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) => err.source(), + } + } + } + + 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 e58a503d6d4..04b85adade2 100644 --- a/gix-odb/src/store_impls/dynamic/verify.rs +++ b/gix-odb/src/store_impls/dynamic/verify.rs @@ -22,27 +22,93 @@ 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(#[from] pack::index::traverse::Error), - #[error(transparent)] - IndexIntegrity(#[from] pack::index::traverse::Error), - #[error(transparent)] - IndexOpen(#[from] pack::index::init::Error), - #[error(transparent)] - LooseObjectStoreIntegrity(#[from] crate::loose::verify::integrity::Error), - #[error(transparent)] - MultiIndexOpen(#[from] pack::multi_index::init::Error), - #[error(transparent)] - PackOpen(#[from] pack::data::init::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.")] + MultiIndexIntegrity(pack::index::traverse::Error), + IndexIntegrity(pack::index::traverse::Error), + IndexOpen(pack::index::init::Error), + LooseObjectStoreIntegrity(crate::loose::verify::integrity::Error), + MultiIndexOpen(pack::multi_index::init::Error), + PackOpen(pack::data::init::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) => err.source(), + Error::IndexIntegrity(err) => err.source(), + Error::IndexOpen(err) => err.source(), + Error::LooseObjectStoreIntegrity(err) => err.source(), + Error::MultiIndexOpen(err) => err.source(), + Error::PackOpen(err) => err.source(), + Error::InitializeODB(err) => err.source(), + Error::NeedsRetryDueToChangeOnDisk => None, + } + } + } + + impl From> for Error { + fn from(err: pack::index::traverse::Error) -> Self { + Error::MultiIndexIntegrity(err) + } + } + + impl From> for Error { + fn from(err: pack::index::traverse::Error) -> Self { + Error::IndexIntegrity(err) + } + } + + impl From for Error { + fn from(err: pack::index::init::Error) -> Self { + Error::IndexOpen(err) + } + } + + impl From for Error { + fn from(err: crate::loose::verify::integrity::Error) -> Self { + Error::LooseObjectStoreIntegrity(err) + } + } + + impl From for Error { + fn from(err: pack::multi_index::init::Error) -> Self { + Error::MultiIndexOpen(err) + } + } + + impl From for Error { + fn from(err: pack::data::init::Error) -> Self { + Error::PackOpen(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 +122,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 b2ab84f2d4e..e7870dd66b1 100644 --- a/gix-odb/src/store_impls/dynamic/write.rs +++ b/gix-odb/src/store_impls/dynamic/write.rs @@ -9,15 +9,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) => err.source(), + Error::LooseWrite(err) => err.source(), + Error::Io(err) => err.source(), + } + } + } + + 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 65e9a980cf1..9b9b6ab5a46 100644 --- a/gix-odb/src/store_impls/loose/find.rs +++ b/gix-odb/src/store_impls/loose/find.rs @@ -3,21 +3,22 @@ use std::{cmp::Ordering, collections::HashSet, io, path::PathBuf}; 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: gix_zlib::inflate::Error, 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)] - Decode(#[from] gix_object::decode::LooseHeaderDecodeError), - #[error("Cannot store {size} in memory as it's not representable")] - OutOfMemory { size: u64 }, - #[error("Could not {action} data at '{path}'")] + SizeMismatch { + actual: u64, + expected: u64, + path: PathBuf, + }, + Decode(gix_object::decode::LooseHeaderDecodeError), + OutOfMemory { + size: u64, + }, Io { source: std::io::Error, action: &'static str, @@ -25,6 +26,41 @@ 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} in memory as it's not representable"), + 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) => err.source(), + Error::Io { source, .. } => Some(source), + Error::SizeMismatch { .. } | Error::OutOfMemory { .. } => None, + } + } +} + +impl From for Error { + fn from(err: gix_object::decode::LooseHeaderDecodeError) -> Self { + Error::Decode(err) + } +} + /// Object lookup impl Store { const OPEN_OR_MAP_ACTION: &'static str = "open or map"; @@ -257,7 +293,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 03292091432..d6e9e3b1554 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 => None, + } + } + } + /// 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 0786021a5f0..005a7ef4939 100644 --- a/gix-odb/src/store_impls/loose/write.rs +++ b/gix-odb/src/store_impls/loose/write.rs @@ -8,24 +8,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: gix_hash::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()?; diff --git a/gix-pack/Cargo.toml b/gix-pack/Cargo.toml index 046605eb3c9..f79cae2d2f2 100644 --- a/gix-pack/Cargo.toml +++ b/gix-pack/Cargo.toml @@ -59,7 +59,6 @@ gix-diff = { version = "^0.66.0", 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/init.rs b/gix-pack/src/bundle/init.rs index a06c9e0d32b..cb1064f31d1 100644 --- a/gix-pack/src/bundle/init.rs +++ b/gix-pack/src/bundle/init.rs @@ -3,15 +3,50 @@ use std::path::{Path, PathBuf}; use crate::Bundle; /// Returned by [`Bundle::at()`] -#[derive(thiserror::Error, Debug)] -#[expect(missing_docs)] +#[derive(Debug)] +#[allow(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), + Pack(crate::data::header::decode::Error), + Index(crate::index::init::Error), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::InvalidPath(path) => { + write!( + f, + "An 'idx' extension is expected of an index file: '{}'", + path.display() + ) + } + Error::Pack(err) => std::fmt::Display::fmt(err, f), + Error::Index(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::Pack(err) => err.source(), + Error::Index(err) => err.source(), + Error::InvalidPath(_) => None, + } + } +} + +impl From for Error { + fn from(err: crate::data::header::decode::Error) -> Self { + Error::Pack(err) + } +} + +impl From for Error { + fn from(err: crate::index::init::Error) -> Self { + Error::Index(err) + } } /// Initialization diff --git a/gix-pack/src/bundle/write/error.rs b/gix-pack/src/bundle/write/error.rs index da44181bb4f..b8d757689f4 100644 --- a/gix-pack/src/bundle/write/error.rs +++ b/gix-pack/src/bundle/write/error.rs @@ -3,15 +3,57 @@ 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)] +#[derive(Debug)] +#[allow(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), + Io(io::Error), + PackIter(crate::data::input::Error), + Persist(gix_tempfile::handle::persist::Error), + IndexWrite(crate::index::write::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("An IO error occurred when reading the pack or creating a temporary file"), + Error::PackIter(err) => std::fmt::Display::fmt(err, f), + Error::Persist(_) => f.write_str("Could not move a temporary file into its desired place"), + Error::IndexWrite(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::Io(err) => Some(err), + Error::PackIter(err) => err.source(), + Error::Persist(err) => Some(err), + Error::IndexWrite(err) => err.source(), + } + } +} + +impl From for Error { + fn from(err: io::Error) -> Self { + Error::Io(err) + } +} + +impl From for Error { + fn from(err: crate::data::input::Error) -> Self { + Error::PackIter(err) + } +} + +impl From> for Error { + fn from(err: gix_tempfile::handle::persist::Error) -> Self { + Error::Persist(err) + } +} + +impl From for Error { + fn from(err: crate::index::write::Error) -> Self { + Error::IndexWrite(err) + } } diff --git a/gix-pack/src/cache/delta/from_offsets.rs b/gix-pack/src/cache/delta/from_offsets.rs index 4be27f0de9d..107a35a16f3 100644 --- a/gix-pack/src/cache/delta/from_offsets.rs +++ b/gix-pack/src/cache/delta/from_offsets.rs @@ -10,20 +10,56 @@ use gix_features::progress::{self, Progress}; use crate::{cache::delta::Tree, data}; /// Returned by [`Tree::from_offsets_in_pack()`] -#[derive(thiserror::Error, Debug)] +#[derive(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")] + Header(crate::data::header::decode::Error), UnresolvedRefDelta { id: gix_hash::ObjectId }, - #[error(transparent)] - Tree(#[from] crate::cache::delta::Error), - #[error("Interrupted")] + Tree(crate::cache::delta::Error), Interrupted, } +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Io { message, .. } => f.write_str(message), + Error::Header(err) => std::fmt::Display::fmt(err, f), + Error::UnresolvedRefDelta { id } => { + write!( + f, + "Could find object with id {id} in this pack. Thin packs are not supported" + ) + } + Error::Tree(err) => std::fmt::Display::fmt(err, f), + Error::Interrupted => f.write_str("Interrupted"), + } + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::Io { source, .. } => Some(source), + Error::Header(err) => err.source(), + Error::Tree(err) => err.source(), + Error::UnresolvedRefDelta { .. } | Error::Interrupted => None, + } + } +} + +impl From for Error { + fn from(err: crate::data::header::decode::Error) -> Self { + Error::Header(err) + } +} + +impl From for Error { + fn from(err: crate::cache::delta::Error) -> Self { + Error::Tree(err) + } +} + const PACK_HEADER_LEN: usize = 12; /// Generate tree from certain input diff --git a/gix-pack/src/cache/delta/mod.rs b/gix-pack/src/cache/delta/mod.rs index 3c2928ce5ce..e358fe11924 100644 --- a/gix-pack/src/cache/delta/mod.rs +++ b/gix-pack/src/cache/delta/mod.rs @@ -1,9 +1,7 @@ /// Returned when using various methods on a [`Tree`] -#[derive(thiserror::Error, Debug)] +#[derive(Debug)] +#[allow(missing_docs)] pub enum Error { - #[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, @@ -12,6 +10,22 @@ pub enum Error { }, } +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::InvariantIncreasingPackOffset { + last_pack_offset, + pack_offset, + } => write!( + f, + "Pack offsets must only increment. The previous pack offset was {last_pack_offset}, the current one is {pack_offset}" + ), + } + } +} + +impl std::error::Error for Error {} + /// pub mod traverse; diff --git a/gix-pack/src/cache/delta/traverse/mod.rs b/gix-pack/src/cache/delta/traverse/mod.rs index 51f86bde968..2444081e636 100644 --- a/gix-pack/src/cache/delta/traverse/mod.rs +++ b/gix-pack/src/cache/delta/traverse/mod.rs @@ -18,41 +18,105 @@ pub(crate) mod util; pub(super) type SharedRefDeltaChildren = OwnShared>; /// Returned by [`Tree::traverse()`] -#[derive(thiserror::Error, Debug)] +#[derive(Debug)] +#[allow(missing_docs)] pub enum Error { - #[error("{message}")] ZlibInflate { source: gix_zlib::inflate::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")] + ResolveFailed { + pack_offset: u64, + }, + EntryType(crate::data::entry::decode::Error), + Inspect(Box), 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), + ObjectHash(gix_hash::hasher::Error), + SpawnThread(std::io::Error), + Delta(crate::data::delta::apply::Error), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::ZlibInflate { message, .. } => f.write_str(message), + Error::ResolveFailed { pack_offset } => write!( + f, + "The resolver failed to obtain the pack entry bytes for the entry at {pack_offset}" + ), + Error::EntryType(err) => std::fmt::Display::fmt(err, f), + Error::Inspect(_) => f.write_str("One of the object inspectors failed"), + Error::Interrupted => f.write_str("Interrupted"), + Error::OutOfMemory => f.write_str("Entry too large to fit in memory"), + Error::OutOfPackRefDelta { base_pack_offset } => write!( + f, + "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." + ), + Error::UnresolvedRefDelta { base_id } => { + write!(f, "The ref-delta base object {base_id} could not be found") + } + Error::ObjectHash(_) => f.write_str("Failed to hash an object while resolving in-pack ref-deltas"), + Error::SpawnThread(_) => f.write_str("Failed to spawn thread when switching to work-stealing mode"), + Error::Delta(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::ZlibInflate { source, .. } => Some(source), + Error::EntryType(err) => err.source(), + Error::Inspect(err) => Some(&**err), + Error::ObjectHash(err) => Some(err), + Error::SpawnThread(err) => Some(err), + Error::Delta(err) => err.source(), + Error::ResolveFailed { .. } + | Error::Interrupted + | Error::OutOfMemory + | Error::OutOfPackRefDelta { .. } + | Error::UnresolvedRefDelta { .. } => None, + } + } +} + +impl From for Error { + fn from(err: crate::data::entry::decode::Error) -> Self { + Error::EntryType(err) + } +} + +impl From> for Error { + fn from(err: Box) -> Self { + Error::Inspect(err) + } +} + +impl From for Error { + fn from(err: gix_hash::hasher::Error) -> Self { + Error::ObjectHash(err) + } +} + +impl From for Error { + fn from(err: std::io::Error) -> Self { + Error::SpawnThread(err) + } +} + +impl From for Error { + fn from(err: crate::data::delta::apply::Error) -> Self { + Error::Delta(err) + } } impl From for Error { diff --git a/gix-pack/src/data/delta.rs b/gix-pack/src/data/delta.rs index fb01e928d7f..53a602f5003 100644 --- a/gix-pack/src/data/delta.rs +++ b/gix-pack/src/data/delta.rs @@ -1,18 +1,27 @@ /// pub mod apply { /// Returned when failing to apply deltas. - #[derive(thiserror::Error, Debug)] - #[expect(missing_docs)] + #[derive(Debug)] + #[allow(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, } + + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Corrupt { message } => write!(f, "Corrupt delta data: {message}"), + Error::UnsupportedCommandCode => f.write_str("Encountered unsupported command code: 0"), + Error::DeltaCopyBaseSliceMismatch => f.write_str("Delta copy from base: byte slices must match"), + Error::DeltaCopyDataSliceMismatch => f.write_str("Delta copy data: byte slices must match"), + } + } + } + + impl std::error::Error for Error {} } /// Given the decompressed pack delta `d`, decode a size in bytes (either the base object size or the result object size) diff --git a/gix-pack/src/data/entry/decode.rs b/gix-pack/src/data/entry/decode.rs index ccd4f130bc1..63df78b19b2 100644 --- a/gix-pack/src/data/entry/decode.rs +++ b/gix-pack/src/data/entry/decode.rs @@ -6,17 +6,26 @@ 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)] +#[derive(Debug)] +#[allow(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, } +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::UnsupportedType { type_id } => write!(f, "Object type {type_id} is unsupported"), + Error::Corrupt { message } => write!(f, "Pack entry is truncated: {message}"), + Error::Overflow => f.write_str("Pack entry header value overflowed while decoding"), + } + } +} + +impl std::error::Error for Error {} + /// 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. @@ -81,6 +90,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[..]), }; diff --git a/gix-pack/src/data/file/decode/mod.rs b/gix-pack/src/data/file/decode/mod.rs index ab520a2d359..284f5796a0e 100644 --- a/gix-pack/src/data/file/decode/mod.rs +++ b/gix-pack/src/data/file/decode/mod.rs @@ -8,19 +8,58 @@ pub mod header; /// 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)] +#[derive(Debug)] +#[allow(missing_docs)] pub enum Error { - #[error("Failed to decompress pack entry")] - ZlibInflate(#[from] gix_zlib::inflate::Error), - #[error("A delta chain could not be followed as the ref base with id {0} could not be found")] + ZlibInflate(gix_zlib::inflate::Error), DeltaBaseUnresolved(gix_hash::ObjectId), - #[error(transparent)] - EntryType(#[from] crate::data::entry::decode::Error), - #[error("Entry too large to fit in memory")] + EntryType(crate::data::entry::decode::Error), OutOfMemory, - #[error(transparent)] - Delta(#[from] crate::data::delta::apply::Error), + Delta(crate::data::delta::apply::Error), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::ZlibInflate(_) => f.write_str("Failed to decompress pack entry"), + Error::DeltaBaseUnresolved(id) => write!( + f, + "A delta chain could not be followed as the ref base with id {id} could not be found" + ), + Error::EntryType(err) => std::fmt::Display::fmt(err, f), + Error::OutOfMemory => f.write_str("Entry too large to fit in memory"), + Error::Delta(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::ZlibInflate(err) => Some(err), + Error::EntryType(err) => err.source(), + Error::Delta(err) => err.source(), + Error::DeltaBaseUnresolved(_) | Error::OutOfMemory => None, + } + } +} + +impl From for Error { + fn from(err: gix_zlib::inflate::Error) -> Self { + Error::ZlibInflate(err) + } +} + +impl From for Error { + fn from(err: crate::data::entry::decode::Error) -> Self { + Error::EntryType(err) + } +} + +impl From for Error { + fn from(err: crate::data::delta::apply::Error) -> Self { + Error::Delta(err) + } } impl From for Error { diff --git a/gix-pack/src/data/header.rs b/gix-pack/src/data/header.rs index 9c4f2460dcb..5d52b3a9655 100644 --- a/gix-pack/src/data/header.rs +++ b/gix-pack/src/data/header.rs @@ -39,17 +39,33 @@ pub fn encode(version: data::Version, num_objects: u32) -> [u8; 12] { /// pub mod decode { /// Returned by [`decode()`][super::decode()]. - #[derive(thiserror::Error, Debug)] - #[expect(missing_docs)] + #[derive(Debug)] + #[allow(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), } + + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Io { path, .. } => write!(f, "Could not open pack file at '{}'", path.display()), + Error::Corrupt(message) => f.write_str(message), + Error::UnsupportedVersion(version) => write!(f, "Unsupported pack version: {version}"), + } + } + } + + impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::Io { source, .. } => Some(source), + Error::Corrupt(_) | Error::UnsupportedVersion(_) => None, + } + } + } } diff --git a/gix-pack/src/data/input/types.rs b/gix-pack/src/data/input/types.rs index d706112d7a3..641d1136150 100644 --- a/gix-pack/src/data/input/types.rs +++ b/gix-pack/src/data/input/types.rs @@ -1,20 +1,61 @@ /// 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)] +#[derive(Debug)] +#[allow(missing_docs)] pub enum Error { - #[error("An IO operation failed while streaming an entry")] - Io(#[from] gix_hash::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.")] + Io(gix_hash::io::Error), + PackParse(crate::data::header::decode::Error), + Verify(gix_hash::verify::Error), IncompletePack { actual: u64, expected: u64 }, - #[error("The object {object_id} could not be decoded or wasn't found")] NotFound { object_id: gix_hash::ObjectId }, } +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Io(_) => f.write_str("An IO operation failed while streaming an entry"), + Error::PackParse(err) => std::fmt::Display::fmt(err, f), + Error::Verify(_) => f.write_str("Failed to verify pack checksum in trailer"), + Error::IncompletePack { actual, expected } => write!( + f, + "pack is incomplete: it was decompressed into {actual} bytes but {expected} bytes where expected." + ), + Error::NotFound { object_id } => { + write!(f, "The object {object_id} could not be decoded or wasn't found") + } + } + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::Io(err) => Some(err), + Error::PackParse(err) => err.source(), + Error::Verify(err) => Some(err), + Error::IncompletePack { .. } | Error::NotFound { .. } => None, + } + } +} + +impl From for Error { + fn from(err: gix_hash::io::Error) -> Self { + Error::Io(err) + } +} + +impl From for Error { + fn from(err: crate::data::header::decode::Error) -> Self { + Error::PackParse(err) + } +} + +impl From for Error { + fn from(err: gix_hash::verify::Error) -> Self { + Error::Verify(err) + } +} + /// 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/output/bytes.rs b/gix-pack/src/data/output/bytes.rs index d4a7a570cd2..4a95f123b47 100644 --- a/gix-pack/src/data/output/bytes.rs +++ b/gix-pack/src/data/output/bytes.rs @@ -3,18 +3,49 @@ use std::io::Write; use crate::{data::output, exact_vec}; /// The error returned by `next()` in the [`FromEntriesIter`] iterator. -#[expect(missing_docs)] -#[derive(Debug, thiserror::Error)] +#[allow(missing_docs)] +#[derive(Debug)] pub enum Error where E: std::error::Error + 'static, { - #[error(transparent)] - Io(#[from] gix_hash::io::Error), - #[error(transparent)] + Io(gix_hash::io::Error), Input(E), } +impl std::fmt::Display for Error +where + E: std::error::Error + 'static, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Io(err) => std::fmt::Display::fmt(err, f), + Error::Input(err) => std::fmt::Display::fmt(err, f), + } + } +} + +impl std::error::Error for Error +where + E: std::error::Error + 'static, +{ + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::Io(err) => err.source(), + Error::Input(err) => err.source(), + } + } +} + +impl From for Error +where + E: std::error::Error + 'static, +{ + fn from(err: gix_hash::io::Error) -> Self { + Error::Io(err) + } +} + /// An implementation of [`Iterator`] to write [encoded entries][output::Entry] to an inner implementation each time /// `next()` is called. pub struct FromEntriesIter { diff --git a/gix-pack/src/data/output/count/objects/types.rs b/gix-pack/src/data/output/count/objects/types.rs index 117a22ccd6d..e03209febbf 100644 --- a/gix-pack/src/data/output/count/objects/types.rs +++ b/gix-pack/src/data/output/count/objects/types.rs @@ -78,19 +78,45 @@ 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)] +#[derive(Debug)] +#[allow(missing_docs)] pub enum Error { - #[error(transparent)] CommitDecode(gix_object::decode::Error), - #[error(transparent)] - FindExisting(#[from] gix_object::find::existing::Error), - #[error(transparent)] + FindExisting(gix_object::find::existing::Error), InputIteration(Box), - #[error(transparent)] TreeTraverse(gix_traverse::tree::breadthfirst::Error), - #[error(transparent)] TreeChanges(gix_diff::tree::Error), - #[error("Operation interrupted")] Interrupted, } + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::CommitDecode(err) => std::fmt::Display::fmt(err, f), + Error::FindExisting(err) => std::fmt::Display::fmt(err, f), + Error::InputIteration(err) => std::fmt::Display::fmt(err, f), + Error::TreeTraverse(err) => std::fmt::Display::fmt(err, f), + Error::TreeChanges(err) => std::fmt::Display::fmt(err, f), + Error::Interrupted => f.write_str("Operation interrupted"), + } + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::CommitDecode(err) => err.source(), + Error::FindExisting(err) => err.source(), + Error::InputIteration(err) => err.source(), + Error::TreeTraverse(err) => err.source(), + Error::TreeChanges(err) => err.source(), + Error::Interrupted => None, + } + } +} + +impl From for Error { + fn from(err: gix_object::find::existing::Error) -> Self { + Error::FindExisting(err) + } +} 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 a9320ae4555..3d009685e52 100644 --- a/gix-pack/src/data/output/entry/iter_from_counts.rs +++ b/gix-pack/src/data/output/entry/iter_from_counts.rs @@ -409,13 +409,35 @@ 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)] + #[derive(Debug)] + #[allow(missing_docs)] pub enum Error { - #[error(transparent)] Find(gix_object::find::Error), - #[error(transparent)] - NewEntry(#[from] entry::Error), + NewEntry(entry::Error), + } + + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Find(err) => std::fmt::Display::fmt(err, f), + Error::NewEntry(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::Find(err) => err.source(), + Error::NewEntry(err) => err.source(), + } + } + } + + impl From for Error { + fn from(err: entry::Error) -> Self { + Error::NewEntry(err) + } } /// 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 5fe178836da..677759d7b83 100644 --- a/gix-pack/src/data/output/entry/mod.rs +++ b/gix-pack/src/data/output/entry/mod.rs @@ -32,13 +32,41 @@ pub enum Kind { } /// The error returned by [`output::Entry::from_data()`]. -#[expect(missing_docs)] -#[derive(Debug, thiserror::Error)] +#[allow(missing_docs)] +#[derive(Debug)] pub enum Error { - #[error("{0}")] - ZlibDeflate(#[from] std::io::Error), - #[error(transparent)] - EntryType(#[from] crate::data::entry::decode::Error), + ZlibDeflate(std::io::Error), + EntryType(crate::data::entry::decode::Error), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::ZlibDeflate(err) => write!(f, "{err}"), + Error::EntryType(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::ZlibDeflate(err) => Some(err), + Error::EntryType(err) => err.source(), + } + } +} + +impl From for Error { + fn from(err: std::io::Error) -> Self { + Error::ZlibDeflate(err) + } +} + +impl From for Error { + fn from(err: crate::data::entry::decode::Error) -> Self { + Error::EntryType(err) + } } impl output::Entry { diff --git a/gix-pack/src/index/init.rs b/gix-pack/src/index/init.rs index 770e19f1d62..c44258cb3ea 100644 --- a/gix-pack/src/index/init.rs +++ b/gix-pack/src/index/init.rs @@ -6,18 +6,38 @@ use std::{ use crate::index::{self, FAN_LEN, V2_SIGNATURE, Version}; /// Returned by [`index::File::at()`]. -#[derive(thiserror::Error, Debug)] -#[expect(missing_docs)] +#[derive(Debug)] +#[allow(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 }, + Corrupt { + message: String, + }, + UnsupportedVersion { + version: u32, + }, +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Io { path, .. } => write!(f, "Could not open pack index file at '{}'", path.display()), + Error::Corrupt { message } => f.write_str(message), + Error::UnsupportedVersion { version } => write!(f, "Unsupported index version: {version})"), + } + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::Io { source, .. } => Some(source), + Error::Corrupt { .. } | Error::UnsupportedVersion { .. } => None, + } + } } const N32_SIZE: usize = size_of::(); diff --git a/gix-pack/src/index/traverse/error.rs b/gix-pack/src/index/traverse/error.rs index b64b9a1a211..9ccb39c65db 100644 --- a/gix-pack/src/index/traverse/error.rs +++ b/gix-pack/src/index/traverse/error.rs @@ -1,44 +1,96 @@ use crate::index; /// Returned by [`index::File::traverse_with_index()`] and [`index::File::traverse_with_lookup`] -#[derive(thiserror::Error, Debug)] -#[expect(missing_docs)] +#[derive(Debug)] +#[allow(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")] + Processor(E), + IndexVerify(index::verify::checksum::Error), + Tree(crate::cache::delta::from_offsets::Error), + TreeTraversal(crate::cache::delta::traverse::Error), + EntryType(crate::data::entry::decode::Error), 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")] + PackMismatch(gix_hash::verify::Error), + PackVerify(crate::verify::checksum::Error), PackObjectVerify { offset: u64, - #[source] source: gix_object::data::verify::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, } + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Processor(_) => f.write_str("One of the traversal processors failed"), + Error::IndexVerify(_) => f.write_str("Failed to verify index file checksum"), + Error::Tree(_) => f.write_str("The pack delta tree index could not be built"), + Error::TreeTraversal(_) => f.write_str("The tree traversal failed"), + Error::EntryType(err) => std::fmt::Display::fmt(err, f), + Error::PackDecode { id, offset, .. } => { + write!(f, "Object {id} at offset {offset} could not be decoded") + } + Error::PackMismatch(_) => f.write_str("The packfiles checksum didn't match the index file checksum"), + Error::PackVerify(_) => f.write_str("Failed to verify pack file checksum"), + Error::PackObjectVerify { offset, .. } => write!( + f, + "Error verifying object at offset {offset} against checksum in the index file" + ), + Error::Crc32Mismatch { + expected, + actual, + offset, + kind, + } => write!( + f, + "The CRC32 of {kind} object at offset {offset} didn't match the checksum in the index file: expected {expected}, got {actual}" + ), + Error::Interrupted => f.write_str("Interrupted"), + } + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::Processor(err) => Some(err), + Error::IndexVerify(err) => Some(err), + Error::Tree(err) => Some(err), + Error::TreeTraversal(err) => Some(err), + Error::EntryType(err) => err.source(), + Error::PackDecode { source, .. } => Some(source), + Error::PackMismatch(err) => Some(err), + Error::PackVerify(err) => Some(err), + Error::PackObjectVerify { source, .. } => Some(source), + Error::Crc32Mismatch { .. } | Error::Interrupted => None, + } + } +} + +impl From for Error { + fn from(err: crate::cache::delta::from_offsets::Error) -> Self { + Error::Tree(err) + } +} + +impl From for Error { + fn from(err: crate::cache::delta::traverse::Error) -> Self { + Error::TreeTraversal(err) + } +} + +impl From for Error { + fn from(err: crate::data::entry::decode::Error) -> Self { + Error::EntryType(err) + } +} diff --git a/gix-pack/src/index/verify.rs b/gix-pack/src/index/verify.rs index 7b907bef494..f4a3f9c676f 100644 --- a/gix-pack/src/index/verify.rs +++ b/gix-pack/src/index/verify.rs @@ -12,20 +12,18 @@ pub mod integrity { use gix_object::bstr::BString; /// Returned by [`index::File::verify_integrity()`][crate::index::File::verify_integrity()]. - #[derive(thiserror::Error, Debug)] - #[expect(missing_docs)] + #[derive(Debug)] + #[allow(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")] + Io(std::io::Error), + Fan { + index: usize, + }, 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, @@ -34,6 +32,44 @@ pub mod integrity { }, } + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Io(_) => f.write_str("Reserialization of an object failed"), + Error::Fan { index } => write!( + f, + "The fan at index {index} is out of order as it's larger then the following value." + ), + Error::ObjectDecode { kind, id, .. } => write!(f, "{kind} object {id} could not be decoded"), + Error::ObjectEncodeMismatch { + kind, + id, + expected, + actual, + } => write!( + f, + "{kind} object {id} wasn't re-encoded without change, wanted\n{expected}\n\nGOT\n\n{actual}" + ), + } + } + } + + impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::Io(err) => Some(err), + Error::ObjectDecode { source, .. } => Some(source), + Error::Fan { .. } | Error::ObjectEncodeMismatch { .. } => None, + } + } + } + + impl From for Error { + fn from(err: std::io::Error) -> Self { + Error::Io(err) + } + } + /// 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. @@ -233,6 +269,7 @@ where } } + #[allow(clippy::too_many_arguments)] fn verify_entry( verify_mode: Mode, encode_buf: &mut Vec, diff --git a/gix-pack/src/index/write/error.rs b/gix-pack/src/index/write/error.rs index ee92a6ce991..cd52b91a4da 100644 --- a/gix-pack/src/index/write/error.rs +++ b/gix-pack/src/index/write/error.rs @@ -1,23 +1,83 @@ /// Returned by [`crate::index::write_data_iter_to_stream()`] -#[derive(thiserror::Error, Debug)] -#[expect(missing_docs)] +#[derive(Debug)] +#[allow(missing_docs)] pub enum Error { - #[error("An error occurred when writing the pack index file")] - Io(#[from] gix_hash::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)] + Io(gix_hash::io::Error), + PackEntryDecode(crate::data::input::Error), 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), + Tree(crate::cache::delta::Error), + TreeTraversal(crate::cache::delta::traverse::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("An error occurred when writing the pack index file"), + Error::PackEntryDecode(_) => f.write_str("A pack entry could not be extracted"), + Error::Unsupported(version) => write!( + f, + "Indices of type {} cannot be written, only {} are supported", + *version as usize, + crate::index::Version::default() as usize + ), + Error::IteratorInvariantNoRefDelta => f.write_str( + "Ref delta objects are not supported as there is no way to look them up. Resolve them beforehand.", + ), + Error::IteratorInvariantTrailer => f.write_str( + "The iterator failed to set a trailing hash over all prior pack entries in the last provided entry", + ), + Error::IteratorInvariantTooManyObjects(count) => { + write!(f, "Only u32::MAX objects can be stored in a pack, found {count}") + } + Error::IteratorInvariantBaseOffset { pack_offset, distance } => { + write!(f, "{pack_offset} is not a valid offset for pack offset {distance}") + } + Error::Tree(err) => std::fmt::Display::fmt(err, f), + Error::TreeTraversal(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::Io(err) => Some(err), + Error::PackEntryDecode(err) => Some(err), + Error::Tree(err) => err.source(), + Error::TreeTraversal(err) => err.source(), + Error::Unsupported(_) + | Error::IteratorInvariantNoRefDelta + | Error::IteratorInvariantTrailer + | Error::IteratorInvariantTooManyObjects(_) + | Error::IteratorInvariantBaseOffset { .. } => None, + } + } +} + +impl From for Error { + fn from(err: gix_hash::io::Error) -> Self { + Error::Io(err) + } +} + +impl From for Error { + fn from(err: crate::data::input::Error) -> Self { + Error::PackEntryDecode(err) + } +} + +impl From for Error { + fn from(err: crate::cache::delta::Error) -> Self { + Error::Tree(err) + } +} + +impl From for Error { + fn from(err: crate::cache::delta::traverse::Error) -> Self { + Error::TreeTraversal(err) + } } diff --git a/gix-pack/src/multi_index/chunk.rs b/gix-pack/src/multi_index/chunk.rs index 1c0583b31f6..0845e746ccd 100644 --- a/gix-pack/src/multi_index/chunk.rs +++ b/gix-pack/src/multi_index/chunk.rs @@ -14,21 +14,32 @@ pub mod index_names { use gix_object::bstr::BString; /// The error returned by [`from_bytes()`][super::from_bytes()]. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] + #[derive(Debug)] + #[allow(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 std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::NotOrderedAlphabetically => f.write_str("The pack names were not ordered alphabetically."), + Error::MissingNullByte => f.write_str("Each pack path name must be terminated with a null byte"), + Error::OutOfMemory => f.write_str("Entry too large to fit in memory"), + Error::PathEncoding { path } => { + write!(f, "Couldn't turn path '{path}' into OS path due to encoding issues") + } + Error::UnknownTrailerBytes => f.write_str("non-padding bytes found after all paths were read."), + } + } + } + + impl std::error::Error for Error {} + impl From for Error { #[cold] fn from(_: TryReserveError) -> Self { diff --git a/gix-pack/src/multi_index/init.rs b/gix-pack/src/multi_index/init.rs index ecea678aedd..52c69f8f378 100644 --- a/gix-pack/src/multi_index/init.rs +++ b/gix-pack/src/multi_index/init.rs @@ -6,30 +6,86 @@ mod error { use crate::multi_index::chunk; /// The error returned by [File::at()][super::File::at()]. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] + #[derive(Debug)] + #[allow(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")] + Corrupt { + message: &'static str, + }, + UnsupportedVersion { + version: u8, + }, + UnsupportedObjectHash { + kind: u8, + }, + ChunkFileQuery(gix_error::Message), + ChunkFileDecode(gix_error::ValidationError), 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 }, + PackNames(chunk::index_names::decode::Error), + InvalidChunkSize { + id: gix_chunk::Id, + message: &'static str, + }, + } + + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Io { path, .. } => write!(f, "Could not open multi-index file at '{}'", path.display()), + Error::Corrupt { message } => f.write_str(message), + Error::UnsupportedVersion { version } => write!(f, "Unsupported multi-index version: {version})"), + Error::UnsupportedObjectHash { kind } => write!(f, "Unsupported hash kind: {kind})"), + Error::ChunkFileQuery(err) => std::fmt::Display::fmt(err, f), + Error::ChunkFileDecode(err) => std::fmt::Display::fmt(err, f), + Error::MultiPackFanSize => { + f.write_str("The multi-pack fan doesn't have the correct size of 256 * 4 bytes") + } + Error::PackNames(err) => std::fmt::Display::fmt(err, f), + Error::InvalidChunkSize { id, message } => write!( + f, + "multi-index chunk {:?} has invalid size: {message}", + String::from_utf8_lossy(id) + ), + } + } + } + + impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::Io { source, .. } => Some(source), + Error::ChunkFileQuery(err) => err.source(), + Error::ChunkFileDecode(err) => err.source(), + Error::PackNames(err) => err.source(), + Error::Corrupt { .. } + | Error::UnsupportedVersion { .. } + | Error::UnsupportedObjectHash { .. } + | Error::MultiPackFanSize + | Error::InvalidChunkSize { .. } => None, + } + } + } + + impl From for Error { + fn from(err: gix_error::Message) -> Self { + Error::ChunkFileQuery(err) + } + } + + impl From for Error { + fn from(err: gix_error::ValidationError) -> Self { + Error::ChunkFileDecode(err) + } + } + + impl From for Error { + fn from(err: chunk::index_names::decode::Error) -> Self { + Error::PackNames(err) + } } } diff --git a/gix-pack/src/multi_index/verify.rs b/gix-pack/src/multi_index/verify.rs index 5052d1127cd..a447214c92b 100644 --- a/gix-pack/src/multi_index/verify.rs +++ b/gix-pack/src/multi_index/verify.rs @@ -9,37 +9,112 @@ 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)] + #[derive(Debug)] + #[allow(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")] + MultiIndexChecksum(crate::multi_index::verify::checksum::Error), + IndexIntegrity(crate::index::verify::integrity::Error), + BundleInit(crate::bundle::init::Error), + UnexpectedObjectCount { + actual: usize, + expected: usize, + }, + OidNotFound { + id: gix_hash::ObjectId, + }, + OutOfOrder { + index: EntryIndex, + }, + Fan { + index: usize, + }, Empty, - #[error("The multi-index path '{path}' has no parent directory")] - InvalidPath { path: std::path::PathBuf }, - #[error("Interrupted")] + InvalidPath { + path: std::path::PathBuf, + }, Interrupted, } + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::PackOffsetMismatch { + id, + expected_pack_offset, + actual_pack_offset, + } => write!( + f, + "Object {id} should be at pack-offset {expected_pack_offset} but was found at {actual_pack_offset}" + ), + Error::MultiIndexChecksum(err) => std::fmt::Display::fmt(err, f), + Error::IndexIntegrity(err) => std::fmt::Display::fmt(err, f), + Error::BundleInit(err) => std::fmt::Display::fmt(err, f), + Error::UnexpectedObjectCount { actual, expected } => { + write!( + f, + "Counted {actual} objects, but expected {expected} as per multi-index" + ) + } + Error::OidNotFound { id } => { + write!(f, "{id} wasn't found in the index referenced in the multi-pack index") + } + Error::OutOfOrder { index } => { + write!(f, "The object id at multi-index entry {index} wasn't in order") + } + Error::Fan { index } => write!( + f, + "The fan at index {index} is out of order as it's larger then the following value." + ), + Error::Empty => f.write_str("The multi-index claims to have no objects"), + Error::InvalidPath { path } => { + write!(f, "The multi-index path '{}' has no parent directory", path.display()) + } + Error::Interrupted => f.write_str("Interrupted"), + } + } + } + + impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::MultiIndexChecksum(err) => err.source(), + Error::IndexIntegrity(err) => err.source(), + Error::BundleInit(err) => err.source(), + Error::PackOffsetMismatch { .. } + | Error::UnexpectedObjectCount { .. } + | Error::OidNotFound { .. } + | Error::OutOfOrder { .. } + | Error::Fan { .. } + | Error::Empty + | Error::InvalidPath { .. } + | Error::Interrupted => None, + } + } + } + + impl From for Error { + fn from(err: crate::multi_index::verify::checksum::Error) -> Self { + Error::MultiIndexChecksum(err) + } + } + + impl From for Error { + fn from(err: crate::index::verify::integrity::Error) -> Self { + Error::IndexIntegrity(err) + } + } + + impl From for Error { + fn from(err: crate::bundle::init::Error) -> Self { + Error::BundleInit(err) + } + } + /// Returned by [`multi_index::File::verify_integrity()`][crate::multi_index::File::verify_integrity()]. pub struct Outcome { /// The computed checksum of the multi-index which matched the stored one. diff --git a/gix-pack/src/multi_index/write.rs b/gix-pack/src/multi_index/write.rs index 778052cea3d..183ec3b2b4e 100644 --- a/gix-pack/src/multi_index/write.rs +++ b/gix-pack/src/multi_index/write.rs @@ -4,15 +4,44 @@ use crate::multi_index; mod error { /// The error returned by [`crate::multi_index::write_from_index_paths()`]. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] + #[derive(Debug)] + #[allow(missing_docs)] pub enum Error { - #[error(transparent)] - Io(#[from] gix_hash::io::Error), - #[error("Interrupted")] + Io(gix_hash::io::Error), Interrupted, - #[error(transparent)] - OpenIndex(#[from] crate::index::init::Error), + OpenIndex(crate::index::init::Error), + } + + 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::Interrupted => f.write_str("Interrupted"), + Error::OpenIndex(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::Io(err) => err.source(), + Error::OpenIndex(err) => err.source(), + Error::Interrupted => None, + } + } + } + + impl From for Error { + fn from(err: gix_hash::io::Error) -> Self { + Error::Io(err) + } + } + + impl From for Error { + fn from(err: crate::index::init::Error) -> Self { + Error::OpenIndex(err) + } } } pub use error::Error; diff --git a/gix-pack/src/verify.rs b/gix-pack/src/verify.rs index aa9bc9ebfae..7024514f808 100644 --- a/gix-pack/src/verify.rs +++ b/gix-pack/src/verify.rs @@ -5,15 +5,44 @@ 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)] + #[derive(Debug)] + #[allow(missing_docs)] pub enum Error { - #[error("Interrupted by user")] Interrupted, - #[error("Failed to hash data")] - Hasher(#[from] gix_hash::hasher::Error), - #[error(transparent)] - Verify(#[from] gix_hash::verify::Error), + Hasher(gix_hash::hasher::Error), + Verify(gix_hash::verify::Error), + } + + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Interrupted => f.write_str("Interrupted by user"), + Error::Hasher(_) => f.write_str("Failed to hash data"), + Error::Verify(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::Hasher(err) => Some(err), + Error::Verify(err) => err.source(), + Error::Interrupted => None, + } + } + } + + impl From for Error { + fn from(err: gix_hash::hasher::Error) -> Self { + Error::Hasher(err) + } + } + + impl From for Error { + fn from(err: gix_hash::verify::Error) -> Self { + Error::Verify(err) + } } } diff --git a/gix-packetline/Cargo.toml b/gix-packetline/Cargo.toml index ec1b99ed23d..2d8a5048fe4 100644 --- a/gix-packetline/Cargo.toml +++ b/gix-packetline/Cargo.toml @@ -42,7 +42,6 @@ required-features = ["blocking-io"] gix-trace = { version = "^0.1.21", path = "../gix-trace" } serde = { version = "1.0.114", optional = true, default-features = false, features = ["std", "derive"] } -thiserror = "2.0.18" faster-hex = { version = "0.10.0", default-features = false, features = ["std"] } bstr = { version = "1.12.0", default-features = false, features = ["std"] } # async support diff --git a/gix-packetline/src/decode.rs b/gix-packetline/src/decode.rs index d3c2530ef62..ad723476fc7 100644 --- a/gix-packetline/src/decode.rs +++ b/gix-packetline/src/decode.rs @@ -3,36 +3,70 @@ 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)] +#[derive(Debug)] #[expect(missing_docs)] pub enum Error { - #[error("Failed to decode the first four hex bytes indicating the line length: {err}")] HexDecode { err: String }, - #[error( - "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 }, } +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::HexDecode { err } => { + write!( + f, + "Failed to decode the first four hex bytes indicating the line length: {err}" + ) + } + Error::DataLengthLimitExceeded { length_in_bytes } => write!( + f, + "The data received claims to be larger than the maximum allowed size: got {length_in_bytes}, exceeds {MAX_DATA_LEN}" + ), + Error::DataIsEmpty => f.write_str("Received an invalid empty line"), + Error::InvalidLineLength => f.write_str("Received an invalid line of length 3"), + Error::Line { data, bytes_consumed } => write!(f, "{data:?} - consumed {bytes_consumed} bytes"), + Error::NotEnoughData { bytes_needed } => { + write!( + f, + "Needing {bytes_needed} additional bytes to decode the line successfully" + ) + } + } + } +} + +impl std::error::Error for Error {} + /// pub mod band { /// The error used in [`PacketLineRef::decode_band()`][super::PacketLineRef::decode_band()]. - #[derive(Debug, thiserror::Error)] + #[derive(Debug)] #[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, } + + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::InvalidSideBand { band_id } => { + write!( + f, + "attempt to decode a non-side channel line or input was malformed: {band_id}" + ) + } + Error::NonDataLine => f.write_str("attempt to decode a non-data line into a side-channel band"), + } + } + } + + impl std::error::Error for Error {} } /// A utility return type to support incremental parsing of packet lines. diff --git a/gix-packetline/src/encode.rs b/gix-packetline/src/encode.rs index eef8e172bab..498075dd734 100644 --- a/gix-packetline/src/encode.rs +++ b/gix-packetline/src/encode.rs @@ -1,15 +1,26 @@ use super::MAX_DATA_LEN; /// The error returned by most functions in the [`encode`](crate::encode) module -#[derive(Debug, thiserror::Error)] +#[derive(Debug)] #[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, } +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::DataLengthLimitExceeded { length_in_bytes } => { + write!(f, "Cannot encode more than {MAX_DATA_LEN} bytes, got {length_in_bytes}") + } + Error::DataIsEmpty => f.write_str("Empty lines are invalid"), + } + } +} + +impl std::error::Error for Error {} + 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-path/Cargo.toml b/gix-path/Cargo.toml index 19ce1afd34a..6e2815372e6 100644 --- a/gix-path/Cargo.toml +++ b/gix-path/Cargo.toml @@ -18,7 +18,6 @@ doctest = true gix-trace = { version = "^0.1.21", path = "../gix-trace" } gix-validate = { version = "^0.11.3", path = "../gix-validate" } bstr = { version = "1.12.0", default-features = false, features = ["std"] } -thiserror = "2.0.18" [dev-dependencies] gix-testtools = { path = "../tests/tools" } diff --git a/gix-path/src/realpath.rs b/gix-path/src/realpath.rs index d320768eb2b..9a20a9435ac 100644 --- a/gix-path/src/realpath.rs +++ b/gix-path/src/realpath.rs @@ -1,21 +1,45 @@ /// The error returned by [`realpath()`][super::realpath()]. -#[derive(Debug, thiserror::Error)] +#[derive(Debug)] #[expect(missing_docs)] pub enum Error { - #[error("The maximum allowed number {} of symlinks in path is exceeded", .max_symlinks)] MaxSymlinksExceeded { max_symlinks: u8 }, - #[error("Cannot resolve symlinks in path with more than {max_symlink_checks} components (takes too long)")] ExcessiveComponentCount { max_symlink_checks: usize }, - #[error(transparent)] ReadLink(std::io::Error), - #[error(transparent)] CurrentWorkingDir(std::io::Error), - #[error("Empty is not a valid path")] EmptyPath, - #[error("Ran out of path components while following parent component '..'")] MissingParent, } +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::MaxSymlinksExceeded { max_symlinks } => write!( + f, + "The maximum allowed number {max_symlinks} of symlinks in path is exceeded" + ), + Error::ExcessiveComponentCount { max_symlink_checks } => write!( + f, + "Cannot resolve symlinks in path with more than {max_symlink_checks} components (takes too long)" + ), + Error::ReadLink(err) | Error::CurrentWorkingDir(err) => std::fmt::Display::fmt(err, f), + Error::EmptyPath => f.write_str("Empty is not a valid path"), + Error::MissingParent => f.write_str("Ran out of path components while following parent component '..'"), + } + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::ReadLink(err) | Error::CurrentWorkingDir(err) => err.source(), + Error::MaxSymlinksExceeded { .. } + | Error::ExcessiveComponentCount { .. } + | Error::EmptyPath + | Error::MissingParent => None, + } + } +} + /// The default amount of symlinks we may follow when resolving a path in [`realpath()`][crate::realpath()]. pub const MAX_SYMLINKS: u8 = 32; diff --git a/gix-path/src/relative_path.rs b/gix-path/src/relative_path.rs index 788cead7f01..9b509e7e8b8 100644 --- a/gix-path/src/relative_path.rs +++ b/gix-path/src/relative_path.rs @@ -37,15 +37,44 @@ impl RelativePath { } /// The error used in [`RelativePath`]. -#[derive(Debug, thiserror::Error)] +#[derive(Debug)] #[expect(missing_docs)] pub enum Error { - #[error("A RelativePath is not allowed to be absolute")] IsAbsolute, - #[error(transparent)] - ContainsInvalidComponent(#[from] gix_validate::path::component::Error), - #[error(transparent)] - IllegalUtf8(#[from] crate::Utf8Error), + ContainsInvalidComponent(gix_validate::path::component::Error), + IllegalUtf8(crate::Utf8Error), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::IsAbsolute => f.write_str("A RelativePath is not allowed to be absolute"), + Error::ContainsInvalidComponent(err) => std::fmt::Display::fmt(err, f), + Error::IllegalUtf8(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::IsAbsolute => None, + Error::ContainsInvalidComponent(err) => err.source(), + Error::IllegalUtf8(err) => err.source(), + } + } +} + +impl From for Error { + fn from(err: gix_validate::path::component::Error) -> Self { + Error::ContainsInvalidComponent(err) + } +} + +impl From for Error { + fn from(err: crate::Utf8Error) -> Self { + Error::IllegalUtf8(err) + } } fn relative_path_from_value_and_path<'a>(path_bstr: &'a BStr, path: &Path) -> Result<&'a RelativePath, Error> { diff --git a/gix-pathspec/Cargo.toml b/gix-pathspec/Cargo.toml index e737e321095..107f73eeb5f 100644 --- a/gix-pathspec/Cargo.toml +++ b/gix-pathspec/Cargo.toml @@ -26,7 +26,6 @@ gix-config-value = { version = "^0.19.0", path = "../gix-config-value" } bstr = { version = "1.12.0", default-features = false, features = ["std"] } bitflags = "2" -thiserror = "2.0.18" [dev-dependencies] gix-testtools = { path = "../tests/tools" } diff --git a/gix-pathspec/src/defaults.rs b/gix-pathspec/src/defaults.rs index d48c259575b..ecfa392f6c1 100644 --- a/gix-pathspec/src/defaults.rs +++ b/gix-pathspec/src/defaults.rs @@ -5,14 +5,36 @@ use crate::{Defaults, MagicSignature, SearchMode}; /// pub mod from_environment { /// The error returned by [Defaults::from_environment()](super::Defaults::from_environment()). - #[derive(Debug, thiserror::Error)] + #[derive(Debug)] #[expect(missing_docs)] pub enum Error { - #[error(transparent)] - ParseValue(#[from] gix_config_value::Error), - #[error("Glob and no-glob settings are mutually exclusive")] + ParseValue(gix_config_value::Error), MixedGlobAndNoGlob, } + + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::ParseValue(err) => std::fmt::Display::fmt(err, f), + Error::MixedGlobAndNoGlob => f.write_str("Glob and no-glob settings are mutually exclusive"), + } + } + } + + impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::ParseValue(err) => err.source(), + Error::MixedGlobAndNoGlob => None, + } + } + } + + impl From for Error { + fn from(err: gix_config_value::Error) -> Self { + Error::ParseValue(err) + } + } } impl Defaults { diff --git a/gix-pathspec/src/lib.rs b/gix-pathspec/src/lib.rs index d18e0a38810..0efa6f24d45 100644 --- a/gix-pathspec/src/lib.rs +++ b/gix-pathspec/src/lib.rs @@ -51,14 +51,30 @@ pub mod normalize { use std::path::PathBuf; /// The error returned by [Pattern::normalize()](super::Pattern::normalize()). - #[derive(Debug, thiserror::Error)] + #[derive(Debug)] #[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 }, } + + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::AbsolutePathOutsideOfWorktree { path, worktree_path } => write!( + f, + "The path '{}' is not inside of the worktree '{}'", + path.display(), + worktree_path.display() + ), + Error::OutsideOfWorktree { path } => { + write!(f, "The path '{}' leaves the repository", path.display()) + } + } + } + } + + impl std::error::Error for Error {} } mod pattern; diff --git a/gix-pathspec/src/parse.rs b/gix-pathspec/src/parse.rs index f5d02d8649f..3f5d4c04eb8 100644 --- a/gix-pathspec/src/parse.rs +++ b/gix-pathspec/src/parse.rs @@ -5,31 +5,53 @@ use bstr::{BStr, BString, ByteSlice, ByteVec}; use crate::{Defaults, MagicSignature, Pattern, SearchMode}; /// The error returned by [parse()][crate::parse()]. -#[derive(thiserror::Error, Debug)] +#[derive(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("Attribute has non-ascii characters or starts with '-': {attribute:?}")] InvalidAttribute { attribute: BString }, - #[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, } +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::EmptyString => f.write_str("An empty string is not a valid pathspec"), + Error::InvalidKeyword { keyword } => { + write!(f, "Found {keyword:?} in signature, which is not a valid keyword") + } + Error::Unimplemented { short_keyword } => write!(f, "Unimplemented short keyword: {short_keyword:?}"), + Error::MissingClosingParenthesis => f.write_str("Missing ')' at the end of pathspec signature"), + Error::InvalidAttribute { attribute } => write!( + f, + "Attribute has non-ascii characters or starts with '-': {attribute:?}" + ), + Error::InvalidAttributeValue { character } => { + write!(f, "Invalid character in attribute value: {character:?}") + } + Error::TrailingEscapeCharacter => { + f.write_str(r"Escape character '\' is not allowed as the last character in an attribute value") + } + Error::EmptyAttribute => f.write_str("Attribute specification cannot be empty"), + Error::MultipleAttributeSpecifications => { + f.write_str("Only one attribute specification is allowed in the same pathspec") + } + Error::IncompatibleSearchModes => { + f.write_str("'literal' and 'glob' keywords cannot be used together in the same pathspec") + } + } + } +} + +impl std::error::Error for Error {} + impl Pattern { /// Try to parse a path-spec pattern from the given `input` bytes. pub fn from_bytes( diff --git a/gix-prompt/Cargo.toml b/gix-prompt/Cargo.toml index cfcf8332b52..c05c0ce75e7 100644 --- a/gix-prompt/Cargo.toml +++ b/gix-prompt/Cargo.toml @@ -18,8 +18,6 @@ doctest = false gix-command = { version = "^0.9.1", path = "../gix-command" } gix-config-value = { version = "^0.19.0", path = "../gix-config-value" } -thiserror = "2.0.18" - [target.'cfg(unix)'.dependencies] rustix = { version = "1.1.2", features = ["termios"] } parking_lot = "0.12.4" diff --git a/gix-prompt/src/types.rs b/gix-prompt/src/types.rs index 705ed89476f..8c029cbcf34 100644 --- a/gix-prompt/src/types.rs +++ b/gix-prompt/src/types.rs @@ -1,21 +1,56 @@ use std::path::PathBuf; /// The error returned by [ask()][crate::ask()]. -#[derive(Debug, thiserror::Error)] +#[derive(Debug)] #[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), + TtyIo(std::io::Error), #[cfg(unix)] - #[error("Failed to obtain or set terminal configuration")] - TerminalConfiguration(#[from] rustix::io::Errno), + TerminalConfiguration(rustix::io::Errno), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Disabled => f.write_str("Terminal prompts are disabled"), + Error::UnsupportedPlatform => { + f.write_str("The current platform has no implementation for prompting in the terminal") + } + Error::TtyIo(_) => write!( + f, + "Failed to open terminal at {:?} for writing prompt, or to write it", + crate::unix::TTY_PATH + ), + #[cfg(unix)] + Error::TerminalConfiguration(_) => f.write_str("Failed to obtain or set terminal configuration"), + } + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::Disabled | Error::UnsupportedPlatform => None, + Error::TtyIo(err) => Some(err), + #[cfg(unix)] + Error::TerminalConfiguration(err) => Some(err), + } + } +} + +impl From for Error { + fn from(err: std::io::Error) -> Self { + Error::TtyIo(err) + } +} + +#[cfg(unix)] +impl From for Error { + fn from(err: rustix::io::Errno) -> Self { + Error::TerminalConfiguration(err) + } } /// The way the user is prompted. diff --git a/gix-protocol/Cargo.toml b/gix-protocol/Cargo.toml index 74a305bd249..02ecd4c269d 100644 --- a/gix-protocol/Cargo.toml +++ b/gix-protocol/Cargo.toml @@ -90,7 +90,6 @@ gix-credentials = { version = "^0.39.0", path = "../gix-credentials", optional = gix-refspec = { version = "^0.44.0", 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 7a3019b451b..50419bb3898 100644 --- a/gix-protocol/src/command.rs +++ b/gix-protocol/src/command.rs @@ -239,14 +239,27 @@ mod with_io { use bstr::BString; /// The error returned by [Command::validate_argument_prefixes()](super::Command::validate_argument_prefixes()). - #[derive(Debug, thiserror::Error)] + #[derive(Debug)] #[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 }, } + + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::UnsupportedArgument { command, argument } => { + write!(f, "{command}: argument {argument} is not known or allowed") + } + Error::UnsupportedCapability { command, feature } => { + write!(f, "{command}: capability {feature} is not supported") + } + } + } + } + + impl std::error::Error for Error {} } } #[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 dbe1598f060..b17dcc9f50a 100644 --- a/gix-protocol/src/fetch/error.rs +++ b/gix-protocol/src/fetch/error.rs @@ -1,30 +1,99 @@ /// The error returned by [`fetch()`](crate::fetch()). -#[derive(Debug, thiserror::Error)] +// TODO(review): hand-written impls preserve the `thiserror` semantics. `Negotiate`/`Client` are +// `#[error(transparent)]`; `ConsumePack` exposes its boxed source via `&**err`. +#[derive(Debug)] #[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}")] + FetchResponse(crate::fetch::response::Error), + Negotiate(crate::fetch::negotiate::Error), + Client(crate::transport::client::Error), MissingServerFeature { feature: &'static str, description: &'static str, }, - #[error("Could not write 'shallow' file to incorporate remote updates after fetching")] - WriteShallowFile(#[from] gix_shallow::write::Error), - #[error("Could not read 'shallow' file to send current shallow boundary")] - ReadShallowFile(#[from] gix_shallow::read::Error), - #[error("'shallow' file could not be locked in preparation for writing changes")] - LockShallowFile(#[from] gix_lock::acquire::Error), - #[error("Receiving objects from shallow remotes is prohibited due to the value of `clone.rejectShallow`")] + WriteShallowFile(gix_shallow::write::Error), + ReadShallowFile(gix_shallow::read::Error), + LockShallowFile(gix_lock::acquire::Error), 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), + ConsumePack(Box), + ReadRemainingBytes(std::io::Error), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::FetchResponse(_) => f.write_str("Could not decode server reply"), + Error::Negotiate(err) => std::fmt::Display::fmt(err, f), + Error::Client(err) => std::fmt::Display::fmt(err, f), + Error::MissingServerFeature { feature, description } => { + write!(f, "Server lack feature {feature:?}: {description}") + } + Error::WriteShallowFile(_) => { + f.write_str("Could not write 'shallow' file to incorporate remote updates after fetching") + } + Error::ReadShallowFile(_) => f.write_str("Could not read 'shallow' file to send current shallow boundary"), + Error::LockShallowFile(_) => { + f.write_str("'shallow' file could not be locked in preparation for writing changes") + } + Error::RejectShallowRemote => f.write_str( + "Receiving objects from shallow remotes is prohibited due to the value of `clone.rejectShallow`", + ), + Error::ConsumePack(_) => f.write_str("Failed to consume the pack sent by the remote"), + Error::ReadRemainingBytes(_) => f.write_str("Failed to read remaining bytes in stream"), + } + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::FetchResponse(err) => Some(err), + Error::Negotiate(err) => err.source(), + Error::Client(err) => err.source(), + Error::MissingServerFeature { .. } | Error::RejectShallowRemote => None, + Error::WriteShallowFile(err) => Some(err), + Error::ReadShallowFile(err) => Some(err), + Error::LockShallowFile(err) => Some(err), + Error::ConsumePack(err) => Some(&**err), + Error::ReadRemainingBytes(err) => Some(err), + } + } +} + +impl From for Error { + fn from(err: crate::fetch::response::Error) -> Self { + Error::FetchResponse(err) + } +} + +impl From for Error { + fn from(err: crate::fetch::negotiate::Error) -> Self { + Error::Negotiate(err) + } +} + +impl From for Error { + fn from(err: crate::transport::client::Error) -> Self { + Error::Client(err) + } +} + +impl From for Error { + fn from(err: gix_shallow::write::Error) -> Self { + Error::WriteShallowFile(err) + } +} + +impl From for Error { + fn from(err: gix_shallow::read::Error) -> Self { + Error::ReadShallowFile(err) + } +} + +impl From for Error { + fn from(err: gix_lock::acquire::Error) -> Self { + Error::LockShallowFile(err) + } } impl crate::transport::IsSpuriousError for Error { diff --git a/gix-protocol/src/fetch/negotiate.rs b/gix-protocol/src/fetch/negotiate.rs index 4d2c5839982..8f5a5176683 100644 --- a/gix-protocol/src/fetch/negotiate.rs +++ b/gix-protocol/src/fetch/negotiate.rs @@ -16,25 +16,82 @@ 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)] +// TODO(review): all variants but `NegotiationFailed` were `#[error(transparent)]`: `Display` and +// `source()` forward to the wrapped error, including through the boxed +// `AlternateRefsAndObjects` which deliberately has no `From`. +#[derive(Debug)] #[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(#[from] gix_revwalk::graph::get_or_insert_default::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)] + LookupCommitInGraph(gix_revwalk::graph::get_or_insert_default::Error), + OpenPackedRefsBuffer(gix_ref::packed::buffer::open::Error), + IO(std::io::Error), + InitRefIter(gix_ref::file::iter::loose_then_packed::Error), + PeelToId(gix_ref::peel::to_id::Error), AlternateRefsAndObjects(Box), } +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::NegotiationFailed { rounds } => write!( + f, + "We were unable to figure out what objects the server should send after {rounds} round(s)" + ), + Error::LookupCommitInGraph(err) => std::fmt::Display::fmt(err, f), + Error::OpenPackedRefsBuffer(err) => std::fmt::Display::fmt(err, f), + Error::IO(err) => std::fmt::Display::fmt(err, f), + Error::InitRefIter(err) => std::fmt::Display::fmt(err, f), + Error::PeelToId(err) => std::fmt::Display::fmt(err, f), + Error::AlternateRefsAndObjects(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::NegotiationFailed { .. } => None, + Error::LookupCommitInGraph(err) => err.source(), + Error::OpenPackedRefsBuffer(err) => err.source(), + Error::IO(err) => err.source(), + Error::InitRefIter(err) => err.source(), + Error::PeelToId(err) => err.source(), + Error::AlternateRefsAndObjects(err) => err.source(), + } + } +} + +impl From for Error { + fn from(err: gix_revwalk::graph::get_or_insert_default::Error) -> Self { + Error::LookupCommitInGraph(err) + } +} + +impl From for Error { + fn from(err: gix_ref::packed::buffer::open::Error) -> Self { + Error::OpenPackedRefsBuffer(err) + } +} + +impl From for Error { + fn from(err: std::io::Error) -> Self { + Error::IO(err) + } +} + +impl From for Error { + fn from(err: gix_ref::file::iter::loose_then_packed::Error) -> Self { + Error::InitRefIter(err) + } +} + +impl From for Error { + fn from(err: gix_ref::peel::to_id::Error) -> Self { + Error::PeelToId(err) + } +} + /// 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-protocol/src/fetch/refmap/init.rs b/gix-protocol/src/fetch/refmap/init.rs index b006a894bd2..7c559ce46ab 100644 --- a/gix-protocol/src/fetch/refmap/init.rs +++ b/gix-protocol/src/fetch/refmap/init.rs @@ -10,15 +10,48 @@ use crate::{ }; /// The error returned by [`crate::Handshake::prepare_lsrefs_or_extract_refmap()`]. -#[derive(Debug, thiserror::Error)] +// TODO(review): `MappingValidation`/`ListRefs` hand-preserve `#[error(transparent)]` semantics: +// `Display` and `source()` forward to the wrapped error. +#[derive(Debug)] #[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), + MappingValidation(gix_refspec::match_group::validate::Error), + ListRefs(crate::ls_refs::Error), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::UnknownObjectFormat { format } => { + write!(f, "The object format {format:?} as used by the remote is unsupported") + } + Error::MappingValidation(err) => std::fmt::Display::fmt(err, f), + Error::ListRefs(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::UnknownObjectFormat { .. } => None, + Error::MappingValidation(err) => err.source(), + Error::ListRefs(err) => err.source(), + } + } +} + +impl From for Error { + fn from(err: gix_refspec::match_group::validate::Error) -> Self { + Error::MappingValidation(err) + } +} + +impl From for Error { + fn from(err: crate::ls_refs::Error) -> Self { + Error::ListRefs(err) + } } /// For use in [`RefMap::from_refs()`]. diff --git a/gix-protocol/src/fetch/response/mod.rs b/gix-protocol/src/fetch/response/mod.rs index 61200b4bae4..bf99234d76b 100644 --- a/gix-protocol/src/fetch/response/mod.rs +++ b/gix-protocol/src/fetch/response/mod.rs @@ -4,23 +4,65 @@ use gix_transport::{Protocol, client}; use crate::{command::Feature, fetch::Response}; /// The error returned in the [response module][crate::fetch::response]. -#[derive(Debug, thiserror::Error)] +// TODO(review): `UploadPack`/`Transport` hand-preserve `#[error(transparent)]` semantics; `Io` +// renders a fixed message but still exposes its field via `source()`. Its +// `From` impl (below) predates the `thiserror` removal and was +// never derive-generated: it downcasts to recover a smuggled `UploadPack` error +// before falling back to plain `Io`. +#[derive(Debug)] #[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")] + Io(std::io::Error), + UploadPack(gix_transport::packetline::read::Error), + Transport(client::Error), MissingServerCapability { feature: &'static str }, - #[error("Encountered an unknown line prefix in {line:?}")] UnknownLineType { line: String }, - #[error("Unknown or unsupported header: {header:?}")] UnknownSectionHeader { header: String }, } +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Io(_) => f.write_str("Failed to read from line reader"), + Error::UploadPack(err) => std::fmt::Display::fmt(err, f), + Error::Transport(err) => std::fmt::Display::fmt(err, f), + Error::MissingServerCapability { feature } => { + write!( + f, + "Currently we require feature {feature:?}, which is not supported by the server" + ) + } + Error::UnknownLineType { line } => write!(f, "Encountered an unknown line prefix in {line:?}"), + Error::UnknownSectionHeader { header } => write!(f, "Unknown or unsupported header: {header:?}"), + } + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::Io(err) => Some(err), + Error::UploadPack(err) => err.source(), + Error::Transport(err) => err.source(), + Error::MissingServerCapability { .. } + | Error::UnknownLineType { .. } + | Error::UnknownSectionHeader { .. } => None, + } + } +} + +impl From for Error { + fn from(err: gix_transport::packetline::read::Error) -> Self { + Error::UploadPack(err) + } +} + +impl From for Error { + fn from(err: client::Error) -> Self { + Error::Transport(err) + } +} + impl From for Error { fn from(err: std::io::Error) -> Self { if err.kind() == std::io::ErrorKind::Other { diff --git a/gix-protocol/src/handshake/mod.rs b/gix-protocol/src/handshake/mod.rs index faac5fe1cb0..aeb99a876e5 100644 --- a/gix-protocol/src/handshake/mod.rs +++ b/gix-protocol/src/handshake/mod.rs @@ -167,23 +167,68 @@ mod error { use crate::{credentials, handshake::refs}; /// The error returned by [`handshake()`][crate::handshake()]. - #[derive(Debug, thiserror::Error)] + // TODO(review): hand-written impls preserve the `thiserror` semantics. `Transport`/`ParseRefs` + // are `#[error(transparent)]`; `Credentials` (`#[from]`) and the `source`-named + // field of `InvalidCredentials` surface as `source()` like before. + #[derive(Debug)] #[expect(missing_docs)] pub enum Error { - #[error("Failed to obtain credentials")] - Credentials(#[from] credentials::protocol::Error), - #[error("No credentials were returned at all as if the credential helper isn't functioning unknowingly")] + Credentials(credentials::protocol::Error), 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" - )] + Transport(client::Error), TransportProtocolPolicyViolation { actual_version: gix_transport::Protocol }, - #[error(transparent)] - ParseRefs(#[from] refs::parse::Error), + ParseRefs(refs::parse::Error), + } + + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Credentials(_) => f.write_str("Failed to obtain credentials"), + Error::EmptyCredentials => f.write_str( + "No credentials were returned at all as if the credential helper isn't functioning unknowingly", + ), + Error::InvalidCredentials { url, .. } => { + write!(f, "Credentials provided for \"{url}\" were not accepted by the remote") + } + Error::Transport(err) => std::fmt::Display::fmt(err, f), + Error::TransportProtocolPolicyViolation { actual_version } => write!( + f, + "The transport didn't accept the advertised server version {actual_version:?} and closed the connection client side" + ), + Error::ParseRefs(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::Credentials(err) => Some(err), + Error::EmptyCredentials | Error::TransportProtocolPolicyViolation { .. } => None, + Error::InvalidCredentials { source, .. } => Some(source), + Error::Transport(err) => err.source(), + Error::ParseRefs(err) => err.source(), + } + } + } + + impl From for Error { + fn from(err: credentials::protocol::Error) -> Self { + Error::Credentials(err) + } + } + + impl From for Error { + fn from(err: client::Error) -> Self { + Error::Transport(err) + } + } + + impl From for Error { + fn from(err: refs::parse::Error) -> Self { + Error::ParseRefs(err) + } } impl gix_transport::IsSpuriousError for Error { diff --git a/gix-protocol/src/handshake/refs/mod.rs b/gix-protocol/src/handshake/refs/mod.rs index 77c04ac74d7..5ccd7968df6 100644 --- a/gix-protocol/src/handshake/refs/mod.rs +++ b/gix-protocol/src/handshake/refs/mod.rs @@ -7,28 +7,83 @@ pub mod parse { use bstr::BString; /// The error returned when parsing References/refs from the server response. - #[derive(Debug, thiserror::Error)] + // TODO(review): `Io`/`DecodePacketline`/`Id` hand-preserve `#[error(transparent)]` semantics: + // `Display` and `source()` forward to the wrapped error. + #[derive(Debug)] #[expect(missing_docs)] pub enum Error { - #[error(transparent)] - Io(#[from] std::io::Error), - #[error(transparent)] - DecodePacketline(#[from] gix_transport::packetline::decode::Error), - #[error(transparent)] - Id(#[from] gix_hash::decode::Error), - #[error("{symref:?} could not be parsed. A symref is expected to look like :.")] + Io(std::io::Error), + DecodePacketline(gix_transport::packetline::decode::Error), + Id(gix_hash::decode::Error), 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 }, } + + 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::DecodePacketline(err) => std::fmt::Display::fmt(err, f), + Error::Id(err) => std::fmt::Display::fmt(err, f), + Error::MalformedSymref { symref } => { + write!( + f, + "{symref:?} could not be parsed. A symref is expected to look like :." + ) + } + Error::MalformedV1RefLine(line) => { + write!( + f, + "{line:?} could not be parsed. A V1 ref line should be ' '." + ) + } + Error::MalformedV2RefLine(line) => write!( + f, + "{line:?} could not be parsed. A V2 ref line should be ' [ (peeled|symref-target):'." + ), + Error::UnknownAttribute { attribute, line } => { + write!(f, "The ref attribute {attribute:?} is unknown. Found in line {line:?}") + } + Error::InvariantViolation { message } => f.write_str(message), + } + } + } + + impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::Io(err) => err.source(), + Error::DecodePacketline(err) => err.source(), + Error::Id(err) => err.source(), + Error::MalformedSymref { .. } + | Error::MalformedV1RefLine(_) + | Error::MalformedV2RefLine(_) + | Error::UnknownAttribute { .. } + | Error::InvariantViolation { .. } => None, + } + } + } + + impl From for Error { + fn from(err: std::io::Error) -> Self { + Error::Io(err) + } + } + + impl From for Error { + fn from(err: gix_transport::packetline::decode::Error) -> Self { + Error::DecodePacketline(err) + } + } + + impl From for Error { + fn from(err: gix_hash::decode::Error) -> Self { + Error::Id(err) + } + } } impl Ref { diff --git a/gix-protocol/src/ls_refs.rs b/gix-protocol/src/ls_refs.rs index 17d4a3bb3a3..66d5964a2cc 100644 --- a/gix-protocol/src/ls_refs.rs +++ b/gix-protocol/src/ls_refs.rs @@ -3,17 +3,61 @@ mod error { use crate::handshake::refs::parse; /// The error returned by invoking a [`super::function::LsRefsCommand`]. - #[derive(Debug, thiserror::Error)] + // TODO(review): all four variants were `#[error(transparent)]`: `Display` and `source()` + // forward to the wrapped error, exactly like the `thiserror`-generated code did. + #[derive(Debug)] #[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), + Io(std::io::Error), + Transport(gix_transport::client::Error), + Parse(parse::Error), + ArgumentValidation(crate::command::validate_argument_prefixes::Error), + } + + 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::Transport(err) => std::fmt::Display::fmt(err, f), + Error::Parse(err) => std::fmt::Display::fmt(err, f), + Error::ArgumentValidation(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::Io(err) => err.source(), + Error::Transport(err) => err.source(), + Error::Parse(err) => err.source(), + Error::ArgumentValidation(err) => err.source(), + } + } + } + + impl From for Error { + fn from(err: std::io::Error) -> Self { + Error::Io(err) + } + } + + impl From for Error { + fn from(err: gix_transport::client::Error) -> Self { + Error::Transport(err) + } + } + + impl From for Error { + fn from(err: parse::Error) -> Self { + Error::Parse(err) + } + } + + impl From for Error { + fn from(err: crate::command::validate_argument_prefixes::Error) -> Self { + Error::ArgumentValidation(err) + } } impl gix_transport::IsSpuriousError for Error { diff --git a/gix-protocol/tests/protocol/fetch/mod.rs b/gix-protocol/tests/protocol/fetch/mod.rs index 259a355e9bc..44b2cff63c6 100644 --- a/gix-protocol/tests/protocol/fetch/mod.rs +++ b/gix-protocol/tests/protocol/fetch/mod.rs @@ -22,19 +22,68 @@ mod error { use gix_transport::client; /// The error used in [`fetch()`][crate::fetch()]. - #[derive(Debug, thiserror::Error)] + #[derive(Debug)] #[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), + Handshake(handshake::Error), + Io(io::Error), + Transport(client::Error), + LsRefs(ls_refs::Error), + Response(response::Error), + } + + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Handshake(err) => std::fmt::Display::fmt(err, f), + Error::Io(_) => f.write_str("Could not access repository or failed to read streaming pack file"), + Error::Transport(err) => std::fmt::Display::fmt(err, f), + Error::LsRefs(err) => std::fmt::Display::fmt(err, f), + Error::Response(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::Handshake(err) => err.source(), + Error::Io(err) => Some(err), + Error::Transport(err) => err.source(), + Error::LsRefs(err) => err.source(), + Error::Response(err) => err.source(), + } + } + } + + impl From for Error { + fn from(err: handshake::Error) -> Self { + Error::Handshake(err) + } + } + + impl From for Error { + fn from(err: io::Error) -> Self { + Error::Io(err) + } + } + + impl From for Error { + fn from(err: client::Error) -> Self { + Error::Transport(err) + } + } + + impl From for Error { + fn from(err: ls_refs::Error) -> Self { + Error::LsRefs(err) + } + } + + impl From for Error { + fn from(err: response::Error) -> Self { + Error::Response(err) + } } } pub use error::Error; diff --git a/gix-ref/Cargo.toml b/gix-ref/Cargo.toml index 95f1f3889a6..e96e120f2da 100644 --- a/gix-ref/Cargo.toml +++ b/gix-ref/Cargo.toml @@ -37,7 +37,6 @@ gix-actor = { version = "^0.41.2", 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/peel.rs b/gix-ref/src/peel.rs index b4b48102cf3..2c21ded9aed 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_object::find::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 { .. } => None, + } + } + } + + 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) + } + } } /// @@ -22,14 +59,43 @@ 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 { .. } | 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 267aea51434..29e1622a346 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 { .. } => None, + } + } + } + + 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..88c97687b66 100644 --- a/gix-ref/src/store/file/log/iter.rs +++ b/gix-ref/src/store/file/log/iter.rs @@ -146,13 +146,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 cfd8b72f3ae..6d5ad418c7f 100644 --- a/gix-ref/src/store/file/log/line.rs +++ b/gix-ref/src/store/file/log/line.rs @@ -17,12 +17,21 @@ 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 {} + impl From for io::Error { fn from(err: Error) -> Self { io::Error::other(err) diff --git a/gix-ref/src/store/file/loose/reference/decode.rs b/gix-ref/src/store/file/loose/reference/decode.rs index eb79c98ae48..b1b57c15d3f 100644 --- a/gix-ref/src/store/file/loose/reference/decode.rs +++ b/gix-ref/src/store/file/loose/reference/decode.rs @@ -9,18 +9,41 @@ enum MaybeUnsafeState { } /// The error returned by [`Reference::try_from_path()`]. -#[derive(Debug, thiserror::Error)] +#[derive(Debug)] #[expect(missing_docs)] pub enum Error { - #[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::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::Parse { .. } => None, + 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..366da5c5a3b 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 => None, + } + } + } } 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 5bfd4678b8f..33d259db7c2 100644 --- a/gix-ref/src/store/file/overlay_iter.rs +++ b/gix-ref/src/store/file/overlay_iter.rs @@ -460,20 +460,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 { .. } => None, + } + } } } pub use error::Error; diff --git a/gix-ref/src/store/file/packed.rs b/gix-ref/src/store/file/packed.rs index 190d4ce09e7..37ad24ac469 100644 --- a/gix-ref/src/store/file/packed.rs +++ b/gix-ref/src/store/file/packed.rs @@ -66,13 +66,43 @@ 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(#[from] gix_lock::acquire::Error), + BufferOpen(packed::buffer::open::Error), + TransactionLock(gix_lock::acquire::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) + } + } + + impl From for Error { + fn from(err: gix_lock::acquire::Error) -> Self { + Error::TransactionLock(err) + } } } diff --git a/gix-ref/src/store/file/transaction/commit.rs b/gix-ref/src/store/file/transaction/commit.rs index 018dc144f18..aea40777e21 100644 --- a/gix-ref/src/store/file/transaction/commit.rs +++ b/gix-ref/src/store/file/transaction/commit.rs @@ -182,21 +182,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 3f2fc0c68a1..1fcd90001ce 100644 --- a/gix-ref/src/store/file/transaction/prepare.rs +++ b/gix-ref/src/store/file/transaction/prepare.rs @@ -474,46 +474,129 @@ 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] gix_lock::acquire::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:?}")] + Packed(packed::buffer::open::Error), + PackedTransactionAcquire(gix_lock::acquire::Error), + PackedTransactionPrepare(packed::transaction::prepare::Error), + PackedFind(packed::find::Error), + PreprocessingFailed(std::io::Error), LockAcquire { source: gix_lock::acquire::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:?}" - )] + 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::MustNotExist { .. } + | Error::MustExist { .. } + | 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..1f403df6d18 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 { .. } => None, + } + } + } + + 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 07549390a75..b1c5e210a8b 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..b0ad2622a0b 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 => None, + 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..43c98a5a620 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 => None, + } + } + } + + 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 => None, + } + } + } + + 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 1dca8b63a75..bd458021c0f 100644 --- a/gix-ref/src/store/packed/iter.rs +++ b/gix-ref/src/store/packed/iter.rs @@ -115,14 +115,28 @@ 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 {} } pub use error::Error; diff --git a/gix-ref/src/store/packed/transaction.rs b/gix-ref/src/store/packed/transaction.rs index 9915d39d7c2..ab7c06e5938 100644 --- a/gix-ref/src/store/packed/transaction.rs +++ b/gix-ref/src/store/packed/transaction.rs @@ -279,13 +279,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) + } } } @@ -294,14 +322,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-refspec/Cargo.toml b/gix-refspec/Cargo.toml index c638740552e..c5621bcd875 100644 --- a/gix-refspec/Cargo.toml +++ b/gix-refspec/Cargo.toml @@ -28,7 +28,6 @@ gix-hash = { version = "^0.26.0", path = "../gix-hash" } gix-glob = { version = "^0.27.0", path = "../gix-glob" } 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/parse.rs b/gix-refspec/src/parse.rs index 6ead38ed4aa..52ceb825310 100644 --- a/gix-refspec/src/parse.rs +++ b/gix-refspec/src/parse.rs @@ -1,31 +1,73 @@ /// The error returned by the [`parse()`][crate::parse()] function. -#[derive(Debug, thiserror::Error)] +#[derive(Debug)] #[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 be object hashes")] NegativeObjectHash, - #[error("Negative specs must be full ref names, starting with \"refs/\"")] NegativePartialName, - #[error("Negative glob patterns are not allowed")] NegativeGlobPattern, - #[error("Fetch destinations must be ref-names, like 'HEAD:refs/heads/branch'")] InvalidFetchDestination, - #[error("Cannot push into an empty destination")] PushToEmpty, - #[error("glob patterns may only involved a single '*' character, found {pattern:?}")] PatternUnsupported { pattern: bstr::BString }, - #[error("Both sides of the specification need a pattern, like 'a/*:b/*'")] PatternUnbalanced, - #[error(transparent)] - ReferenceName(#[from] gix_validate::reference::name::Error), - #[error(transparent)] - RevSpec(#[from] gix_revision::spec::parse::Error), + ReferenceName(gix_validate::reference::name::Error), + RevSpec(gix_revision::spec::parse::Error), +} + +// TODO(review): this implementation hand-preserves `#[error(transparent)]` semantics for the +// `ReferenceName` and `RevSpec` variants: `Display` passes the formatter through +// and `source()` forwards to the inner error's source, exactly like the +// `thiserror`-generated code did. +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Empty => f.write_str("Empty refspecs are invalid"), + Error::NegativeWithDestination => { + f.write_str("Negative refspecs cannot have destinations as they exclude sources") + } + Error::NegativeEmpty => f.write_str("Negative specs must not be empty"), + Error::NegativeObjectHash => f.write_str("Negative specs must be object hashes"), + Error::NegativePartialName => f.write_str("Negative specs must be full ref names, starting with \"refs/\""), + Error::NegativeGlobPattern => f.write_str("Negative glob patterns are not allowed"), + Error::InvalidFetchDestination => { + f.write_str("Fetch destinations must be ref-names, like 'HEAD:refs/heads/branch'") + } + Error::PushToEmpty => f.write_str("Cannot push into an empty destination"), + Error::PatternUnsupported { pattern } => { + write!( + f, + "glob patterns may only involved a single '*' character, found {pattern:?}" + ) + } + Error::PatternUnbalanced => f.write_str("Both sides of the specification need a pattern, like 'a/*:b/*'"), + Error::ReferenceName(err) => std::fmt::Display::fmt(err, f), + Error::RevSpec(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::ReferenceName(err) => err.source(), + Error::RevSpec(err) => err.source(), + _ => None, + } + } +} + +impl From for Error { + fn from(err: gix_validate::reference::name::Error) -> Self { + Error::ReferenceName(err) + } +} + +impl From for Error { + fn from(err: gix_revision::spec::parse::Error) -> Self { + Error::RevSpec(err) + } } /// Define how the parsed refspec should be used. diff --git a/gix-revwalk/Cargo.toml b/gix-revwalk/Cargo.toml index 8a00a29d80f..3e74933b3af 100644 --- a/gix-revwalk/Cargo.toml +++ b/gix-revwalk/Cargo.toml @@ -28,7 +28,6 @@ gix-date = { version = "^0.15.6", path = "../gix-date" } gix-hashtable = { version = "^0.16.0", path = "../gix-hashtable" } gix-commitgraph = { version = "^0.38.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..dc1f73170b1 100644 --- a/gix-revwalk/src/graph/commit.rs +++ b/gix-revwalk/src/graph/commit.rs @@ -149,27 +149,93 @@ impl Iterator for Parents<'_, '_> { /// pub mod iter_parents { /// The error returned by the [`Parents`][super::Parents] iterator. - #[derive(Debug, thiserror::Error)] + #[derive(Debug)] #[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), + DecodeCommit(gix_object::decode::Error), + DecodeCommitGraph(gix_error::Message), + } + + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::DecodeCommit(_) => f.write_str("An error occurred when parsing commit parents"), + Error::DecodeCommitGraph(_) => { + f.write_str("An error occurred when parsing parents from the commit graph") + } + } + } + } + + impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::DecodeCommit(err) => Some(err), + Error::DecodeCommitGraph(err) => Some(err), + } + } + } + + impl From for Error { + fn from(err: gix_object::decode::Error) -> Self { + Error::DecodeCommit(err) + } + } + + impl From for Error { + fn from(err: gix_error::Message) -> Self { + Error::DecodeCommitGraph(err) + } } } /// pub mod to_owned { /// The error returned by [`to_owned()`][crate::graph::LazyCommit::to_owned()]. - #[derive(Debug, thiserror::Error)] + #[derive(Debug)] #[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}")] + Decode(gix_object::decode::Error), + CommitGraphParent(gix_error::Message), CommitGraphTime { actual: u64 }, } + + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Decode(_) => f.write_str("A commit could not be decoded during traversal"), + Error::CommitGraphParent(_) => { + f.write_str("Could not find commit position in graph when traversing parents") + } + Error::CommitGraphTime { actual } => { + write!( + f, + "Commit-graph time could not be presented as signed integer: {actual}" + ) + } + } + } + } + + impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::Decode(err) => Some(err), + Error::CommitGraphParent(err) => Some(err), + Error::CommitGraphTime { .. } => None, + } + } + } + + impl From for Error { + fn from(err: gix_object::decode::Error) -> Self { + Error::Decode(err) + } + } + + impl From for Error { + fn from(err: gix_error::Message) -> Self { + Error::CommitGraphParent(err) + } + } } diff --git a/gix-revwalk/src/graph/mod.rs b/gix-revwalk/src/graph/mod.rs index ce428136267..8d06a00509c 100644 --- a/gix-revwalk/src/graph/mod.rs +++ b/gix-revwalk/src/graph/mod.rs @@ -17,15 +17,54 @@ mod errors { use crate::graph::commit::iter_parents; /// The error returned by [`insert_parents()`](crate::Graph::insert_parents()). - #[derive(Debug, thiserror::Error)] + // TODO(review): these implementations hand-preserve `#[error(transparent)]` semantics for + // `Lookup` and `Parent`: `Display` passes the formatter through and `source()` + // forwards to the inner error's source, exactly like the `thiserror`-generated + // code did. The same pattern is used for `get_or_insert_default::Error` below. + #[derive(Debug)] #[expect(missing_docs)] pub enum Error { - #[error(transparent)] - Lookup(#[from] gix_object::find::existing_iter::Error), - #[error("A commit could not be decoded during traversal")] - Decode(#[from] gix_object::decode::Error), - #[error(transparent)] - Parent(#[from] iter_parents::Error), + Lookup(gix_object::find::existing_iter::Error), + Decode(gix_object::decode::Error), + Parent(iter_parents::Error), + } + + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Lookup(err) => std::fmt::Display::fmt(err, f), + Error::Decode(_) => f.write_str("A commit could not be decoded during traversal"), + Error::Parent(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::Lookup(err) => err.source(), + Error::Decode(err) => Some(err), + Error::Parent(err) => err.source(), + } + } + } + + impl From for Error { + fn from(err: gix_object::find::existing_iter::Error) -> Self { + Error::Lookup(err) + } + } + + impl From for Error { + fn from(err: gix_object::decode::Error) -> Self { + Error::Decode(err) + } + } + + impl From for Error { + fn from(err: iter_parents::Error) -> Self { + Error::Parent(err) + } } } @@ -34,13 +73,41 @@ mod errors { 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)] + #[derive(Debug)] #[expect(missing_docs)] pub enum Error { - #[error(transparent)] - Lookup(#[from] gix_object::find::existing_iter::Error), - #[error(transparent)] - ToOwned(#[from] to_owned::Error), + Lookup(gix_object::find::existing_iter::Error), + ToOwned(to_owned::Error), + } + + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Lookup(err) => std::fmt::Display::fmt(err, f), + Error::ToOwned(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::Lookup(err) => err.source(), + Error::ToOwned(err) => err.source(), + } + } + } + + impl From for Error { + fn from(err: gix_object::find::existing_iter::Error) -> Self { + Error::Lookup(err) + } + } + + impl From for Error { + fn from(err: to_owned::Error) -> Self { + Error::ToOwned(err) + } } } } diff --git a/gix-shallow/Cargo.toml b/gix-shallow/Cargo.toml index 8c781e132bf..dbaf762afef 100644 --- a/gix-shallow/Cargo.toml +++ b/gix-shallow/Cargo.toml @@ -27,7 +27,6 @@ serde = ["dep:serde", "gix-hash/serde", "nonempty/serialize"] gix-hash = { version = "^0.26.0", path = "../gix-hash" } gix-lock = { version = "^24.0.0", path = "../gix-lock" } -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"] } diff --git a/gix-shallow/src/lib.rs b/gix-shallow/src/lib.rs index e130cc0ea02..ac370d14dfa 100644 --- a/gix-shallow/src/lib.rs +++ b/gix-shallow/src/lib.rs @@ -110,28 +110,87 @@ pub mod write { } /// The error returned by [`write()`](crate::write()). - #[derive(Debug, thiserror::Error)] + #[derive(Debug)] #[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")] + Commit(gix_lock::commit::Error), + RemoveEmpty(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::Commit(err) => std::fmt::Display::fmt(err, f), + Error::RemoveEmpty(_) => f.write_str("Could not remove an empty shallow file"), + Error::Io(_) => f.write_str("Failed to write object id to shallow file"), + } + } + } + + impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::Commit(err) => err.source(), + Error::RemoveEmpty(err) => Some(err), + Error::Io(_) => None, + } + } + } + + impl From> for Error { + fn from(err: gix_lock::commit::Error) -> Self { + Error::Commit(err) + } + } + + impl From for Error { + fn from(err: std::io::Error) -> Self { + Error::RemoveEmpty(err) + } + } } pub use write::function::write; /// pub mod read { /// The error returned by [`read`](crate::read()). - #[derive(Debug, thiserror::Error)] + #[derive(Debug)] #[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), + Io(std::io::Error), + DecodeHash(gix_hash::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("Could not open shallow file for reading"), + Error::DecodeHash(_) => { + f.write_str("Could not decode a line in shallow file as hex-encoded object hash") + } + } + } + } + + impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::Io(err) => Some(err), + Error::DecodeHash(err) => Some(err), + } + } + } + + impl From for Error { + fn from(err: std::io::Error) -> Self { + Error::Io(err) + } + } + + impl From for Error { + fn from(err: gix_hash::decode::Error) -> Self { + Error::DecodeHash(err) + } } } diff --git a/gix-status/Cargo.toml b/gix-status/Cargo.toml index 92bbc08ae7e..c450d26987f 100644 --- a/gix-status/Cargo.toml +++ b/gix-status/Cargo.toml @@ -38,7 +38,6 @@ gix-pathspec = { version = "^0.19.0", path = "../gix-pathspec" } gix-dir = { version = "^0.28.0", path = "../gix-dir", optional = true } gix-diff = { version = "^0.66.0", 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/types.rs b/gix-status/src/index_as_worktree/types.rs index c59f68cb9c2..9874cb389c9 100644 --- a/gix-status/src/index_as_worktree/types.rs +++ b/gix-status/src/index_as_worktree/types.rs @@ -4,24 +4,65 @@ use bstr::{BStr, BString}; use gix_index::entry; /// The error returned by [index_as_worktree()`](crate::index_as_worktree()). -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] +#[derive(Debug)] +#[allow(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] gix_hash::io::Error), - #[error("Failed to obtain blob from object database")] - Find(#[from] gix_object::find::existing_object::Error), - #[error("Could not determine status for submodule at '{rela_path}'")] + Time(std::time::SystemTimeError), + Io(gix_hash::io::Error), + Find(gix_object::find::existing_object::Error), SubmoduleStatus { rela_path: BString, source: Box, }, } +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::IllformedUtf8 => f.write_str("Could not convert path to UTF8"), + Error::Time(_) => { + f.write_str("The clock was off when reading file related metadata after updating a file on disk") + } + Error::Io(_) => f.write_str("IO error while writing blob or reading file metadata or changing filetype"), + Error::Find(_) => f.write_str("Failed to obtain blob from object database"), + Error::SubmoduleStatus { rela_path, .. } => { + write!(f, "Could not determine status for submodule at '{rela_path}'") + } + } + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::Time(err) => Some(err), + Error::Io(err) => Some(err), + Error::Find(err) => Some(err), + Error::SubmoduleStatus { source, .. } => Some(&**source), + Error::IllformedUtf8 => None, + } + } +} + +impl From for Error { + fn from(err: std::time::SystemTimeError) -> Self { + Error::Time(err) + } +} + +impl From for Error { + fn from(err: gix_hash::io::Error) -> Self { + Error::Io(err) + } +} + +impl From for Error { + fn from(err: gix_object::find::existing_object::Error) -> Self { + Error::Find(err) + } +} + /// 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/types.rs b/gix-status/src/index_as_worktree_with_renames/types.rs index 2134b35ca42..8daa5cca236 100644 --- a/gix-status/src/index_as_worktree_with_renames/types.rs +++ b/gix-status/src/index_as_worktree_with_renames/types.rs @@ -5,27 +5,75 @@ 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)] +// TODO(review): these implementations hand-preserve the `thiserror` semantics of the variants below. +// The six `#[error(transparent)]` variants (`TrackedFileModifications`, `DirWalk`, +// `SpawnThread`, `HashFile`, `ConvertToGit`, `RewriteTracker`) pass `Display` through to the +// wrapped error and forward `source()` to *its* source. The three text variants that wrap a +// bare `std::io::Error` (`SetAttributeContext`, `OpenWorktreeFile`, `ReadLink`) carry no +// source, matching `thiserror`, which treats only a `#[from]`/`#[source]` field or a field +// named `source` as the error source — never an unnamed, unannotated field. +#[derive(Debug)] +#[allow(missing_docs)] pub enum Error { - #[error(transparent)] - TrackedFileModifications(#[from] crate::index_as_worktree::Error), - #[error(transparent)] + TrackedFileModifications(crate::index_as_worktree::Error), DirWalk(gix_dir::walk::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(gix_hash::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), + ConvertToGit(gix_filter::pipeline::convert::to_git::Error), + RewriteTracker(gix_diff::rewrites::tracker::emit::Error), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::TrackedFileModifications(err) => std::fmt::Display::fmt(err, f), + Error::DirWalk(err) => std::fmt::Display::fmt(err, f), + Error::SpawnThread(err) => std::fmt::Display::fmt(err, f), + Error::SetAttributeContext(_) => { + f.write_str("Failed to change the context for querying gitattributes to the respective path") + } + Error::OpenWorktreeFile(_) => f.write_str("Could not open worktree file for reading"), + Error::HashFile(err) => std::fmt::Display::fmt(err, f), + Error::ReadLink(_) => f.write_str("Could not read worktree link content"), + Error::ConvertToGit(err) => std::fmt::Display::fmt(err, f), + Error::RewriteTracker(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::TrackedFileModifications(err) => err.source(), + Error::DirWalk(err) => err.source(), + Error::SpawnThread(err) => err.source(), + Error::HashFile(err) => err.source(), + Error::ConvertToGit(err) => err.source(), + Error::RewriteTracker(err) => err.source(), + Error::SetAttributeContext(_) | Error::OpenWorktreeFile(_) | Error::ReadLink(_) => None, + } + } +} + +impl From for Error { + fn from(err: crate::index_as_worktree::Error) -> Self { + Error::TrackedFileModifications(err) + } +} + +impl From for Error { + fn from(err: gix_filter::pipeline::convert::to_git::Error) -> Self { + Error::ConvertToGit(err) + } +} + +impl From for Error { + fn from(err: gix_diff::rewrites::tracker::emit::Error) -> Self { + Error::RewriteTracker(err) + } } /// The way all output should be sorted. diff --git a/gix-status/src/stack.rs b/gix-status/src/stack.rs index 19541ac7bf7..caf0b4e2b58 100644 --- a/gix-status/src/stack.rs +++ b/gix-status/src/stack.rs @@ -8,10 +8,17 @@ use gix_fs::{Stack, stack::ToNormalPathComponents}; use crate::SymlinkCheck; -#[derive(Debug, thiserror::Error)] -#[error("Cannot step through symlink to perform an lstat")] +#[derive(Debug)] struct CannotStepThroughSymlink; +impl std::fmt::Display for CannotStepThroughSymlink { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("Cannot step through symlink to perform an lstat") + } +} + +impl std::error::Error for CannotStepThroughSymlink {} + pub(crate) fn is_symlink_step_error(err: &std::io::Error) -> bool { err.get_ref() .and_then(|source| source.downcast_ref::()) diff --git a/gix-submodule/Cargo.toml b/gix-submodule/Cargo.toml index de1538c0d63..dbc9b8238f3 100644 --- a/gix-submodule/Cargo.toml +++ b/gix-submodule/Cargo.toml @@ -28,7 +28,6 @@ gix-path = { version = "^0.12.3", path = "../gix-path" } gix-url = { version = "^0.37.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/config.rs b/gix-submodule/src/config.rs index a219a35b2b0..cba348e9543 100644 --- a/gix-submodule/src/config.rs +++ b/gix-submodule/src/config.rs @@ -138,30 +138,54 @@ 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)] +#[derive(Debug)] #[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, } +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "The '{}' field of submodule '{}' was invalid: '{}'", + self.field, self.submodule, self.actual + ) + } +} + +impl std::error::Error for Error {} + /// pub mod branch { use bstr::BString; /// The error returned by [File::branch()](crate::File::branch). - #[derive(Debug, thiserror::Error)] + #[derive(Debug)] #[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_refspec::parse::Error, } + + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "The value '{}' of the 'branch' field of submodule '{}' couldn't be turned into a valid fetch refspec", + self.actual, self.submodule + ) + } + } + + impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + Some(&self.source) + } + } } /// @@ -169,14 +193,31 @@ pub mod update { use bstr::BString; /// The error returned by [File::update()](crate::File::update). - #[derive(Debug, thiserror::Error)] + #[derive(Debug)] #[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 }, } + + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::CommandForbiddenInModulesConfiguration { submodule, actual } => write!( + f, + "The 'update' field of submodule '{submodule}' tried to set command '{actual}' to be shared" + ), + Error::Invalid { submodule, actual } => { + write!( + f, + "The 'update' field of submodule '{submodule}' was invalid: '{actual}'" + ) + } + } + } + } + + impl std::error::Error for Error {} } /// @@ -184,16 +225,41 @@ pub mod url { use bstr::BString; /// The error returned by [File::url()](crate::File::url). - #[derive(Debug, thiserror::Error)] + #[derive(Debug)] #[expect(missing_docs)] pub enum Error { - #[error("The url of submodule '{submodule}' could not be parsed")] Parse { submodule: BString, source: gix_url::parse::Error, }, - #[error("The submodule '{submodule}' was missing its 'url' field or it was empty")] - Missing { submodule: BString }, + Missing { + submodule: BString, + }, + } + + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Parse { submodule, .. } => { + write!(f, "The url of submodule '{submodule}' could not be parsed") + } + Error::Missing { submodule } => { + write!( + f, + "The submodule '{submodule}' was missing its 'url' field or it was empty" + ) + } + } + } + } + + impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::Parse { source, .. } => Some(source), + Error::Missing { .. } => None, + } + } } } @@ -202,14 +268,32 @@ pub mod path { use bstr::BString; /// The error returned by [File::path()](crate::File::path). - #[derive(Debug, thiserror::Error)] + #[derive(Debug)] #[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 }, } + + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Absolute { actual, submodule } => { + write!(f, "The path '{actual}' of submodule '{submodule}' needs to be relative") + } + Error::Missing { submodule } => { + write!( + f, + "The submodule '{submodule}' was missing its 'path' field or it was empty" + ) + } + Error::OutsideOfWorktree { actual, .. } => { + write!(f, "The path '{actual}' would lead outside of the repository worktree") + } + } + } + } + + impl std::error::Error for Error {} } diff --git a/gix-submodule/src/is_active_platform.rs b/gix-submodule/src/is_active_platform.rs index e8781a6c375..fbf639748d2 100644 --- a/gix-submodule/src/is_active_platform.rs +++ b/gix-submodule/src/is_active_platform.rs @@ -3,13 +3,43 @@ 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)] +// TODO(review): both variants were `#[error(transparent)]`, so `Display` and `source()` forward +// to the wrapped error. +#[derive(Debug)] #[expect(missing_docs)] pub enum Error { - #[error(transparent)] - NormalizePattern(#[from] gix_pathspec::normalize::Error), - #[error(transparent)] - ParsePattern(#[from] gix_pathspec::parse::Error), + NormalizePattern(gix_pathspec::normalize::Error), + ParsePattern(gix_pathspec::parse::Error), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::NormalizePattern(err) => std::fmt::Display::fmt(err, f), + Error::ParsePattern(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::NormalizePattern(err) => err.source(), + Error::ParsePattern(err) => err.source(), + } + } +} + +impl From for Error { + fn from(err: gix_pathspec::normalize::Error) -> Self { + Error::NormalizePattern(err) + } +} + +impl From for Error { + fn from(err: gix_pathspec::parse::Error) -> Self { + Error::ParsePattern(err) + } } impl IsActivePlatform { diff --git a/gix-submodule/src/lib.rs b/gix-submodule/src/lib.rs index cb1f1b5e551..93ee421fe9a 100644 --- a/gix-submodule/src/lib.rs +++ b/gix-submodule/src/lib.rs @@ -112,17 +112,52 @@ pub mod init { /// Lifecycle /// The error returned when parsing a submodule configuration file. - #[derive(Debug, thiserror::Error)] + #[derive(Debug)] pub enum Error { /// The configuration could not be parsed. - #[error(transparent)] - Parse(#[from] gix_config::parse::Error), + Parse(gix_config::parse::Error), /// Applying configuration overrides exceeded the supported span size. - #[error(transparent)] - Span(#[from] gix_config::parse::span::Error), + Span(gix_config::parse::span::Error), /// Applying configuration overrides failed. - #[error(transparent)] - SectionValue(#[from] gix_config::file::section::value::Error), + SectionValue(gix_config::file::section::value::Error), + } + + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Parse(err) => std::fmt::Display::fmt(err, f), + Error::Span(err) => std::fmt::Display::fmt(err, f), + Error::SectionValue(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::Parse(err) => err.source(), + Error::Span(err) => err.source(), + Error::SectionValue(err) => err.source(), + } + } + } + + impl From for Error { + fn from(err: gix_config::parse::Error) -> Self { + Error::Parse(err) + } + } + + impl From for Error { + fn from(err: gix_config::parse::span::Error) -> Self { + Error::Span(err) + } + } + + impl From for Error { + fn from(err: gix_config::file::section::value::Error) -> Self { + Error::SectionValue(err) + } } impl File { diff --git a/gix-transport/Cargo.toml b/gix-transport/Cargo.toml index 5a2368cece1..320e7fd4d2f 100644 --- a/gix-transport/Cargo.toml +++ b/gix-transport/Cargo.toml @@ -108,7 +108,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/blocking_io/http/curl/mod.rs b/gix-transport/src/client/blocking_io/http/curl/mod.rs index cb2733c74bb..d6f99141ead 100644 --- a/gix-transport/src/client/blocking_io/http/curl/mod.rs +++ b/gix-transport/src/client/blocking_io/http/curl/mod.rs @@ -25,17 +25,59 @@ 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)] +#[derive(Debug)] +#[allow(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(#[from] gix_credentials::protocol::Error), + Curl(curl::Error), + Redirect(http::redirect::Error), + ReadPostBody(std::io::Error), + Authenticate(gix_credentials::protocol::Error), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Curl(err) => std::fmt::Display::fmt(err, f), + Error::Redirect(err) => std::fmt::Display::fmt(err, f), + Error::ReadPostBody(_) => f.write_str("Could not finish reading all data to post to the remote"), + Error::Authenticate(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::Curl(err) => err.source(), + Error::Redirect(err) => err.source(), + Error::ReadPostBody(err) => Some(err), + Error::Authenticate(err) => err.source(), + } + } +} + +impl From for Error { + fn from(err: curl::Error) -> Self { + Error::Curl(err) + } +} + +impl From for Error { + fn from(err: http::redirect::Error) -> Self { + Error::Redirect(err) + } +} + +impl From for Error { + fn from(err: std::io::Error) -> Self { + Error::ReadPostBody(err) + } +} + +impl From for Error { + fn from(err: gix_credentials::protocol::Error) -> Self { + Error::Authenticate(err) + } } impl crate::IsSpuriousError for Error { @@ -139,6 +181,7 @@ impl Default for Curl { } } +#[allow(clippy::type_complexity)] impl http::Http for Curl { type Headers = io::pipe::Reader; type ResponseBody = io::pipe::Reader; diff --git a/gix-transport/src/client/blocking_io/http/redirect.rs b/gix-transport/src/client/blocking_io/http/redirect.rs index 817f6d930d9..28f3c9525c5 100644 --- a/gix-transport/src/client/blocking_io/http/redirect.rs +++ b/gix-transport/src/client/blocking_io/http/redirect.rs @@ -1,13 +1,25 @@ /// 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" -)] +#[derive(Debug)] pub struct Error { redirect_url: String, expected_url: String, } +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let Error { + redirect_url, + expected_url, + } = self; + write!( + f, + "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" + ) + } +} + +impl std::error::Error for Error {} + #[derive(Default, Clone, Copy, Debug, Eq, PartialEq)] pub(crate) enum Action { Follow, 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 ac2c9cb6121..c58e943de15 100644 --- a/gix-transport/src/client/blocking_io/http/reqwest/remote.rs +++ b/gix-transport/src/client/blocking_io/http/reqwest/remote.rs @@ -17,17 +17,59 @@ 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)] +#[derive(Debug)] +#[allow(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), + Reqwest(reqwest::Error), + ReadPostBody(std::io::Error), + ConfigureRequest(Box), + Redirect(redirect::Error), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Reqwest(err) => std::fmt::Display::fmt(err, f), + Error::ReadPostBody(_) => f.write_str("Could not finish reading all data to post to the remote"), + Error::ConfigureRequest(_) => f.write_str("Request configuration failed"), + Error::Redirect(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::Reqwest(err) => err.source(), + Error::ReadPostBody(err) => Some(err), + Error::ConfigureRequest(err) => Some(&**err), + Error::Redirect(err) => err.source(), + } + } +} + +impl From for Error { + fn from(err: reqwest::Error) -> Self { + Error::Reqwest(err) + } +} + +impl From for Error { + fn from(err: std::io::Error) -> Self { + Error::ReadPostBody(err) + } +} + +impl From> for Error { + fn from(err: Box) -> Self { + Error::ConfigureRequest(err) + } +} + +impl From for Error { + fn from(err: redirect::Error) -> Self { + Error::Redirect(err) + } } impl crate::IsSpuriousError for Error { diff --git a/gix-transport/src/client/blocking_io/http/traits.rs b/gix-transport/src/client/blocking_io/http/traits.rs index ce131774971..040e3383499 100644 --- a/gix-transport/src/client/blocking_io/http/traits.rs +++ b/gix-transport/src/client/blocking_io/http/traits.rs @@ -1,17 +1,42 @@ use crate::client::WriteMode; /// The error used by the [Http] trait. -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] +#[derive(Debug)] +#[allow(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), + Detail { + description: String, + }, + PostBody(std::io::Error), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::InitHttpClient { .. } => f.write_str("Could not initialize the http client"), + Error::Detail { description } => f.write_str(description), + Error::PostBody(_) => f.write_str("An IO error occurred while uploading the body of a POST request"), + } + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::InitHttpClient { source } => Some(&**source), + Error::Detail { .. } => None, + Error::PostBody(err) => Some(err), + } + } +} + +impl From for Error { + fn from(err: std::io::Error) -> Self { + Error::PostBody(err) + } } impl crate::IsSpuriousError for Error { @@ -86,7 +111,7 @@ impl From> for GetResponse { /// A trait to abstract the HTTP operations needed to power all git interactions: read via GET and write via POST. /// Note that 401 must be turned into `std::io::Error(PermissionDenied)`, and other non-success http statuses must be transformed /// into `std::io::Error(Other)` -#[expect(clippy::type_complexity)] +#[allow(clippy::type_complexity)] pub trait Http { /// A type providing headers line by line. type Headers: std::io::BufRead + Unpin; diff --git a/gix-transport/src/client/blocking_io/ssh/mod.rs b/gix-transport/src/client/blocking_io/ssh/mod.rs index ce9b45e0cff..6feba1295a5 100644 --- a/gix-transport/src/client/blocking_io/ssh/mod.rs +++ b/gix-transport/src/client/blocking_io/ssh/mod.rs @@ -8,15 +8,32 @@ use gix_url::{ArgumentSafety::*, Url}; use crate::{Protocol, client::blocking_io::file::SpawnProcessOnDemand}; /// The error used in [`connect()`]. -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] +#[derive(Debug)] +#[allow(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 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 {} + impl crate::IsSpuriousError for Error {} /// The kind of SSH programs we have built-in support for. @@ -43,14 +60,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)] - #[expect(missing_docs)] + #[derive(Debug)] + #[allow(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 +76,24 @@ 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 {} } /// @@ -101,10 +137,7 @@ pub mod connect { /// The `desired_version` is the preferred protocol version when establishing the connection, but note that it can be /// downgraded by servers not supporting it. /// If `trace` is `true`, all packetlines received or sent will be passed to the facilities of the `gix-trace` crate. -#[expect( - clippy::result_large_err, - reason = "will be removed once `gix-error` is used consistently" -)] +#[allow(clippy::result_large_err)] pub fn connect( url: Url, desired_version: Protocol, @@ -128,10 +161,7 @@ pub fn connect( )) } -#[expect( - clippy::result_large_err, - reason = "will be removed once `gix-error` is used consistently" -)] +#[allow(clippy::result_large_err)] fn determine_client_kind( known_kind: Option, ssh_cmd: &OsStr, @@ -151,10 +181,7 @@ fn determine_client_kind( Ok(kind) } -#[expect( - clippy::result_large_err, - reason = "will be removed once `gix-error` is used consistently" -)] +#[allow(clippy::result_large_err)] fn build_client_feature_check_command(ssh_cmd: &OsStr, url: &Url, disallow_shell: bool) -> Result { let mut prepare = gix_command::prepare(ssh_cmd) .stderr(Stdio::null()) diff --git a/gix-transport/src/client/capabilities.rs b/gix-transport/src/client/capabilities.rs index 0e1a005e195..59fa03e79ab 100644 --- a/gix-transport/src/client/capabilities.rs +++ b/gix-transport/src/client/capabilities.rs @@ -5,21 +5,51 @@ use crate::Protocol; use crate::client; /// The error used in [`Capabilities::from_bytes()`] and [`Capabilities::from_lines()`]. -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] +#[derive(Debug)] +#[allow(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), + Io(std::io::Error), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::MissingDelimitingNullByte => { + f.write_str("Capabilities were missing entirely as there was no 0 byte") + } + Error::NoCapabilities => f.write_str("there was not a single capability behind the delimiter"), + Error::MissingVersionLine => f.write_str("a version line was expected, but none was retrieved"), + Error::MalformattedVersionLine(line) => write!(f, "expected 'version X', got {line:?}"), + Error::UnsupportedVersion { desired, actual } => { + write!(f, "Got unsupported version {actual:?}, expected {}", *desired as u8) + } + Error::Io(_) => f.write_str("An IO error occurred while reading V2 lines"), + } + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::Io(err) => Some(err), + Error::MissingDelimitingNullByte + | Error::NoCapabilities + | Error::MissingVersionLine + | Error::MalformattedVersionLine(_) + | Error::UnsupportedVersion { .. } => None, + } + } +} + +impl From for Error { + fn from(err: std::io::Error) -> Self { + Error::Io(err) + } } /// A structure to represent multiple [capabilities](Capability) or features supported by the server. @@ -255,6 +285,7 @@ pub mod blocking_recv { /// #[cfg(feature = "async-client")] +#[allow(missing_docs)] pub mod async_recv { use bstr::ByteVec; use futures_io::AsyncRead; diff --git a/gix-transport/src/client/git/blocking_io.rs b/gix-transport/src/client/git/blocking_io.rs index 87594f82e42..1b62a0534a9 100644 --- a/gix-transport/src/client/git/blocking_io.rs +++ b/gix-transport/src/client/git/blocking_io.rs @@ -179,15 +179,39 @@ pub mod connect { use crate::client::git; /// The error used in [`connect()`]. - #[derive(Debug, thiserror::Error)] - #[expect(missing_docs)] + #[derive(Debug)] + #[allow(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]")] + Io(std::io::Error), VirtualHostInvalid { host: String }, } + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Io(_) => f.write_str("An IO error occurred when connecting to the server"), + Error::VirtualHostInvalid { host } => { + write!(f, "Could not parse {host:?} as virtual host with format [:port]") + } + } + } + } + + impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::Io(err) => Some(err), + Error::VirtualHostInvalid { .. } => None, + } + } + } + + impl From for Error { + fn from(err: std::io::Error) -> Self { + Error::Io(err) + } + } + impl crate::IsSpuriousError for Error { fn is_spurious(&self) -> bool { match self { diff --git a/gix-transport/src/client/non_io_types.rs b/gix-transport/src/client/non_io_types.rs index 5b457c84c42..f6f0fa0297d 100644 --- a/gix-transport/src/client/non_io_types.rs +++ b/gix-transport/src/client/non_io_types.rs @@ -48,27 +48,77 @@ 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)] + // TODO(review): this implementation hand-preserves `#[error(transparent)]` semantics for `Url`: + // `Display` passes the formatter through and `source()` forwards to the inner error's + // source, exactly like the `thiserror`-generated code did. The same pattern is used + // for `client::Error::{Http, SshInvocation}` and the http backend errors + // (`http::Error`, curl, reqwest). + #[derive(Debug)] + #[allow(missing_docs)] pub enum Error { - #[error(transparent)] - Url(#[from] gix_url::parse::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")] + Url(gix_url::parse::Error), + PathConversion(bstr::Utf8Error), + Connection(Box), 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), } + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Url(err) => std::fmt::Display::fmt(err, f), + Error::PathConversion(_) => f.write_str("The git repository path could not be converted to UTF8"), + Error::Connection(_) => f.write_str("connection failed"), + Error::UnsupportedUrlTokens { url, scheme } => write!( + f, + "The url {url:?} contains information that would not be used by the {scheme} protocol" + ), + Error::UnsupportedScheme(scheme) => write!(f, "The '{scheme}' protocol is currently unsupported"), + #[cfg(not(any(feature = "http-client-curl", feature = "http-client-reqwest")))] + Error::CompiledWithoutHttp(scheme) => write!( + f, + "'{scheme}' is not compiled in. Compile with the 'http-client-curl' or 'http-client-reqwest' cargo feature" + ), + } + } + } + + impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::Url(err) => err.source(), + Error::PathConversion(err) => Some(err), + Error::Connection(err) => Some(&**err), + Error::UnsupportedUrlTokens { .. } | Error::UnsupportedScheme(_) => None, + #[cfg(not(any(feature = "http-client-curl", feature = "http-client-reqwest")))] + Error::CompiledWithoutHttp(_) => None, + } + } + } + + impl From for Error { + fn from(err: gix_url::parse::Error) -> Self { + Error::Url(err) + } + } + + impl From for Error { + fn from(err: bstr::Utf8Error) -> Self { + Error::PathConversion(err) + } + } + + impl From> for Error { + fn from(err: Box) -> Self { + Error::Connection(err) + } + } + // TODO: maybe fix this workaround: want `IsSpuriousError` in `Connection(…)` impl crate::IsSpuriousError for Error { fn is_spurious(&self) -> bool { @@ -110,43 +160,99 @@ mod error { type SshInvocationError = std::convert::Infallible; /// The error used in most methods of the [`client`][crate::client] module - #[derive(thiserror::Error, Debug)] - #[expect(missing_docs)] + #[derive(Debug)] + #[allow(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: capabilities::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 std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + 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(identity) => { + write!(f, "The transport layer refuses to use a given identity: {identity}") + } + Error::UnsupportedProtocolVersion(version) => { + write!(f, "The protocol version indicated by {version:?} is unsupported") + } + Error::InvokeProgram { command, .. } => write!(f, "Failed to invoke program {command:?}"), + 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::Capabilities { err } => Some(err), + Error::LineDecode { err } => Some(err), + Error::InvokeProgram { source, .. } => Some(source), + Error::Http(err) => err.source(), + Error::SshInvocation(err) => err.source(), + Error::MissingHandshake + | Error::ExpectedLine(_) + | Error::ExpectedDataLine + | Error::AuthenticationUnsupported + | Error::AuthenticationRefused(_) + | Error::UnsupportedProtocolVersion(_) + | Error::AmbiguousPath { .. } => 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 } + } + } + + impl From for Error { + fn from(err: gix_packetline::decode::Error) -> Self { + Error::LineDecode { err } + } + } + + impl From for Error { + fn from(err: HttpError) -> Self { + Error::Http(err) + } + } + impl crate::IsSpuriousError for Error { fn is_spurious(&self) -> bool { match self { diff --git a/gix-traverse/Cargo.toml b/gix-traverse/Cargo.toml index 73972b8db4c..fcb5347adcd 100644 --- a/gix-traverse/Cargo.toml +++ b/gix-traverse/Cargo.toml @@ -28,7 +28,6 @@ gix-hashtable = { version = "^0.16.0", path = "../gix-hashtable" } gix-revwalk = { version = "^0.34.0", path = "../gix-revwalk" } gix-commitgraph = { version = "^0.38.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 e4997d600ee..6f289e34d5d 100644 --- a/gix-traverse/src/commit/simple.rs +++ b/gix-traverse/src/commit/simple.rs @@ -69,15 +69,54 @@ pub enum Sorting { } /// The error is part of the item returned by the [Ancestors](super::Simple) iterator. -#[derive(Debug, thiserror::Error)] +// TODO(review): these implementations hand-preserve `#[error(transparent)]` semantics for all +// variants: `Display` passes the formatter through and `source()` forwards to the +// inner error's source, exactly like the `thiserror`-generated code did. The same +// pattern is used in `topo::Error` and `tree::breadthfirst::Error`. +#[derive(Debug)] #[expect(missing_docs)] pub enum Error { - #[error(transparent)] - Find(#[from] gix_object::find::existing_iter::Error), - #[error(transparent)] - ObjectDecode(#[from] gix_object::decode::Error), - #[error(transparent)] - HiddenGraph(#[from] gix_revwalk::graph::get_or_insert_default::Error), + Find(gix_object::find::existing_iter::Error), + ObjectDecode(gix_object::decode::Error), + HiddenGraph(gix_revwalk::graph::get_or_insert_default::Error), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Find(err) => std::fmt::Display::fmt(err, f), + Error::ObjectDecode(err) => std::fmt::Display::fmt(err, f), + Error::HiddenGraph(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::Find(err) => err.source(), + Error::ObjectDecode(err) => err.source(), + Error::HiddenGraph(err) => err.source(), + } + } +} + +impl From for Error { + fn from(err: gix_object::find::existing_iter::Error) -> Self { + Error::Find(err) + } +} + +impl From for Error { + fn from(err: gix_object::decode::Error) -> Self { + Error::ObjectDecode(err) + } +} + +impl From for Error { + fn from(err: gix_revwalk::graph::get_or_insert_default::Error) -> Self { + Error::HiddenGraph(err) + } } use Result as Either; diff --git a/gix-traverse/src/commit/topo/mod.rs b/gix-traverse/src/commit/topo/mod.rs index fbe9812620e..00535f4c018 100644 --- a/gix-traverse/src/commit/topo/mod.rs +++ b/gix-traverse/src/commit/topo/mod.rs @@ -3,17 +3,46 @@ use bitflags::bitflags; /// The errors that can occur during creation and iteration. -#[derive(thiserror::Error, Debug)] +#[derive(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(#[from] gix_object::find::existing_iter::Error), + ObjectDecode(gix_object::decode::Error), + Find(gix_object::find::existing_iter::Error), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::MissingIndegreeUnexpected => f.write_str("Indegree information is missing"), + Error::MissingStateUnexpected => f.write_str("Internal state (bitflags) not found"), + Error::ObjectDecode(err) => std::fmt::Display::fmt(err, f), + Error::Find(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::MissingIndegreeUnexpected | Error::MissingStateUnexpected => None, + Error::ObjectDecode(err) => err.source(), + Error::Find(err) => err.source(), + } + } +} + +impl From for Error { + fn from(err: gix_object::decode::Error) -> Self { + Error::ObjectDecode(err) + } +} + +impl From for Error { + fn from(err: gix_object::find::existing_iter::Error) -> Self { + Error::Find(err) + } } bitflags! { diff --git a/gix-traverse/src/tree/breadthfirst.rs b/gix-traverse/src/tree/breadthfirst.rs index 36ae5afaa87..c694e6ac505 100644 --- a/gix-traverse/src/tree/breadthfirst.rs +++ b/gix-traverse/src/tree/breadthfirst.rs @@ -4,15 +4,44 @@ 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)] +#[derive(Debug)] #[expect(missing_docs)] pub enum Error { - #[error(transparent)] - Find(#[from] gix_object::find::existing_iter::Error), - #[error("The delegate cancelled the operation")] + Find(gix_object::find::existing_iter::Error), Cancelled, - #[error(transparent)] - ObjectDecode(#[from] gix_object::decode::Error), + ObjectDecode(gix_object::decode::Error), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Find(err) => std::fmt::Display::fmt(err, f), + Error::Cancelled => f.write_str("The delegate cancelled the operation"), + Error::ObjectDecode(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::Find(err) => err.source(), + Error::Cancelled => None, + Error::ObjectDecode(err) => err.source(), + } + } +} + +impl From for Error { + fn from(err: gix_object::find::existing_iter::Error) -> Self { + Error::Find(err) + } +} + +impl From for Error { + fn from(err: gix_object::decode::Error) -> Self { + Error::ObjectDecode(err) + } } /// The state used and potentially shared by multiple tree traversals. diff --git a/gix-url/Cargo.toml b/gix-url/Cargo.toml index b42ccd8a2fa..6281c7ebf80 100644 --- a/gix-url/Cargo.toml +++ b/gix-url/Cargo.toml @@ -23,7 +23,6 @@ gix-path = { version = "^0.12.3", path = "../gix-path" } gix-utils = { version = "^0.3.5", path = "../gix-utils", features = ["bstr"] } serde = { version = "1.0.114", optional = true, default-features = false, features = ["std", "derive"] } -thiserror = "2.0.18" bstr = { version = "1.12.0", default-features = false, features = ["std"] } percent-encoding = "2.3.1" diff --git a/gix-url/src/expand_path.rs b/gix-url/src/expand_path.rs index 6b3156a9814..149ba9b27c8 100644 --- a/gix-url/src/expand_path.rs +++ b/gix-url/src/expand_path.rs @@ -23,15 +23,32 @@ impl From for Option { } /// The error used by [`parse()`], [`with()`] and [`expand_path()`](crate::expand_path()). -#[derive(Debug, thiserror::Error)] +#[derive(Debug)] #[expect(missing_docs)] pub enum Error { - #[error("UTF8 conversion on non-unix system failed for path: {path:?}")] IllformedUtf8 { path: BString }, - #[error("Home directory could not be obtained for {}", match user {Some(user) => format!("user '{user}'"), None => "current user".into()})] MissingHome { user: Option }, } +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::IllformedUtf8 { path } => { + write!(f, "UTF8 conversion on non-unix system failed for path: {path:?}") + } + Error::MissingHome { user } => { + let user: std::borrow::Cow<'_, str> = match user { + Some(user) => format!("user '{user}'").into(), + None => "current user".into(), + }; + write!(f, "Home directory could not be obtained for {user}") + } + } + } +} + +impl std::error::Error for Error {} + fn path_segments(path: &BStr) -> Option> { if path.starts_with(b"/") { Some(path[1..].split(|c| *c == b'/')) diff --git a/gix-url/src/parse.rs b/gix-url/src/parse.rs index ee9424c0dd7..44df1617943 100644 --- a/gix-url/src/parse.rs +++ b/gix-url/src/parse.rs @@ -5,28 +5,59 @@ use bstr::{BStr, BString, ByteSlice}; use crate::Scheme; /// The error returned by [parse()](crate::parse()). -#[derive(Debug, thiserror::Error)] +#[derive(Debug)] #[expect(missing_docs)] pub enum Error { - #[error("{} \"{url}\" is not valid UTF-8", kind.as_str())] Utf8 { url: BString, kind: UrlKind, source: std::str::Utf8Error, }, - #[error("{} {url:?} can not be parsed as valid URL", kind.as_str())] Url { url: String, kind: UrlKind, source: crate::simple_url::UrlParseError, }, - #[error("The host portion of the following URL is too long ({} bytes, {len} bytes total): {truncated_url:?}", truncated_url.len())] - TooLong { truncated_url: BString, len: usize }, - #[error("{} \"{url}\" does not specify a path to a repository", kind.as_str())] - MissingRepositoryPath { url: BString, kind: UrlKind }, - #[error("URL {url:?} is relative which is not allowed in this context")] - RelativeUrl { url: String }, + TooLong { + truncated_url: BString, + len: usize, + }, + MissingRepositoryPath { + url: BString, + kind: UrlKind, + }, + RelativeUrl { + url: String, + }, +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Utf8 { url, kind, .. } => write!(f, "{} \"{url}\" is not valid UTF-8", kind.as_str()), + Error::Url { url, kind, .. } => write!(f, "{} {url:?} can not be parsed as valid URL", kind.as_str()), + Error::TooLong { truncated_url, len } => write!( + f, + "The host portion of the following URL is too long ({} bytes, {len} bytes total): {truncated_url:?}", + truncated_url.len() + ), + Error::MissingRepositoryPath { url, kind } => { + write!(f, "{} \"{url}\" does not specify a path to a repository", kind.as_str()) + } + Error::RelativeUrl { url } => write!(f, "URL {url:?} is relative which is not allowed in this context"), + } + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::Utf8 { source, .. } => Some(source), + Error::Url { source, .. } => Some(source), + Error::TooLong { .. } | Error::MissingRepositoryPath { .. } | Error::RelativeUrl { .. } => None, + } + } } impl From for Error { diff --git a/gix-url/src/simple_url.rs b/gix-url/src/simple_url.rs index f481ad1498b..8f206e25e1d 100644 --- a/gix-url/src/simple_url.rs +++ b/gix-url/src/simple_url.rs @@ -13,18 +13,27 @@ pub(crate) struct ParsedUrl { } /// Minimal parse error type to replace url::ParseError -#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum UrlParseError { - #[error("relative URL without a base")] RelativeUrlWithoutBase, - #[error("invalid port number - must be between 1-65535")] InvalidPort, - #[error("invalid domain character")] InvalidDomainCharacter, - #[error("Scheme requires host")] SchemeRequiresHost, } +impl std::fmt::Display for UrlParseError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + UrlParseError::RelativeUrlWithoutBase => f.write_str("relative URL without a base"), + UrlParseError::InvalidPort => f.write_str("invalid port number - must be between 1-65535"), + UrlParseError::InvalidDomainCharacter => f.write_str("invalid domain character"), + UrlParseError::SchemeRequiresHost => f.write_str("Scheme requires host"), + } + } +} + +impl std::error::Error for UrlParseError {} + /// Check if a character is valid in a URL scheme. /// Valid scheme characters: alphanumeric, +, -, or . fn is_valid_scheme_char(c: char) -> bool { diff --git a/gix-worktree-state/Cargo.toml b/gix-worktree-state/Cargo.toml index f5ff87fed5c..0af6bbb0ba7 100644 --- a/gix-worktree-state/Cargo.toml +++ b/gix-worktree-state/Cargo.toml @@ -32,7 +32,6 @@ gix-features = { version = "^0.49.0", path = "../gix-features" } gix-filter = { version = "^0.33.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/mod.rs b/gix-worktree-state/src/checkout/mod.rs index d0d26b99e22..c7269fd361e 100644 --- a/gix-worktree-state/src/checkout/mod.rs +++ b/gix-worktree-state/src/checkout/mod.rs @@ -75,31 +75,104 @@ pub struct Options { } /// The error returned by the [checkout()][crate::checkout()] function. -#[derive(Debug, thiserror::Error)] -#[expect(missing_docs)] +// TODO(review): these implementations hand-preserve `#[error(transparent)]` semantics for `Filter`, +// `FilterListDelayed` and `FilterFetchDelayed`: `Display` passes the formatter through +// and `source()` forwards to the inner error's source, exactly like the +// `thiserror`-generated code did. +#[derive(Debug)] +#[allow(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())] + IllformedUtf8 { + path: BString, + }, + Time(std::time::SystemTimeError), + Io(std::io::Error), Find { - #[source] err: gix_object::find::existing_object::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("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 }, + Filter(gix_filter::pipeline::convert::to_worktree::Error), + FilterListDelayed(gix_filter::driver::delayed::list::Error), + FilterFetchDelayed(gix_filter::driver::delayed::fetch::Error), + FilterPathUnknown { + rela_path: BString, + }, + FilterPathsUnprocessed { + rela_paths: Vec, + }, +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::IllformedUtf8 { path } => write!(f, "Could not convert path to UTF8: {path}"), + Error::Time(_) => { + f.write_str("The clock was off when reading file related metadata after updating a file on disk") + } + Error::Io(_) => f.write_str("IO error while writing blob or reading file metadata or changing filetype"), + Error::Find { path, .. } => write!( + f, + "object for checkout at {} could not be retrieved from object database", + path.display() + ), + Error::Filter(err) => std::fmt::Display::fmt(err, f), + Error::FilterListDelayed(err) => std::fmt::Display::fmt(err, f), + Error::FilterFetchDelayed(err) => std::fmt::Display::fmt(err, f), + Error::FilterPathUnknown { rela_path } => write!( + f, + "The entry at path '{rela_path}' was listed as delayed by the filter process, but we never passed it" + ), + Error::FilterPathsUnprocessed { .. } => f.write_str( + "The following paths were delayed and apparently forgotten to be processed by the filter driver: ", + ), + } + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::Time(err) => Some(err), + Error::Io(err) => Some(err), + Error::Find { err, .. } => Some(err), + Error::Filter(err) => err.source(), + Error::FilterListDelayed(err) => err.source(), + Error::FilterFetchDelayed(err) => err.source(), + Error::IllformedUtf8 { .. } | Error::FilterPathUnknown { .. } | Error::FilterPathsUnprocessed { .. } => { + None + } + } + } +} + +impl From for Error { + fn from(err: std::time::SystemTimeError) -> Self { + Error::Time(err) + } +} + +impl From for Error { + fn from(err: std::io::Error) -> Self { + Error::Io(err) + } +} + +impl From for Error { + fn from(err: gix_filter::pipeline::convert::to_worktree::Error) -> Self { + Error::Filter(err) + } +} + +impl From for Error { + fn from(err: gix_filter::driver::delayed::list::Error) -> Self { + Error::FilterListDelayed(err) + } +} + +impl From for Error { + fn from(err: gix_filter::driver::delayed::fetch::Error) -> Self { + Error::FilterFetchDelayed(err) + } } mod chunk; diff --git a/gix-zlib/Cargo.toml b/gix-zlib/Cargo.toml index 495659d25b9..ed925f2b1ba 100644 --- a/gix-zlib/Cargo.toml +++ b/gix-zlib/Cargo.toml @@ -20,7 +20,6 @@ serde = ["dep:serde"] [dependencies] zlib-rs = "0.6.2" -thiserror = "2.0.18" serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"] } [dev-dependencies] diff --git a/gix-zlib/src/decompress.rs b/gix-zlib/src/decompress.rs index e1a6d06a8d2..ca7e3802d54 100644 --- a/gix-zlib/src/decompress.rs +++ b/gix-zlib/src/decompress.rs @@ -5,19 +5,28 @@ use zlib_rs::InflateError; use crate::{Decompress, FlushDecompress, Status}; /// /// The error produced by [`Decompress::decompress()`]. -#[derive(Debug, thiserror::Error)] +#[derive(Debug)] #[expect(missing_docs)] pub enum DecompressError { - #[error("stream error")] StreamError, - #[error("Not enough memory")] InsufficientMemory, - #[error("Invalid input data")] DataError, - #[error("Decompressing this input requires a dictionary")] NeedDict, } +impl std::fmt::Display for DecompressError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + DecompressError::StreamError => "stream error", + DecompressError::InsufficientMemory => "Not enough memory", + DecompressError::DataError => "Invalid input data", + DecompressError::NeedDict => "Decompressing this input requires a dictionary", + }) + } +} + +impl std::error::Error for DecompressError {} + impl Default for Decompress { fn default() -> Self { Self::new() diff --git a/gix-zlib/src/inflate.rs b/gix-zlib/src/inflate.rs index 6e1c67d88b8..b4adf2ab90e 100644 --- a/gix-zlib/src/inflate.rs +++ b/gix-zlib/src/inflate.rs @@ -1,17 +1,46 @@ use crate::{FlushDecompress, Inflate, Status}; /// The error returned by various [Inflate methods][super::Inflate] -#[derive(Debug, thiserror::Error)] +#[derive(Debug)] #[expect(missing_docs)] pub enum Error { - #[error("Could not write all bytes when decompressing content")] - WriteInflated(#[from] std::io::Error), - #[error("Could not decode zip stream, status was '{0}'")] - Inflate(#[from] super::DecompressError), - #[error("The zlib status indicated an error, status was '{0:?}'")] + WriteInflated(std::io::Error), + Inflate(super::DecompressError), Status(super::Status), } +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::WriteInflated(_) => f.write_str("Could not write all bytes when decompressing content"), + Error::Inflate(status) => write!(f, "Could not decode zip stream, status was '{status}'"), + Error::Status(status) => write!(f, "The zlib status indicated an error, status was '{status:?}'"), + } + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::WriteInflated(err) => Some(err), + Error::Inflate(err) => Some(err), + Error::Status(_) => None, + } + } +} + +impl From for Error { + fn from(err: std::io::Error) -> Self { + Error::WriteInflated(err) + } +} + +impl From for Error { + fn from(err: super::DecompressError) -> Self { + Error::Inflate(err) + } +} + 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> { diff --git a/gix-zlib/src/stream/deflate.rs b/gix-zlib/src/stream/deflate.rs index 0b260ca242b..6d74df9dd52 100644 --- a/gix-zlib/src/stream/deflate.rs +++ b/gix-zlib/src/stream/deflate.rs @@ -75,17 +75,26 @@ impl Compress { } /// The error produced by [`Compress::compress()`]. -#[derive(Debug, thiserror::Error)] +#[derive(Debug)] #[expect(missing_docs)] pub enum CompressError { - #[error("stream error")] StreamError, - #[error("The input is not a valid deflate stream.")] DataError, - #[error("Not enough memory")] InsufficientMemory, } +impl std::fmt::Display for CompressError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + CompressError::StreamError => "stream error", + CompressError::DataError => "The input is not a valid deflate stream.", + CompressError::InsufficientMemory => "Not enough memory", + }) + } +} + +impl std::error::Error for CompressError {} + impl From for CompressError { fn from(value: zlib_rs::DeflateError) -> Self { match value { @@ -100,6 +109,7 @@ impl From for CompressError { /// in-memory data. #[derive(Copy, Clone, PartialEq, Eq, Debug)] #[non_exhaustive] +#[allow(clippy::unnecessary_cast)] pub enum FlushCompress { /// A typical parameter for passing to compression/decompression functions, /// this indicates that the underlying stream to decide how much data to diff --git a/gix/src/clone/checkout.rs b/gix/src/clone/checkout.rs index d2296203cac..8da32ba09c2 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::{ErrorExt, 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_index::init::from_tree::Error, - }, - #[error("Couldn't obtain configuration for core.protect*")] - BooleanConfig(#[from] crate::config::boolean::Error), - #[error(transparent)] - WriteIndex(#[from] gix_index::file::write::Error), - #[error(transparent)] - CheckoutOptions(#[from] crate::config::checkout_options::Error), - #[error(transparent)] - IndexCheckout(#[from] gix_worktree_state::checkout::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() + .map_err(gix_error::Error::from_error)?, + ), + 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,15 @@ 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, - })?; + 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).map_err(|err| { + gix_error::Error::from( + err.and_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,16 +127,21 @@ 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, opts, - )?; + ) + .map_err(gix_error::Error::from_error)?; files.show_throughput(start); bytes.show_throughput(start); - index.write(Default::default())?; + index.write(Default::default()).map_err(gix_error::Error::from_error)?; 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 70799c3b39c..b23b49d20c1 100644 --- a/gix/src/clone/fetch/mod.rs +++ b/gix/src/clone/fetch/mod.rs @@ -4,6 +4,8 @@ use crate::{ }; use gix_ref::Category; +// TODO(review): kept concrete. Callers match its variants directly: `gix/tests/gix/clone.rs:204` +// matches `gix::clone::fetch::Error::Fetch(gix::remote::fetch::Error::Fetch(..))`. /// The error returned by [`PrepareFetch::fetch_only()`]. #[derive(Debug, thiserror::Error)] #[expect(missing_docs)] diff --git a/gix/src/clone/mod.rs b/gix/src/clone/mod.rs index a1f236ec332..1e1730ea94f 100644 --- a/gix/src/clone/mod.rs +++ b/gix/src/clone/mod.rs @@ -46,6 +46,9 @@ pub struct PrepareFetch { remove_worktree_on_drop: bool, } +// TODO(review): kept concrete. Callers match its variants directly: `gix/tests/gix/clone.rs:746` +// matches `gix::clone::Error::Init(gix::init::Error::Init(gix::create::Error:: +// DirectoryExists { ref path }))`. /// The error returned by [`PrepareFetch::new()`]. #[derive(Debug, thiserror::Error)] #[expect(missing_docs)] diff --git a/gix/src/commit.rs b/gix/src/commit.rs index 2b7600af6ed..a6bb1a69099 100644 --- a/gix/src/commit.rs +++ b/gix/src/commit.rs @@ -1,39 +1,16 @@ //! #![allow(clippy::empty_docs)] -use std::convert::Infallible; - /// 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()). -#[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; @@ -51,7 +28,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())) } @@ -66,7 +46,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(); @@ -78,23 +61,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)] @@ -110,7 +77,7 @@ pub mod describe { impl SelectRef { fn names(&self, repo: &Repository) -> Result>, Error> { - let platform = repo.references()?; + let platform = repo.references().map_err(gix_error::Error::from_error)?; Ok(match self { SelectRef::AllTags | SelectRef::AllRefs => { @@ -244,8 +211,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/config/cache/access.rs b/gix/src/config/cache/access.rs index 2973a14d572..0e438945253 100644 --- a/gix/src/config/cache/access.rs +++ b/gix/src/config/cache/access.rs @@ -2,6 +2,7 @@ use std::{path::PathBuf, time::Duration}; use gix_config::file::Metadata; +use gix_error::ResultExt; use gix_lock::acquire::Fail; use crate::{ @@ -19,15 +20,22 @@ use crate::{ impl Cache { #[cfg(feature = "blob-diff")] pub(crate) fn diff_algorithm(&self) -> Result { - use crate::config::{cache::util::ApplyLeniencyDefault, diff::algorithm::Error, tree::Diff}; + use crate::config::{cache::util::ApplyLeniencyDefault, tree::Diff}; self.diff_algorithm .get_or_try_init(|| { let name = self.resolved.string(Diff::ALGORITHM).unwrap_or_else(|| "myers".into()); + // `try_into_algorithm()`'s error is erased, so we can no longer match its + // `Unimplemented` variant here; re-derive the same condition from `name` instead, + // matching the one place `try_into_algorithm()` returns that particular error. + let is_unimplemented = name.eq_ignore_ascii_case(b"patience"); config::tree::Diff::ALGORITHM .try_into_algorithm(name) - .or_else(|err| match err { - Error::Unimplemented { .. } if self.lenient_config => Ok(gix_diff::blob::Algorithm::Histogram), - err => Err(err), + .or_else(|err| { + if is_unimplemented && self.lenient_config { + Ok(gix_diff::blob::Algorithm::Histogram) + } else { + Err(err) + } }) .with_lenient_default(self.lenient_config) }) @@ -64,11 +72,14 @@ 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), - })?; + .or_raise(|| { + gix_error::message!( + "Failed to parse value of 'diff.{name}.{attribute}'", + name = driver.name, + attribute = "binary" + ) + }) + .map_err(gix_error::Error::from)?; } if let Some(command) = section.value(config::tree::Diff::DRIVER_COMMAND.name) { driver.command = command.into(); @@ -77,20 +88,27 @@ impl Cache { driver.binary_to_text_command = textconv.into(); } if let Some(algorithm) = section.value("algorithm") { + // See the comment in `diff_algorithm()` above: re-derive the `Unimplemented` + // condition from `algorithm` since `try_into_algorithm()`'s error is now erased. + let is_unimplemented = algorithm.eq_ignore_ascii_case(b"patience"); driver.algorithm = config::tree::Diff::DRIVER_ALGORITHM .try_into_algorithm(algorithm) - .or_else(|err| match err { - config::diff::algorithm::Error::Unimplemented { .. } if self.lenient_config => { + .or_else(|err| { + if is_unimplemented && self.lenient_config { Ok(gix_diff::blob::Algorithm::Histogram) + } else { + Err(err) } - 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), - })? + .or_raise(|| { + gix_error::message!( + "Failed to parse value of 'diff.{name}.{attribute}'", + name = driver.name, + attribute = "algorithm" + ) + }) + .map_err(gix_error::Error::from)? .into(); } } @@ -138,7 +156,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_error)?, }) } @@ -147,8 +165,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_error)?, + fs: self.fs_capabilities().map_err(gix_error::Error::from_error)?, }) } @@ -274,16 +292,20 @@ 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_error)?, + use_nsec: boolean(self, "gitoxide.core.useNsec", &gitoxide::Core::USE_NSEC, false) + .map_err(gix_error::Error::from_error)?, + use_stdev: boolean(self, "gitoxide.core.useStdev", &gitoxide::Core::USE_STDEV, false) + .map_err(gix_error::Error::from_error)?, 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_error)? .unwrap_or(true), }) } @@ -320,13 +342,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_error)?; + let capabilities = self.fs_capabilities().map_err(gix_error::Error::from_error)?; let filters = { let mut filters = gix_filter::Pipeline::new(repo.command_context()?, crate::filter::Pipeline::options(repo)?); @@ -342,29 +366,27 @@ impl Cache { "gitoxide.core.filterProcessDelay", &gitoxide::Core::FILTER_PROCESS_DELAY, true, - )? { + ) + .map_err(gix_error::Error::from_error)? + { 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_error)?, filters, attributes: self - .assemble_attribute_globals(git_dir, attributes_source, self.attributes)? + .assemble_attribute_globals(git_dir, attributes_source, self.attributes) + .map_err(gix_error::Error::from_error)? .0, fs: capabilities, thread_limit, 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()?, }) } @@ -388,14 +410,18 @@ 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() + .or_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").map_err(gix_error::Error::from_error)?, }; - let parse_ignore = self.ignore_pattern_parser()?; + let parse_ignore = self.ignore_pattern_parser().map_err(gix_error::Error::from_error)?; 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, diff --git a/gix/src/config/mod.rs b/gix/src/config/mod.rs index aad8cb67f1d..e5fa81d20bd 100644 --- a/gix/src/config/mod.rs +++ b/gix/src/config/mod.rs @@ -56,20 +56,15 @@ 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(#[from] gix_config::file::set_raw_value::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, - } + pub type Error = gix_error::Error; } +// TODO(review): kept concrete. Callers match its variants directly: `gix/tests/gix/repository/ +// open.rs:358` (`PathInterpolation { .. }`), `:458` (`ObjectFormatRequiresV1`), `:477` +// (`UnsupportedRepositoryFormatVersion { version: 2 }`), and `gix/tests/gix/id.rs:47` +// (`CoreAbbrev(_)`). Separately, one parent already has an erased slot filled and +// would gain a second if this type were erased too: `clone::fetch::Error::ApplyConfig` +// (`gix/src/clone/fetch/mod.rs`, erased slot `ParseConfig`). /// The error returned when failing to initialize the repository configuration. /// /// This configuration is on the critical path when opening a repository. @@ -95,6 +90,11 @@ pub enum Error { ObjectFormatRequiresV1, #[error("Unsupported repository format version {version}; only versions 0 and 1 are supported")] UnsupportedRepositoryFormatVersion { version: usize }, + // TODO(review): embeds an erased `gix_error::Error` via `abbrev::Error`; erasing any other + // `#[from]` member of this enum — e.g. `ConfigBoolean` (`boolean::Error`), or + // any `gix_config`-wrapping variant such as `Init` (`gix_config::file::init::Error`), + // still concrete pending a deferred `gix-config` erasure batch — would give it + // a second `From` impl and fail to compile (E0119). #[error(transparent)] CoreAbbrev(#[from] abbrev::Error), #[error("Could not read configuration file at \"{}\"", path.display())] @@ -130,23 +130,13 @@ 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; } } @@ -154,124 +144,58 @@ pub mod merge { pub mod diff { /// pub mod algorithm { - use crate::bstr::BString; - /// The error produced when obtaining `diff.algorithm`. - #[derive(Debug, thiserror::Error)] - #[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 }, - } + pub type Error = gix_error::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 = "attributes")] 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_config::value::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_config::path::interpolate::Error), - #[error(transparent)] - ParsePreciousEnabled(#[from] config::boolean::Error), - } + pub type Error = gix_error::Error; } /// pub mod attribute_stack { + // TODO(review): kept concrete due to an E0119 collision. `repository::diff_resource_cache::Error` + // (`gix/src/repository/mod.rs`, `diff_resource_cache` module) embeds both this type, + // via `AttributeStack(#[from] config::attribute_stack::Error)`, and the already-erased + // `crate::diff::resource_cache::Error`, via `ResourceCache(#[from] ...)`. Erasing + // `attribute_stack::Error` would give `repository::diff_resource_cache::Error` two + // `From` impls. /// The error produced when setting up the attribute stack to query `gitattributes`. #[derive(Debug, thiserror::Error)] #[expect(missing_docs)] @@ -289,6 +213,12 @@ pub mod protocol { pub mod allow { use crate::bstr::BString; + // TODO(review): kept concrete due to an E0119 collision. `crate::remote::connect::Error` + // (`gix/src/remote/connect.rs`) embeds both this type, via + // `SchemePermission(#[from] config::protocol::allow::Error)`, and the + // already-erased `config::ssh_connect_options::Error`, via + // `SshOptions(#[from] ...)`. Erasing `protocol::allow::Error` would give + // `remote::connect::Error` two `From` impls. /// The error returned when obtaining the permission for a particular scheme. #[derive(Debug, thiserror::Error)] #[expect(missing_docs)] @@ -303,9 +233,7 @@ pub mod protocol { /// 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; } /// @@ -337,6 +265,16 @@ pub mod key { _ => panic!("BUG: invalid suffix kind - add a case for it here"), } } + // TODO(review): kept concrete. Generic over the caller-supplied source error `E` and the const + // `PREFIX`/`SUFFIX` parameters that select this key's message wording (see + // `prefix()`/`suffix()` above); a `pub type Error = gix_error::Error` alias can't + // carry any of them, and doing so would collapse the ~14 differently-worded + // aliases built on top of it (`boolean::Error`, `GenericErrorWithValue`, + // `time::Error`, …) into one indistinguishable type, losing both the per-key + // message and the caller's original `source`. The `impl From<&'static T> for Error` below would + // also become an orphan impl (`std::convert::From` on `gix_error::Error`, both + // foreign to this crate) if the parameters were dropped to force an alias — E0117. /// 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. @@ -410,19 +348,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; } /// @@ -438,17 +365,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; } /// @@ -468,6 +386,14 @@ pub mod time { /// pub mod commit_signature { + // TODO(review): kept concrete due to an E0119 collision. `crate::clone::fetch::Error` + // (`gix/src/clone/fetch/mod.rs`) embeds both this type, via + // `CommitterOrFallback(#[from] crate::config::commit_signature::Error)`, and the + // already-erased `config::overrides::Error`, via `ParseConfig(#[from] ...)` + // (see the comment on `remote::save::AsError` in `gix/src/remote/save.rs`, + // which stays concrete for the identical reason). Erasing + // `commit_signature::Error` would give `clone::fetch::Error` two + // `From` impls. /// The error produced when obtaining or installing a fallback commit signature. #[derive(Debug, thiserror::Error)] #[allow(missing_docs)] @@ -535,70 +461,16 @@ 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_config::value::Error, - key: &'static str, - }, - #[error("Could not interpolate path at key {key:?}")] - InterpolatePath { - source: gix_config::path::interpolate::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_url::parse::Error), - #[error("Could obtain configuration for an HTTP url")] - Http(#[from] http::Error), - } + // `InvalidInteger` and `ConfigValue`, two of the former variants of this now-erased + // type, were never constructed anywhere in the workspace and carried no message-preservation + // obligation. + 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 2249450c8bf..656833f745c 100644 --- a/gix/src/config/overrides.rs +++ b/gix/src/config/overrides.rs @@ -1,18 +1,7 @@ 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)] - 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), -} +pub type Error = gix_error::Error; pub(crate) fn append( config: &mut gix_config::File, @@ -26,15 +15,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) + .map_err(gix_error::Error::from_error)?; 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), - }?; + } + .map_err(gix_error::Error::from_error)?; } - config.append(file)?; + config.append(file).map_err(gix_error::Error::from_error)?; Ok(()) } diff --git a/gix/src/config/snapshot/access.rs b/gix/src/config/snapshot/access.rs index 0e0a2438f9d..4e8a8bdac68 100644 --- a/gix/src/config/snapshot/access.rs +++ b/gix/src/config/snapshot/access.rs @@ -121,7 +121,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::message( + "The key needs a subsection parameter to be valid.", + ))); } let value = new_value.as_bstr(); key.validate(value)?; @@ -129,8 +131,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::Error::from_error)?, + None => self + .config + .set_raw_value_by(section.name(), None, key.name(), value) + .map_err(gix_error::Error::from_error)?, }; Ok(current) } @@ -144,7 +150,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::message( + "The key must not be used with a subsection", + ))); } let value = new_value.as_bstr(); key.validate(value)?; @@ -156,7 +164,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::Error::from_error)?; Ok(current) } diff --git a/gix/src/config/snapshot/credential_helpers.rs b/gix/src/config/snapshot/credential_helpers.rs index a018e62f46b..590f27584ed 100644 --- a/gix/src/config/snapshot/credential_helpers.rs +++ b/gix/src/config/snapshot/credential_helpers.rs @@ -3,22 +3,8 @@ 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_config::value::Error, - }, - #[error("core.askpass could not be read")] - CoreAskpass(#[from] gix_config::path::interpolate::Error), - #[error(transparent)] - BooleanConfig(#[from] crate::config::boolean::Error), - } + pub type Error = gix_error::Error; } impl Snapshot<'_> { @@ -49,6 +35,8 @@ impl Snapshot<'_> { } pub(super) mod function { + use gix_error::ResultExt; + use crate::{ bstr::{ByteSlice, ByteVec}, config::{ @@ -168,9 +156,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, - section: section.header().to_bstring(), + .or_raise(|| { + gix_error::message!( + "Could not parse 'useHttpPath' key in section {section}", + section = section.header().to_bstring() + ) }) .map(|b| b.0) }) @@ -184,7 +174,8 @@ pub(super) mod function { protect_protocol_key .enrich_error(gix_config::Boolean::try_from(value).map(|value| Some(value.0))) }) - .transpose()? + .transpose() + .map_err(gix_error::Error::from_error)? .flatten() { context_options.protect_protocol = toggle; @@ -203,10 +194,12 @@ pub(super) mod function { is_lenient_config, environment, ) - .ignore_empty()?, + .ignore_empty() + .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)? + .with_leniency(is_lenient_config) + .map_err(gix_error::Error::from_error)? .and_then(|val| (!val).then_some(gix_prompt::Mode::Disable)) .unwrap_or_default(), } @@ -224,7 +217,8 @@ pub(super) mod function { query_user_only: url.scheme == gix_url::Scheme::Ssh, stderr: Credentials::HELPER_STDERR .enrich_error(config.boolean(Credentials::HELPER_STDERR)) - .with_leniency(is_lenient_config)? + .with_leniency(is_lenient_config) + .map_err(gix_error::Error::from_error)? .unwrap_or(true), }, action, diff --git a/gix/src/config/tree/keys.rs b/gix/src/config/tree/keys.rs index 81e3e98aa77..9304d6c2f06 100644 --- a/gix/src/config/tree/keys.rs +++ b/gix/src/config/tree/keys.rs @@ -132,7 +132,9 @@ 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| gix_error::Error::from_error(std::io::Error::other(err))) } fn section(&self) -> &dyn Section { diff --git a/gix/src/config/tree/mod.rs b/gix/src/config/tree/mod.rs index a51e81dbaa3..05bd4f0c9d9 100644 --- a/gix/src/config/tree/mod.rs +++ b/gix/src/config/tree/mod.rs @@ -123,24 +123,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/core.rs b/gix/src/config/tree/sections/core.rs index 241f7eb04bd..d25b8eee050 100644 --- a/gix/src/config/tree/sections/core.rs +++ b/gix/src/config/tree/sections/core.rs @@ -205,11 +205,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::message!( + "The encoding named '{encoding}' seen in key '{key}={value}' is unsupported", + key = self.logical_name(), + encoding = encoding.as_bstr() + )) })?, ); } @@ -390,10 +390,9 @@ mod abbrev { let hex_len_str = hex_len_str.as_bstr(); let max = object_hash.len_in_hex() as u8; if hex_len_str.trim().is_empty() { - return Err(Error { - value: hex_len_str.into(), - max, - }); + return Err(gix_error::Error::from_error(gix_error::message!( + "Invalid value for 'core.abbrev' = '{hex_len_str}'. It must be between 4 and {max}" + ))); } if hex_len_str.trim().eq_ignore_ascii_case(b"auto") { Ok(None) @@ -403,20 +402,21 @@ 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(|_| { + gix_error::Error::from_error(gix_error::message!( + "Invalid value for 'core.abbrev' = '{hex_len_str}'. It must be between 4 and {max}" + )) })? .to_decimal() - .ok_or_else(|| Error { - value: hex_len_str.into(), - max, + .ok_or_else(|| { + gix_error::Error::from_error(gix_error::message!( + "Invalid value for 'core.abbrev' = '{hex_len_str}'. It must be between 4 and {max}" + )) })?; if value < 4 || value as usize > object_hash.len_in_hex() { - return Err(Error { - value: hex_len_str.into(), - max, - }); + return Err(gix_error::Error::from_error(gix_error::message!( + "Invalid value for 'core.abbrev' = '{hex_len_str}'. It must be between 4 and {max}" + ))); } Ok(Some(value as usize)) } diff --git a/gix/src/config/tree/sections/diff.rs b/gix/src/config/tree/sections/diff.rs index 55dfee468be..6a3623afc65 100644 --- a/gix/src/config/tree/sections/diff.rs +++ b/gix/src/config/tree/sections/diff.rs @@ -78,7 +78,6 @@ pub type Binary = keys::Any; mod algorithm { use crate::{ bstr::ByteSlice, - config, config::{ diff::algorithm, key, @@ -112,9 +111,13 @@ mod algorithm { } else if name.eq_ignore_ascii_case(b"histogram") { gix_diff::blob::Algorithm::Histogram } else if name.eq_ignore_ascii_case(b"patience") { - return Err(config::diff::algorithm::Error::Unimplemented { name: name.into() }); + return Err(gix_error::Error::from_error(gix_error::message!( + "The '{name}' algorithm is not yet implemented" + ))); } else { - return Err(algorithm::Error::Unknown { name: name.into() }); + return Err(gix_error::Error::from_error(gix_error::message!( + "Unknown diff algorithm named '{name}'" + ))); }; Ok(algo) } diff --git a/gix/src/config/tree/traits.rs b/gix/src/config/tree/traits.rs index 146172d9a24..a210e319b69 100644 --- a/gix/src/config/tree/traits.rs +++ b/gix/src/config/tree/traits.rs @@ -2,6 +2,7 @@ use crate::{ bstr::{BStr, BString, ByteVec}, config::tree::key::validate_assignment, }; +use gix_error::ResultExt; /// Provide information about a configuration section. pub trait Section { @@ -162,10 +163,11 @@ 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 { - self.validate(value)?; + self.validate(value) + .or_raise(|| gix_error::message("Failed to validate the value to be assigned to this key"))?; 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::message!("{message}")))?; key.push(b'='); key.push_str(value); Ok(key) @@ -188,10 +190,11 @@ pub trait Key: std::fmt::Debug { value: &BStr, subsection: &BStr, ) -> Result { - self.validate(value)?; + self.validate(value) + .or_raise(|| gix_error::message("Failed to validate the value to be assigned to this key"))?; 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::message!("{message}")))?; key.push(b'='); key.push_str(value); Ok(key) diff --git a/gix/src/create.rs b/gix/src/create.rs index 5eff998329c..45a9f70faa5 100644 --- a/gix/src/create.rs +++ b/gix/src/create.rs @@ -6,6 +6,9 @@ use std::{ use gix_discover::DOT_GIT_DIR; +// TODO(review): kept concrete. Its `DirectoryExists` variant's `path` field is read at +// `gix/tests/gix/clone.rs:746`, which matches `gix::clone::Error::Init(gix::init:: +// Error::Init(gix::create::Error::DirectoryExists { ref path }))`. /// The error used in [`into()`]. #[derive(Debug, thiserror::Error)] #[expect(missing_docs)] diff --git a/gix/src/diff.rs b/gix/src/diff.rs index 5c4cc72379f..27589ef83c1 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; } } @@ -141,33 +135,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 +163,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) + .map_err(gix_error::Error::from_error)? { Some(renames) => match renames { Tracking::Disabled => return Ok((None, true)), @@ -205,7 +180,8 @@ pub(crate) mod utils { copies, limit: rename_limit .try_into_usize(config.integer(rename_limit)) - .with_leniency(lenient)? + .with_leniency(lenient) + .map_err(gix_error::Error::from_error)? .unwrap_or(default.limit), ..default } diff --git a/gix/src/dirwalk/iter.rs b/gix/src/dirwalk/iter.rs index 7cd369c9b72..01dc51467b7 100644 --- a/gix/src/dirwalk/iter.rs +++ b/gix/src/dirwalk/iter.rs @@ -5,6 +5,8 @@ use crate::{ PathspecDetached, Repository, bstr::BString, dirwalk, util::OwnedOrStaticAtomicBool, worktree::IndexPersistedOrInMemory, }; +#[cfg_attr(not(feature = "parallel"), expect(unused_imports))] +use gix_error::ResultExt; /// An entry of the directory walk as returned by the [iterator](Iter). pub struct Item { @@ -42,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 { @@ -81,7 +71,7 @@ impl Iter { index, excludes: out.excludes.detach(), pathspec: out.pathspec.detach().map_err(|err| { - dirwalk::Error::Walk(gix_dir::walk::Error::ReadDir { + gix_error::Error::from_error(gix_dir::walk::Error::ReadDir { path: repo.git_dir().to_owned(), source: err, }) @@ -90,7 +80,8 @@ impl Iter { dirwalk: out.dirwalk, }) } - })?; + }) + .or_raise(|| gix_error::message("Failed to spawn producer thread"))?; Ok(Iter { rx_and_join: Some((rx, handle)), @@ -105,7 +96,7 @@ impl Iter { let out = Outcome { index, excludes: out.excludes.detach(), - pathspec: out.pathspec.detach()?, + pathspec: out.pathspec.detach().map_err(gix_error::Error::from_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..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(#[from] gix_dir::walk::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 560a3567c10..829947c3922 100644 --- a/gix/src/discover.rs +++ b/gix/src/discover.rs @@ -6,14 +6,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] upwards::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, @@ -36,13 +29,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)?; + let (path, trust) = upwards_opts(directory.as_ref(), options).map_err(gix_error::Error::from_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)?); - Self::open_from_paths(git_dir, worktree_dir, options).map_err(Into::into) + options.current_dir = Some( + gix_fs::current_dir(false).map_err(|err| gix_error::Error::from_error(upwards::Error::CurrentDir(err)))?, + ); + Self::open_from_paths(git_dir, worktree_dir, options).map_err(gix_error::Error::from_error) } /// Try to open a git repository directly from the environment. @@ -88,7 +83,8 @@ 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) + .map_err(gix_error::Error::from_error); } options = apply_additional_environment(options.apply_environment()); diff --git a/gix/src/env.rs b/gix/src/env.rs index 1d633c86723..3c909c46e4f 100644 --- a/gix/src/env.rs +++ b/gix/src/env.rs @@ -51,6 +51,18 @@ pub mod collate { /// pub mod fetch { + // TODO(review): kept concrete. Generic over the caller-supplied error `E` (default + // `std::convert::Infallible`); a `pub type Error = gix_error::Error` alias + // can't carry that parameter, and doing so would erase the caller's own error + // carried by `Other(E)`, which is the entire point of this type being generic — + // exercised at `gix/tests/gix/remote/fetch.rs:228` (`Error`) + // and `:245` (`Error::Other`). The `impl crate::protocol::transport:: + // IsSpuriousError for Error` below would also become an orphan impl (that + // trait is `gix_transport`'s (`gix-transport/src/lib.rs:67`), merely re-exported + // through `gix_protocol::transport` and then as `crate::protocol`) on a foreign + // type if forced into a non-generic alias — E0117. `is_corrupted()` below also + // matches specific variants directly, e.g. `Open(open::Error:: + // NotARepository { .. } | ...::Config(_))`. /// 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 diff --git a/gix/src/filter.rs b/gix/src/filter.rs index 2f2be0926d1..c8ad6c6dc29 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,69 +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_config::value::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; } } @@ -108,17 +66,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, - )?; + ) + .map_err(gix_error::Error::from_error)?; 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) + .map_err(gix_error::Error::from_error)? .unwrap_or_default(); let eol = config .string("core.eol") .map(|value| Core::EOL.try_into_eol(value)) - .transpose()?; + .transpose() + .map_err(gix_error::Error::from_error)?; let drivers = extract_drivers(repo)?; Ok(gix_filter::pipeline::Options { drivers, @@ -162,24 +123,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_error) } /// Convert a `src` buffer located at `rela_path` (in the index) from what's in `git` to the worktree representation. @@ -197,15 +163,20 @@ 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)?; - Ok(self.inner.convert_to_worktree( - src, - rela_path, - &mut |_, attrs| { - entry.matching_attributes(attrs); - }, - options, - )?) + let entry = self + .cache + .at_entry(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_worktree( + src, + rela_path, + &mut |_, attrs| { + entry.matching_attributes(attrs); + }, + options, + ) + .map_err(gix_error::Error::from_error) } /// Add the worktree file at `rela_path` to the object database and return its `(id, entry, symlink_metadata)` for use in a tree or in the index, for instance. @@ -225,11 +196,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, @@ -237,18 +208,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 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())?; (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)?, @@ -325,9 +309,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, + .map_err(|err| { + gix_error::Error::from(err.and_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..b2123a94a71 100644 --- a/gix/src/head/peel.rs +++ b/gix/src/head/peel.rs @@ -5,43 +5,27 @@ 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; + // TODO(review): kept concrete. Matched in `Repository::head_tree_id_or_empty()` + // (`gix/src/repository/reference.rs`), via + // `err.downcast_any_ref::()` then + // `Some(to_commit::Error::PeelToObject(to_object::Error::Unborn { .. }))`. /// The error returned by [`Head::peel_to_commit()`](super::Head::peel_to_commit()). #[derive(Debug, thiserror::Error)] #[expect(missing_docs)] @@ -55,6 +39,9 @@ pub mod to_commit { /// pub mod to_object { + // TODO(review): kept concrete. Matched (nested inside a `to_commit::Error` match) in + // `Repository::head_tree_id_or_empty()` (`gix/src/repository/reference.rs`): + // `to_commit::Error::PeelToObject(to_object::Error::Unborn { .. })`. /// The error returned by [`Head::peel_to_object()`](super::Head::peel_to_object()). #[derive(Debug, thiserror::Error)] #[expect(missing_docs)] @@ -74,7 +61,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::message!("Branch '{name}' does not have any commits")) + } Kind::Detached { .. } => unreachable!("id can be returned after peeling"), }) } @@ -125,11 +114,15 @@ impl<'repo> Head<'repo> { } => (*peeled).attach(self.repo), Kind::Detached { peeled: None, target } => { let id = target.attach(self.repo); - if id.header()?.kind() == gix_object::Kind::Commit { + if id.header().map_err(gix_error::Error::from_error)?.kind() == gix_object::Kind::Commit { id } else { { - let obj = id.object()?.peel_tags_to_end()?; + let obj = id + .object() + .map_err(gix_error::Error::from_error)? + .peel_tags_to_end() + .map_err(gix_error::Error::from_error)?; self.kind = Kind::Detached { peeled: Some(obj.id), target: *target, @@ -142,7 +135,7 @@ impl<'repo> Head<'repo> { let mut nr = r.clone().attach(self.repo); let peeled = nr.peel_to_id(); *r = nr.detach(); - peeled? + peeled.map_err(gix_error::Error::from_error)? } })) } @@ -167,7 +160,7 @@ impl<'repo> Head<'repo> { name: self.referent_name().expect("unborn").to_owned(), })?; id.object() - .map_err(|err| to_object::Error::Peel(Error::FindExistingObject(err))) + .map_err(|err| to_object::Error::Peel(gix_error::Error::from_error(err))) } /// Follow the symbolic reference of this head until its target object and peel it by following tag objects until there is no diff --git a/gix/src/id.rs b/gix/src/id.rs index e3eba8ee38d..8ca6850eac2 100644 --- a/gix/src/id.rs +++ b/gix/src/id.rs @@ -42,17 +42,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, + ) + .map_err(gix_error::Error::from_error)?; 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) + .map_err(gix_error::Error::from_error)? + .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 +82,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 c4b842d01bd..81a6cd2d54e 100644 --- a/gix/src/init.rs +++ b/gix/src/init.rs @@ -20,6 +20,9 @@ use crate::{ /// We use `main` instead of `master`. pub const DEFAULT_BRANCH_NAME: &str = "main"; +// TODO(review): kept concrete. Callers match its variants directly: `gix/tests/gix/init.rs:118` +// and `:142` destructure `InvalidBranchName { name, source }` (reading both fields), +// and `gix/tests/gix/clone.rs:746` matches `gix::init::Error::Init(..)`. /// The error returned by [`crate::init()`]. #[derive(Debug, thiserror::Error)] #[expect(missing_docs)] diff --git a/gix/src/lib.rs b/gix/src/lib.rs index a7baea5c597..3b9904c7e2a 100644 --- a/gix/src/lib.rs +++ b/gix/src/lib.rs @@ -254,10 +254,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) } @@ -265,10 +261,6 @@ pub fn discover(directory: impl AsRef) -> Result, options: discover::upwards::Options<'_>, @@ -288,10 +280,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 { diff --git a/gix/src/mailmap.rs b/gix/src/mailmap.rs index fcc78c0c1a0..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_config::path::interpolate::Error), - #[error("Could not find object configured in `mailmap.blob`")] - FindExisting(#[from] crate::object::find::existing::Error), - } + pub type Error = gix_error::Error; } diff --git a/gix/src/object/blob.rs b/gix/src/object/blob.rs index 17860f16fdc..35d9fb51104 100644 --- a/gix/src/object/blob.rs +++ b/gix/src/object/blob.rs @@ -25,6 +25,10 @@ pub mod diff { use crate::bstr::BStr; /// The error returned by [Platform::lines()](super::Platform::lines()). + // TODO(review): kept concrete. Generic over the caller-supplied hunk-processing error `E`; + // a type alias to `gix_error::Error` can't carry that type parameter, and doing + // so would erase the caller's own error type inside `ProcessHunk(E)`, which is + // the entire point of this type being generic. #[derive(Debug, thiserror::Error)] #[expect(missing_docs)] pub enum Error diff --git a/gix/src/object/commit.rs b/gix/src/object/commit.rs index 06341a60174..824b37ce460 100644 --- a/gix/src/object/commit.rs +++ b/gix/src/object/commit.rs @@ -3,6 +3,11 @@ use crate::{Commit, ObjectDetached, Tree, bstr, bstr::BStr}; mod error { use crate::object; + // TODO(review): kept concrete. Erasing this would give it a second `From` impl + // where it's embedded via `CommitTree(#[from] crate::object::commit::Error)` in + // `submodule::head_id::Error` (`gix/src/submodule/errors.rs`), which already has one + // via `HeadCommit(#[from] crate::reference::head_commit::Error)` + // (`reference::head_commit::Error` is already erased) — E0119. #[derive(Debug, thiserror::Error)] #[expect(missing_docs)] pub enum Error { diff --git a/gix/src/object/errors.rs b/gix/src/object/errors.rs index b8f352bac5b..35036ccf587 100644 --- a/gix/src/object/errors.rs +++ b/gix/src/object/errors.rs @@ -1,26 +1,13 @@ /// 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 +16,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 +24,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..6c5a9e80fca 100644 --- a/gix/src/object/mod.rs +++ b/gix/src/object/mod.rs @@ -1,5 +1,6 @@ //! #![allow(clippy::empty_docs)] +use gix_error::OptionExt; use gix_hash::ObjectId; pub use gix_object::Kind; @@ -22,6 +23,13 @@ pub mod tree; /// pub mod try_into { + // TODO(review): kept concrete. Its public fields (`actual`, `expected`, `id`) are read via + // struct-pattern destructuring at `crate::object::commit::Error::ObjectKind`'s + // construction site (`gix/src/object/commit.rs`, `Err(crate::object::try_into::Error + // { actual, expected, .. }) => ...`) and in + // `gix/src/revision/spec/parse/delegate/navigate.rs` + // (`let object::try_into::Error { actual, expected, id } = err;`). Erasing would + // delete those fields out from under both readers. #[derive(thiserror::Error, Debug)] #[expect(missing_docs)] #[error("Object named {id} was supposed to be of kind {expected}, but was kind {actual}.")] @@ -172,13 +180,18 @@ 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()? + let commit = gix_object::Data::new(&self.data, self.kind, self.id.kind()) + .decode() + .map_err(gix_error::Error::from_error)? .into_commit() - .ok_or(conversion::Error::UnexpectedType { - expected: gix_object::Kind::Commit, - actual: self.kind, - }) + .ok_or_raise(|| { + gix_error::message!( + "Expected object type {}, but got {}", + gix_object::Kind::Commit, + self.kind + ) + })?; + Ok(commit) } /// Obtain an iterator over commit tokens like in [`to_commit_iter()`][Object::try_to_commit_ref_iter()]. @@ -229,13 +242,14 @@ 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()? + let tag = gix_object::Data::new(&self.data, self.kind, self.id.kind()) + .decode() + .map_err(gix_error::Error::from_error)? .into_tag() - .ok_or(conversion::Error::UnexpectedType { - expected: gix_object::Kind::Tag, - actual: self.kind, - }) + .ok_or_raise(|| { + gix_error::message!("Expected object type {}, but got {}", gix_object::Kind::Tag, self.kind) + })?; + Ok(tag) } /// Return the attached id of this object. diff --git a/gix/src/object/peel.rs b/gix/src/object/peel.rs index 3156688d3a3..d42017696f1 100644 --- a/gix/src/object/peel.rs +++ b/gix/src/object/peel.rs @@ -7,25 +7,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 use error::Error; + /// The error returned by [`Object::peel_to_kind()`][crate::Object::peel_to_kind()]. + pub type Error = gix_error::Error; } impl<'repo> Object<'repo> { @@ -48,20 +31,21 @@ impl<'repo> Object<'repo> { .expect("valid commit"); let repo = self.repo; drop(self); - self = repo.find_object(tree_id)?; + self = repo.find_object(tree_id).map_err(gix_error::Error::from_error)?; } Kind::Tag => { let target_id = self.to_tag_ref_iter().target_id().expect("valid tag"); let repo = self.repo; drop(self); - self = repo.find_object(target_id)?; + self = repo.find_object(target_id).map_err(gix_error::Error::from_error)?; } 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::message!( + "Last encountered object {oid} was {actual} while trying to peel to {expected}", + oid = self.id().shorten().unwrap_or_else(|_| self.id.into()), + actual = self.kind, + expected = kind, + ))); } } } diff --git a/gix/src/object/tree/diff/for_each.rs b/gix/src/object/tree/diff/for_each.rs index 43ae7b0f34a..c1946260e90 100644 --- a/gix/src/object/tree/diff/for_each.rs +++ b/gix/src/object/tree/diff/for_each.rs @@ -1,21 +1,10 @@ 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), - #[error(transparent)] - ResourceCache(#[from] crate::repository::diff_resource_cache::Error), - #[error("Failure during rename tracking")] - RenameTracking(#[from] tracker::emit::Error), -} +pub type Error = gix_error::Error; /// Add the item to compare to. impl<'old> Platform<'_, 'old> { @@ -69,13 +58,15 @@ impl<'old> Platform<'_, 'old> { let mut storage; let cache = match resource_cache { None => { - storage = repo.diff_resource_cache(gix_diff::blob::pipeline::Mode::ToGit, Default::default())?; + storage = repo + .diff_resource_cache(gix_diff::blob::pipeline::Mode::ToGit, Default::default()) + .map_err(gix_error::Error::from_error)?; &mut storage } Some(cache) => cache, }; let opts = self.options.into(); - Ok(gix_diff::tree_with_rewrites( + gix_diff::tree_with_rewrites( TreeRefIter::from_bytes(&self.lhs.data, self.lhs.id.kind()), TreeRefIter::from_bytes(&other.data, other.id.kind()), cache, @@ -88,6 +79,7 @@ impl<'old> Platform<'_, 'old> { }) }, opts, - )?) + ) + .map_err(gix_error::Error::from_error) } } diff --git a/gix/src/object/tree/diff/mod.rs b/gix/src/object/tree/diff/mod.rs index 44fadfc390c..9e046d681a8 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 { @@ -171,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 @@ -193,7 +182,11 @@ impl Platform<'_, '_> { /// 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 { // 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()?; + let mut resource_cache = self + .lhs + .repo + .diff_resource_cache_for_tree_diff() + .map_err(gix_error::Error::from_error)?; let (mut files_changed, mut lines_added, mut lines_removed) = (0, 0, 0); self.for_each_to_obtain_tree(other, |change| { diff --git a/gix/src/object/tree/editor.rs b/gix/src/object/tree/editor.rs index 6a29865916b..dde30a960ce 100644 --- a/gix/src/object/tree/editor.rs +++ b/gix/src/object/tree/editor.rs @@ -1,3 +1,4 @@ +use gix_error::ErrorExt; 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().map_err(gix_error::Error::from_error)?; let repo = tree.repo; - let validate = repo.config.protect_options()?; + let validate = repo.config.protect_options().map_err(gix_error::Error::from_error)?; Ok(super::Editor { inner: gix_object::tree::Editor::new(tree_ref.into(), &repo.objects, repo.object_hash()), validate, @@ -297,6 +271,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 @@ -305,18 +280,21 @@ 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()) diff --git a/gix/src/open/mod.rs b/gix/src/open/mod.rs index 431d167fdc0..4c47d635f77 100644 --- a/gix/src/open/mod.rs +++ b/gix/src/open/mod.rs @@ -40,6 +40,14 @@ pub struct Options { pub(crate) current_dir: Option, } +// TODO(review): kept concrete. Callers match its variants directly: +// `env::collate::fetch::Error::is_corrupted()` (`gix/src/env.rs`) matches +// `Error::NotARepository { .. } | Error::Config(_)`; `Submodule::open()` +// (`gix/src/submodule/mod.rs`) matches `Error::NotARepository { .. }`; and +// `gix/tests/gix/repository/open.rs:435` and `:502`, the former reading the `path` +// field. Separately, `clone::fetch::Error::ReopenWithObjectHash` +// (`gix/src/clone/fetch/mod.rs`, `cfg(feature = "sha256")`) already has an erased +// slot via `ParseConfig`, so this type is doubly blocked from erasure there. /// The error returned by [`crate::open()`]. #[derive(Debug, thiserror::Error)] #[expect(missing_docs)] diff --git a/gix/src/pathspec.rs b/gix/src/pathspec.rs index c76ad3d770a..4ab57eb418a 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(#[from] 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), - #[error(transparent)] - RepoPrefix(#[from] gix_path::realpath::Error), - } + pub type Error = gix_error::Error; } /// Lifecycle @@ -47,12 +35,13 @@ impl<'repo> Pathspec<'repo> { let patterns = patterns .into_iter() .map(move |p| parse(p.as_ref(), defaults)) - .collect::, _>>()?; + .collect::, _>>() + .map_err(gix_error::Error::from_error)?; 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::Error::from_error)? }; let search = Search::from_specs( patterns, @@ -61,9 +50,23 @@ impl<'repo> Pathspec<'repo> { repo.workdir().unwrap_or_else(|| repo.git_dir()), repo.options.current_dir_or_empty(), gix_path::realpath::MAX_SYMLINKS, - )?, - )?; - let cache = needs_cache.then(make_attributes).transpose()?; + ) + .map_err(gix_error::Error::from_error)?, + ) + .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() + // TODO(review): `make_attributes` yields `Box`, which does not implement + // `std::error::Error` (the std blanket needs a `Sized` inner), so + // `Error::from_error` can't take it directly. `io::Error::other` bridges + // it while preserving the boxed error's `Display` and causes. A dedicated + // `gix_error::Error::from_boxed` would be cleaner and is worth proposing. + .map_err(|err| gix_error::Error::from_error(std::io::Error::other(err)))?; gix_trace::debug!( longest_prefix = ?search.longest_common_directory(), diff --git a/gix/src/reference/edits.rs b/gix/src/reference/edits.rs index a6210ca7a01..1e4b46adf59 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,14 +24,21 @@ 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(), - id, - PreviousValue::MustExistAndMatch(Target::Object(current_id.to_owned())), - reflog_message, - )?; + let changed = self + .repo + .reference( + self.name(), + id, + PreviousValue::MustExistAndMatch(Target::Object(current_id.to_owned())), + reflog_message, + ) + .map_err(gix_error::Error::from_error)?; *self = changed; } } diff --git a/gix/src/reference/errors.rs b/gix/src/reference/errors.rs index eb8d7a4cede..6fb9fd75530 100644 --- a/gix/src/reference/errors.rs +++ b/gix/src/reference/errors.rs @@ -2,6 +2,13 @@ pub mod edit { use crate::config; + // TODO(review): kept concrete due to an E0119 collision. `clone::fetch::Error` + // (`gix/src/clone/fetch/mod.rs`) embeds this type via + // `HeadUpdate(#[from] crate::reference::edit::Error)`, and already embeds the + // erased `config::overrides::Error`, via `ParseConfig(#[from] ...)`. Erasing + // `edit::Error` would give `clone::fetch::Error` two `From` + // impls. (`init::Error::EditHeadForDefaultBranch` at `gix/src/init.rs` has no + // erased member, so it isn't a second blocker.) /// The error returned by [`edit_references(…)`][crate::Repository::edit_references()], and others /// which ultimately create a reference. #[derive(Debug, thiserror::Error)] @@ -24,6 +31,10 @@ pub mod edit { /// pub mod peel { + // TODO(review): kept concrete. Matched in `update()` + // (`gix/src/remote/connection/fetch/update_refs/mod.rs`) to detect unborn refs: + // `Error::ToId(gix_ref::peel::to_id::Error:: + // FollowToObject(gix_ref::peel::to_object::Error::Follow(_)))`. /// 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)] @@ -38,18 +49,7 @@ pub mod peel { /// 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,67 +58,32 @@ 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; } /// @@ -127,6 +92,13 @@ pub mod find { pub mod existing { use gix_ref::PartialName; + // TODO(review): kept concrete. Callers match its `NotFound` variant directly, in + // `Repository::head()` (`gix/src/repository/reference.rs`) and + // `Delegate::nth_checked_out_branch()` + // (`gix/src/revision/spec/parse/delegate/revision.rs`). Separately, + // `repository::index_or_load_from_head_or_empty::Error::ReadHead` + // (`gix/src/repository/mod.rs`) already has an erased slot via `PeelToTree`, + // so this type is doubly blocked from erasure there. /// The error returned by [`find_reference(…)`][crate::Repository::find_reference()], and others. #[derive(Debug, thiserror::Error)] #[expect(missing_docs)] @@ -139,10 +111,5 @@ pub mod find { } /// 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..230f15fa317 100644 --- a/gix/src/reference/iter.rs +++ b/gix/src/reference/iter.rs @@ -38,7 +38,10 @@ 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().map_err(gix_error::Error::from_error)?, + )) } /// Return an iterator over all references that match the given `prefix`. @@ -48,14 +51,22 @@ 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::Error::from_error)?; + Ok(Iter::new( + self.repo, + self.platform.prefixed(prefix).map_err(gix_error::Error::from_error)?, + )) } /// 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::Error::from_error)?; + Ok(Iter::new( + self.repo, + self.platform.prefixed(prefix).map_err(gix_error::Error::from_error)?, + )) } // TODO: tests @@ -63,16 +74,20 @@ impl<'repo> Platform<'repo> { /// /// They are all prefixed with `refs/heads`. pub fn local_branches(&self) -> Result, init::Error> { + let prefix = b"refs/heads/".try_into().map_err(gix_error::Error::from_error)?; Ok(Iter::new( self.repo, - self.platform.prefixed(b"refs/heads/".try_into()?)?, + self.platform.prefixed(prefix).map_err(gix_error::Error::from_error)?, )) } // 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().map_err(gix_error::Error::from_error)?, + )) } // TODO: tests @@ -80,9 +95,10 @@ impl<'repo> Platform<'repo> { /// /// They are all prefixed with `refs/remotes`. pub fn remote_branches(&self) -> Result, init::Error> { + let prefix = b"refs/remotes/".try_into().map_err(gix_error::Error::from_error)?; Ok(Iter::new( self.repo, - self.platform.prefixed(b"refs/remotes/".try_into()?)?, + self.platform.prefixed(prefix).map_err(gix_error::Error::from_error)?, )) } } @@ -127,14 +143,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 ff8847a8c30..4bac971d21e 100644 --- a/gix/src/reference/mod.rs +++ b/gix/src/reference/mod.rs @@ -163,7 +163,7 @@ impl<'repo> Reference<'repo> { #[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( + gix_error::Error::from_error(gix_ref::peel::to_object::Error::Follow( file::find::existing::Error::Find(file::find::Error::PackedOpen(err)), )) })?; @@ -221,9 +221,13 @@ 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) + .map_err(gix_error::Error::from_error)? .attach(self.repo); - Ok(target.object()?.peel_to_kind(kind)?) + target + .object() + .map_err(gix_error::Error::from_error)? + .peel_to_kind(kind) } /// Follow all symbolic references we point to up to the first object, which is typically (but not always) a tag, @@ -233,7 +237,7 @@ impl<'repo> Reference<'repo> { #[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( + gix_error::Error::from_error(gix_ref::peel::to_object::Error::Follow( file::find::existing::Error::Find(file::find::Error::PackedOpen(err)), )) })?; @@ -249,7 +253,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) + .map_err(gix_error::Error::from_error)? .attach(self.repo)) } diff --git a/gix/src/remote/connect.rs b/gix/src/remote/connect.rs index c0347141766..488701f285e 100644 --- a/gix/src/remote/connect.rs +++ b/gix/src/remote/connect.rs @@ -13,6 +13,17 @@ mod error { use super::connect; use crate::{bstr::BString, config, remote}; + // TODO(review): kept concrete, blocked three independent ways: + // 1. `impl gix_protocol::transport::IsSpuriousError for Error` below matches + // `Connect`; as a `gix_error::Error` alias this becomes an orphan impl (neither + // the trait nor `gix_error::Error` are local to this crate) — E0117. This trait + // also carries design meaning: it decides whether a connection failure is + // worth retrying. + // 2. `clone::fetch::Error` (`gix/src/clone/fetch/mod.rs`) embeds this type via + // `Connect(#[from] crate::remote::connect::Error)`, but has already used its + // one erased slot via `ParseConfig(#[from] crate::config::overrides::Error)` + // — E0119. + // 3. Callers match on `ProtocolDenied { .. }`: `gix/tests/gix/remote/connect.rs:16`. /// The error returned by [connect()][crate::Remote::connect()]. #[derive(Debug, thiserror::Error)] #[expect(missing_docs)] diff --git a/gix/src/remote/connection/fetch/error.rs b/gix/src/remote/connection/fetch/error.rs index bd06a1306da..80f63b44309 100644 --- a/gix/src/remote/connection/fetch/error.rs +++ b/gix/src/remote/connection/fetch/error.rs @@ -1,5 +1,18 @@ use crate::config; +// TODO(review): kept concrete, blocked three independent ways: +// 1. `impl gix_protocol::transport::IsSpuriousError for Error` below matches specific +// variants (`Fetch`, `Client`); as a `gix_error::Error` alias this becomes an orphan +// impl (neither the trait nor `gix_error::Error` are local to this crate) — E0117. +// 2. `clone::fetch::Error` (`gix/src/clone/fetch/mod.rs`) embeds this type via +// `Fetch(#[from] crate::remote::fetch::Error)`, but has already used its one erased +// slot via `ParseConfig(#[from] crate::config::overrides::Error)` — E0119. +// 3. Callers match on specific variants in `gix/src/env.rs`'s `is_corrupted()`: +// `PackThreads`, `PackIndexVersion`, `RemovePackKeepFile { .. }`, +// `Fetch(gix_protocol::fetch::Error::Negotiate(_))`, plus `Error::Fetch` in the +// `IsSpuriousError` impl on `env::collate::fetch::Error` (that enum has no +// `Client` variant — `Error::Client` is matched only in this type's own +// `IsSpuriousError` impl below, already covered by point 1). /// The error returned by [`receive()`](super::Prepare::receive()). // TODO: remove unused variants #[derive(Debug, thiserror::Error)] diff --git a/gix/src/remote/connection/fetch/mod.rs b/gix/src/remote/connection/fetch/mod.rs index ace1a506cff..3f446c2b01a 100644 --- a/gix/src/remote/connection/fetch/mod.rs +++ b/gix/src/remote/connection/fetch/mod.rs @@ -101,6 +101,21 @@ pub use gix_protocol::fetch::ProgressId; /// pub mod prepare { + // TODO(review): kept concrete, blocked three independent ways: + // 1. `impl gix_protocol::transport::IsSpuriousError for Error` below matches + // `RefMap`; as a `gix_error::Error` alias this becomes an orphan impl (neither the + // trait nor `gix_error::Error` are local to this crate) — E0117. + // 2. `clone::fetch::Error` (`gix/src/clone/fetch/mod.rs`) embeds this type via + // `PrepareFetch(#[from] crate::remote::fetch::prepare::Error)`, but has already + // used its one erased slot via + // `ParseConfig(#[from] crate::config::overrides::Error)` — E0119. + // 3. Callers match on `RefMap(...)`: `PrepareFetch::fetch_only()` + // (`gix/src/clone/fetch/mod.rs`) destructures further into + // `ref_map::Error::InitRefMap(...)`, but + // `env::collate::fetch::Error::is_corrupted()` (`gix/src/env.rs`) destructures + // into `ref_map::Error::GatherTransportConfig { .. } | ConfigureCredentials(_)` + // instead (a different variant) — plus `Error::PrepareFetch` in the + // `IsSpuriousError` impl on `env::collate::fetch::Error`. /// The error returned by [`prepare_fetch()`][super::Connection::prepare_fetch()]. #[derive(Debug, thiserror::Error)] #[expect(missing_docs)] diff --git a/gix/src/remote/connection/fetch/update_refs/mod.rs b/gix/src/remote/connection/fetch/update_refs/mod.rs index 1ab7c233021..7a193b72996 100644 --- a/gix/src/remote/connection/fetch/update_refs/mod.rs +++ b/gix/src/remote/connection/fetch/update_refs/mod.rs @@ -1,6 +1,7 @@ #![allow(clippy::result_large_err)] use std::{collections::BTreeMap, path::PathBuf}; +use gix_error::ResultExt; use gix_object::Exists; use gix_ref::{ Target, TargetRef, @@ -154,7 +155,12 @@ pub(crate) fn update( let is_fast_forward = match dry_run { fetch::DryRun::No => { let ancestors = repo - .find_object(local_id)? + .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(|_| ())) @@ -220,11 +226,23 @@ pub(crate) fn update( PreviousValue::MustExistAndMatch(existing.target().into_owned()), ) } - Err(err) => return Err(err.into()), + Err(err) => { + return Err(err) + .or_raise(|| { + gix_error::message("Could not peel symbolic local reference to its ID") + }) + .map_err(Into::into); + } } } None => { - let name: gix_ref::FullName = name.try_into()?; + let name: Result = name.try_into(); + let name = 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", @@ -318,13 +336,19 @@ pub(crate) fn update( } } + // Every conversion below carries the same context, as this whole block corresponds to the former + // `update::Error::EditReferences` variant, which carried `crate::reference::edit::Error` as its + // `#[from]` source while attaching this fixed message (it was message-bearing, not `#[error(transparent)]`). + const EDIT_REFERENCES_CONTEXT: &str = + "Failed to update references to their new position to match their remote locations"; 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)?; + .map_err(crate::reference::edit::Error::from) + .or_raise(|| gix_error::message(EDIT_REFERENCES_CONTEXT))?; repo.refs .transaction() .packed_refs( @@ -335,9 +359,16 @@ 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)? + .map_err(crate::reference::edit::Error::from) + .or_raise(|| gix_error::message(EDIT_REFERENCES_CONTEXT))? + .commit( + repo.committer() + .transpose() + .map_err(crate::reference::edit::Error::ParseCommitterTime) + .or_raise(|| gix_error::message(EDIT_REFERENCES_CONTEXT))?, + ) + .map_err(crate::reference::edit::Error::from) + .or_raise(|| gix_error::message(EDIT_REFERENCES_CONTEXT))? } fetch::DryRun::Yes => edits, }; @@ -402,7 +433,14 @@ 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 => { + let name: Result = target.try_into(); + Target::Symbolic(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?", + ) + })?) + } } } else { Target::Object(remote_id.expect("unborn case handled earlier").to_owned()) @@ -422,7 +460,9 @@ fn insert_head( let mut cursor = head.try_into_referent(); while let Some(ref_) = cursor { ref_chain.push(ref_.name().to_owned()); - cursor = ref_.follow().transpose()?; + cursor = ref_.follow().transpose().or_raise(|| { + gix_error::message("Failed to follow a symbolic reference to assure worktree isn't affected") + })?; } for name in ref_chain { out.entry(name).or_default().push(wd.to_owned()); @@ -434,8 +474,13 @@ fn insert_head( fn worktree_branches(repo: &Repository) -> Result>, update::Error> { let mut map = BTreeMap::new(); insert_head(repo.head().ok(), &mut map)?; - for proxy in repo.worktrees()? { - let repo = proxy.into_repo_with_possibly_inaccessible_worktree()?; + for proxy in repo + .worktrees() + .or_raise(|| gix_error::message("Failed to read or iterate worktree dir"))? + { + let repo = proxy + .into_repo_with_possibly_inaccessible_worktree() + .or_raise(|| gix_error::message("Could not open worktree repository"))?; insert_head(repo.head().ok(), &mut map)?; } Ok(map) diff --git a/gix/src/remote/connection/fetch/update_refs/update.rs b/gix/src/remote/connection/fetch/update_refs/update.rs index bda67480a15..b646aec25de 100644 --- a/gix/src/remote/connection/fetch/update_refs/update.rs +++ b/gix/src/remote/connection/fetch/update_refs/update.rs @@ -2,35 +2,8 @@ use std::path::PathBuf; 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 use error::Error; +/// The error returned when updating references. +pub type Error = gix_error::Error; /// The outcome of the refs-update operation at the end of a fetch. #[derive(Debug, Clone)] diff --git a/gix/src/remote/connection/ref_map.rs b/gix/src/remote/connection/ref_map.rs index fe3d0127894..d81068fcfe1 100644 --- a/gix/src/remote/connection/ref_map.rs +++ b/gix/src/remote/connection/ref_map.rs @@ -9,6 +9,17 @@ use crate::{ remote::{Connection, connection::ConnectionDetached, fetch}, }; +// TODO(review): kept concrete, blocked three independent ways: +// 1. `impl gix_protocol::transport::IsSpuriousError for Error` below matches specific +// variants (`Transport`, `Handshake`); as a `gix_error::Error` alias this becomes an +// orphan impl (neither the trait nor `gix_error::Error` are local to this crate) — E0117. +// 2. `clone::fetch::Error` (`gix/src/clone/fetch/mod.rs`) embeds this type via +// `RefMap(#[from] crate::remote::ref_map::Error)`, but has already used its one erased +// slot via `ParseConfig(#[from] crate::config::overrides::Error)` — E0119. +// 3. Callers match on specific variants: `PrepareFetch::fetch_only()` +// (`gix/src/clone/fetch/mod.rs`) matches `ref_map::Error::InitRefMap`, and +// `env::collate::fetch::Error::is_corrupted()` (`gix/src/env.rs`) matches +// `GatherTransportConfig { .. }` / `ConfigureCredentials(_)`. /// The error returned by [`Connection::ref_map()`]. #[derive(Debug, thiserror::Error)] #[expect(missing_docs)] diff --git a/gix/src/remote/errors.rs b/gix/src/remote/errors.rs index 97dd20349f2..dccb4175b86 100644 --- a/gix/src/remote/errors.rs +++ b/gix/src/remote/errors.rs @@ -1,33 +1,19 @@ /// 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; + // TODO(review): kept concrete. Matched at `gix/tests/gix/repository/remote.rs:229`: + // `gix::remote::find::existing::Error::NotFound { .. }`. Separately, + // `env::collate::fetch::Error::FindExistingRemote` (`gix/src/env.rs`) + // already has an erased slot via `CredentialHelperConfig` (feature + // `credentials`, on by default), so this type is now doubly blocked from + // erasure there. Its other `#[from]` parent, `remote::find::for_fetch:: + // Error::FindExisting` (below), still has no other erased member. /// The error returned by [`Repository::find_remote(…)`](crate::Repository::find_remote()). #[derive(Debug, thiserror::Error)] #[expect(missing_docs)] @@ -43,6 +29,9 @@ pub mod find { /// pub mod for_fetch { + // TODO(review): kept concrete. Matched at `gix/tests/gix/reference/remote.rs:84`: + // `Err(gix::remote::find::for_fetch::Error::ExactlyOneRemoteNotAvailable)`. + // No `#[from]` parents embed this type. /// The error returned by [`Repository::find_fetch_remote(…)`](crate::Repository::find_fetch_remote()). #[derive(Debug, thiserror::Error)] #[expect(missing_docs)] diff --git a/gix/src/remote/init.rs b/gix/src/remote/init.rs index 2a7ce564c86..db6ad2fd8b9 100644 --- a/gix/src/remote/init.rs +++ b/gix/src/remote/init.rs @@ -5,6 +5,11 @@ use crate::{Remote, Repository, config, remote}; mod error { use crate::bstr::BString; + // TODO(review): kept concrete. Erasing this would give `clone::fetch::Error` + // (`gix/src/clone/fetch/mod.rs`) a second `From` impl: it embeds + // this type via `RemoteInit(#[from] crate::remote::init::Error)`, but has already + // used its one erased slot via `ParseConfig(#[from] crate::config::overrides::Error)` + // (`config::overrides::Error` is already erased) — E0119. /// The error returned by [`Repository::remote_at(…)`][crate::Repository::remote_at()]. #[derive(Debug, thiserror::Error)] #[expect(missing_docs)] diff --git a/gix/src/remote/name.rs b/gix/src/remote/name.rs index 96172413b9a..307a0f77f3f 100644 --- a/gix/src/remote/name.rs +++ b/gix/src/remote/name.rs @@ -3,6 +3,13 @@ use std::borrow::Cow; use super::Name; use crate::bstr::{BStr, BString, ByteSlice, ByteVec}; +// TODO(review): kept concrete. Erasing this would give `remote::save::AsError` (`gix/src/remote/save.rs`) +// a second `From` impl: it embeds this type via +// `Name(#[from] crate::remote::name::Error)`, but has already used its one erased slot +// via `Save(#[from] Error)` (`Error` there is already `gix_error::Error`) — E0119. +// Separately, this is a struct with `pub` fields (`source`, `name`); no caller currently +// reads them (`.source`/`.name`/`Error { .. }` all unused outside this file), but erasure +// would still remove that public surface. /// The error returned by [validated()]. #[derive(Debug, thiserror::Error)] #[error("remote names must be valid within refspecs for fetching: {name:?}")] diff --git a/gix/src/remote/save.rs b/gix/src/remote/save.rs index 90a56c35b29..191f4932464 100644 --- a/gix/src/remote/save.rs +++ b/gix/src/remote/save.rs @@ -2,17 +2,13 @@ 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)] - Span(#[from] gix_config::parse::span::Error), - #[error(transparent)] - ConfigValue(#[from] gix_config::file::section::value::Error), -} +pub type Error = gix_error::Error; +// TODO(review): kept concrete due to an E0119 collision. `clone::fetch::Error` +// (`gix/src/clone/fetch/mod.rs`) embeds this type via +// `SaveConfig(#[from] crate::remote::save::AsError)`, and already embeds the +// erased `config::overrides::Error`, via `ParseConfig(#[from] ...)`. Erasing +// `AsError` would give `clone::fetch::Error` two `From` impls. /// 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. @@ -32,18 +28,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; @@ -104,26 +99,38 @@ 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, "") + .map_err(gix_error::Error::from_error)?; } for url in &self.urls { - section.push("url", url.to_bstring())?; + section + .push("url", url.to_bstring()) + .map_err(gix_error::Error::from_error)?; } if needs_push_url_reset { - section.push(config::tree::Remote::PUSH_URL.name, "")?; + section + .push(config::tree::Remote::PUSH_URL.name, "") + .map_err(gix_error::Error::from_error)?; } for url in &self.push_urls { - section.push("pushurl", url.to_bstring())?; + section + .push("pushurl", url.to_bstring()) + .map_err(gix_error::Error::from_error)?; } 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") + } + }), + ) + .map_err(gix_error::Error::from_error)?; } for (key, spec) in self .fetch_specs @@ -131,7 +138,9 @@ 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()) + .map_err(gix_error::Error::from_error)?; } Ok(()) } @@ -141,10 +150,6 @@ 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(); diff --git a/gix/src/repository/attributes.rs b/gix/src/repository/attributes.rs index ea62cea328e..2dac9d712ba 100644 --- a/gix/src/repository/attributes.rs +++ b/gix/src/repository/attributes.rs @@ -2,14 +2,7 @@ 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,11 +31,14 @@ 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 (attributes, mut buf) = self + .config + .assemble_attribute_globals( + self.common_dir(), + attributes_source, + self.options.permissions.attributes, + ) + .map_err(gix_error::Error::from_error)?; let ignore = self.config .assemble_exclude_globals(self.common_dir(), exclude_overrides, ignore_source, &mut buf)?; diff --git a/gix/src/repository/blame.rs b/gix/src/repository/blame.rs index 62d088d127a..54d8f6dc57c 100644 --- a/gix/src/repository/blame.rs +++ b/gix/src/repository/blame.rs @@ -16,7 +16,9 @@ impl Repository { options: blame_file::Options, ) -> Result { let cache = self.commit_graph_if_enabled()?; - let mut resource_cache = self.diff_resource_cache_for_tree_diff()?; + let mut resource_cache = self + .diff_resource_cache_for_tree_diff() + .map_err(gix_error::Error::from_error)?; let blame_file::Options { diff_algorithm, @@ -44,7 +46,8 @@ impl Repository { &mut resource_cache, file_path, options, - )?; + ) + .map_err(gix_error::Error::from_error)?; Ok(outcome) } diff --git a/gix/src/repository/config/branch.rs b/gix/src/repository/config/branch.rs index 254f2c7b738..56faa6bdb0d 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::ResultExt; use gix_ref::{FullName, FullNameRef}; use crate::{ @@ -58,13 +59,14 @@ impl crate::Repository { } else { gix_ref::Category::LocalBranch.to_full_name(name.as_bstr()) } + .or_raise(|| gix_error::message("The configured name of the remote ref to merge wasn't valid")) .map_err(Into::into) }) } 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(gix_error::Error::from_error(err))), }; if remote.push_specs.is_empty() { let push_default = @@ -78,7 +80,7 @@ impl crate::Repository { .with_lenient_default(self.config.lenient_config) }) { Ok(v) => v, - Err(err) => return Some(Err(err.into())), + Err(err) => return Some(Err(gix_error::Error::from_error(err))), }; match push_default { push::Default::Nothing => None, @@ -91,8 +93,12 @@ impl crate::Repository { }, } } else { - matching_remote(name, remote.push_specs.iter(), self.object_hash()) - .map(|res| res.map_err(Into::into)) + matching_remote(name, remote.push_specs.iter(), self.object_hash()).map(|res| { + res.or_raise(|| { + gix_error::message("The configured name of the remote ref to merge wasn't valid") + }) + .map_err(Into::into) + }) } } } @@ -121,11 +127,15 @@ impl crate::Repository { name: &FullNameRef, direction: remote::Direction, ) -> Option> { - let remote_ref = match self.branch_remote_ref_name(name, direction)? { + let remote_ref = match self.branch_remote_ref_name(name, direction)?.or_raise(|| { + gix_error::message("Could not get the remote reference to translate into the local tracking branch") + }) { Ok(r) => r, Err(err) => return Some(Err(err.into())), }; - let remote = match self.branch_remote(name.shorten(), direction)? { + let remote = match self.branch_remote(name.shorten(), direction)?.or_raise(|| { + gix_error::message("Couldn't find remote to obtain fetch-specs for mapping to the tracking reference") + }) { Ok(r) => r, Err(err) => return Some(Err(err.into())), }; @@ -133,8 +143,10 @@ impl crate::Repository { 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.or_raise(|| gix_error::message("The name of the tracking reference was invalid")) + .map_err(Into::into) + }) } /// Given a local `tracking_branch` name, find the remote that maps to it along with the name of the branch on @@ -150,11 +162,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::message!( + "The input branch '{}' needs to be a remote tracking branch", + tracking_branch.as_bstr() + ))); } let null = self.object_hash().null(); @@ -166,7 +178,7 @@ impl crate::Repository { let mut candidates = Vec::new(); let mut ambiguous_remotes = Vec::new(); for remote_name in self.remote_names() { - let remote = self.find_remote(remote_name)?; + let remote = self.find_remote(remote_name).map_err(gix_error::Error::from_error)?; let match_group = gix_refspec::MatchGroup::from_fetch_specs( remote .refspecs(remote::Direction::Fetch) @@ -184,7 +196,9 @@ impl crate::Repository { if candidates.len() == 1 { let (remote, candidate) = candidates.pop().expect("just checked for one entry"); let upstream_branch = match candidate { - 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(gix_error::Error::from_error)? + } gix_refspec::match_group::SourceRef::ObjectId(_) => { unreachable!("Such a reverse mapping isn't ever produced") } @@ -192,14 +206,20 @@ impl crate::Repository { return Ok(Some((upstream_branch, remote))); } if ambiguous_remotes.len() + 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: Vec> = ambiguous_remotes + .into_iter() + .map(|r| r.name) + .chain(candidates.into_iter().map(|(r, _)| r.name)) + .flatten() + .collect(); + return Err(gix_error::Error::from_error(gix_error::message!( + "Found ambiguous remotes without 1:1 mapping or more than one match: {}", + remotes + .iter() + .map(|r| r.as_bstr().to_string()) + .collect::>() + .join(", ") + ))); } Ok(None) } @@ -254,7 +274,7 @@ impl crate::Repository { .map_err(Into::into) .and_then(|url| { self.remote_at(url) - .map_err(|err| remote::find::existing::Error::Find(remote::find::Error::Init(err))) + .map_err(|err| remote::find::existing::Error::Find(gix_error::Error::from_error(err))) }) .into(), remote::Name::Symbol(_) => None, diff --git a/gix/src/repository/config/mod.rs b/gix/src/repository/config/mod.rs index 3ae4dfcd7e5..dffc1808d04 100644 --- a/gix/src/repository/config/mod.rs +++ b/gix/src/repository/config/mod.rs @@ -102,7 +102,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_error)?, }; Ok(opts) } @@ -113,16 +114,18 @@ impl crate::Repository { pub fn command_context(&self) -> Result { use crate::config::{cache::util::ApplyLeniency, tree::gitoxide}; - let pathspec_boolean = |key: &'static config::tree::keys::Boolean| { + let pathspec_boolean = |key: &'static config::tree::keys::Boolean| -> Result, gix_error::Error> { key.enrich_error(self.config.resolved.boolean(key)) .with_leniency(self.config.lenient_config) + .map_err(gix_error::Error::from_error) }; Ok(gix_command::Context { 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_error)? .unwrap_or(true) .into() }, @@ -132,7 +135,8 @@ impl crate::Repository { &self.config.resolved, self.config.lenient_config, self.filter_config_section(), - )? + ) + .map_err(gix_error::Error::from_error)? .map(|enabled| !enabled), ref_namespace: self.refs.namespace.as_ref().map(|ns| ns.as_bstr().to_owned()), literal_pathspecs: pathspec_boolean(&gitoxide::Pathspec::LITERAL)?, diff --git a/gix/src/repository/config/transport.rs b/gix/src/repository/config/transport.rs index 466d355515a..67af78596ea 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,7 +27,7 @@ impl crate::Repository { url: impl Into<&'a BStr>, remote_name: Option<&BStr>, ) -> Result>, crate::config::transport::Error> { - let url = gix_url::parse(url.into())?; + let url = gix_url::parse(url.into()).or_raise(|| gix_error::message("Invalid URL passed for configuration"))?; use gix_url::Scheme::*; match &url.scheme { @@ -64,11 +65,12 @@ impl crate::Repository { 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| config::transport::Error::IllformedUtf8 { - source: err, - key: key_str.into(), + .or_raise(|| { + gix_error::message!("Could not decode value at key {key_str:?} as UTF-8 string") }) + .map_err(gix_error::Error::from) .map(Some) .with_leniency(lenient) } @@ -78,11 +80,15 @@ impl crate::Repository { ) -> 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 } - }) + key_type + .try_into_proxy_auth_method(method) + .or_raise(|| { + gix_error::message!("The proxy authentication at key `{key}` is invalid") + }) + .map_err(gix_error::Error::from) }) - .transpose()? + .transpose() + .or_raise(|| gix_error::message("Could obtain configuration for an HTTP url"))? .unwrap_or_default(); Ok(value) } @@ -102,13 +108,11 @@ impl crate::Repository { 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) - }) + .map(|v| key.try_into_ssl_version(v).map_err(gix_error::Error::from_error)) .transpose() .with_leniency(lenient) - .map_err(Into::into) + .or_raise(|| gix_error::message("Could obtain configuration for an HTTP url")) + .map_err(gix_error::Error::from) } fn proxy( @@ -139,10 +143,7 @@ impl crate::Repository { .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(), - })? + .or_raise(|| gix_error::message!("Could not decode value at key {key:?} as UTF-8 string"))? .unwrap_or_default() }; @@ -154,18 +155,23 @@ impl crate::Repository { 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)? + .or_raise(|| { + gix_error::message("The follow redirects value 'initial', or boolean true or false") + }) + .or_raise(|| gix_error::message("Could obtain configuration for an HTTP url"))? }; 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)? + .map_err(gix_error::Error::from_error) + .or_raise(|| gix_error::message("Could obtain configuration for an HTTP url"))? .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)? + .map_err(gix_error::Error::from_error) + .or_raise(|| gix_error::message("Could obtain configuration for an HTTP url"))? .unwrap_or_default(); opts.proxy = proxy( remote_name @@ -252,25 +258,32 @@ impl crate::Repository { .as_deref() .filter(|url| !url.is_empty()) .map(gix_url::parse) - .transpose()? + .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)?; + self.config_snapshot().credential_helpers(url).or_raise(|| { + gix_error::message( + "Could not configure the credential helpers for the authenticated proxy url", + ) + })?; Ok(( action_with_normalized_url, Arc::new(Mutex::new(move |action| cascade.invoke(action, prompt_opts.clone()))) as Arc>, )) }) - .transpose()?; + .transpose() + .or_raise(|| gix_error::message("Could obtain configuration for an HTTP url"))?; 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)? + .map_err(gix_error::Error::from_error) + .with_leniency(lenient) + .or_raise(|| gix_error::message("Could obtain configuration for an HTTP url"))? }; { let key = "http.userAgent"; @@ -288,9 +301,11 @@ impl crate::Repository { .map(|v| { config::tree::Http::VERSION .try_into_http_version(v) - .map_err(config::transport::http::Error::InvalidHttpVersion) + .or_raise(|| gix_error::message("The HTTP version must be 'HTTP/2' or 'HTTP/1.1'")) + .map_err(gix_error::Error::from) }) - .transpose()?; + .transpose() + .or_raise(|| gix_error::message("Could obtain configuration for an HTTP url"))?; } { @@ -306,7 +321,8 @@ impl crate::Repository { 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)? + .map_err(gix_error::Error::from_error) + .or_raise(|| gix_error::message("Could obtain configuration for an HTTP url"))? .unwrap_or(true) }; @@ -324,7 +340,7 @@ impl crate::Repository { }) .transpose() .with_leniency(lenient) - .map_err(|err| config::transport::Error::InterpolatePath { source: err, key })?; + .or_raise(|| gix_error::message!("Could not interpolate path at key {key:?}"))?; } { @@ -368,7 +384,8 @@ impl crate::Repository { 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)? + .map_err(gix_error::Error::from_error) + .or_raise(|| gix_error::message("Could obtain configuration for an HTTP url"))? .unwrap_or_default(); if ssl_no_verify { @@ -378,7 +395,8 @@ impl crate::Repository { opts.ssl_verify = config::tree::Http::SSL_VERIFY .enrich_error(config.boolean_filter(key, &mut trusted_only)) .with_leniency(lenient) - .map_err(config::transport::http::Error::from)? + .map_err(gix_error::Error::from_error) + .or_raise(|| gix_error::message("Could obtain configuration for an HTTP url"))? .unwrap_or(true); } } @@ -389,7 +407,8 @@ impl crate::Repository { 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)?; + .map_err(gix_error::Error::from_error) + .or_raise(|| gix_error::message("Could obtain configuration for an HTTP url"))?; let backend = gix_protocol::transport::client::blocking_io::http::curl::Options { schannel_check_revoke }; opts.backend = diff --git a/gix/src/repository/diff.rs b/gix/src/repository/diff.rs index 52c601cb198..2e22acd5833 100644 --- a/gix/src/repository/diff.rs +++ b/gix/src/repository/diff.rs @@ -53,7 +53,9 @@ impl Repository { new_tree: impl Into>>, options: impl Into>, ) -> Result, diff_tree_to_tree::Error> { - let mut cache = self.diff_resource_cache(gix_diff::blob::pipeline::Mode::ToGit, Default::default())?; + let mut cache = self + .diff_resource_cache(gix_diff::blob::pipeline::Mode::ToGit, Default::default()) + .map_err(gix_error::Error::from_error)?; let opts = options .into() .map_or_else(|| crate::diff::Options::from_configuration(&self.config), Ok)? @@ -74,7 +76,8 @@ impl Repository { Ok(std::ops::ControlFlow::Continue(())) }, opts, - )?; + ) + .map_err(gix_error::Error::from_error)?; Ok(out) } diff --git a/gix/src/repository/dirwalk.rs b/gix/src/repository/dirwalk.rs index eb8e4b6e5e7..09966dc8f06 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::{OptionExt, ResultExt}; impl Repository { /// Return default options suitable for performing a directory walk on this repository. @@ -40,7 +41,9 @@ 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_raise(|| gix_error::message("A working tree is required to perform a directory walk"))?; let mut excludes = self.excludes( index, None, @@ -55,19 +58,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::Error::from_error)?; + let fs_caps = self.filesystem_options().map_err(gix_error::Error::from_error)?; 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::Error::from_error)?; worktree_relative_worktree_dirs_storage = linked_worktrees .into_iter() .filter_map(|proxy| proxy.base().ok()) @@ -104,7 +111,8 @@ impl Repository { }, opts, delegate, - )?; + ) + .map_err(gix_error::Error::from_error)?; Ok(dirwalk::Outcome { dirwalk: outcome, diff --git a/gix/src/repository/filter.rs b/gix/src/repository/filter.rs index ffd7330ec8d..7b1a15145c4 100644 --- a/gix/src/repository/filter.rs +++ b/gix/src/repository/filter.rs @@ -1,24 +1,11 @@ +use gix_error::ResultExt; + use crate::{Id, 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( + let 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)) + .or_raise(|| gix_error::message("Could not obtain head commit of bare repository")) + .map_err(gix_error::Error::from) + .and_then(|c| c.tree_id().map(Id::detach).map_err(gix_error::Error::from_error)) }, 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) + .map_err(gix_error::Error::from_error)?; (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 index = self.index_or_empty().map_err(gix_error::Error::from_error)?; + let cache = self + .attributes_only( + &index, + gix_worktree::stack::state::attributes::Source::WorktreeThenIdMapping, + ) + .map_err(gix_error::Error::from_error)?; (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..f180d14de22 100644 --- a/gix/src/repository/graph.rs +++ b/gix/src/repository/graph.rs @@ -34,14 +34,14 @@ impl crate::Repository { pub fn commit_graph_if_enabled( &self, ) -> Result, super::commit_graph_if_enabled::Error> { - Ok(self - .config - .may_use_commit_graph()? + self.config + .may_use_commit_graph() + .map_err(gix_error::Error::from_error)? .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/index.rs b/gix/src/repository/index.rs index fd543e1e473..a8765630b30 100644 --- a/gix/src/repository/index.rs +++ b/gix/src/repository/index.rs @@ -155,11 +155,11 @@ impl crate::Repository { pub fn index_or_load_from_head( &self, ) -> Result { - Ok(match self.try_index()? { + Ok(match self.try_index().map_err(gix_error::Error::from_error)? { Some(index) => IndexPersistedOrInMemory::Persisted(index), None => { - let tree = self.head_commit()?.tree_id()?; - IndexPersistedOrInMemory::InMemory(self.index_from_tree(&tree)?) + let tree = self.head_commit()?.tree_id().map_err(gix_error::Error::from_error)?; + IndexPersistedOrInMemory::InMemory(self.index_from_tree(&tree).map_err(gix_error::Error::from_error)?) } }) } diff --git a/gix/src/repository/mailmap.rs b/gix/src/repository/mailmap.rs index 6ee80d2d6c9..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,36 +52,73 @@ 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)); } } } - 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( + 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)); } 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..dbf641cdee1 100644 --- a/gix/src/repository/merge.rs +++ b/gix/src/repository/merge.rs @@ -23,11 +23,14 @@ impl Repository { &self, worktree_roots: gix_merge::blob::pipeline::WorktreeRoots, ) -> Result { - let index = self.index_or_load_from_head_or_empty()?; + let index = self + .index_or_load_from_head_or_empty() + .map_err(gix_error::Error::from_error)?; 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) + .map_err(gix_error::Error::from_error)? .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 }, - )? + ) + .map_err(gix_error::Error::from_error)? .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()?); @@ -72,7 +76,8 @@ impl Repository { .try_into_conflict_style(value) .with_lenient_default(self.config.lenient_config) }) - .transpose()? + .transpose() + .map_err(gix_error::Error::from_error)? .unwrap_or_default(), marker_size: text::Conflict::DEFAULT_MARKER_SIZE.try_into().unwrap(), }, @@ -131,7 +136,9 @@ impl Repository { labels: gix_merge::blob::builtin_driver::text::Labels<'_>, options: crate::merge::tree::Options, ) -> Result, merge_trees::Error> { - let mut diff_cache = self.diff_resource_cache_for_tree_diff()?; + let mut diff_cache = self + .diff_resource_cache_for_tree_diff() + .map_err(gix_error::Error::from_error)?; let mut blob_merge = self.merge_resource_cache(Default::default())?; let gix_merge::tree::Outcome { tree, @@ -148,9 +155,10 @@ impl Repository { &mut diff_cache, &mut blob_merge, options.into(), - )?; + ) + .map_err(gix_error::Error::from_error)?; - let validate = self.config.protect_options()?; + let validate = self.config.protect_options().map_err(gix_error::Error::from_error)?; Ok(crate::merge::tree::Outcome { tree: crate::object::tree::Editor { inner: tree, @@ -185,7 +193,9 @@ impl Repository { labels: gix_merge::blob::builtin_driver::text::Labels<'_>, options: crate::merge::commit::Options, ) -> Result, merge_commits::Error> { - let mut diff_cache = self.diff_resource_cache_for_tree_diff()?; + let mut diff_cache = self + .diff_resource_cache_for_tree_diff() + .map_err(gix_error::Error::from_error)?; let mut blob_merge = self.merge_resource_cache(Default::default())?; let commit_graph = self.commit_graph_if_enabled()?; let mut graph = self.revision_graph(commit_graph.as_ref()); @@ -209,9 +219,10 @@ impl Repository { self, &mut |id| id.to_owned().attach(self).shorten_or_id().to_string(), options.into(), - )?; + ) + .map_err(gix_error::Error::from_error)?; - let validate = self.config.protect_options()?; + let validate = self.config.protect_options().map_err(gix_error::Error::from_error)?; let tree_merge = crate::merge::tree::Outcome { tree: crate::object::tree::Editor { inner: tree, @@ -245,7 +256,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 +270,12 @@ 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() + .map_err(gix_error::Error::from_error)?; let commit_id = first.attach(self); return Ok(crate::merge::virtual_merge_base::Outcome { virtual_merge_bases: Vec::new(), @@ -270,7 +284,9 @@ impl Repository { }); }; - let mut diff_cache = self.diff_resource_cache_for_tree_diff()?; + let mut diff_cache = self + .diff_resource_cache_for_tree_diff() + .map_err(gix_error::Error::from_error)?; let mut blob_merge = self.merge_resource_cache(Default::default())?; let gix_merge::commit::virtual_merge_base::Outcome { @@ -287,7 +303,8 @@ impl Repository { self, &mut |id| id.to_owned().attach(self).shorten_or_id().to_string(), options.into(), - )?; + ) + .map_err(gix_error::Error::from_error)?; 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 83e9afef265..9fff0c8f769 100644 --- a/gix/src/repository/mod.rs +++ b/gix/src/repository/mod.rs @@ -63,29 +63,13 @@ 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; } /// @@ -105,212 +89,99 @@ 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(#[from] gix_blame::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_with_rewrites::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()). + // TODO(review): kept concrete because `status::tree_index::Error` already embeds the erased + // `diff::new_rewrites::Error` (via `RewritesConfiguration`); erasing this one would give + // it a second `From` via `DiffResourceCache`. `status::tree_index::Error` in + // turn has to stay concrete because `status::iter::Error` already embeds the erased + // `status::index_worktree::Error`, and erasing `tree_index::Error` would give it a second + // `From` too - and `status::iter::Error` must stay concrete since callers match + // its variants. `index_or_load_from_head_or_empty` also has to stay concrete, because its + // erasure would give this enum's own `ResourceCache` variant (already erased) a colliding + // sibling in `Index`. #[derive(Debug, thiserror::Error)] #[expect(missing_docs)] pub enum Error { @@ -327,69 +198,40 @@ pub mod diff_resource_cache { #[cfg(feature = "tree-editor")] 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). + // TODO(review): kept concrete due to two separate E0119 collisions on external parents that embed + // this type via `#[from]`: `repository::index_or_load_from_head_or_empty::Error` + // (`gix/src/repository/mod.rs`, via `TraverseTree`) already embeds the erased + // `object::peel::to_kind::Error` via `PeelToTree`; `status::tree_index::Error` + // (`gix/src/status/tree_index.rs`, via `IndexFromMTree`) already embeds the erased + // `diff::new_rewrites::Error` via `RewritesConfiguration`. Erasing `index_from_tree::Error` + // would give either enum a second `From` impl. #[derive(Debug, thiserror::Error)] #[expect(missing_docs)] pub enum Error { @@ -406,54 +248,27 @@ pub mod index_from_tree { /// 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 { + // TODO(review): kept concrete. Callers match its variants directly: + // `gix/tests/repository.rs:74` matches `Error::OutsideOfRepository { .. }`, and + // `:99` matches `Error::AbsolutePathOutsideOfRepository { .. }`. No `#[from]` + // parents embed this type. /// The error returned by [Repository::normalize_path()](crate::Repository::normalize_path()). #[derive(Debug, thiserror::Error)] #[expect(missing_docs)] @@ -478,38 +293,24 @@ pub mod normalize_path { #[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_pathspec::defaults::from_environment::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()). + // TODO(review): kept concrete because `diff_resource_cache::Error` already embeds the erased + // `diff::resource_cache::Error` (via `ResourceCache`); erasing this one would give it a + // second `From` via `Index`. `diff_resource_cache::Error` in turn has to stay + // concrete for the reasons noted on its own `TODO(review)`. #[derive(thiserror::Error, Debug)] #[expect(missing_docs)] pub enum Error { @@ -532,25 +333,7 @@ pub mod index_or_load_from_head_or_empty { #[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 131ca3810bc..164dbf398ae 100644 --- a/gix/src/repository/object.rs +++ b/gix/src/repository/object.rs @@ -25,7 +25,7 @@ impl crate::Repository { id: impl Into, ) -> Result, crate::repository::edit_tree::Error> { let tree = self.find_tree(id)?; - Ok(tree.edit()?) + tree.edit() } } @@ -85,7 +85,10 @@ impl crate::Repository { &self, id: impl Into, ) -> Result, object::find::existing::with_conversion::Error> { - Ok(self.find_object(id)?.try_into_commit()?) + self.find_object(id) + .map_err(gix_error::Error::from_error)? + .try_into_commit() + .map_err(gix_error::Error::from_error) } /// Find a tree with `id` or fail if there was no object or the object wasn't a tree. @@ -105,12 +108,18 @@ impl crate::Repository { &self, id: impl Into, ) -> Result, object::find::existing::with_conversion::Error> { - Ok(self.find_object(id)?.try_into_tree()?) + self.find_object(id) + .map_err(gix_error::Error::from_error)? + .try_into_tree() + .map_err(gix_error::Error::from_error) } /// 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()?) + self.find_object(id) + .map_err(gix_error::Error::from_error)? + .try_into_tag() + .map_err(gix_error::Error::from_error) } /// Find a blob with `id` or fail if there was no object or the object wasn't a blob. @@ -118,7 +127,10 @@ impl crate::Repository { &self, id: impl Into, ) -> Result, object::find::existing::with_conversion::Error> { - Ok(self.find_object(id)?.try_into_blob()?) + self.find_object(id) + .map_err(gix_error::Error::from_error)? + .try_into_blob() + .map_err(gix_error::Error::from_error) } /// Obtain information about an object without fully decoding it, or fail if the object doesn't exist. @@ -204,7 +216,9 @@ impl crate::Repository { size: 0, })); } - self.objects.try_header(&id).map_err(Into::into) + self.objects + .try_header(&id) + .map_err(|err| gix_error::Error::from_error(std::io::Error::other(err))) } /// Try to find the object with `id` or return `None` if it wasn't found. @@ -232,7 +246,11 @@ 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) + .map_err(|err| gix_error::Error::from_error(std::io::Error::other(err)))? + { Some(obj) => { let kind = obj.kind; Ok(Some(Object::from_data(id, kind, buf, self))) @@ -250,16 +268,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()).map_err(gix_error::Error::from_error)?; 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).map_err(gix_error::Error::from_error)?; if self.objects.exists(&oid) { return Ok(oid.attach(self)); } @@ -267,7 +282,7 @@ impl crate::Repository { self.objects .write_buf_with_known_id(kind, buf, oid) .map(|oid| oid.attach(self)) - .map_err(Into::into) + .map_err(|err| gix_error::Error::from_error(std::io::Error::other(err))) } /// Write a blob from the given `bytes`. @@ -290,13 +305,13 @@ 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)?; + .map_err(gix_error::Error::from_error)?; if self.objects.exists(&oid) { return Ok(oid.attach(self)); } self.objects .write_buf_with_known_id(gix_object::Kind::Blob, bytes, oid) - .map_err(Into::into) + .map_err(|err| gix_error::Error::from_error(std::io::Error::other(err))) .map(|oid| oid.attach(self)) } @@ -308,22 +323,21 @@ 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()).map_err(gix_error::Error::from_error)?; 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)?; + .map_err(gix_error::Error::from_error)?; if self.objects.exists(&oid) { return Ok(oid.attach(self)); } self.objects .write_buf_with_known_id(gix_object::Kind::Blob, buf, oid) - .map_err(Into::into) + .map_err(|err| gix_error::Error::from_error(std::io::Error::other(err))) .map(|oid| oid.attach(self)) } } @@ -348,12 +362,16 @@ 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() + .map_err(gix_error::Error::from_error)?, message: message.as_ref().into(), pgp_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) + .map_err(gix_error::Error::from_error) } /// Similar to [`commit(…)`](crate::Repository::commit()), but allows to create the commit with `committer` and `author` specified. @@ -370,12 +388,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().map_err(gix_error::Error::from_error)?, message.as_ref(), tree.into(), parents.into_iter().map(Into::into).collect(), @@ -437,7 +455,8 @@ impl crate::Repository { deref: true, }), Some(committer), - )?; + ) + .map_err(gix_error::Error::from_error)?; Ok(commit_id) } @@ -468,10 +487,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")))? + .map_err(gix_error::Error::from_error)?; + let committer = self + .committer() + .ok_or_else(|| gix_error::Error::from_error(gix_error::message("Committer identity is not configured")))? + .map_err(gix_error::Error::from_error)?; self.commit_as(committer, author, reference, message, tree, parents) } @@ -487,9 +512,15 @@ 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::message("Author identity is not configured")))? + .map_err(gix_error::Error::from_error)?; + let committer = self + .committer() + .ok_or_else(|| gix_error::Error::from_error(gix_error::message("Committer identity is not configured")))? + .map_err(gix_error::Error::from_error)?; + self.new_commit_as(committer, author, message, tree, parents) } /// Create a nwe commit object with `message` referring to `tree` with `parents`, using the specified @@ -515,7 +546,7 @@ impl crate::Repository { extra_headers: Default::default(), }; let id = self.write_object(commit)?; - Ok(id.object()?.into_commit()) + Ok(id.object().map_err(gix_error::Error::from_error)?.into_commit()) } /// Return an empty tree object, suitable for [getting changes](Tree::changes()). diff --git a/gix/src/repository/pathspec.rs b/gix/src/repository/pathspec.rs index 60801b4cf25..544a3b29b8c 100644 --- a/gix/src/repository/pathspec.rs +++ b/gix/src/repository/pathspec.rs @@ -1,3 +1,4 @@ +use gix_error::ResultExt; use gix_pathspec::MagicSignature; use crate::{AttributeStack, Pathspec, Repository, bstr::BStr, config::cache::util::ApplyLeniencyDefault}; @@ -43,12 +44,15 @@ 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::Error::from_error)?; if inherit_ignore_case && self .config .fs_capabilities() - .with_lenient_default(self.config.lenient_config)? + .with_lenient_default(self.config.lenient_config) + .or_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 9ee74fab2e7..784bf5bd1ea 100644 --- a/gix/src/repository/reference.rs +++ b/gix/src/repository/reference.rs @@ -1,3 +1,4 @@ +use gix_error::ErrorExt; use gix_hash::ObjectId; use gix_ref::{ FullName, PartialNameRef, Target, @@ -209,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().map_err(gix_error::Error::from_error)?.into_peeled_id() } /// Return the name to the symbolic reference `HEAD` points to, or `None` if the head is detached. @@ -248,7 +249,10 @@ impl crate::Repository { /// # Ok(()) } /// ``` pub fn head_commit(&self) -> Result, reference::head_commit::Error> { - Ok(self.head()?.peel_to_commit()?) + self.head() + .map_err(gix_error::Error::from_error)? + .peel_to_commit() + .map_err(gix_error::Error::from_error) } /// Return the tree id the `HEAD` reference currently points to after peeling it fully, @@ -258,23 +262,35 @@ 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()?) + self.head_commit()?.tree_id().map_err(gix_error::Error::from_error) } /// 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) + // We can't recover the unborn-HEAD case from `self.head_tree_id()` once it has failed: by then, + // `head_commit()` has already erased `Head::peel_to_commit()`'s concrete error into `gix_error::Error`, + // and under the `auto-chain-error` feature without `tree-error`, that erasure flattens the error into + // a `ChainedError` whose original concrete type is no longer reachable by *any* downcast - the frame + // tree that `downcast_any_ref()` walks only exists prior to that conversion. + // + // So instead we call `peel_to_commit()` ourselves and inspect its still-concrete error before it gets + // erased, exactly like `Repository::commit_graph_if_enabled()` does in `graph.rs`: raise it into an + // `Exn` and use `downcast_any_ref()` (`gix-error/src/exn/impls.rs`, not cfg-gated) to look for the + // nested `to_object::Error::Unborn`. This works regardless of the `tree-error`/`auto-chain-error` + // feature combination. + let mut head = self.head().map_err(gix_error::Error::from_error)?; + match head.peel_to_commit() { + Ok(commit) => Ok(commit.tree_id().map_err(gix_error::Error::from_error)?), + Err(err) => { + let err = err.raise(); + match err.downcast_any_ref::() { + Some(crate::head::peel::to_commit::Error::PeelToObject( + crate::head::peel::to_object::Error::Unborn { .. }, + )) => Ok(self.empty_tree().id()), + _ => Err(err.into_error()), + } } - }) + } } /// Return the tree object the `HEAD^{tree}` reference currently points to after peeling it fully, @@ -296,7 +312,7 @@ impl crate::Repository { /// # Ok(()) } /// ``` pub fn head_tree(&self) -> Result, reference::head_tree::Error> { - Ok(self.head_commit()?.tree()?) + self.head_commit()?.tree().map_err(gix_error::Error::from_error) } /// Find the reference with the given partial or full `name`, like `main`, `HEAD`, `heads/branch` or `origin/other`, @@ -327,7 +343,7 @@ impl crate::Repository { let partial_name = name .clone() .try_into() - .map_err(|err| reference::find::Error::Find(gix_ref::file::find::Error::from(err)))?; + .map_err(|err| gix_error::Error::from_error(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(), @@ -375,7 +391,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 451a8db9261..ea0a619cdf1 100644 --- a/gix/src/repository/remote.rs +++ b/gix/src/repository/remote.rs @@ -1,4 +1,6 @@ #![allow(clippy::result_large_err)] +use gix_error::ResultExt; + use crate::{Remote, bstr::BStr, config, remote, remote::find}; impl crate::Repository { @@ -188,13 +190,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) + .or_raise(|| gix_error::message!("{kind} ref-spec under `remote.{name_or_url}` was invalid")) }) .collect::, _>>() + .map_err(Into::into) .map(|mut specs| { specs.sort(); specs.dedup(); @@ -232,13 +232,12 @@ 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).or_raise(|| { + gix_error::message!("The {kind} url under `remote.{name_or_url}` was invalid") }) }) - .collect() + .collect::, _>>() + .map_err(Into::into) }) }; let urls = config_urls(&config::tree::Remote::URL, "fetch"); @@ -268,9 +267,12 @@ 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).or_raise(|| { + gix_error::message( + "The value for 'remote..tagOpt` is invalid and must either be '--tags' or '--no-tags'", + ) + }) + .map_err(Into::into) }); let fetch_tags = match fetch_tags { Some(Ok(v)) => v, @@ -310,11 +312,13 @@ 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(source) + .or_raise(|| { + gix_error::message!("The fetch url under `remote.{name_or_url}` was invalid") + }) + .map_err(Into::into), + ); } Err(_) => {} } @@ -331,7 +335,7 @@ impl crate::Repository { fetch_tags, self, ) - .map_err(Into::into), + .map_err(gix_error::Error::from_error), ) } } diff --git a/gix/src/repository/revision.rs b/gix/src/repository/revision.rs index 905a40d3189..1f6567518c3 100644 --- a/gix/src/repository/revision.rs +++ b/gix/src/repository/revision.rs @@ -44,9 +44,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 +68,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) + .map_err(gix_error::Error::from_error)? + .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 +93,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) + .map_err(gix_error::Error::from_error)? + .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 +139,8 @@ 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)?) + self.merge_bases_many_with_graph(one, others, &mut graph) + .map_err(gix_error::Error::from_error) } /// Return the best merge-base among all `commits`, or fail if `commits` yields no commit or no merge-base was found. @@ -143,14 +152,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) + .map_err(gix_error::Error::from_error)? + .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 +176,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 a70cc4df1a2..ac23c252257 100644 --- a/gix/src/repository/submodule.rs +++ b/gix/src/repository/submodule.rs @@ -1,5 +1,7 @@ use std::rc::Rc; +use gix_error::{ErrorExt, ResultExt}; + use crate::{Repository, submodule}; impl Repository { @@ -21,17 +23,19 @@ 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(err + .and_raise(gix_error::message("Could not read '.gitmodules' file")) + .into()); + } }; if metadata.file_type().is_symlink() { return Ok(None); } - let buf = std::fs::read(&path)?; - Ok(Some(gix_submodule::File::from_bytes( - &buf, - path, - &self.config.resolved, - )?)) + 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::Error::from_error)?, + )) } /// Return a shared [`.gitmodules` file](submodule::File) which is updated automatically if the in-memory snapshot @@ -55,20 +59,31 @@ impl Repository { )? { Some(m) => Ok(Some(m)), None => { - let id = match self.try_index()?.and_then(|index| { - index - .entry_by_path(submodule::MODULES_FILE.into()) - .map(|entry| entry.id) - }) { + let id = match self + .try_index() + .map_err(gix_error::Error::from_error)? + .and_then(|index| { + index + .entry_by_path(submodule::MODULES_FILE.into()) + .map(|entry| entry.id) + }) { Some(id) => id, None => match self - .head()? + .head() + .map_err(gix_error::Error::from_error)? .try_peel_to_id()? .map(|id| -> Result, submodule::modules::Error> { Ok(id - .object()? - .peel_to_commit()? - .tree()? + .object() + .or_raise(|| { + gix_error::message( + "Could not find the .gitmodules file by id in the object database", + ) + })? + .peel_to_commit() + .map_err(gix_error::Error::from_error)? + .tree() + .map_err(gix_error::Error::from_error)? .find_entry(submodule::MODULES_FILE) .map(|entry| entry.inner.oid.to_owned())) }) @@ -80,9 +95,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(submodule::open_modules_file::Error::from)? - .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::Error::from_error)? + .into(), ))) } } diff --git a/gix/src/repository/worktree.rs b/gix/src/repository/worktree.rs index e37457a938d..d22adca8faf 100644 --- a/gix/src/repository/worktree.rs +++ b/gix/src/repository/worktree.rs @@ -80,20 +80,21 @@ 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)?; - if !header.kind().is_tree() { - return Err(crate::repository::worktree_stream::Error::NotATree { - id, - actual: header.kind(), - }); + let header = self.objects.header(id).map_err(gix_error::Error::from_error)?; + let actual = header.kind(); + if !actual.is_tree() { + return Err(gix_error::Error::from_error(gix_error::message!( + "Needed {id} to be a tree to turn into a workspace stream, got {actual}" + ))); } // TODO(perf): potential performance improvements could be to use the index at `HEAD` if possible (`index_from_head_tree…()`) // TODO(perf): when loading a non-HEAD tree, we effectively traverse the tree twice. This is usually fast though, and sharing // 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 index = self.index_from_tree(&id).map_err(gix_error::Error::from_error)?; let mut cache = self - .attributes_only(&index, gix_worktree::stack::state::attributes::Source::IdMapping)? + .attributes_only(&index, gix_worktree::stack::state::attributes::Source::IdMapping) + .map_err(gix_error::Error::from_error)? .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"); 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 f39b7b70959..711ec442b5d 100644 --- a/gix/src/revision/walk.rs +++ b/gix/src/revision/walk.rs @@ -5,16 +5,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(#[from] gix_traverse::commit::simple::Error), - #[error(transparent)] - ShallowCommits(#[from] crate::shallow::read::Error), - #[error(transparent)] - ConfigBoolean(#[from] crate::config::boolean::Error), -} +pub type Error = gix_error::Error; /// Specify how to sort commits during a [revision::Walk] traversal. /// @@ -291,7 +282,7 @@ impl<'repo> Platform<'repo> { gix_traverse::commit::Simple::filtered(tips, &repo.objects, { // Note that specific shallow handling for commit-graphs isn't needed as these contain // all information there is, and exclude shallow parents to be structurally consistent. - let shallow_commits = repo.shallow_commits()?; + let shallow_commits = repo.shallow_commits().map_err(gix_error::Error::from_error)?; let mut grafted_parents_to_skip = Vec::new(); let mut buf = Vec::new(); move |id| { @@ -320,16 +311,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::Error::from_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) + .map_err(gix_error::Error::from_error)? .then(|| self.repo.commit_graph().ok()) .flatten()), ) - .hide(hidden)? - .map(|res| res.map_err(iter::Error::from)), + .hide(hidden) + .map_err(gix_error::Error::from_error)? + .map(|res| res.map_err(gix_error::Error::from_error)), ), }) } @@ -347,12 +341,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(#[from] gix_traverse::commit::simple::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 2b72ddd2bd6..47aed01d8bd 100644 --- a/gix/src/status/index_worktree.rs +++ b/gix/src/status/index_worktree.rs @@ -9,26 +9,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(#[from] gix_status::index_as_worktree_with_renames::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)] @@ -108,7 +89,11 @@ impl Repository { E: std::error::Error + Send + Sync + 'static, { let _span = gix_trace::coarse!("gix::index_worktree_status"); - let workdir = self.workdir().ok_or(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 attrs_and_excludes = self.attributes( index, crate::worktree::stack::state::attributes::Source::WorktreeThenIdMapping, @@ -119,11 +104,13 @@ impl Repository { 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::Error::from_error)?; + let fs_caps = self.filesystem_options().map_err(gix_error::Error::from_error)?; 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) + .map_err(gix_error::Error::from_error)? // 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,7 +155,8 @@ impl Repository { dirwalk: options.dirwalk_options.map(Into::into), rewrites: options.rewrites, }, - )?; + ) + .map_err(gix_error::Error::from_error)?; Ok(out) } @@ -179,7 +167,7 @@ impl Repository { options: Option<&crate::dirwalk::Options>, ) -> Result, E> where - E: From + From, + E: From, { let empty_patterns_match_prefix = options.is_some_and(|opts| opts.empty_patterns_match_prefix); let attrs_and_excludes = self.attributes( @@ -248,15 +236,7 @@ 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), - } + pub type Error = gix_error::Error; impl gix_status::index_as_worktree::traits::SubmoduleStatus for BuiltinSubmoduleStatus { type Output = crate::submodule::Status; @@ -291,18 +271,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) + .map_err(gix_error::Error::from_error)?; 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().map_err(gix_error::Error::from_error)?.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).map_err(gix_error::Error::from_error)?; Ok(status.is_dirty().and_then(|dirty| dirty.then_some(status))) } } diff --git a/gix/src/status/iter/mod.rs b/gix/src/status/iter/mod.rs index 15e51e401c5..f3e2554fbd5 100644 --- a/gix/src/status/iter/mod.rs +++ b/gix/src/status/iter/mod.rs @@ -1,5 +1,8 @@ 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::{ @@ -42,13 +45,20 @@ where patterns: impl IntoIterator, ) -> Result { let index = match self.index { - None => IndexPersistedOrInMemory::Persisted(self.repo.index_or_empty()?), + None => { + IndexPersistedOrInMemory::Persisted(self.repo.index_or_empty().map_err(gix_error::Error::from_error)?) + } Some(index) => index, }; 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,7 +66,8 @@ 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) + .map_err(gix_error::Error::from_error)? .unwrap_or_default(); let should_interrupt = self.should_interrupt.clone().unwrap_or_default(); let submodule = BuiltinSubmoduleStatus::new(self.repo.clone().into_sync(), self.submodules)?; @@ -105,7 +116,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 +152,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)), @@ -164,21 +179,24 @@ where self.index_worktree_options.dirwalk_options.as_ref(), )?; let mut items = Vec::new(); - let tree_index = self.repo.tree_index_status( - &tree_id, - &index, - Some(&mut pathspec), - self.tree_index_renames, - |change, _, _| { - items.push(change.into_owned().into()); - let action = if should_interrupt.load(Ordering::Acquire) { - std::ops::ControlFlow::Break(()) - } else { - std::ops::ControlFlow::Continue(()) - }; - Ok::<_, std::convert::Infallible>(action) - }, - )?; + let tree_index = self + .repo + .tree_index_status( + &tree_id, + &index, + Some(&mut pathspec), + self.tree_index_renames, + |change, _, _| { + items.push(change.into_owned().into()); + let action = if should_interrupt.load(Ordering::Acquire) { + std::ops::ControlFlow::Break(()) + } else { + std::ops::ControlFlow::Continue(()) + }; + Ok::<_, std::convert::Infallible>(action) + }, + ) + .map_err(gix_error::Error::from_error)?; (items, Some(tree_index)) } None => (Vec::new(), None), @@ -219,6 +237,12 @@ where } } +// TODO(review): kept concrete. Matched in `status::index_worktree::Iter::next()` +// (`gix/src/status/index_worktree.rs`): +// `Error::IndexWorktree(err) => err` and `Error::TreeIndex(_) => { .. }`. Separately, +// `submodule::status::Error::NextStatusItem` (`gix/src/submodule/mod.rs`) already +// has an erased slot via `StatusIter`, so this type is doubly blocked from erasure +// there. /// The error returned for each item returned by [`Iter`]. #[derive(Debug, thiserror::Error)] #[expect(missing_docs)] diff --git a/gix/src/status/mod.rs b/gix/src/status/mod.rs index 99d40d945ff..05353b0c8d7 100644 --- a/gix/src/status/mod.rs +++ b/gix/src/status/mod.rs @@ -63,6 +63,14 @@ impl Default for Submodule { } } +// TODO(review): kept concrete due to an E0119 collision. `submodule::status::Error` +// (`gix/src/submodule/mod.rs`) embeds both this type, via +// `StatusPlatform(#[from] crate::status::Error)`, and the already-erased +// `status::into_iter::Error`, via `StatusIter(#[from] ...)`. Erasing `status::Error` +// would give `submodule::status::Error` two `From` impls. (The +// other former blocker, `status::is_dirty::Error::StatusPlatform`, no longer applies: +// `is_dirty::Error` is itself now `gix_error::Error`, so it can no longer embed +// anything via `#[from]`.) /// The error returned by [status()](Repository::status). #[derive(Debug, thiserror::Error)] #[expect(missing_docs)] @@ -138,20 +146,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. @@ -173,20 +168,22 @@ pub mod is_dirty { // Run this first as there is a high likelihood to find something, and it's very fast. self.tree_index_status( &head_tree_id, - &*self.index_or_empty()?, + &*self.index_or_empty().map_err(gix_error::Error::from_error)?, None, crate::status::tree_index::TrackRenames::Disabled, |_, _, _| { index_is_dirty = true; Ok::<_, Infallible>(std::ops::ControlFlow::Break(())) }, - )?; + ) + .map_err(gix_error::Error::from_error)?; if index_is_dirty { return Ok(true); } } let is_dirty = self - .status(gix_features::progress::Discard)? + .status(gix_features::progress::Discard) + .map_err(gix_error::Error::from_error)? .index_worktree_rewrites(None) .index_worktree_submodules(crate::status::Submodule::AsConfigured { check_dirty: true }) .index_worktree_options_mut(|opts| { @@ -204,32 +201,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 cb89faf78a0..ed380b346a2 100644 --- a/gix/src/status/tree_index.rs +++ b/gix/src/status/tree_index.rs @@ -1,5 +1,11 @@ use crate::{Repository, config::tree}; +// TODO(review): kept concrete due to an E0119 collision. `status::iter::Error` +// (`gix/src/status/iter/mod.rs`) embeds both this type, via +// `TreeIndex(#[from] tree_index::Error)`, and the already-erased +// `status::index_worktree::Error`, via `IndexWorktree(#[from] ...)`. Erasing +// `tree_index::Error` would give `status::iter::Error` two `From` +// impls. /// The error returned by [Repository::tree_index_status()]. #[derive(Debug, thiserror::Error)] #[expect(missing_docs)] diff --git a/gix/src/submodule/errors.rs b/gix/src/submodule/errors.rs index 7ffb0eb0a3d..0b8307c6a85 100644 --- a/gix/src/submodule/errors.rs +++ b/gix/src/submodule/errors.rs @@ -1,73 +1,34 @@ /// 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_submodule::init::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(#[from] gix_submodule::is_active_platform::Error), - #[error(transparent)] - QueryIsActive(#[from] gix_config::value::Error), - #[error(transparent)] - InitAttributes(#[from] crate::config::attribute_stack::Error), - #[error(transparent)] - InitPathspecDefaults(#[from] gix_pathspec::defaults::from_environment::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 { + // TODO(review): kept concrete. Matched at `gix/tests/gix/submodule.rs:343-344` and `:800-801`: + // `Error::GitDir(git_dir_try_old_form::Error::InvalidGitDirFileTarget { .. })` / + // `::GitDir(..)`. Separately, `submodule::status::Error::OpenRepository` + // (`gix/src/submodule/mod.rs`) already has an erased slot via `StatusIter`, so + // this type is doubly blocked from erasure there. /// The error returned by [Submodule::open()](crate::Submodule::open()). #[derive(Debug, thiserror::Error)] #[expect(missing_docs)] @@ -85,6 +46,11 @@ pub mod open { /// pub mod git_dir_try_old_form { + // TODO(review): kept concrete. Callers destructure its `InvalidGitDirFileTarget { gitdir_file, + // target, source }` variant at `gix/tests/gix/submodule.rs:325`, `:334`, `:344`, + // `:358`, `:398` and `:412`; `:793` matches its `GitDir(..)` variant. Its two + // `#[from]` parents — `open::Error::GitDir` (above) and `state::Error:: + // GitDirTryOldForm` (below) — have no other erased member. /// The error returned by [Submodule::git_dir_try_old_form()](crate::Submodule::git_dir_try_old_form()). #[derive(Debug, thiserror::Error)] #[expect(missing_docs)] @@ -105,6 +71,10 @@ pub mod git_dir_try_old_form { /// pub mod state { + // TODO(review): kept concrete. Matched at `gix/tests/gix/submodule.rs:333`, `:411` and `:808`: + // `Error::GitDirTryOldForm(..)`. Separately, `submodule::status::Error::State` + // (`gix/src/submodule/mod.rs`) already has an erased slot via `StatusIter`, so + // this type is doubly blocked from erasure there. /// The error returned by [Submodule::state()](crate::Submodule::state()). #[derive(Debug, thiserror::Error)] #[expect(missing_docs)] @@ -121,11 +91,16 @@ pub mod state { /// pub mod index_id { /// The error returned by [Submodule::index_id()](crate::Submodule::index_id()). + // TODO(review): kept concrete because `submodule::status::Error` already embeds the erased + // `status::into_iter::Error` (via `StatusIter`); erasing this one would give it + // a second `From` via `IndexId`. #[derive(Debug, thiserror::Error)] #[expect(missing_docs)] pub enum Error { #[error(transparent)] PathConfiguration(#[from] gix_submodule::config::path::Error), + // TODO(review): embeds an erased `gix_error::Error`; erasing a sibling variant would collide + // with it via a duplicate `From` impl (E0119). #[error(transparent)] Index(#[from] crate::repository::index_or_load_from_head::Error), } @@ -134,6 +109,9 @@ pub mod index_id { /// pub mod head_id { /// The error returned by [Submodule::head_id()](crate::Submodule::head_id()). + // TODO(review): kept concrete because `submodule::status::Error` already embeds the erased + // `status::into_iter::Error` (via `StatusIter`); erasing this one would give it + // a second `From` via `HeadId`. #[derive(Debug, thiserror::Error)] #[expect(missing_docs)] pub enum Error { diff --git a/gix/src/submodule/mod.rs b/gix/src/submodule/mod.rs index 13a481d4583..3c60560112c 100644 --- a/gix/src/submodule/mod.rs +++ b/gix/src/submodule/mod.rs @@ -62,7 +62,14 @@ impl<'repo> SharedState<'repo> { if state.is_none() { let platform = self .modules - .is_active_platform(&self.repo.config.resolved, self.repo.config.pathspec_defaults()?)?; + .is_active_platform( + &self.repo.config.resolved, + self.repo + .config + .pathspec_defaults() + .map_err(gix_error::Error::from_error)?, + ) + .map_err(gix_error::Error::from_error)?; let index = self.index()?; let attributes = self .repo @@ -70,7 +77,8 @@ impl<'repo> SharedState<'repo> { &index, gix_worktree::stack::state::attributes::Source::WorktreeThenIdMapping .adjust_for_bare(self.repo.is_bare()), - )? + ) + .map_err(gix_error::Error::from_error)? .detach(); *state = Some(IsActiveState { platform, attributes }); } @@ -132,11 +140,19 @@ 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())? { - Some(val) => Some(val), - None => crate::config::tree::Fetch::RECURSE_SUBMODULES - .try_into_recurse_submodules(self.state.repo.config.resolved.boolean("fetch.recurseSubmodules"))?, - }) + Ok( + match self + .state + .modules + .fetch_recurse(self.name()) + .map_err(gix_error::Error::from_error)? + { + 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_error)?, + }, + ) } /// Return the `ignore` field from this submodule's configuration, if present, or `None`. @@ -158,16 +174,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(gix_error::Error::from_error)?; Ok(is_active) } @@ -387,6 +405,9 @@ pub mod status { use super::{Status, head_id, index_id, open, state}; use crate::Submodule; + // TODO(review): kept concrete. Matched at `gix/tests/gix/submodule.rs:356-358` and `:410-412`: + // `Error::State(state::Error::GitDirTryOldForm(git_dir_try_old_form::Error:: + // InvalidGitDirFileTarget { .. }))`. /// The error returned by [Submodule::status()]. #[derive(Debug, thiserror::Error)] #[expect(missing_docs)] 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 0d1e66b3136..2c0dbf1a71f 100644 --- a/gix/src/worktree/mod.rs +++ b/gix/src/worktree/mod.rs @@ -119,6 +119,10 @@ pub mod proxy; #[cfg(feature = "index")] pub mod open_index { /// The error returned by [`Worktree::open_index()`][crate::Worktree::open_index()]. + // TODO(review): kept concrete due to an E0119 collision. `repository::index_or_load_from_head_or_empty::Error` + // (`gix/src/repository/mod.rs`, via `OpenIndex`) already embeds the erased + // `object::peel::to_kind::Error` via `PeelToTree`. Erasing `open_index::Error` + // would give it a second `From` impl. #[derive(Debug, thiserror::Error)] #[expect(missing_docs)] pub enum Error { @@ -151,14 +155,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. @@ -171,12 +168,12 @@ 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> { - let index = self.index()?; - Ok(self.parent.excludes( + let index = self.index().map_err(gix_error::Error::from_error)?; + self.parent.excludes( &index, overrides, gix_worktree::stack::state::ignore::Source::WorktreeThenIdMappingIfNotSkipped, - )?) + ) } } } @@ -187,14 +184,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. @@ -204,24 +194,24 @@ 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> { - let index = self.index()?; - Ok(self.parent.attributes( + let index = self.index().map_err(gix_error::Error::from_error)?; + 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()?; + let index = self.index().map_err(gix_error::Error::from_error)?; self.parent .attributes_only( &index, gix_worktree::stack::state::attributes::Source::WorktreeThenIdMapping, ) - .map_err(|err| Error::CreateCache(err.into())) + .map_err(gix_error::Error::from_error) } } } @@ -229,6 +219,8 @@ pub mod attributes { /// #[cfg(feature = "attributes")] pub mod pathspec { + use gix_error::ResultExt; + use crate::{ Worktree, bstr::BStr, @@ -236,14 +228,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 @@ -258,7 +243,7 @@ pub mod pathspec { &self, patterns: impl IntoIterator>, ) -> Result, Error> { - let index = self.index()?; + let index = self.index().map_err(gix_error::Error::from_error)?; let inherit_ignore_case = gitoxide::Pathspec::INHERIT_IGNORE_CASE .enrich_error( self.parent @@ -270,15 +255,17 @@ 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())))? + .or_raise(|| { + gix_error::message("Filesystem configuration could not be obtained to learn about case sensitivity") + })? .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 a2577785128..87702aeb41c 100644 --- a/gix/src/worktree/proxy.rs +++ b/gix/src/worktree/proxy.rs @@ -9,19 +9,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> { @@ -100,11 +89,15 @@ 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().map_err(gix_error::Error::from_error)?; 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 repo = ThreadSafeRepository::open_from_paths(self.git_dir, base.into(), self.parent.options.clone())?; + let repo = ThreadSafeRepository::open_from_paths(self.git_dir, base.into(), self.parent.options.clone()) + .map_err(gix_error::Error::from_error)?; Ok(repo.into()) } } diff --git a/gix/tests/fixtures/generated-archives/make_config_repos.tar b/gix/tests/fixtures/generated-archives/make_config_repos.tar index c4edb92b0d8..ed1966c6794 100644 Binary files a/gix/tests/fixtures/generated-archives/make_config_repos.tar and b/gix/tests/fixtures/generated-archives/make_config_repos.tar differ diff --git a/gix/tests/fixtures/generated-archives/make_config_repos_sha256.tar b/gix/tests/fixtures/generated-archives/make_config_repos_sha256.tar index d846cddc9da..9b086a219f0 100644 Binary files a/gix/tests/fixtures/generated-archives/make_config_repos_sha256.tar and b/gix/tests/fixtures/generated-archives/make_config_repos_sha256.tar differ diff --git a/gix/tests/fixtures/make_config_repos.sh b/gix/tests/fixtures/make_config_repos.sh index 47a7fb73b89..8f775452e2e 100755 --- a/gix/tests/fixtures/make_config_repos.sh +++ b/gix/tests/fixtures/make_config_repos.sh @@ -223,6 +223,11 @@ git init big-file-threshold git config core.bigFileThreshold 42 ) +git init diff-algorithm-patience +(cd diff-algorithm-patience + git config diff.algorithm patience +) + git init --bare bare-repo mkdir bare-link (cd bare-link 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/config/diff_algorithm.rs b/gix/tests/gix/repository/config/diff_algorithm.rs new file mode 100644 index 00000000000..07b31e7e8cd --- /dev/null +++ b/gix/tests/gix/repository/config/diff_algorithm.rs @@ -0,0 +1,25 @@ +use gix_diff::blob::Algorithm; + +use crate::repository::config::{repo, repo_opts}; + +#[test] +fn patience_falls_back_to_histogram_when_lenient() -> crate::Result { + let repo = repo_opts("diff-algorithm-patience", |opts| opts.strict_config(false)); + assert_eq!( + repo.diff_algorithm()?, + Algorithm::Histogram, + "lenient config quietly substitutes the unimplemented 'patience' algorithm with 'histogram'" + ); + Ok(()) +} + +#[test] +fn patience_errors_when_strict() { + let repo = repo("diff-algorithm-patience"); + let err = repo.diff_algorithm().unwrap_err(); + assert_eq!( + err.to_string(), + "The 'patience' algorithm is not yet implemented", + "strict config surfaces the same error the algorithm-key parser produces" + ); +} diff --git a/gix/tests/gix/repository/config/mod.rs b/gix/tests/gix/repository/config/mod.rs index fff507d5155..55b67db2b4e 100644 --- a/gix/tests/gix/repository/config/mod.rs +++ b/gix/tests/gix/repository/config/mod.rs @@ -1,4 +1,6 @@ mod config_snapshot; +#[cfg(feature = "blob-diff")] +mod diff_algorithm; mod identity; mod remote; diff --git a/gix/tests/gix/repository/worktree.rs b/gix/tests/gix/repository/worktree.rs index 6e6bd66a968..cfa810b3785 100644 --- a/gix/tests/gix/repository/worktree.rs +++ b/gix/tests/gix/repository/worktree.rs @@ -462,12 +462,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..fc73a18f232 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 } + └─ Message("Last encountered object 0000000000c was tree while trying to peel to tag") "#); use std::error::Error; assert_eq!(