rtb_forge/git/error.rs
1//! `RepoError` — the wrapped error model for the git-ops slice.
2//!
3//! Per v0.5 scope A8 (resolution 2026-05-12), backend errors (`gix`
4//! errors and `git` subprocess failures) are wrapped, not leaked, so
5//! the public API stays stable across backend swaps. Each operation
6//! stringifies its backend error into the `cause` field of its own
7//! variant at the call site.
8//! Tests assert on variant shape (`matches!(err, RepoError::OpenFailed
9//! { .. })`) rather than backend internals.
10//!
11//! See §3.4 of spec 0023 for the full variant table:
12//! <https://gitlab.com/phpboyscout/rust-tool-base/-/wikis/specs/0023-v0.5-scope>
13
14use std::path::PathBuf;
15
16use miette::Diagnostic;
17use thiserror::Error;
18
19/// Errors surfaced by every method on [`crate::git::Repo`].
20///
21/// Variant shape is part of the public API; backend internals are not.
22/// See the module docs for the rationale.
23#[derive(Debug, Error, Diagnostic)]
24#[non_exhaustive]
25pub enum RepoError {
26 /// Filesystem-path failure (missing parent, permission denied,
27 /// symlink loop). Path carried for diagnostics.
28 #[error("io error at `{path}`: {source}")]
29 #[diagnostic(code(rtb_forge::git::io))]
30 Io {
31 /// The path the operation tried to use.
32 path: PathBuf,
33 /// Underlying I/O failure.
34 #[source]
35 source: std::io::Error,
36 },
37
38 /// `Repo::open` could not load the repository at the given path.
39 /// Backend-specific reason in `cause` (stringified).
40 #[error("could not open repository at `{path}`: {cause}")]
41 #[diagnostic(code(rtb_forge::git::open_failed))]
42 OpenFailed {
43 /// The path that failed to open.
44 path: PathBuf,
45 /// Stringified backend error.
46 cause: String,
47 },
48
49 /// `Repo::init` could not create a repository at the given path.
50 #[error("could not init repository at `{path}`: {cause}")]
51 #[diagnostic(code(rtb_forge::git::init_failed))]
52 InitFailed {
53 /// The path where init was attempted.
54 path: PathBuf,
55 /// Stringified backend error.
56 cause: String,
57 },
58
59 /// Clone setup or transport failure (URL parse, refs negotiation,
60 /// network). Surfaces in `Repo::clone` (v0.5 commit 3).
61 #[error("could not clone `{url}`: {cause}")]
62 #[diagnostic(code(rtb_forge::git::clone_failed))]
63 CloneFailed {
64 /// The URL being cloned from.
65 url: String,
66 /// Stringified backend error.
67 cause: String,
68 },
69
70 /// Fetch transport / refs failure. Surfaces in `Repo::fetch`
71 /// (v0.5 commit 4).
72 #[error("could not fetch from `{remote}`: {cause}")]
73 #[diagnostic(code(rtb_forge::git::fetch_failed))]
74 FetchFailed {
75 /// The remote name (typically `origin`).
76 remote: String,
77 /// Stringified backend error.
78 cause: String,
79 },
80
81 /// Checkout could not switch to `revspec`. Surfaces in
82 /// `Repo::checkout` (v0.5 commit 4).
83 #[error("could not check out `{revspec}`: {cause}")]
84 #[diagnostic(code(rtb_forge::git::checkout_failed))]
85 CheckoutFailed {
86 /// The revspec the caller asked for.
87 revspec: String,
88 /// Stringified backend error.
89 cause: String,
90 },
91
92 /// Commit creation / staging failure. Surfaces in `Repo::commit`
93 /// (v0.5 commit 3).
94 #[error("could not create commit: {cause}")]
95 #[diagnostic(code(rtb_forge::git::commit_failed))]
96 CommitFailed {
97 /// Stringified backend error.
98 cause: String,
99 },
100
101 /// Status walk failure. Surfaces in `Repo::status` when gix
102 /// cannot iterate the worktree (e.g. permission errors).
103 #[error("could not compute status: {cause}")]
104 #[diagnostic(code(rtb_forge::git::status_failed))]
105 StatusFailed {
106 /// Stringified backend error.
107 cause: String,
108 },
109
110 /// Commit-graph walk failure. Surfaces in `Repo::walk` for
111 /// failures that aren't [`RepoError::RevspecNotFound`] (gix
112 /// internal error, object-store I/O, unsupported revspec kind).
113 #[error("could not walk commits: {cause}")]
114 #[diagnostic(code(rtb_forge::git::walk_failed))]
115 WalkFailed {
116 /// Stringified backend error.
117 cause: String,
118 },
119
120 /// Tree-diff failure. Surfaces in `Repo::diff` for failures
121 /// that aren't [`RepoError::RevspecNotFound`] (gix internal
122 /// error reading trees, blob diffing).
123 #[error("could not diff trees: {cause}")]
124 #[diagnostic(code(rtb_forge::git::diff_failed))]
125 DiffFailed {
126 /// Stringified backend error.
127 cause: String,
128 },
129
130 /// Push transport / refs failure. Surfaces in `Repo::push` when
131 /// `git push` cannot be run or exits non-zero.
132 #[error("could not push `{refspec}` to `{remote}`: {cause}")]
133 #[diagnostic(code(rtb_forge::git::push_failed))]
134 PushFailed {
135 /// The remote name.
136 remote: String,
137 /// The refspec being pushed.
138 refspec: String,
139 /// Stringified backend error.
140 cause: String,
141 },
142
143 /// Caller-facing error for revspecs (e.g. `HEAD~999`, a missing
144 /// tag) that don't resolve to a known object.
145 #[error("revspec `{revspec}` not found")]
146 #[diagnostic(code(rtb_forge::git::revspec_not_found))]
147 RevspecNotFound {
148 /// The revspec the caller asked for.
149 revspec: String,
150 },
151
152 /// Dirty-working-tree guard tripped. Listed paths are dirty.
153 /// Operations affected: `checkout`, path-based `commit`.
154 #[error("working tree is dirty: {} path(s) need attention", paths.len())]
155 #[diagnostic(code(rtb_forge::git::dirty_working_tree))]
156 DirtyWorkingTree {
157 /// The dirty paths discovered.
158 paths: Vec<PathBuf>,
159 },
160
161 /// Credential resolution failed. Wraps `rtb-credentials`'s
162 /// existing error — not a backend leak because `rtb-credentials`
163 /// is part of the framework's stable public surface.
164 #[error("credential resolution failed: {0}")]
165 #[diagnostic(code(rtb_forge::git::auth))]
166 Auth(
167 #[from]
168 #[source]
169 rtb_credentials::CredentialError,
170 ),
171}