wtg_cli/
output.rs

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
17/// Display tag with humor - tags aren't supported yet
18fn 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
42/// Display enriched info - the main display logic
43/// Order depends on what the user searched for
44fn display_enriched(info: EnrichedInfo) {
45    match &info.entry_point {
46        EntryPoint::IssueNumber(_) => {
47            // User searched for issue - lead with issue
48            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); // true = show as "the fix"
58                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            // User searched for PR - lead with PR
79            display_identification(&info.entry_point);
80            println!();
81
82            if let Some(pr) = &info.pr {
83                display_pr_section(pr, false); // false = not a fix, just a PR
84                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            // User searched for commit or something else - lead with commit
105            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
137/// Display what the user searched for
138fn 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
178/// Display commit information (the core section, always present when resolved)
179fn 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    // Show commit author
193    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    // Show commit message if not a PR
201    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
212/// Display PR information (enrichment layer 1)
213fn 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    // PR author - different wording if this is shown as "the fix" for an issue
222    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    // PR description (overrides commit message)
232    print_message_with_essay_joke(&pr.title, pr.body.as_deref(), pr.title.lines().count());
233
234    // Merge status
235    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
244/// Display issue information (enrichment layer 2)
245fn 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    // Issue author (who's whining)
254    if let Some(author) = &issue.author {
255        print_author_subsection("Who's whining:", author, "", issue.author_url.as_deref());
256    }
257
258    // Issue description
259    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
268/// Display missing information (graceful degradation)
269fn display_missing_info(info: &EnrichedInfo) {
270    // Issue without PR
271    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    // PR without commit (not merged)
290    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
301// Helper functions for consistent formatting
302
303/// Print a clickable URL with consistent styling
304fn print_link(url: &str) {
305    println!("   {} {}", "🔗".blue(), url.blue().underlined());
306}
307
308/// Print author information as a subsection (indented)
309fn 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
328/// Print a message/description with essay joke if it's long
329fn print_message_with_essay_joke(first_line: &str, full_text: Option<&str>, line_count: usize) {
330    println!("   {} {}", "📝".yellow(), first_line.white().bold());
331
332    // Check if we should show the essay joke
333    if let Some(text) = full_text {
334        let char_count = text.len();
335
336        // Show essay joke if >100 chars or multi-line
337        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
354/// Display file information (special case)
355fn 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    // Get the author URL for the last commit (first in the list)
362    let last_commit_author_url = file_result.author_urls.first().and_then(|opt| opt.as_ref());
363
364    // Display the commit section (consistent with PR/issue flow)
365    display_commit_section(
366        &info.last_commit,
367        file_result.commit_url.as_ref(),
368        last_commit_author_url,
369        None, // Files don't have associated PRs
370    );
371
372    // Count how many times the last commit author appears in previous commits
373    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    // Add snarky comment if they're a repeat offender
381    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    // Previous authors - snarky hall of shame (deduplicated)
410    if !info.previous_authors.is_empty() {
411        // Deduplicate authors - track who we've seen
412        let mut seen_authors = std::collections::HashSet::new();
413        seen_authors.insert(last_author_name.clone()); // Skip the last commit author
414
415        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    // Release info
446    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            // Display tag name (or release name if it's a GitHub release)
455            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                // Show published date if available
468                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                // Use the release URL if available
475                if let Some(url) = &tag.release_url {
476                    print_link(url);
477                }
478            } else {
479                // Plain git tag
480                println!("   {} {}", "🏷️ ".yellow(), tag.name.as_str().cyan().bold());
481
482                // Build GitHub URLs if we have a commit URL
483                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}