1use crate::error::Result;
2use crate::identifier::{EnrichedInfo, EntryPoint, FileResult, IdentifiedThing};
3use crossterm::style::Stylize;
4
5pub fn display(thing: IdentifiedThing) -> Result<()> {
6 match thing {
7 IdentifiedThing::Enriched(info) => display_enriched(*info),
8 IdentifiedThing::File(file_result) => display_file(*file_result),
9 IdentifiedThing::TagOnly(tag_info, github_url) => {
10 display_tag_warning(*tag_info, github_url);
11 }
12 }
13
14 Ok(())
15}
16
17fn display_tag_warning(tag_info: crate::git::TagInfo, github_url: Option<String>) {
19 println!(
20 "{} {}",
21 "🏷️ Found tag:".green().bold(),
22 tag_info.name.cyan()
23 );
24 println!();
25 println!("{}", "🐎 Whoa there, slow down cowboy!".yellow().bold());
26 println!();
27 println!(
28 " {}",
29 "Tags aren't fully baked yet. I found it, but can't tell you much about it.".white()
30 );
31 println!(
32 " {}",
33 "Come back when you have a commit hash, PR, or issue to look up!".white()
34 );
35
36 if let Some(url) = github_url {
37 println!();
38 print_link(&url);
39 }
40}
41
42fn display_enriched(info: EnrichedInfo) {
45 match &info.entry_point {
46 EntryPoint::IssueNumber(_) => {
47 display_identification(&info.entry_point);
49 println!();
50
51 if let Some(issue) = &info.issue {
52 display_issue_section(issue);
53 println!();
54 }
55
56 if let Some(pr) = &info.pr {
57 display_pr_section(pr, true); println!();
59 }
60
61 if let Some(commit) = &info.commit {
62 display_commit_section(
63 commit,
64 info.commit_url.as_ref(),
65 info.commit_author_github_url.as_ref(),
66 info.pr.as_ref(),
67 );
68 println!();
69 }
70
71 display_missing_info(&info);
72
73 if info.commit.is_some() {
74 display_release_info(info.release, info.commit_url.as_deref());
75 }
76 }
77 EntryPoint::PullRequestNumber(_) => {
78 display_identification(&info.entry_point);
80 println!();
81
82 if let Some(pr) = &info.pr {
83 display_pr_section(pr, false); println!();
85 }
86
87 if let Some(commit) = &info.commit {
88 display_commit_section(
89 commit,
90 info.commit_url.as_ref(),
91 info.commit_author_github_url.as_ref(),
92 info.pr.as_ref(),
93 );
94 println!();
95 }
96
97 display_missing_info(&info);
98
99 if info.commit.is_some() {
100 display_release_info(info.release, info.commit_url.as_deref());
101 }
102 }
103 _ => {
104 display_identification(&info.entry_point);
106 println!();
107
108 if let Some(commit) = &info.commit {
109 display_commit_section(
110 commit,
111 info.commit_url.as_ref(),
112 info.commit_author_github_url.as_ref(),
113 info.pr.as_ref(),
114 );
115 println!();
116 }
117
118 if let Some(pr) = &info.pr {
119 display_pr_section(pr, false);
120 println!();
121 }
122
123 if let Some(issue) = &info.issue {
124 display_issue_section(issue);
125 println!();
126 }
127
128 display_missing_info(&info);
129
130 if info.commit.is_some() {
131 display_release_info(info.release, info.commit_url.as_deref());
132 }
133 }
134 }
135}
136
137fn display_identification(entry_point: &EntryPoint) {
139 match entry_point {
140 EntryPoint::Commit(hash) => {
141 println!(
142 "{} {}",
143 "🔍 Found commit:".green().bold(),
144 hash.as_str().cyan()
145 );
146 }
147 EntryPoint::PullRequestNumber(num) => {
148 println!(
149 "{} #{}",
150 "🔀 Found PR:".green().bold(),
151 num.to_string().cyan()
152 );
153 }
154 EntryPoint::IssueNumber(num) => {
155 println!(
156 "{} #{}",
157 "🐛 Found issue:".green().bold(),
158 num.to_string().cyan()
159 );
160 }
161 EntryPoint::FilePath(path) => {
162 println!(
163 "{} {}",
164 "📄 Found file:".green().bold(),
165 path.as_str().cyan()
166 );
167 }
168 EntryPoint::Tag(tag) => {
169 println!(
170 "{} {}",
171 "🏷️ Found tag:".green().bold(),
172 tag.as_str().cyan()
173 );
174 }
175 }
176}
177
178fn display_commit_section(
180 commit: &crate::git::CommitInfo,
181 commit_url: Option<&String>,
182 author_url: Option<&String>,
183 pr: Option<&crate::github::PullRequestInfo>,
184) {
185 println!("{}", "💻 The Commit:".cyan().bold());
186 println!(
187 " {} {}",
188 "Hash:".yellow(),
189 commit.short_hash.as_str().cyan()
190 );
191
192 print_author_subsection(
194 "Who wrote this gem:",
195 &commit.author_name,
196 &commit.author_email,
197 author_url.map(String::as_str),
198 );
199
200 if pr.is_none() {
202 print_message_with_essay_joke(&commit.message, None, commit.message_lines);
203 }
204
205 println!(" {} {}", "📅".yellow(), commit.date.as_str().dark_grey());
206
207 if let Some(url) = commit_url {
208 print_link(url);
209 }
210}
211
212fn display_pr_section(pr: &crate::github::PullRequestInfo, is_fix: bool) {
214 println!("{}", "🔀 The Pull Request:".magenta().bold());
215 println!(
216 " {} #{}",
217 "Number:".yellow(),
218 pr.number.to_string().cyan()
219 );
220
221 if let Some(author) = &pr.author {
223 let header = if is_fix {
224 "Who's brave:"
225 } else {
226 "Who merged this beauty:"
227 };
228 print_author_subsection(header, author, "", pr.author_url.as_deref());
229 }
230
231 print_message_with_essay_joke(&pr.title, pr.body.as_deref(), pr.title.lines().count());
233
234 if let Some(merge_sha) = &pr.merge_commit_sha {
236 println!(" {} {}", "✅ Merged:".green(), merge_sha[..7].cyan());
237 } else {
238 println!(" {}", "❌ Not merged yet".yellow().italic());
239 }
240
241 print_link(&pr.url);
242}
243
244fn display_issue_section(issue: &crate::github::IssueInfo) {
246 println!("{}", "🐛 The Issue:".red().bold());
247 println!(
248 " {} #{}",
249 "Number:".yellow(),
250 issue.number.to_string().cyan()
251 );
252
253 if let Some(author) = &issue.author {
255 print_author_subsection("Who's whining:", author, "", issue.author_url.as_deref());
256 }
257
258 print_message_with_essay_joke(
260 &issue.title,
261 issue.body.as_deref(),
262 issue.title.lines().count(),
263 );
264
265 print_link(&issue.url);
266}
267
268fn display_missing_info(info: &EnrichedInfo) {
270 if let Some(issue) = info.issue.as_ref()
272 && info.pr.is_none()
273 {
274 let message = if info.commit.is_none() {
275 if issue.state == octocrab::models::IssueState::Closed {
276 "🔍 Issue closed, but the trail's cold. Some stealthy hero dropped a fix and vanished without a PR."
277 } else {
278 "🔍 Couldn't trace this issue, still open. No one cares, probably!"
279 }
280 } else if issue.state == octocrab::models::IssueState::Closed {
281 "🤷 Issue closed, but no PR found... Some stealthy hero dropped a fix and vanished without a PR."
282 } else {
283 "🤷 No PR found for this issue... no one cares, probably!"
284 };
285 println!("{}", message.yellow().italic());
286 println!();
287 }
288
289 if info.pr.is_some() && info.commit.is_none() {
291 println!(
292 "{}",
293 "⏳ This PR hasn't been merged yet, too scared to commit!"
294 .yellow()
295 .italic()
296 );
297 println!();
298 }
299}
300
301fn print_link(url: &str) {
305 println!(" {} {}", "🔗".blue(), url.blue().underlined());
306}
307
308fn print_author_subsection(
310 header: &str,
311 name: &str,
312 email_or_username: &str,
313 profile_url: Option<&str>,
314) {
315 println!(" {} {}", "👤".yellow(), header.dark_grey());
316
317 if email_or_username.is_empty() {
318 println!(" {}", name.cyan());
319 } else {
320 println!(" {} ({})", name.cyan(), email_or_username.dark_grey());
321 }
322
323 if let Some(url) = profile_url {
324 println!(" {} {}", "🔗".blue(), url.blue().underlined());
325 }
326}
327
328fn print_message_with_essay_joke(first_line: &str, full_text: Option<&str>, line_count: usize) {
330 println!(" {} {}", "📝".yellow(), first_line.white().bold());
331
332 if let Some(text) = full_text {
334 let char_count = text.len();
335
336 if char_count > 100 || line_count > 1 {
338 let extra_lines = line_count.saturating_sub(1);
339 let message = if extra_lines > 0 {
340 format!(
341 "Someone likes to write essays... {} more line{}",
342 extra_lines,
343 if extra_lines == 1 { "" } else { "s" }
344 )
345 } else {
346 format!("Someone likes to write essays... {char_count} characters")
347 };
348
349 println!(" {} {}", "📚".yellow(), message.dark_grey().italic());
350 }
351 }
352}
353
354fn display_file(file_result: FileResult) {
356 let info = file_result.file_info;
357
358 println!("{} {}", "📄 Found file:".green().bold(), info.path.cyan());
359 println!();
360
361 let last_commit_author_url = file_result.author_urls.first().and_then(|opt| opt.as_ref());
363
364 display_commit_section(
366 &info.last_commit,
367 file_result.commit_url.as_ref(),
368 last_commit_author_url,
369 None, );
371
372 let last_author_name = &info.last_commit.author_name;
374 let repeat_count = info
375 .previous_authors
376 .iter()
377 .filter(|(_, name, _)| name == last_author_name)
378 .count();
379
380 if repeat_count > 0 {
382 let joke = match repeat_count {
383 1 => format!(
384 " 💀 {} can't stop touching this file... {} more time before this!",
385 last_author_name.as_str().cyan(),
386 repeat_count
387 ),
388 2 => format!(
389 " 💀 {} really loves this file... {} more times before this!",
390 last_author_name.as_str().cyan(),
391 repeat_count
392 ),
393 3 => format!(
394 " 💀 {} is obsessed... {} more times before this!",
395 last_author_name.as_str().cyan(),
396 repeat_count
397 ),
398 _ => format!(
399 " 💀 {} REALLY needs to leave this alone... {} more times before this!",
400 last_author_name.as_str().cyan(),
401 repeat_count
402 ),
403 };
404 println!("{}", joke.dark_grey().italic());
405 }
406
407 println!();
408
409 if !info.previous_authors.is_empty() {
411 let mut seen_authors = std::collections::HashSet::new();
413 seen_authors.insert(last_author_name.clone()); let unique_authors: Vec<_> = info
416 .previous_authors
417 .iter()
418 .enumerate()
419 .filter(|(_, (_, name, _))| seen_authors.insert(name.clone()))
420 .collect();
421
422 if !unique_authors.is_empty() {
423 let count = unique_authors.len();
424 let header = if count == 1 {
425 "👻 One ghost from the past:"
426 } else {
427 "👻 The usual suspects (who else touched this):"
428 };
429 println!("{}", header.yellow().bold());
430
431 for (original_idx, (hash, name, _email)) in unique_authors {
432 print!(" → {} • {}", hash.as_str().cyan(), name.as_str().cyan());
433
434 if let Some(Some(url)) = file_result.author_urls.get(original_idx) {
435 print!(" {} {}", "🔗".blue(), url.as_str().blue().underlined());
436 }
437
438 println!();
439 }
440
441 println!();
442 }
443 }
444
445 display_release_info(file_result.release, file_result.commit_url.as_deref());
447}
448
449fn display_release_info(release: Option<crate::git::TagInfo>, commit_url: Option<&str>) {
450 println!("{}", "📦 First shipped in:".magenta().bold());
451
452 match release {
453 Some(tag) => {
454 if tag.is_release {
456 if let Some(release_name) = &tag.release_name {
457 println!(
458 " {} {} {}",
459 "🎉".yellow(),
460 release_name.as_str().cyan().bold(),
461 format!("({})", tag.name).as_str().dark_grey()
462 );
463 } else {
464 println!(" {} {}", "🎉".yellow(), tag.name.as_str().cyan().bold());
465 }
466
467 if let Some(published) = &tag.published_at
469 && let Some(date_part) = published.split('T').next()
470 {
471 println!(" {} {}", "📅".dark_grey(), date_part.dark_grey());
472 }
473
474 if let Some(url) = &tag.release_url {
476 print_link(url);
477 }
478 } else {
479 println!(" {} {}", "🏷️ ".yellow(), tag.name.as_str().cyan().bold());
481
482 if let Some(url) = commit_url
484 && let Some((base_url, _)) = url.rsplit_once("/commit/")
485 {
486 let tag_url = format!("{base_url}/tree/{}", tag.name);
487 print_link(&tag_url);
488 }
489 }
490 }
491 None => {
492 println!(
493 " {}",
494 "🔥 Not shipped yet, still cooking in main!"
495 .yellow()
496 .italic()
497 );
498 }
499 }
500}