package cmd import ( "fmt" "strings" "forgejo.zerova.net/sid/fgj-sid/internal/api" "forgejo.zerova.net/sid/fgj-sid/internal/config" "github.com/spf13/cobra" ) var prDiffCmd = &cobra.Command{ Use: "diff ", Short: "Show the diff for a pull request", Long: "Fetch and display the diff for a pull request.", Example: ` # View the diff for PR #123 fgj pr diff 123 # Colorized diff output fgj pr diff 123 --color always # Show only changed file names fgj pr diff 123 --name-only # Show diffstat summary fgj pr diff 123 --stat`, Args: cobra.ExactArgs(1), RunE: runPRDiff, } func init() { prCmd.AddCommand(prDiffCmd) prDiffCmd.Flags().StringP("repo", "R", "", "Repository in owner/name format") prDiffCmd.Flags().String("color", "auto", "Colorize the diff output: auto, always, never") prDiffCmd.Flags().Bool("name-only", false, "Show only changed file names") prDiffCmd.Flags().Bool("stat", false, "Show diffstat summary") } func runPRDiff(cmd *cobra.Command, args []string) error { repo, _ := cmd.Flags().GetString("repo") colorMode, _ := cmd.Flags().GetString("color") nameOnly, _ := cmd.Flags().GetBool("name-only") stat, _ := cmd.Flags().GetBool("stat") prNumber, err := parseIssueArg(args[0]) if err != nil { return fmt.Errorf("invalid pull request number: %w", err) } owner, name, err := parseRepo(repo) if err != nil { return err } cfg, err := config.Load() if err != nil { return err } client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) if err != nil { return err } diffURL := fmt.Sprintf("https://%s/api/v1/repos/%s/%s/pulls/%d.diff", client.Hostname(), owner, name, prNumber) ios.StartSpinner("Fetching diff...") diff, err := client.GetRawLog(diffURL) ios.StopSpinner() if err != nil { return fmt.Errorf("failed to get pull request diff: %w", err) } if nameOnly { return printNameOnly(diff) } if stat { return printDiffStat(diff) } // Start pager for diffs if err := ios.StartPager(); err != nil { fmt.Fprintf(ios.ErrOut, "warning: failed to start pager: %v\n", err) } defer ios.StopPager() useColor := shouldColorize(colorMode) if useColor { return printColorizedDiff(diff) } fmt.Fprint(ios.Out, diff) return nil } // shouldColorize determines whether to colorize output based on the color flag. func shouldColorize(mode string) bool { switch strings.ToLower(mode) { case "always": return true case "never": return false default: // "auto" return ios.ColorEnabled() } } // printNameOnly extracts and prints changed file names from the diff. func printNameOnly(diff string) error { seen := make(map[string]bool) for _, line := range strings.Split(diff, "\n") { if strings.HasPrefix(line, "+++ b/") { name := strings.TrimPrefix(line, "+++ b/") if name != "" && !seen[name] { seen[name] = true fmt.Fprintln(ios.Out, name) } } } return nil } // fileStat holds per-file diff statistics. type fileStat struct { name string additions int deletions int } // printDiffStat parses the diff and prints a diffstat summary. func printDiffStat(diff string) error { var stats []fileStat var current *fileStat inHeader := true for _, line := range strings.Split(diff, "\n") { if strings.HasPrefix(line, "diff --git ") { if current != nil { stats = append(stats, *current) } current = &fileStat{} inHeader = true continue } if current == nil { continue } if strings.HasPrefix(line, "+++ b/") { current.name = strings.TrimPrefix(line, "+++ b/") continue } if strings.HasPrefix(line, "@@") { inHeader = false continue } if inHeader { continue } if strings.HasPrefix(line, "+") { current.additions++ } else if strings.HasPrefix(line, "-") { current.deletions++ } } if current != nil && current.name != "" { stats = append(stats, *current) } if len(stats) == 0 { fmt.Fprintln(ios.Out, "0 files changed") return nil } cs := ios.ColorScheme() // Find the longest file name for alignment maxNameLen := 0 maxChanges := 0 for _, s := range stats { if len(s.name) > maxNameLen { maxNameLen = len(s.name) } total := s.additions + s.deletions if total > maxChanges { maxChanges = total } } // Cap the bar width barWidth := maxChanges if barWidth > 50 { barWidth = 50 } totalAdditions := 0 totalDeletions := 0 for _, s := range stats { total := s.additions + s.deletions totalAdditions += s.additions totalDeletions += s.deletions var bar string if maxChanges > 0 { scaledAdd := s.additions scaledDel := s.deletions if maxChanges > 50 { scaledAdd = s.additions * 50 / maxChanges scaledDel = s.deletions * 50 / maxChanges if s.additions > 0 && scaledAdd == 0 { scaledAdd = 1 } if s.deletions > 0 && scaledDel == 0 { scaledDel = 1 } } bar = cs.Green(strings.Repeat("+", scaledAdd)) + cs.Red(strings.Repeat("-", scaledDel)) } fmt.Fprintf(ios.Out, " %-*s | %4d %s\n", maxNameLen, s.name, total, bar) } fmt.Fprintf(ios.Out, " %d file", len(stats)) if len(stats) != 1 { fmt.Fprint(ios.Out, "s") } fmt.Fprintf(ios.Out, " changed, %d insertion", totalAdditions) if totalAdditions != 1 { fmt.Fprint(ios.Out, "s") } fmt.Fprintf(ios.Out, "(+), %d deletion", totalDeletions) if totalDeletions != 1 { fmt.Fprint(ios.Out, "s") } fmt.Fprintln(ios.Out, "(-)") return nil } // printColorizedDiff prints the diff with ANSI color codes using ColorScheme. func printColorizedDiff(diff string) error { cs := ios.ColorScheme() for _, line := range strings.Split(diff, "\n") { switch { case strings.HasPrefix(line, "diff --git "): fmt.Fprintln(ios.Out, cs.Bold(line)) case strings.HasPrefix(line, "index "), strings.HasPrefix(line, "--- "), strings.HasPrefix(line, "+++ "), strings.HasPrefix(line, "new file"), strings.HasPrefix(line, "deleted file"), strings.HasPrefix(line, "similarity index"), strings.HasPrefix(line, "rename from"), strings.HasPrefix(line, "rename to"): fmt.Fprintln(ios.Out, cs.Bold(line)) case strings.HasPrefix(line, "@@"): fmt.Fprintln(ios.Out, cs.Cyan(line)) case strings.HasPrefix(line, "+"): fmt.Fprintln(ios.Out, cs.Green(line)) case strings.HasPrefix(line, "-"): fmt.Fprintln(ios.Out, cs.Red(line)) default: fmt.Fprintln(ios.Out, line) } } return nil }