Move from forgejo.zerova.net/sid/fgj-sid to forgejo.zerova.net/public/fgj-sid to reflect the new public org.
270 lines
6.2 KiB
Go
270 lines
6.2 KiB
Go
package cmd
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"forgejo.zerova.net/public/fgj-sid/internal/api"
|
|
"forgejo.zerova.net/public/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(), getCwd())
|
|
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
|
|
}
|