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
315pub trait LogError<T> {
317 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}