Compare commits

..

No commits in common. "debcf94f2ecff77254927287f160d2454678fda3" and "f483a36de8107b61fa139459540e83795a70dd49" have entirely different histories.

3 changed files with 11 additions and 47 deletions

View File

@ -2,17 +2,12 @@ package cmd
import ( import (
"fmt" "fmt"
"strings"
"github.com/fatih/color" "github.com/fatih/color"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"gmgauthier.com/grokkit/config" "gmgauthier.com/grokkit/config"
"gmgauthier.com/grokkit/internal/git"
) )
// gitDiff is the mockable entry point (exactly like gitRun used elsewhere).
var gitDiff = git.Diff
var prDescribeCmd = &cobra.Command{ var prDescribeCmd = &cobra.Command{
Use: "pr-describe", Use: "pr-describe",
Short: "Generate full PR description from current branch", Short: "Generate full PR description from current branch",
@ -20,32 +15,30 @@ var prDescribeCmd = &cobra.Command{
} }
func init() { func init() {
prDescribeCmd.Flags().StringP("base", "b", "master", "Base branch to compare against (default: master)") prDescribeCmd.Flags().StringP("base", "b", "master", "Base branch to compare against")
} }
func runPRDescribe(cmd *cobra.Command, _ []string) { func runPRDescribe(cmd *cobra.Command, _ []string) {
base, _ := cmd.Flags().GetString("base") base, _ := cmd.Flags().GetString("base")
// Prefer local base, fallback to origin/<base>. diff, err := gitRun([]string{"diff", fmt.Sprintf("%s..HEAD", base), "--no-color"})
diff, err := gitDiff([]string{"diff", fmt.Sprintf("%s..HEAD", base), "--no-color"}) if err != nil || diff == "" {
if err != nil || strings.TrimSpace(diff) == "" { diff, err = gitRun([]string{"diff", fmt.Sprintf("origin/%s..HEAD", base), "--no-color"})
diff, err = gitDiff([]string{"diff", fmt.Sprintf("origin/%s..HEAD", base), "--no-color"})
if err != nil { if err != nil {
color.Red("Failed to get branch diff: %v", err) color.Red("Failed to get branch diff: %v", err)
return return
} }
} }
if strings.TrimSpace(diff) == "" { if diff == "" {
color.Yellow("No changes on this branch compared to %s/origin/%s.", base, base) color.Yellow("No changes on this branch compared to %s/origin/%s.", base, base)
return return
} }
modelFlag, _ := cmd.Flags().GetString("model") modelFlag, _ := cmd.Flags().GetString("model")
model := config.GetModel("prdescribe", modelFlag) model := config.GetModel("prdescribe", modelFlag)
client := newGrokClient() client := newGrokClient()
messages := buildPRDescribeMessages(diff) messages := buildPRDescribeMessages(diff)
color.Yellow("Writing PR description (base=%s)...", base) color.Yellow("Writing PR description...")
client.Stream(messages, model) client.Stream(messages, model)
} }

View File

@ -47,13 +47,6 @@ func withMockGit(fn func([]string) (string, error)) func() {
return func() { gitRun = orig } return func() { gitRun = orig }
} }
// withMockGitDiff injects a fake for the new git.Diff (mockable var).
func withMockGitDiff(fn func([]string) (string, error)) func() {
orig := gitDiff
gitDiff = fn
return func() { gitDiff = orig }
}
// testCmd returns a minimal cobra command with common flags registered. // testCmd returns a minimal cobra command with common flags registered.
func testCmd() *cobra.Command { func testCmd() *cobra.Command {
c := &cobra.Command{} c := &cobra.Command{}
@ -267,7 +260,7 @@ func TestRunPRDescribe(t *testing.T) {
t.Run("no changes on branch — skips AI", func(t *testing.T) { t.Run("no changes on branch — skips AI", func(t *testing.T) {
mock := &mockStreamer{} mock := &mockStreamer{}
defer withMockClient(mock)() defer withMockClient(mock)()
defer withMockGitDiff(func(args []string) (string, error) { defer withMockGit(func(args []string) (string, error) {
return "", nil // both diff calls return empty return "", nil // both diff calls return empty
})() })()
@ -282,7 +275,7 @@ func TestRunPRDescribe(t *testing.T) {
mock := &mockStreamer{response: "## PR Title\n\nDescription"} mock := &mockStreamer{response: "## PR Title\n\nDescription"}
defer withMockClient(mock)() defer withMockClient(mock)()
callCount := 0 callCount := 0
defer withMockGitDiff(func(args []string) (string, error) { defer withMockGit(func(args []string) (string, error) {
callCount++ callCount++
if callCount == 1 { if callCount == 1 {
return "diff --git a/foo.go b/foo.go", nil return "diff --git a/foo.go b/foo.go", nil
@ -301,7 +294,7 @@ func TestRunPRDescribe(t *testing.T) {
mock := &mockStreamer{response: "PR description"} mock := &mockStreamer{response: "PR description"}
defer withMockClient(mock)() defer withMockClient(mock)()
callCount := 0 callCount := 0
defer withMockGitDiff(func(args []string) (string, error) { defer withMockGit(func(args []string) (string, error) {
callCount++ callCount++
if callCount == 2 { if callCount == 2 {
return "diff --git a/bar.go b/bar.go", nil return "diff --git a/bar.go b/bar.go", nil
@ -320,7 +313,7 @@ func TestRunPRDescribe(t *testing.T) {
mock := &mockStreamer{response: "PR description"} mock := &mockStreamer{response: "PR description"}
defer withMockClient(mock)() defer withMockClient(mock)()
var capturedArgs []string var capturedArgs []string
defer withMockGitDiff(func(args []string) (string, error) { defer withMockGit(func(args []string) (string, error) {
capturedArgs = args capturedArgs = args
return "diff content", nil return "diff content", nil
})() })()
@ -352,7 +345,7 @@ func TestRunPRDescribe(t *testing.T) {
mock := &mockStreamer{response: "PR description"} mock := &mockStreamer{response: "PR description"}
defer withMockClient(mock)() defer withMockClient(mock)()
var capturedArgs []string var capturedArgs []string
defer withMockGitDiff(func(args []string) (string, error) { defer withMockGit(func(args []string) (string, error) {
capturedArgs = args capturedArgs = args
return "diff content", nil return "diff content", nil
})() })()

View File

@ -1,7 +1,6 @@
package git package git
import ( import (
"errors"
"fmt" "fmt"
"os/exec" "os/exec"
"strings" "strings"
@ -61,24 +60,3 @@ func LogSince(since string) (string, error) {
args := []string{"log", "--pretty=format:%s%n%b%n---", since + "..HEAD"} args := []string{"log", "--pretty=format:%s%n%b%n---", since + "..HEAD"}
return Run(args) return Run(args)
} }
// Diff runs `git diff` and tolerates exit code 1 (differences found).
// This is normal git behavior when there *are* changes — unlike .Output().
func Diff(args []string) (string, error) {
cmdStr := "git " + strings.Join(args, " ")
logger.Debug("executing git diff", "command", cmdStr)
// nolint:gosec // intentional git subprocess
cmd := exec.Command("git", args...)
out, err := cmd.CombinedOutput()
if err != nil {
var exitErr *exec.ExitError
if errors.As(err, &exitErr) && exitErr.ExitCode() == 1 {
// legitimate diff (changes exist)
return string(out), nil
}
logger.Error("git diff failed", "command", cmdStr, "error", err)
return "", fmt.Errorf("git diff failed: %w", err)
}
return string(out), nil
}