From 91a19667e9b1c76dfd6225b1ad8540b5b6720078 Mon Sep 17 00:00:00 2001 From: gifflet Date: Thu, 27 Mar 2025 22:17:33 -0300 Subject: [PATCH 1/5] feat: initial implementation of AI-powered code review integration with suggested changes --- cmd/root.go | 9 +++++ go.mod | 21 ++++++++++- go.sum | 32 ++++++++++++++++ internal/config/config.go | 77 +++++++++++++++++++++++++++++++++++++++ main.go | 14 +++++++ 5 files changed, 151 insertions(+), 2 deletions(-) create mode 100644 internal/config/config.go diff --git a/cmd/root.go b/cmd/root.go index fce2f3a..63ced47 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -4,6 +4,7 @@ import ( "fmt" "os" + "github.com/gifflet/git-review/internal/config" "github.com/spf13/cobra" ) @@ -18,6 +19,9 @@ var ( // Application version AppVersion = "dev" + // Configuration + appConfig *config.Config + // Root command rootCmd = &cobra.Command{ Use: "git-review", @@ -49,6 +53,11 @@ changes between Git commits.`, } ) +// SetConfig sets the application configuration +func SetConfig(cfg *config.Config) { + appConfig = cfg +} + // Execute adds all child commands to the root command and sets flags func Execute() { if err := rootCmd.Execute(); err != nil { diff --git a/go.mod b/go.mod index d73aacd..e0f4798 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,26 @@ module github.com/gifflet/git-review -go 1.20 +go 1.21.0 + +toolchain go1.22.6 + +require github.com/spf13/cobra v1.9.1 require ( + github.com/fsnotify/fsnotify v1.8.0 // indirect + github.com/go-viper/mapstructure/v2 v2.2.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/spf13/cobra v1.9.1 + github.com/pelletier/go-toml/v2 v2.2.3 // 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/spf13/viper v1.20.0 // 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 ) diff --git a/go.sum b/go.sum index ffae55e..b7bf952 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,42 @@ 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/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +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/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +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/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..49b5a1b --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,77 @@ +package config + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/spf13/viper" +) + +// Config holds all configuration for the application +type Config struct { + AI struct { + Provider string `mapstructure:"provider"` + OpenAI struct { + Token string `mapstructure:"token"` + Model string `mapstructure:"model" default:"gpt-4o"` + } `mapstructure:"openai"` + } `mapstructure:"ai"` +} + +// LoadConfig reads configuration from files and environment variables +func LoadConfig() (*Config, error) { + v := viper.New() + + // Set config name and type + v.SetConfigName("config") + v.SetConfigType("yaml") + + // Add global config paths + homeDir, err := os.UserHomeDir() + if err != nil { + return nil, fmt.Errorf("failed to get user home directory: %w", err) + } + + // Linux/Mac global config path + v.AddConfigPath(filepath.Join(homeDir, ".config", "git-review")) + + // Windows global config path + v.AddConfigPath(filepath.Join(homeDir, "AppData", "Roaming", "git-review")) + + // Local project config + v.AddConfigPath(".") + v.SetConfigName(".gitreview") + + // Read config + if err := v.ReadInConfig(); err != nil { + if _, ok := err.(viper.ConfigFileNotFoundError); ok { + return nil, fmt.Errorf("configuration file not found. Please create one of the following files:\n"+ + "- %s\n"+ + "- %s\n"+ + "- %s\n"+ + "\nExample configuration:\n"+ + "ai:\n"+ + " provider: \"openai\"\n"+ + " openai:\n"+ + " token: \"your-token-here\"\n"+ + " model: \"gpt-4o\"", + filepath.Join(homeDir, ".config", "git-review", "config.yaml"), + filepath.Join(homeDir, "AppData", "Roaming", "git-review", "config.yaml"), + ".gitreview.yaml") + } + return nil, fmt.Errorf("failed to read config file: %w", err) + } + + // Set environment variable prefix + v.SetEnvPrefix("GIT_REVIEW") + v.AutomaticEnv() + + // Create config struct + config := &Config{} + if err := v.Unmarshal(config); err != nil { + return nil, fmt.Errorf("failed to unmarshal config: %w", err) + } + + return config, nil +} diff --git a/main.go b/main.go index dacd009..4150c05 100644 --- a/main.go +++ b/main.go @@ -1,16 +1,30 @@ package main import ( + "fmt" + "os" + "github.com/gifflet/git-review/cmd" + "github.com/gifflet/git-review/internal/config" ) // AppVersion is defined during compilation var AppVersion = "dev" func main() { + // Load configuration + cfg, err := config.LoadConfig() + if err != nil { + fmt.Printf("Error loading config: %v\n", err) + os.Exit(1) + } + // Set the application version cmd.AppVersion = AppVersion + // Set config in cmd package + cmd.SetConfig(cfg) + // Execute the root command cmd.Execute() } From 7899e5f6348e2cec01dd8ebe9073ba0b18eeeddd Mon Sep 17 00:00:00 2001 From: gifflet Date: Sat, 29 Mar 2025 22:20:28 -0300 Subject: [PATCH 2/5] refactor(config): pass project path explicitly to LoadConfig - Changed 'LoadConfig' to accept 'projectPath' as a parameter for better flexibility. - Updated flag variable 'projectPath' to 'ProjectPath' to align with Go naming conventions. - Introduced 'InitializeFlags' function to parse command-line flags before execution. - Adjusted 'main.go' to call 'InitializeFlags' before loading configuration. --- cmd/review.go | 2 +- cmd/root.go | 10 ++++++++-- internal/config/config.go | 4 ++-- main.go | 10 ++++++++-- 4 files changed, 19 insertions(+), 7 deletions(-) diff --git a/cmd/review.go b/cmd/review.go index 4ebdc0b..3b7a50b 100644 --- a/cmd/review.go +++ b/cmd/review.go @@ -22,7 +22,7 @@ func executeReview() { 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) diff --git a/cmd/root.go b/cmd/root.go index 63ced47..07e346d 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -13,7 +13,7 @@ var ( initialCommit string finalCommit string mainBranch string - projectPath string + ProjectPath string outputDir string // Application version @@ -58,6 +58,12 @@ 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 { @@ -71,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 diff --git a/internal/config/config.go b/internal/config/config.go index 49b5a1b..e90d41e 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -20,7 +20,7 @@ type Config struct { } // LoadConfig reads configuration from files and environment variables -func LoadConfig() (*Config, error) { +func LoadConfig(projectPath string) (*Config, error) { v := viper.New() // Set config name and type @@ -40,7 +40,7 @@ func LoadConfig() (*Config, error) { v.AddConfigPath(filepath.Join(homeDir, "AppData", "Roaming", "git-review")) // Local project config - v.AddConfigPath(".") + v.AddConfigPath(projectPath) v.SetConfigName(".gitreview") // Read config diff --git a/main.go b/main.go index 4150c05..942f10d 100644 --- a/main.go +++ b/main.go @@ -12,8 +12,14 @@ import ( var AppVersion = "dev" func main() { - // Load configuration - cfg, err := config.LoadConfig() + // Initialize command flags before loading config + if err := cmd.InitializeFlags(); err != nil { + fmt.Printf("Error initializing flags: %v\n", err) + os.Exit(1) + } + + // Load configuration with project path + cfg, err := config.LoadConfig(cmd.ProjectPath) if err != nil { fmt.Printf("Error loading config: %v\n", err) os.Exit(1) From d669c2073d3699e437a2225c2154df51ffcfeac0 Mon Sep 17 00:00:00 2001 From: gifflet Date: Sun, 30 Mar 2025 17:08:07 -0300 Subject: [PATCH 3/5] feat(ai): integrate OpenAI provider and configuration - Added OpenAI provider with functionality to generate text based on a prompt. - Introduced an AI provider interface for easy addition of future AI providers. - Updated configuration to support AI provider setup. - Bumped Go version to 1.22.0 and updated dependencies. --- go.mod | 12 +++++++++--- go.sum | 28 ++++++++++++++++++++++++++++ internal/ai/openai.go | 27 +++++++++++++++++++++++++++ internal/ai/provider.go | 21 +++++++++++++++++++++ internal/config/config.go | 7 +++++++ 5 files changed, 92 insertions(+), 3 deletions(-) create mode 100644 internal/ai/openai.go create mode 100644 internal/ai/provider.go diff --git a/go.mod b/go.mod index e0f4798..e324b6e 100644 --- a/go.mod +++ b/go.mod @@ -1,22 +1,28 @@ module github.com/gifflet/git-review -go 1.21.0 +go 1.22.0 toolchain go1.22.6 -require github.com/spf13/cobra v1.9.1 +require ( + github.com/spf13/cobra v1.9.1 + github.com/spf13/viper v1.20.0 + github.com/tmc/langchaingo v0.1.13 +) require ( + 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/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/spf13/viper v1.20.0 // 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 diff --git a/go.sum b/go.sum index b7bf952..f4b98fc 100644 --- a/go.sum +++ b/go.sum @@ -1,15 +1,33 @@ 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= @@ -27,8 +45,12 @@ 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= @@ -38,5 +60,11 @@ 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= diff --git a/internal/ai/openai.go b/internal/ai/openai.go new file mode 100644 index 0000000..e685c8f --- /dev/null +++ b/internal/ai/openai.go @@ -0,0 +1,27 @@ +package ai + +import ( + "context" + + "github.com/tmc/langchaingo/llms" + "github.com/tmc/langchaingo/llms/openai" +) + +type OpenAIProvider struct { + llm llms.LLM +} + +func NewOpenAIProvider(token, model string) (Provider, error) { + llm, err := openai.New( + openai.WithToken(token), + openai.WithModel(model), + ) + if err != nil { + return nil, err + } + return &OpenAIProvider{llm: llm}, nil +} + +func (p *OpenAIProvider) Generate(ctx context.Context, prompt string) (string, error) { + return llms.GenerateFromSinglePrompt(ctx, p.llm, prompt) +} diff --git a/internal/ai/provider.go b/internal/ai/provider.go new file mode 100644 index 0000000..960a2d8 --- /dev/null +++ b/internal/ai/provider.go @@ -0,0 +1,21 @@ +package ai + +import ( + "context" + "fmt" +) + +// Provider defines the interface for AI providers +type Provider interface { + Generate(ctx context.Context, prompt string) (string, error) +} + +// NewProvider creates a new AI provider based on configuration +func NewProvider(providerName, token, model string) (Provider, error) { + switch providerName { + case "openai": + return NewOpenAIProvider(token, model) + default: + return nil, fmt.Errorf("unsupported AI provider: %s", providerName) + } +} diff --git a/internal/config/config.go b/internal/config/config.go index e90d41e..09795fd 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -5,6 +5,7 @@ import ( "os" "path/filepath" + "github.com/gifflet/git-review/internal/ai" "github.com/spf13/viper" ) @@ -19,6 +20,12 @@ type Config struct { } `mapstructure:"ai"` } +// GetAIProvider creates a new AI provider based on configuration +func (c *Config) GetAIProvider() (ai.Provider, error) { + cfg := c.AI + return ai.NewProvider(cfg.Provider, cfg.OpenAI.Token, cfg.OpenAI.Model) +} + // LoadConfig reads configuration from files and environment variables func LoadConfig(projectPath string) (*Config, error) { v := viper.New() From 697fd045aa6f7481e503e728ee97923fe61765de Mon Sep 17 00:00:00 2001 From: gifflet Date: Mon, 31 Mar 2025 17:39:54 -0300 Subject: [PATCH 4/5] feat(ai): improve AI provider integration and configuration handling - Introduced 'systemPrompt' in 'OpenAIProvider' to enable configurable system messages - Refactored 'NewOpenAIProvider' to accept 'systemPrompt' for more contextual AI responses - Replaced 'Provider' interface definition in 'ai/provider.go' with a structured 'types.Provider' - Enhanced 'Config' to expose getter methods for AI configuration values - Added default values for AI settings, including a default system prompt - Created 'internal/types/common.go' to define AI provider interface and configuration abstraction --- cmd/review.go | 6 ++++++ cmd/root.go | 4 ++-- internal/ai/openai.go | 21 +++++++++++++++++---- internal/ai/provider.go | 12 ++++-------- internal/config/config.go | 26 ++++++++++++++++++++++---- internal/types/common.go | 15 +++++++++++++++ 6 files changed, 66 insertions(+), 18 deletions(-) create mode 100644 internal/types/common.go diff --git a/cmd/review.go b/cmd/review.go index 3b7a50b..c9ee663 100644 --- a/cmd/review.go +++ b/cmd/review.go @@ -18,6 +18,12 @@ 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) diff --git a/cmd/root.go b/cmd/root.go index 07e346d..af673d5 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -20,7 +20,7 @@ var ( AppVersion = "dev" // Configuration - appConfig *config.Config + AppConfig *config.Config // Root command rootCmd = &cobra.Command{ @@ -55,7 +55,7 @@ changes between Git commits.`, // SetConfig sets the application configuration func SetConfig(cfg *config.Config) { - appConfig = cfg + AppConfig = cfg } // InitializeFlags parses command line flags before executing the main command diff --git a/internal/ai/openai.go b/internal/ai/openai.go index e685c8f..c5f8ba2 100644 --- a/internal/ai/openai.go +++ b/internal/ai/openai.go @@ -3,15 +3,17 @@ package ai import ( "context" + "github.com/gifflet/git-review/internal/types" "github.com/tmc/langchaingo/llms" "github.com/tmc/langchaingo/llms/openai" ) type OpenAIProvider struct { - llm llms.LLM + llm llms.LLM + systemPrompt string } -func NewOpenAIProvider(token, model string) (Provider, error) { +func NewOpenAIProvider(token, model, systemPrompt string) (types.Provider, error) { llm, err := openai.New( openai.WithToken(token), openai.WithModel(model), @@ -19,9 +21,20 @@ func NewOpenAIProvider(token, model string) (Provider, error) { if err != nil { return nil, err } - return &OpenAIProvider{llm: llm}, nil + return &OpenAIProvider{ + llm: llm, + systemPrompt: systemPrompt, + }, nil } func (p *OpenAIProvider) Generate(ctx context.Context, prompt string) (string, error) { - return llms.GenerateFromSinglePrompt(ctx, p.llm, prompt) + content := []llms.MessageContent{ + llms.TextParts(llms.ChatMessageTypeSystem, p.systemPrompt), + llms.TextParts(llms.ChatMessageTypeHuman, prompt), + } + completion, err := p.llm.GenerateContent(ctx, content) + if err != nil { + return "", err + } + return completion.Choices[0].Content, nil } diff --git a/internal/ai/provider.go b/internal/ai/provider.go index 960a2d8..f99dd93 100644 --- a/internal/ai/provider.go +++ b/internal/ai/provider.go @@ -1,20 +1,16 @@ package ai import ( - "context" "fmt" -) -// Provider defines the interface for AI providers -type Provider interface { - Generate(ctx context.Context, prompt string) (string, error) -} + "github.com/gifflet/git-review/internal/types" +) // NewProvider creates a new AI provider based on configuration -func NewProvider(providerName, token, model string) (Provider, error) { +func NewProvider(providerName string, c types.AIConfig) (types.Provider, error) { switch providerName { case "openai": - return NewOpenAIProvider(token, model) + return NewOpenAIProvider(c.GetOpenAIToken(), c.GetOpenAIModel(), c.GetSystemPrompt()) default: return nil, fmt.Errorf("unsupported AI provider: %s", providerName) } diff --git a/internal/config/config.go b/internal/config/config.go index 09795fd..1f0b49e 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -6,6 +6,7 @@ import ( "path/filepath" "github.com/gifflet/git-review/internal/ai" + "github.com/gifflet/git-review/internal/types" "github.com/spf13/viper" ) @@ -15,15 +16,27 @@ type Config struct { Provider string `mapstructure:"provider"` OpenAI struct { Token string `mapstructure:"token"` - Model string `mapstructure:"model" default:"gpt-4o"` + Model string `mapstructure:"model"` } `mapstructure:"openai"` + SystemPrompt string `mapstructure:"system_prompt"` } `mapstructure:"ai"` } +func (c *Config) GetOpenAIToken() string { + return c.AI.OpenAI.Token +} + +func (c *Config) GetOpenAIModel() string { + return c.AI.OpenAI.Model +} + +func (c *Config) GetSystemPrompt() string { + return c.AI.SystemPrompt +} + // GetAIProvider creates a new AI provider based on configuration -func (c *Config) GetAIProvider() (ai.Provider, error) { - cfg := c.AI - return ai.NewProvider(cfg.Provider, cfg.OpenAI.Token, cfg.OpenAI.Model) +func (c *Config) GetAIProvider() (types.Provider, error) { + return ai.NewProvider(c.AI.Provider, c) } // LoadConfig reads configuration from files and environment variables @@ -34,6 +47,11 @@ func LoadConfig(projectPath string) (*Config, error) { v.SetConfigName("config") v.SetConfigType("yaml") + // Set default values + v.SetDefault("ai.provider", "openai") + v.SetDefault("ai.openai.model", "gpt-4o") + v.SetDefault("ai.system_prompt", "You are a helpful assistant that reviews code changes. You are given a git diff with the changes made to the code. You need to review the changes and provide a list of potential improvements.") + // Add global config paths homeDir, err := os.UserHomeDir() if err != nil { diff --git a/internal/types/common.go b/internal/types/common.go new file mode 100644 index 0000000..4064224 --- /dev/null +++ b/internal/types/common.go @@ -0,0 +1,15 @@ +package types + +import "context" + +// Provider defines the interface for AI providers +type Provider interface { + Generate(ctx context.Context, prompt string) (string, error) +} + +// AIConfig holds the configuration for AI providers +type AIConfig interface { + GetOpenAIToken() string + GetOpenAIModel() string + GetSystemPrompt() string +} From 31ce715965ac21b6c4664b277ff2003a1bf9af57 Mon Sep 17 00:00:00 2001 From: gifflet Date: Sat, 12 Apr 2025 22:32:34 -0300 Subject: [PATCH 5/5] feat: implement AI code review functionality - Add Review struct and ReviewList type - Implement GetSystemPrompt with default prompt - Create Review function for AI-powered code analysis - Add comprehensive tests for review functionality - Update review command to use AI provider - Handle both JSON and text format responses --- cmd/review.go | 44 ++++++++--- go.mod | 3 + internal/ai/ai_review.go | 86 +++++++++++++++++++++ internal/ai/ai_review_test.go | 139 ++++++++++++++++++++++++++++++++++ internal/config/config.go | 18 +++++ internal/types/common.go | 10 +++ 6 files changed, 290 insertions(+), 10 deletions(-) create mode 100644 internal/ai/ai_review.go create mode 100644 internal/ai/ai_review_test.go diff --git a/cmd/review.go b/cmd/review.go index c9ee663..33fe540 100644 --- a/cmd/review.go +++ b/cmd/review.go @@ -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 @@ -18,11 +21,11 @@ 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) - // } + 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) @@ -81,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 @@ -93,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 diff --git a/go.mod b/go.mod index e324b6e..5878f3a 100644 --- a/go.mod +++ b/go.mod @@ -7,10 +7,12 @@ toolchain go1.22.6 require ( 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 @@ -18,6 +20,7 @@ require ( 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 diff --git a/internal/ai/ai_review.go b/internal/ai/ai_review.go new file mode 100644 index 0000000..25bba97 --- /dev/null +++ b/internal/ai/ai_review.go @@ -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 +} diff --git a/internal/ai/ai_review_test.go b/internal/ai/ai_review_test.go new file mode 100644 index 0000000..e16a1da --- /dev/null +++ b/internal/ai/ai_review_test.go @@ -0,0 +1,139 @@ +package ai + +import ( + "context" + "testing" + + "github.com/gifflet/git-review/internal/types" + "github.com/stretchr/testify/assert" +) + +// MockProvider implements types.Provider for testing +type MockProvider struct { + response string + err error +} + +func (m *MockProvider) Generate(ctx context.Context, prompt string) (string, error) { + if m.err != nil { + return "", m.err + } + return m.response, nil +} + +func TestReview(t *testing.T) { + tests := []struct { + name string + filePath string + fileDiff string + mockResponse string + expectedError error + expectedReview types.ReviewList + }{ + { + name: "successful JSON response", + filePath: "test.go", + fileDiff: "test diff content", + mockResponse: `[ + { + "Comment": "Consider using a more descriptive variable name", + "FilePath": "test.go", + "LinePosition": 10 + } + ]`, + expectedError: nil, + expectedReview: types.ReviewList{ + { + Comment: "Consider using a more descriptive variable name", + FilePath: "test.go", + LinePosition: 10, + }, + }, + }, + { + name: "successful text response", + filePath: "test.go", + fileDiff: "test diff content", + mockResponse: `Line 10: +Consider using a more descriptive variable name`, + expectedError: nil, + expectedReview: types.ReviewList{ + { + Comment: "Consider using a more descriptive variable name", + FilePath: "test.go", + LinePosition: 10, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockProvider := &MockProvider{ + response: tt.mockResponse, + err: tt.expectedError, + } + + reviews, err := Review(context.Background(), mockProvider, tt.filePath, tt.fileDiff) + + if tt.expectedError != nil { + assert.Error(t, err) + assert.Equal(t, tt.expectedError, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expectedReview, reviews) + } + }) + } +} + +func TestParseTextResponse(t *testing.T) { + tests := []struct { + name string + response string + filePath string + expectedReview types.ReviewList + }{ + { + name: "single review", + response: `Line 10: +Consider using a more descriptive variable name`, + filePath: "test.go", + expectedReview: types.ReviewList{ + { + Comment: "Consider using a more descriptive variable name", + FilePath: "test.go", + LinePosition: 10, + }, + }, + }, + { + name: "multiple reviews", + response: `Line 10: +Consider using a more descriptive variable name + +Line 20: +This function could be simplified`, + filePath: "test.go", + expectedReview: types.ReviewList{ + { + Comment: "Consider using a more descriptive variable name", + FilePath: "test.go", + LinePosition: 10, + }, + { + Comment: "This function could be simplified", + FilePath: "test.go", + LinePosition: 20, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + reviews := parseTextResponse(tt.response, tt.filePath) + assert.Equal(t, tt.expectedReview, reviews) + }) + } +} diff --git a/internal/config/config.go b/internal/config/config.go index 1f0b49e..3945d09 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -31,6 +31,24 @@ func (c *Config) GetOpenAIModel() string { } func (c *Config) GetSystemPrompt() string { + if c.AI.SystemPrompt == "" { + return `You are a code review assistant. Your task is to review code changes and provide constructive feedback. + +Please analyze the provided git diff and suggest improvements focusing on: +1. Code quality and best practices +2. Potential bugs or issues +3. Performance considerations +4. Security implications +5. Maintainability and readability + +Format your response as a list of reviews, where each review contains: +- The file path +- The line position in the file +- A clear and constructive comment explaining the suggested improvement + +Only suggest meaningful, high-impact changes that would significantly improve the code. +Avoid nitpicking or suggesting minor stylistic changes unless they significantly impact readability.` + } return c.AI.SystemPrompt } diff --git a/internal/types/common.go b/internal/types/common.go index 4064224..9e69be0 100644 --- a/internal/types/common.go +++ b/internal/types/common.go @@ -13,3 +13,13 @@ type AIConfig interface { GetOpenAIModel() string GetSystemPrompt() string } + +// Review represents a single review comment for a specific file and line +type Review struct { + Comment string + FilePath string + LinePosition int +} + +// ReviewList represents a list of reviews +type ReviewList []Review