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(
256 "Who spotted the trouble:",
257 author,
258 "",
259 issue.author_url.as_deref(),
260 );
261 }
262
263 print_message_with_essay_joke(
265 &issue.title,
266 issue.body.as_deref(),
267 issue.title.lines().count(),
268 );
269
270 print_link(&issue.url);
271}
272
273fn display_missing_info(info: &EnrichedInfo) {
275 if let Some(issue) = info.issue.as_ref()
277 && info.pr.is_none()
278 {
279 let message = if info.commit.is_none() {
280 if issue.state == octocrab::models::IssueState::Closed {
281 "🔍 Issue closed, but the trail's cold. Some stealthy hero dropped a fix and vanished without a PR."
282 } else {
283 "🔍 Couldn't trace this issue, still open. Waiting for a brave soul to pick it up..."
284 }
285 } else if issue.state == octocrab::models::IssueState::Closed {
286 "🤷 Issue closed, but no PR found... Some stealthy hero dropped a fix and vanished without a PR."
287 } else {
288 "🤷 No PR found for this issue... still hunting for the fix!"
289 };
290 println!("{}", message.yellow().italic());
291 println!();
292 }
293
294 if info.pr.is_some() && info.commit.is_none() {
296 println!(
297 "{}",
298 "⏳ This PR hasn't been merged yet, too scared to commit!"
299 .yellow()
300 .italic()
301 );
302 println!();
303 }
304}
305
306fn print_link(url: &str) {
310 println!(" {} {}", "🔗".blue(), url.blue().underlined());
311}
312
313fn print_author_subsection(
315 header: &str,
316 name: &str,
317 email_or_username: &str,
318 profile_url: Option<&str>,
319) {
320 println!(" {} {}", "👤".yellow(), header.dark_grey());
321
322 if email_or_username.is_empty() {
323 println!(" {}", name.cyan());
324 } else {
325 println!(" {} ({})", name.cyan(), email_or_username.dark_grey());
326 }
327
328 if let Some(url) = profile_url {
329 println!(" {} {}", "🔗".blue(), url.blue().underlined());
330 }
331}
332
333fn print_message_with_essay_joke(first_line: &str, full_text: Option<&str>, line_count: usize) {
335 println!(" {} {}", "📝".yellow(), first_line.white().bold());
336
337 if let Some(text) = full_text {
339 let char_count = text.len();
340
341 if char_count > 100 || line_count > 1 {
343 let extra_lines = line_count.saturating_sub(1);
344 let message = if extra_lines > 0 {
345 format!(
346 "Someone likes to write essays... {} more line{}",
347 extra_lines,
348 if extra_lines == 1 { "" } else { "s" }
349 )
350 } else {
351 format!("Someone likes to write essays... {char_count} characters")
352 };
353
354 println!(" {} {}", "📚".yellow(), message.dark_grey().italic());
355 }
356 }
357}
358
359fn display_file(file_result: FileResult) {
361 let info = file_result.file_info;
362
363 println!("{} {}", "📄 Found file:".green().bold(), info.path.cyan());
364 println!();
365
366 let last_commit_author_url = file_result.author_urls.first().and_then(|opt| opt.as_ref());
368
369 display_commit_section(
371 &info.last_commit,
372 file_result.commit_url.as_ref(),
373 last_commit_author_url,
374 None, );
376
377 let last_author_name = &info.last_commit.author_name;
379 let repeat_count = info
380 .previous_authors
381 .iter()
382 .filter(|(_, name, _)| name == last_author_name)
383 .count();
384
385 if repeat_count > 0 {
387 let joke = match repeat_count {
388 1 => format!(
389 " 💀 {} can't stop touching this file... {} more time before this!",
390 last_author_name.as_str().cyan(),
391 repeat_count
392 ),
393 2 => format!(
394 " 💀 {} really loves this file... {} more times before this!",
395 last_author_name.as_str().cyan(),
396 repeat_count
397 ),
398 3 => format!(
399 " 💀 {} is obsessed... {} more times before this!",
400 last_author_name.as_str().cyan(),
401 repeat_count
402 ),
403 _ => format!(
404 " 💀 {} REALLY needs to leave this alone... {} more times before this!",
405 last_author_name.as_str().cyan(),
406 repeat_count
407 ),
408 };
409 println!("{}", joke.dark_grey().italic());
410 }
411
412 println!();
413
414 if !info.previous_authors.is_empty() {
416 let mut seen_authors = std::collections::HashSet::new();
418 seen_authors.insert(last_author_name.clone()); let unique_authors: Vec<_> = info
421 .previous_authors
422 .iter()
423 .enumerate()
424 .filter(|(_, (_, name, _))| seen_authors.insert(name.clone()))
425 .collect();
426
427 if !unique_authors.is_empty() {
428 let count = unique_authors.len();
429 let header = if count == 1 {
430 "👻 One ghost from the past:"
431 } else {
432 "👻 The usual suspects (who else touched this):"
433 };
434 println!("{}", header.yellow().bold());
435
436 for (original_idx, (hash, name, _email)) in unique_authors {
437 print!(" → {} • {}", hash.as_str().cyan(), name.as_str().cyan());
438
439 if let Some(Some(url)) = file_result.author_urls.get(original_idx) {
440 print!(" {} {}", "🔗".blue(), url.as_str().blue().underlined());
441 }
442
443 println!();
444 }
445
446 println!();
447 }
448 }
449
450 display_release_info(file_result.release, file_result.commit_url.as_deref());
452}
453
454fn display_release_info(release: Option<crate::git::TagInfo>, commit_url: Option<&str>) {
455 println!("{}", "📦 First shipped in:".magenta().bold());
456
457 match release {
458 Some(tag) => {
459 if tag.is_release {
461 if let Some(release_name) = &tag.release_name {
462 println!(
463 " {} {} {}",
464 "🎉".yellow(),
465 release_name.as_str().cyan().bold(),
466 format!("({})", tag.name).as_str().dark_grey()
467 );
468 } else {
469 println!(" {} {}", "🎉".yellow(), tag.name.as_str().cyan().bold());
470 }
471
472 if let Some(published) = &tag.published_at
474 && let Some(date_part) = published.split('T').next()
475 {
476 println!(" {} {}", "📅".dark_grey(), date_part.dark_grey());
477 }
478
479 if let Some(url) = &tag.release_url {
481 print_link(url);
482 }
483 } else {
484 println!(" {} {}", "🏷️ ".yellow(), tag.name.as_str().cyan().bold());
486
487 if let Some(url) = commit_url
489 && let Some((base_url, _)) = url.rsplit_once("/commit/")
490 {
491 let tag_url = format!("{base_url}/tree/{}", tag.name);
492 print_link(&tag_url);
493 }
494 }
495 }
496 None => {
497 println!(
498 " {}",
499 "🔥 Not shipped yet, still cooking in main!"
500 .yellow()
501 .italic()
502 );
503 }
504 }
505}