Add PR checks command, iostreams/text packages for colored table output, top-level run/workflow aliases matching gh CLI structure. Enhance actions, issues, PRs, releases, repos, labels, milestones, and wiki commands with improved flags, JSON output, and error handling.
270 lines
6.2 KiB
Go
270 lines
6.2 KiB
Go
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 <number>",
|
|
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
|
|
}
|