blob: 12c7f37778f412ec9e9a444370f5f3810f857cb7 [file]
// Copyright 2026 The Pigweed Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy of
// the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations under
// the License.
package pw_ghish
import (
"encoding/base64"
"fmt"
"sort"
"strings"
"github.com/andygrunwald/go-gerrit"
"github.com/spf13/cobra"
)
var (
diffNameOnly bool
diffStat bool
)
func formatDiffStat(files map[string]gerrit.FileInfo) string {
var paths []string
maxPathLen := 0
totalInsertions := 0
totalDeletions := 0
for p := range files {
if p == "/COMMIT_MSG" {
continue
}
paths = append(paths, p)
if len(p) > maxPathLen {
maxPathLen = len(p)
}
}
sort.Strings(paths)
var sb strings.Builder
for _, p := range paths {
info := files[p]
ins := info.LinesInserted
del := info.LinesDeleted
total := ins + del
totalInsertions += ins
totalDeletions += del
graph := ""
if total > 0 {
plusCount := ins
minusCount := del
if total > 40 {
plusCount = (ins * 40) / total
minusCount = (del * 40) / total
}
graph = strings.Repeat("+", plusCount) + strings.Repeat("-", minusCount)
}
sb.WriteString(fmt.Sprintf(" %-*s | %d %s\n", maxPathLen, p, total, graph))
}
fileWord := "files"
if len(paths) == 1 {
fileWord = "file"
}
sb.WriteString(fmt.Sprintf(" %d %s changed, %d insertions(+), %d deletions(-)\n", len(paths), fileWord, totalInsertions, totalDeletions))
return sb.String()
}
var diffCmd = &cobra.Command{
Use: "diff [<id>[/<patchset>]]",
Short: "View changes as a diff",
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
chCtx, err := ResolveChangeContext(cmd, args)
if err != nil {
return err
}
if diffNameOnly || diffStat {
files, _, err := chCtx.Client.Changes.ListFiles(chCtx.Context, chCtx.ChangeID, chCtx.Revision, nil)
if err != nil {
errStr := err.Error()
if (strings.Contains(errStr, "404") || strings.Contains(errStr, "Not Found")) && chCtx.Revision != "current" && chCtx.Revision != "" {
return fmt.Errorf("error listing files for change %s (patchset %s): patchset not found (HTTP 404).\n\nTo view the change and available patchsets, run:\n gh pr view %s\n\nUnderlying error: %w", chCtx.ChangeID, chCtx.Revision, chCtx.ChangeID, err)
}
return chCtx.FormatError(err, "listing files for")
}
if diffNameOnly {
var paths []string
for p := range files {
if p == "/COMMIT_MSG" {
continue
}
paths = append(paths, p)
}
sort.Strings(paths)
for _, p := range paths {
fmt.Fprintln(cmd.OutOrStdout(), p)
}
return nil
}
fmt.Fprint(cmd.OutOrStdout(), formatDiffStat(files))
return nil
}
patchPtr, _, err := chCtx.Client.Changes.GetPatch(chCtx.Context, chCtx.ChangeID, chCtx.Revision, nil)
if err != nil {
errStr := err.Error()
if (strings.Contains(errStr, "404") || strings.Contains(errStr, "Not Found")) && chCtx.Revision != "current" && chCtx.Revision != "" {
return fmt.Errorf("error getting patch for change %s (patchset %s): patchset not found (HTTP 404).\n\nTo view the change and available patchsets, run:\n gh pr view %s\n\nUnderlying error: %w", chCtx.ChangeID, chCtx.Revision, chCtx.ChangeID, err)
}
return chCtx.FormatError(err, "getting patch for")
}
if patchPtr == nil {
fmt.Fprintln(cmd.OutOrStdout(), "No patch returned.")
return nil
}
decoded, err := base64.StdEncoding.DecodeString(*patchPtr)
if err != nil {
// If it's not base64, check if it's already a raw diff
trimmed := strings.TrimSpace(*patchPtr)
if strings.HasPrefix(trimmed, "diff --git") || strings.HasPrefix(trimmed, "From ") || strings.HasPrefix(trimmed, "Index: ") {
fmt.Fprintln(cmd.OutOrStdout(), *patchPtr)
return nil
}
return fmt.Errorf("failed to decode patch diff: %w", err)
}
fmt.Fprintln(cmd.OutOrStdout(), string(decoded))
return nil
},
}
func init() {
diffCmd.Flags().BoolVar(&diffNameOnly, "name-only", false, "Show only names of changed files")
diffCmd.Flags().BoolVar(&diffStat, "stat", false, "Show diffstat of changed files")
PrCmd.AddCommand(diffCmd)
}