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
337pub trait LogError<T> {
339 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}