Skip to main content

wtg_cli/
error.rs

1use crossterm::style::Stylize;
2use http::StatusCode;
3use octocrab::Error as OctoError;
4use std::fmt;
5
6pub type WtgResult<T> = std::result::Result<T, WtgError>;
7
8#[derive(Debug, strum::EnumIs)]
9pub enum WtgError {
10    EmptyInput,
11    NotInGitRepo,
12    NotFound(String),
13    TagNotFound(String),
14    Unsupported(String),
15    Git(git2::Error),
16    GhConnectionLost,
17    GhRateLimit(OctoError),
18    GhSaml(OctoError),
19    GhBadCredentials(OctoError),
20    GitHub(OctoError),
21    MultipleMatches(Vec<String>),
22    Io(std::io::Error),
23    Cli { message: String, code: i32 },
24    Timeout,
25    NotGitHubUrl(String),
26    MalformedGitHubUrl(String),
27    SecurityRejection(String),
28    GitHubClientFailed,
29}
30
31impl fmt::Display for WtgError {
32    #[allow(clippy::too_many_lines)]
33    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34        match self {
35            Self::NotInGitRepo => {
36                writeln!(
37                    f,
38                    "{}",
39                    "โŒ What the git are you asking me to do?".red().bold()
40                )?;
41                writeln!(f, "   {}", "This isn't even a git repository! ๐Ÿ˜ฑ".red())
42            }
43            Self::NotFound(input) => {
44                writeln!(
45                    f,
46                    "{}",
47                    "๐Ÿค” Couldn't find this anywhere - are you sure you didn't make it up?"
48                        .yellow()
49                        .bold()
50                )?;
51                writeln!(f)?;
52                writeln!(f, "   {}", "Tried:".yellow())?;
53                writeln!(f, "   {} Commit hash (local + remote)", "โŒ".red())?;
54                writeln!(f, "   {} GitHub issue/PR", "โŒ".red())?;
55                writeln!(f, "   {} File in repo", "โŒ".red())?;
56                writeln!(f, "   {} Git tag", "โŒ".red())?;
57                writeln!(f)?;
58                writeln!(f, "   {}: {}", "Input was".yellow(), input.as_str().cyan())
59            }
60            Self::TagNotFound(tag_name) => {
61                writeln!(
62                    f,
63                    "{}",
64                    "๐Ÿท๏ธ  Tag not found! Never heard of it.".yellow().bold()
65                )?;
66                writeln!(f)?;
67                writeln!(
68                    f,
69                    "   {}: {}",
70                    "Looking for".yellow(),
71                    tag_name.as_str().cyan()
72                )?;
73                writeln!(f)?;
74                writeln!(f, "   {}", "Check your spelling! ๐Ÿ”".yellow())
75            }
76            Self::Unsupported(operation) => {
77                writeln!(f, "{}", "๐Ÿšซ Can't do that here!".yellow().bold())?;
78                writeln!(f)?;
79                writeln!(
80                    f,
81                    "   {} is not supported with the current backend.",
82                    operation.as_str().cyan()
83                )
84            }
85            Self::Git(e) => write!(f, "Git error: {e}"),
86            Self::GhConnectionLost => {
87                writeln!(
88                    f,
89                    "{}",
90                    "๐Ÿ“ก Houston, we have a problem! Connection lost mid-flight!"
91                        .red()
92                        .bold()
93                )?;
94                writeln!(f)?;
95                writeln!(
96                    f,
97                    "   {}",
98                    "GitHub was there a second ago, now it's playing hide and seek. ๐Ÿ‘ป".red()
99                )?;
100                writeln!(
101                    f,
102                    "   {}",
103                    "Check your internet connection and try again!".yellow()
104                )
105            }
106            Self::GhRateLimit(_) => {
107                writeln!(
108                    f,
109                    "{}",
110                    "โฑ๏ธ  Whoa there, speed demon! GitHub says you're moving too fast."
111                        .yellow()
112                        .bold()
113                )?;
114                writeln!(f)?;
115                writeln!(
116                    f,
117                    "   {}",
118                    "You've hit the rate limit. Maybe take a coffee break? โ˜•".yellow()
119                )?;
120                writeln!(
121                    f,
122                    "   {}",
123                    "Or set a GITHUB_TOKEN to get higher limits.".yellow()
124                )
125            }
126            Self::GhSaml(_) => {
127                writeln!(
128                    f,
129                    "{}",
130                    "๐Ÿ” Halt! Who goes there? Your GitHub org wants to see some ID!"
131                        .red()
132                        .bold()
133                )?;
134                writeln!(f)?;
135                writeln!(
136                    f,
137                    "   {}",
138                    "Looks like SAML SSO is standing between you and your data. ๐Ÿšง".red()
139                )?;
140                writeln!(
141                    f,
142                    "   {}",
143                    "Try authenticating your GITHUB_TOKEN with SAML first!".red()
144                )
145            }
146            Self::GhBadCredentials(_) => {
147                writeln!(
148                    f,
149                    "{}",
150                    "๐Ÿ”‘ Nope! GitHub doesn't recognize that token."
151                        .yellow()
152                        .bold()
153                )?;
154                writeln!(f)?;
155                writeln!(
156                    f,
157                    "   {}",
158                    "Expired? Revoked? Typo? It happens to the best of us. ๐Ÿคท".yellow()
159                )?;
160                writeln!(
161                    f,
162                    "   {}",
163                    "Check your GITHUB_TOKEN and try again!".yellow()
164                )
165            }
166            Self::GitHub(e) => write!(f, "GitHub error: {e}"),
167            Self::MultipleMatches(types) => {
168                writeln!(f, "{}", "๐Ÿ’ฅ OH MY, YOU BLEW ME UP!".red().bold())?;
169                writeln!(f)?;
170                writeln!(
171                    f,
172                    "   {}",
173                    "This matches EVERYTHING and I don't know what to do! ๐Ÿคฏ".red()
174                )?;
175                writeln!(f)?;
176                writeln!(f, "   {}", "Matches:".yellow())?;
177                for t in types {
178                    writeln!(f, "   {} {}", "โœ“".green(), t)?;
179                }
180                panic!("๐Ÿ’ฅ BOOM! You broke me!");
181            }
182            Self::Io(e) => write!(f, "I/O error: {e}"),
183            Self::Cli { message, .. } => write!(f, "{message}"),
184            Self::Timeout => {
185                writeln!(
186                    f,
187                    "{}",
188                    "โฐ Time's up! The internet took a nap.".red().bold()
189                )?;
190                writeln!(f)?;
191                writeln!(
192                    f,
193                    "   {}",
194                    "Did you forget to pay your internet bill? ๐Ÿ’ธ".red()
195                )
196            }
197            Self::NotGitHubUrl(url) => {
198                writeln!(
199                    f,
200                    "{}",
201                    "๐Ÿคจ That's a URL alright, but it's not GitHub!"
202                        .yellow()
203                        .bold()
204                )?;
205                writeln!(f)?;
206                writeln!(f, "   {}: {}", "You gave me".yellow(), url.clone().cyan())?;
207                writeln!(f)?;
208                writeln!(f, "   {}", "I only speak GitHub URLs, buddy! ๐Ÿ™".yellow())
209            }
210            Self::MalformedGitHubUrl(url) => {
211                writeln!(
212                    f,
213                    "{}",
214                    "๐Ÿ˜ต That GitHub URL is more broken than my ex's promises!"
215                        .red()
216                        .bold()
217                )?;
218                writeln!(f)?;
219                writeln!(f, "   {}: {}", "You gave me".red(), url.clone().cyan())?;
220                writeln!(f)?;
221                writeln!(
222                    f,
223                    "   {}",
224                    "Expected something like: https://github.com/owner/repo/issues/123".yellow()
225                )?;
226                writeln!(f, "   {}", "But this? This is just sad. ๐Ÿ˜ข".red())
227            }
228            Self::SecurityRejection(reason) => {
229                writeln!(f, "{}", "๐Ÿšจ Whoa there! Security alert!".red().bold())?;
230                writeln!(f)?;
231                writeln!(
232                    f,
233                    "   {}",
234                    "I can't process that input for personal reasons. ๐Ÿ›ก๏ธ".red()
235                )?;
236                writeln!(f)?;
237                writeln!(f, "   {}: {}", "Reason".yellow(), reason.clone())?;
238                writeln!(f)?;
239                writeln!(f, "   {}", "Please, try something safer? ๐Ÿ™".yellow())
240            }
241            Self::EmptyInput => {
242                writeln!(
243                    f,
244                    "{}",
245                    "๐Ÿซฅ Excuse me, but I can't read minds!".yellow().bold()
246                )?;
247                writeln!(f)?;
248                writeln!(
249                    f,
250                    "   {}",
251                    "You gave me... nothing. Nada. Zilch. The void! ๐Ÿ‘ป".yellow()
252                )?;
253                writeln!(f)?;
254                writeln!(
255                    f,
256                    "   {}",
257                    "Try giving me something to work with, please!".yellow()
258                )
259            }
260            Self::GitHubClientFailed => {
261                writeln!(
262                    f,
263                    "{}",
264                    "๐Ÿ”‘ Can't connect to GitHub! Something's blocking the path..."
265                        .red()
266                        .bold()
267                )?;
268                writeln!(f)?;
269                writeln!(
270                    f,
271                    "   {}",
272                    "You explicitly asked for GitHub data, but I can't reach it. ๐Ÿ˜ž".red()
273                )?;
274                writeln!(f)?;
275                writeln!(
276                    f,
277                    "   {}",
278                    "Check your GITHUB_TOKEN and network connection!".yellow()
279                )
280            }
281        }
282    }
283}
284
285impl std::error::Error for WtgError {}
286
287impl From<git2::Error> for WtgError {
288    fn from(err: git2::Error) -> Self {
289        Self::Git(err)
290    }
291}
292
293impl From<OctoError> for WtgError {
294    fn from(err: OctoError) -> Self {
295        if let OctoError::GitHub { ref source, .. } = err {
296            match source.status_code {
297                StatusCode::UNAUTHORIZED => return Self::GhBadCredentials(err),
298                StatusCode::TOO_MANY_REQUESTS => return Self::GhRateLimit(err),
299                StatusCode::FORBIDDEN => {
300                    let msg_lower = source.message.to_ascii_lowercase();
301
302                    if msg_lower.contains("saml") {
303                        return Self::GhSaml(err);
304                    }
305
306                    if msg_lower.contains("rate limit") {
307                        return Self::GhRateLimit(err);
308                    }
309
310                    return Self::GitHub(err);
311                }
312                _ => {
313                    return Self::GitHub(err);
314                }
315            }
316        }
317
318        Self::GitHub(err)
319    }
320}
321
322impl From<std::io::Error> for WtgError {
323    fn from(err: std::io::Error) -> Self {
324        Self::Io(err)
325    }
326}
327
328impl WtgError {
329    pub const fn exit_code(&self) -> i32 {
330        match self {
331            Self::Cli { code, .. } => *code,
332            _ => 1,
333        }
334    }
335}
336
337/// Extension trait for logging errors before discarding them.
338pub trait LogError<T> {
339    /// Log the error at debug level and convert to Option.
340    fn log_err(self, context: &str) -> Option<T>;
341}
342
343impl<T> LogError<T> for WtgResult<T> {
344    fn log_err(self, context: &str) -> Option<T> {
345        match self {
346            Ok(v) => Some(v),
347            Err(e) => {
348                log::debug!("{context}: {e:?}");
349                None
350            }
351        }
352    }
353}