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