1use std::backtrace::Backtrace;
19use std::fmt;
20
21#[derive(Debug)]
27pub(crate) enum GitwayErrorKind {
28 Io(std::io::Error),
30 Ssh(russh::Error),
32 Keys(russh::keys::Error),
34 HostKeyMismatch { fingerprint: String },
39 AuthenticationFailed,
41 NoKeyFound,
43 InvalidConfig { message: String },
45}
46
47impl fmt::Display for GitwayErrorKind {
48 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49 match self {
50 Self::Io(e) => write!(f, "I/O error: {e}"),
51 Self::Ssh(e) => write!(f, "SSH protocol error: {e}"),
52 Self::Keys(e) => write!(f, "SSH key error: {e}"),
53 Self::HostKeyMismatch { fingerprint } => {
54 write!(
55 f,
56 "host key mismatch — received fingerprint {fingerprint} \
57 does not match any pinned fingerprint"
58 )
59 }
60 Self::AuthenticationFailed => write!(f, "public-key authentication failed"),
61 Self::NoKeyFound => {
62 write!(f, "no SSH identity key found on any search path or agent")
63 }
64 Self::InvalidConfig { message } => write!(f, "invalid configuration: {message}"),
65 }
66 }
67}
68
69#[derive(Debug)]
88pub struct GitwayError {
89 kind: GitwayErrorKind,
90 backtrace: Backtrace,
91}
92
93impl GitwayError {
94 pub(crate) fn new(kind: GitwayErrorKind) -> Self {
96 Self {
97 kind,
98 backtrace: Backtrace::capture(),
99 }
100 }
101
102 pub fn host_key_mismatch(fingerprint: impl Into<String>) -> Self {
105 Self::new(GitwayErrorKind::HostKeyMismatch {
106 fingerprint: fingerprint.into(),
107 })
108 }
109
110 #[must_use]
111 pub fn authentication_failed() -> Self {
112 Self::new(GitwayErrorKind::AuthenticationFailed)
113 }
114
115 #[must_use]
116 pub fn no_key_found() -> Self {
117 Self::new(GitwayErrorKind::NoKeyFound)
118 }
119
120 pub fn invalid_config(message: impl Into<String>) -> Self {
121 Self::new(GitwayErrorKind::InvalidConfig {
122 message: message.into(),
123 })
124 }
125
126 #[must_use]
130 pub fn is_io(&self) -> bool {
131 matches!(self.kind, GitwayErrorKind::Io(_))
132 }
133
134 #[must_use]
136 pub fn is_host_key_mismatch(&self) -> bool {
137 matches!(self.kind, GitwayErrorKind::HostKeyMismatch { .. })
138 }
139
140 #[must_use]
142 pub fn is_authentication_failed(&self) -> bool {
143 matches!(self.kind, GitwayErrorKind::AuthenticationFailed)
144 }
145
146 #[must_use]
148 pub fn is_no_key_found(&self) -> bool {
149 matches!(self.kind, GitwayErrorKind::NoKeyFound)
150 }
151
152 #[must_use]
154 pub fn is_key_encrypted(&self) -> bool {
155 matches!(
156 self.kind,
157 GitwayErrorKind::Keys(russh::keys::Error::KeyIsEncrypted)
158 )
159 }
160
161 #[must_use]
163 pub fn fingerprint(&self) -> Option<&str> {
164 match &self.kind {
165 GitwayErrorKind::HostKeyMismatch { fingerprint } => Some(fingerprint),
166 _ => None,
167 }
168 }
169
170 #[must_use]
179 pub fn error_code(&self) -> &'static str {
180 match &self.kind {
181 GitwayErrorKind::InvalidConfig { .. } => "USAGE_ERROR",
182 GitwayErrorKind::NoKeyFound => "NOT_FOUND",
183 GitwayErrorKind::HostKeyMismatch { .. } | GitwayErrorKind::AuthenticationFailed => {
184 "PERMISSION_DENIED"
185 }
186 GitwayErrorKind::Io(_) | GitwayErrorKind::Ssh(_) | GitwayErrorKind::Keys(_) => {
187 "GENERAL_ERROR"
188 }
189 }
190 }
191
192 #[must_use]
201 pub fn exit_code(&self) -> u32 {
202 match &self.kind {
203 GitwayErrorKind::InvalidConfig { .. } => 2,
204 GitwayErrorKind::NoKeyFound => 3,
205 GitwayErrorKind::HostKeyMismatch { .. } | GitwayErrorKind::AuthenticationFailed => 4,
206 GitwayErrorKind::Io(_) | GitwayErrorKind::Ssh(_) | GitwayErrorKind::Keys(_) => 1,
207 }
208 }
209
210 #[must_use]
212 pub fn hint(&self) -> &'static str {
213 match &self.kind {
214 GitwayErrorKind::HostKeyMismatch { .. } => {
215 "Run 'gitway --test --verbose' to diagnose, \
216 or check ~/.config/gitway/known_hosts"
217 }
218 GitwayErrorKind::AuthenticationFailed => {
219 "Ensure your SSH public key is registered with the Git hosting service, \
220 or run 'ssh-add' to load a key into the agent"
221 }
222 GitwayErrorKind::NoKeyFound => {
223 "Run 'ssh-keygen -t ed25519' to generate a key, or use --identity to specify one"
224 }
225 GitwayErrorKind::InvalidConfig { .. } => {
226 "Run 'gitway --help' for usage information"
227 }
228 GitwayErrorKind::Io(_) | GitwayErrorKind::Ssh(_) | GitwayErrorKind::Keys(_) => {
229 "Run 'gitway --test --verbose' to diagnose the connection"
230 }
231 }
232 }
233}
234
235impl fmt::Display for GitwayError {
238 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
239 write!(f, "{}", self.kind)?;
240 let bt = self.backtrace.to_string();
241 if !bt.is_empty() && bt != "disabled backtrace" {
242 write!(f, "\n\nstack backtrace:\n{bt}")?;
243 }
244 Ok(())
245 }
246}
247
248impl std::error::Error for GitwayError {
249 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
250 match &self.kind {
251 GitwayErrorKind::Io(e) => Some(e),
252 GitwayErrorKind::Ssh(e) => Some(e),
253 GitwayErrorKind::Keys(e) => Some(e),
254 _ => None,
255 }
256 }
257
258}
259
260impl From<russh::Error> for GitwayError {
261 fn from(e: russh::Error) -> Self {
262 Self::new(GitwayErrorKind::Ssh(e))
263 }
264}
265
266impl From<russh::keys::Error> for GitwayError {
267 fn from(e: russh::keys::Error) -> Self {
268 Self::new(GitwayErrorKind::Keys(e))
269 }
270}
271
272impl From<std::io::Error> for GitwayError {
273 fn from(e: std::io::Error) -> Self {
274 Self::new(GitwayErrorKind::Io(e))
275 }
276}
277
278impl From<russh::AgentAuthError> for GitwayError {
279 fn from(e: russh::AgentAuthError) -> Self {
280 match e {
281 russh::AgentAuthError::Send(_) => Self::new(GitwayErrorKind::Ssh(russh::Error::SendError)),
282 russh::AgentAuthError::Key(k) => Self::new(GitwayErrorKind::Keys(k)),
283 }
284 }
285}