Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 36 additions & 6 deletions cmd/review.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package cmd

import (
"context"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"

"github.com/gifflet/git-review/internal/ai"
)

// formatCommitHash truncates the commit hash to 7 characters
Expand All @@ -18,11 +21,17 @@ func formatCommitHash(hash string) string {

// executeReview executes the main logic of the program
func executeReview() {
provider, err := AppConfig.GetAIProvider()
if err != nil {
fmt.Printf("Error getting AI provider: %v\n", err)
os.Exit(1)
}

// Format commit hashes
initialCommit = formatCommitHash(initialCommit)

// Convert relative path to absolute if needed
absProjectPath, err := filepath.Abs(projectPath)
absProjectPath, err := filepath.Abs(ProjectPath)
if err != nil {
fmt.Printf("Error resolving project path: %v\n", err)
os.Exit(1)
Expand Down Expand Up @@ -75,7 +84,7 @@ func executeReview() {
os.Exit(0)
}

// For each file, extract and save the diff
// For each file, extract diff and generate review
for _, file := range files {
diffCmd := exec.Command("git", "diff", initialCommit, finalCommit, "--", file)
diffCmd.Dir = absProjectPath
Expand All @@ -87,19 +96,40 @@ func executeReview() {

// Create output filename (replacing '/' with '_')
safeFileName := strings.ReplaceAll(file, "/", "_")
outputPath := filepath.Join(dirName, safeFileName+".diff")
diffOutputPath := filepath.Join(dirName, safeFileName+".diff")
reviewOutputPath := filepath.Join(dirName, safeFileName+".review")

// Save diff to file
err = os.WriteFile(outputPath, diff, 0644)
err = os.WriteFile(diffOutputPath, diff, 0644)
if err != nil {
fmt.Printf("Error saving diff for file %s: %v\n", file, err)
continue
}

fmt.Printf("Diff saved for: %s\n", file)
// Generate review using AI
reviews, err := ai.Review(context.Background(), provider, file, string(diff))
if err != nil {
fmt.Printf("Error generating review for file %s: %v\n", file, err)
continue
}

// Format review output
var reviewOutput strings.Builder
for _, review := range reviews {
reviewOutput.WriteString(fmt.Sprintf("Line %d:\n%s\n\n", review.LinePosition, review.Comment))
}

// Save review to file
err = os.WriteFile(reviewOutputPath, []byte(reviewOutput.String()), 0644)
if err != nil {
fmt.Printf("Error saving review for file %s: %v\n", file, err)
continue
}

fmt.Printf("Review generated for: %s\n", file)
}

fmt.Printf("\nAll diffs have been saved to directory: %s\n", dirName)
fmt.Printf("\nAll reviews have been saved to directory: %s\n", dirName)
}

// getModifiedFiles gets the list of files modified between two commits
Expand Down
19 changes: 17 additions & 2 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"os"

"github.com/gifflet/git-review/internal/config"
"github.com/spf13/cobra"
)

Expand All @@ -12,12 +13,15 @@ var (
initialCommit string
finalCommit string
mainBranch string
projectPath string
ProjectPath string
outputDir string

// Application version
AppVersion = "dev"

// Configuration
AppConfig *config.Config

// Root command
rootCmd = &cobra.Command{
Use: "git-review",
Expand Down Expand Up @@ -49,6 +53,17 @@ changes between Git commits.`,
}
)

// SetConfig sets the application configuration
func SetConfig(cfg *config.Config) {
AppConfig = cfg
}

// InitializeFlags parses command line flags before executing the main command
func InitializeFlags() error {
// Parse flags without executing the root command
return rootCmd.ParseFlags(os.Args[1:])
}

// Execute adds all child commands to the root command and sets flags
func Execute() {
if err := rootCmd.Execute(); err != nil {
Expand All @@ -62,7 +77,7 @@ func init() {
rootCmd.Flags().StringVarP(&initialCommit, "initial", "i", "", "Starting commit hash for comparison (required)")
rootCmd.Flags().StringVarP(&finalCommit, "final", "f", "HEAD", "Ending commit hash (defaults to HEAD)")
rootCmd.Flags().StringVarP(&mainBranch, "main-branch", "m", "", "Main branch for refined comparisons")
rootCmd.Flags().StringVarP(&projectPath, "project-path", "p", ".", "Project directory path")
rootCmd.Flags().StringVarP(&ProjectPath, "project-path", "p", ".", "Project directory path")
rootCmd.Flags().StringVarP(&outputDir, "output-dir", "o", "git-review", "Output directory for diff files")

// Mark initialCommit as required
Expand Down
30 changes: 28 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,9 +1,35 @@
module github.com/gifflet/git-review

go 1.20
go 1.22.0

toolchain go1.22.6

require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/spf13/cobra v1.9.1
github.com/spf13/viper v1.20.0
github.com/stretchr/testify v1.10.0
github.com/tmc/langchaingo v0.1.13
)

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dlclark/regexp2 v1.10.0 // indirect
github.com/fsnotify/fsnotify v1.8.0 // indirect
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/pkoukk/tiktoken-go v0.1.6 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/sagikazarmark/locafero v0.7.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.12.0 // indirect
github.com/spf13/cast v1.7.1 // indirect
github.com/spf13/pflag v1.0.6 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.9.0 // indirect
golang.org/x/sys v0.29.0 // indirect
golang.org/x/text v0.21.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
60 changes: 60 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,10 +1,70 @@
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0=
github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
github.com/pkoukk/tiktoken-go v0.1.6 h1:JF0TlJzhTbrI30wCvFuiw6FzP2+/bR+FIxUdgEAcUsw=
github.com/pkoukk/tiktoken-go v0.1.6/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo=
github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs=
github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4=
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.20.0 h1:zrxIyR3RQIOsarIrgL8+sAvALXul9jeEPa06Y0Ph6vY=
github.com/spf13/viper v1.20.0/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/tmc/langchaingo v0.1.13 h1:rcpMWBIi2y3B90XxfE4Ao8dhCQPVDMaNPnN5cGB1CaA=
github.com/tmc/langchaingo v0.1.13/go.mod h1:vpQ5NOIhpzxDfTZK9B6tf2GM/MoaHewPWM5KXXGh7hg=
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=
86 changes: 86 additions & 0 deletions internal/ai/ai_review.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package ai

import (
"context"
"encoding/json"
"fmt"
"strconv"
"strings"

"github.com/gifflet/git-review/internal/types"
)

// Review generates a list of code review comments for a given file diff
func Review(ctx context.Context, provider types.Provider, filePath string, fileDiff string) (types.ReviewList, error) {
// Create the user message with the file diff
userMessage := fmt.Sprintf("Please review the following changes in file %s:\n\n%s", filePath, fileDiff)

// Generate the review using the AI provider
response, err := provider.Generate(ctx, userMessage)
if err != nil {
return nil, fmt.Errorf("failed to generate review: %w", err)
}

// Parse the response into a list of reviews
var reviews types.ReviewList

// Try to parse the response as a JSON array first
if err := json.Unmarshal([]byte(response), &reviews); err != nil {
// If JSON parsing fails, try to parse the response as a text format
reviews = parseTextResponse(response, filePath)
}

return reviews, nil
}

// parseTextResponse parses a text response into a list of reviews
func parseTextResponse(response string, filePath string) types.ReviewList {
var reviews types.ReviewList
lines := strings.Split(response, "\n")

var currentReview *types.Review
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" {
continue
}

// Check if line contains a line number
if strings.Contains(line, "Line") || strings.Contains(line, "line") {
// If we have a previous review, add it to the list
if currentReview != nil {
reviews = append(reviews, *currentReview)
}

// Create a new review
currentReview = &types.Review{
FilePath: filePath,
}

// Try to extract line number using a more robust approach
parts := strings.Split(line, ":")
if len(parts) > 0 {
numStr := strings.TrimSpace(strings.ToLower(parts[0]))
numStr = strings.TrimPrefix(numStr, "line")
numStr = strings.TrimSpace(numStr)
if num, err := strconv.Atoi(numStr); err == nil {
currentReview.LinePosition = num
}
}
} else if currentReview != nil {
// Append the line to the current review's comment
if currentReview.Comment == "" {
currentReview.Comment = line
} else {
currentReview.Comment += "\n" + line
}
}
}

// Add the last review if exists
if currentReview != nil {
reviews = append(reviews, *currentReview)
}

return reviews
}
Loading