From fd0d2f77315b25b1e4a573c2ec9a8f472559afa7 Mon Sep 17 00:00:00 2001 From: kevin_mesiab Date: Tue, 16 Jan 2024 19:33:35 -0800 Subject: [PATCH] Initial project. Seems to be working ok. --- Makefile | 4 + README.md | 61 ++++++++------ github_diff_client.go | 28 +++++++ github_diff_client_mock.go | 69 ++++++++++++++++ go.mod | 23 ++++++ go.sum | 78 ++++++++++++++++++ main.go | 162 +++++++++++++++++++++++++++++++++++++ main_test.go | 42 ++++++++++ openai.go | 61 ++++++++++++++ 9 files changed, 502 insertions(+), 26 deletions(-) create mode 100644 github_diff_client.go create mode 100644 github_diff_client_mock.go create mode 100644 go.sum create mode 100644 main.go create mode 100644 main_test.go create mode 100644 openai.go diff --git a/Makefile b/Makefile index f4579ed..89f6664 100644 --- a/Makefile +++ b/Makefile @@ -34,6 +34,10 @@ test-verbose: test-race: CGO_ENABLED=1 go test -race -cover ./... +convey: + @echo "๐Ÿงช Conveying tests in browser..." + goconvey -excludedDirs=vendor + # Tooling install-tools: @echo "๐Ÿ› ๏ธ Installing tools" diff --git a/README.md b/README.md index 365564e..f4ffacd 100644 --- a/README.md +++ b/README.md @@ -9,19 +9,23 @@ ## Overview ๐ŸŒŸ -Cadre CLI is a command-line application designed to automate code reviews across various programming -languages, utilizing OpenAI's ChatGPT API. It offers intelligent insights and suggestions to +Cadre CLI is a command-line application designed to automate code +reviews across various programming languages, utilizing OpenAI's +ChatGPT API. It offers intelligent insights and suggestions to improve code quality and developer efficiency. ## Features ๐Ÿ› ๏ธ -- **Language-Agnostic Analysis**: Compatible with multiple programming languages. +- **Language-Agnostic Analysis**: Compatible with multiple programming +languages. - **AI-Powered Insights**: Employs ChatGPT for in-depth code analysis. -- **User-Friendly CLI**: Simple and intuitive command-line interface for easy usage. +- **User-Friendly CLI**: Simple and intuitive command-line interface +for easy usage. ## Installation ๐Ÿ”ง -To install Cadre CLI, you need to have Go installed on your machine. Follow these steps: +To install Cadre CLI, you need to have Go installed on your machine. +Follow these steps: ```bash go install github.com/kmesiab/cadre@latest @@ -33,8 +37,9 @@ Set your OpenAI API Key: export OPENAI_API_KEY=sk-[SECRET] ``` -Set your Ignore Files. These are file types that will be excluded from code reviews. They should be a -comma-separated list of file extensions. For example: +Set your Ignore Files. These are file types that will be excluded from +code reviews. They should be a comma-separated list of file extensions. +For example: ```bash export IGNORE_FILES=.mod,.sum @@ -48,7 +53,8 @@ cadre ## Usage ๐Ÿ’ก -**Usage instructions for Cadre CLI go here. Provide examples and explain how users can interact with it.** +**Usage instructions for Cadre CLI go here. Provide examples and explain +how users can interact with it.** ## Development and Testing ๐Ÿงช @@ -85,41 +91,43 @@ make lint-markdown ### Forking and Sending a Pull Request -1. **Fork the Repository**: Click the 'Fork' button at the top right of this page. +1. **Fork the Repository**: Click the 'Fork' button at the top right of this + page. 2. **Clone Your Fork**: -```bash -git clone https://github.com/kmesiab/cadre -cd cadre -``` + ```bash + git clone https://github.com/kmesiab/cadre + cd cadre + ``` 3. **Create a New Branch**: -```bash -git checkout -b your-branch-name -``` + ```bash + git checkout -b your-branch-name + ``` 4. **Make Your Changes**: Implement your changes or fix issues. 5. **Commit and Push**: -```bash -git commit -m "Add your commit message" -git push origin your-branch-name -``` + ```bash + git commit -m "Add your commit message" + git push origin your-branch-name + ``` -6. **Create a Pull Request**: Go to your fork on GitHub and click the 'Compare & pull request' button. +6. **Create a Pull Request**: Go to your fork on GitHub and click the + 'Compare & pull request' button. ## Github Guidelines -Please ensure your code adheres to the project's -[standards and guidelines](https://github.com/kmesiab/cadre/discussions/). +Please ensure your code adheres to the project's +[standards and guidelines](https://github.com/kmesiab/ai-code-critic/discussions/24). ### Quick Tips Run `make lint` before committing to ensure your code is properly formatted. 1. **Always rebase, never merge commit** -2. Always use a descriptive commit message +2. Always use a description commit message 3. Separate your title from your description 4. Keep commit messages under 50 characters 5. Start your branch with `feat|bugfix|docs|style|refactor|perf|test` @@ -127,8 +135,9 @@ Run `make lint` before committing to ensure your code is properly formatted. ## License ๐Ÿ“ -Information regarding the licensing of Cadre CLI will be included here. +Information regarding the licensing of cadre will be included here. --- -*Note: This project is under active development. Additional features and documentation will be updated in due course.* ๐ŸŒˆ +*Note: This project is under active development. Additional features +and documentation will be updated in due course.* ๐ŸŒˆ diff --git a/github_diff_client.go b/github_diff_client.go new file mode 100644 index 0000000..0084ea8 --- /dev/null +++ b/github_diff_client.go @@ -0,0 +1,28 @@ +package main + +import ( + "context" + + "github.com/google/go-github/v57/github" + diff "github.com/kmesiab/go-github-diff" +) + +// GithubDiffClient is a wrapper around the go-github-diff +// client, such that it implements the GithubDiffClientInterface +type GithubDiffClient struct{} + +func (c *GithubDiffClient) ParsePullRequestURL(url string) (*diff.PullRequestURL, error) { + return diff.ParsePullRequestURL(url) +} + +func (c *GithubDiffClient) GetPullRequest( + ctx context.Context, + pullRequestURL *diff.PullRequestURL, + ghClient *github.Client, +) (string, error) { + return diff.GetPullRequest(ctx, pullRequestURL, ghClient) +} + +func (c *GithubDiffClient) ParseGitDiff(diffString string, ignoreFiles []string) []*diff.GitDiff { + return diff.ParseGitDiff(diffString, ignoreFiles) +} diff --git a/github_diff_client_mock.go b/github_diff_client_mock.go new file mode 100644 index 0000000..32b590d --- /dev/null +++ b/github_diff_client_mock.go @@ -0,0 +1,69 @@ +package main + +import ( + "context" + + "github.com/google/go-github/v57/github" + diff "github.com/kmesiab/go-github-diff" +) + +type GithubDiffClientInterface interface { + ParsePullRequestURL(pullRequestURL string) (*diff.PullRequestURL, error) + GetPullRequest(ctx context.Context, pr *diff.PullRequestURL, client *github.Client) (string, error) + ParseGitDiff(diff string, ignoreList []string) []*diff.GitDiff +} + +type MockGithubClient struct{} + +// ParsePullRequestURL is a pass-through to the +// real client. It doesn't need to be mocked. +func (c *MockGithubClient) ParsePullRequestURL(url string) (*diff.PullRequestURL, error) { + return diff.ParsePullRequestURL(url) +} + +func (c *MockGithubClient) GetPullRequest( + _ context.Context, + _ *diff.PullRequestURL, + _ *github.Client, +) (string, error) { + return ` +diff --git a/file1.txt b/file1.txt +index abcdef1..1234567 100644 +--- a/file1.txt ++++ b/file1.txt +@@ -1,4 +1,4 @@ + This is the old content +-This is a line added in the PR ++This is a line added in the PR - Modified + This is the rest of the content +`, nil +} + +func (c *MockGithubClient) ParseGitDiff(_ string, _ []string) []*diff.GitDiff { + // Create a mock GitDiff slice for testing + mockDiffs := []*diff.GitDiff{ + { + FilePathOld: "file1.txt", + FilePathNew: "file1.txt", + Index: "abcdef1..1234567", + DiffContents: ` +This is the old content +-This is a line added in the PR ++This is a line added in the PR - Modified +This is the rest of the content +`, + }, + { + FilePathOld: "file2.txt", + FilePathNew: "file2.txt", + Index: "1234567..abcdef1", + DiffContents: ` +Another file diff ++Added a line +-Removed a line +`, + }, + } + + return mockDiffs +} diff --git a/go.mod b/go.mod index 27b1c10..1be34d9 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,26 @@ module cadre go 1.21 + +require ( + github.com/Netflix/go-env v0.0.0-20220526054621-78278af1949d + github.com/google/go-github/v57 v57.0.0 + github.com/kmesiab/go-github-diff v0.1.0-alpha + github.com/mkideal/cli v0.2.7 + github.com/sashabaranov/go-openai v1.18.2 + github.com/stretchr/testify v1.8.4 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/google/go-querystring v1.1.0 // indirect + github.com/labstack/gommon v0.4.2 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mkideal/expr v0.1.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + golang.org/x/crypto v0.18.0 // indirect + golang.org/x/sys v0.16.0 // indirect + golang.org/x/term v0.16.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..ae2ce77 --- /dev/null +++ b/go.sum @@ -0,0 +1,78 @@ +github.com/Netflix/go-env v0.0.0-20220526054621-78278af1949d h1:wvStE9wLpws31NiWUx+38wny1msZ/tm+eL5xmm4Y7So= +github.com/Netflix/go-env v0.0.0-20220526054621-78278af1949d/go.mod h1:9XMFaCeRyW7fC9XJOWQ+NdAv8VLG7ys7l3x4ozEGLUQ= +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/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +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/go-github/v57 v57.0.0 h1:L+Y3UPTY8ALM8x+TV0lg+IEBI+upibemtBD8Q9u7zHs= +github.com/google/go-github/v57 v57.0.0/go.mod h1:s0omdnye0hvK/ecLvpsGfJMiRt85PimQh4oygmLIxHw= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/kmesiab/go-github-diff v0.1.0-alpha h1:Iq05K3nXD/iyh2RGTjKudmI5YDs/nI1I8+ChE27gcN8= +github.com/kmesiab/go-github-diff v0.1.0-alpha/go.mod h1:eqE039Qx1HUnJ4hjLBPU9UHE+3KxbEmUOear9QcV12M= +github.com/labstack/gommon v0.3.0 h1:JEeO0bvc78PKdyHxloTKiF8BD5iGrH8T6MSeGvSgob0= +github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= +github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= +github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.7 h1:bQGKb3vps/j0E9GfJQ03JyhRuxsvdAanXlT9BTw3mdw= +github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= +github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mkideal/cli v0.2.7 h1:mB/XrMzuddmTJ8f7KY1c+KzfYoM149tYGAnzmqRdvOU= +github.com/mkideal/cli v0.2.7/go.mod h1:efaTeFI4jdPqzAe0bv3myLB2NW5yzMBLvWB70a6feco= +github.com/mkideal/expr v0.1.0 h1:fzborV9TeSUmLm0aEQWTWcexDURFFo4v5gHSc818Kl8= +github.com/mkideal/expr v0.1.0/go.mod h1:vL1DsSb87ZtU6IEjOtUfxw98z0FQbzS8xlGtnPkKdzg= +github.com/mkideal/pkg v0.1.3/go.mod h1:u/enAxPeRcYSsxtu1NUifWSeOTU/31VsCaOPg54SMJ4= +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/sashabaranov/go-openai v1.18.2 h1:UnC307Mgc+fiIDUmEJCiCvRoMxdFrLtQlg8A594pnG8= +github.com/sashabaranov/go-openai v1.18.2/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY= +golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221 h1:/ZHdbVpdR/jk3g30/d4yUL0JU9kksj8+F/bnQUVLGDM= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE= +golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +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/main.go b/main.go new file mode 100644 index 0000000..318d72e --- /dev/null +++ b/main.go @@ -0,0 +1,162 @@ +package main + +import ( + "context" + "fmt" + "os" + "path" + "time" + + goenv "github.com/Netflix/go-env" + "github.com/google/go-github/v57/github" + gh "github.com/kmesiab/go-github-diff" + "github.com/mkideal/cli" +) + +type argT struct { + URL string `cli:"*url" usage:"The GitHub pull request URL"` + ApiKey string `cli:"key" env:"OPENAI_API_KEY" usage:"Your OpenAI API key. Leave this blank to use environment variable OPENAI_API_KEY"` +} + +type ReviewedDiff struct { + Diff gh.GitDiff `json:"diff"` + Review string `json:"review"` + Model string `json:"model"` + Error error `json:"error"` +} + +func main() { + os.Exit(cli.Run(new(argT), func(ctx *cli.Context) error { + argv := ctx.Argv().(*argT) + + mergedArgs, err := coalesceConfiguration(argv) + if err != nil { + return fmt.Errorf("couldn't figure out the configuration. %s", err) + } + + if mergedArgs.ApiKey == "" { + return fmt.Errorf( + "no API key provided, either pass it with the `--key` flag " + + "or set the OPENAI_API_KEY environment variable") + } + + parsedDiffFiles, err := processPullRequest(mergedArgs.URL, &GithubDiffClient{}) + if err != nil { + return err + } + + fmt.Printf("Processing %d diff files. This may take a while...\n", len(parsedDiffFiles)) + + reviews, err := getCodeReviews(parsedDiffFiles, "gpt-4", mergedArgs.ApiKey, &OpenAICompletionService{}) + if err != nil { + return err + } + + for _, review := range reviews { + + if review.Error != nil { + fmt.Printf("ERROR: couldn't get the review for %s: %s\n", + path.Base(review.Diff.FilePathNew), + review.Error, + ) + + continue + } + + filename := "reviews/" + path.Base(review.Diff.FilePathNew) + ".md" + err := os.WriteFile(filename, []byte(review.Review), 0o644) + + fmt.Printf("Saved review to %s\n", filename) + + if err != nil { + fmt.Printf("couldn't save the review for %s: %s", + filename, + err, + ) + + continue + } + } + + return nil + })) +} + +func getCodeReviews(diffs []*gh.GitDiff, model, apiKey string, svc CompletionServiceInterface) ([]ReviewedDiff, error) { + reviewChan := make(chan ReviewedDiff) + var reviews []ReviewedDiff + + for _, diff := range diffs { + go func(d *gh.GitDiff) { + fmt.Printf("Processing %s\n", path.Base(d.FilePathNew)) + + review, err := svc.GetCompletion(d.DiffContents, model, apiKey) + + rDiff := ReviewedDiff{ + Error: err, + Diff: *d, + Review: review, + Model: model, + } + + reviewChan <- rDiff + }(diff) + } + + for range diffs { + review := <-reviewChan + reviews = append(reviews, review) + } + + close(reviewChan) + + return reviews, nil +} + +func processPullRequest(prURL string, ghClient GithubDiffClientInterface) ([]*gh.GitDiff, error) { + pullRequestUrl, err := ghClient.ParsePullRequestURL(prURL) + if err != nil { + return nil, err + } + + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + + client := github.NewClient(nil) + diffString, err := ghClient.GetPullRequest(ctx, pullRequestUrl, client) + if err != nil { + return nil, err + } + + ignoreList := []string{ + ".github", + ".gitignore", + ".travis.yml", + "LICENSE", + ".md", + ".mod", + ".sum", + } + + parsedDiff := ghClient.ParseGitDiff(diffString, ignoreList) + + return parsedDiff, nil +} + +func coalesceConfiguration(cliArgs *argT) (*argT, error) { + envArgs := &argT{} + + // Unmarshal environment variables into the envArgs struct + _, err := goenv.UnmarshalFromEnviron(envArgs) + if err != nil { + return nil, err + } + + // Default to the command line overriding + // the environment variables + if cliArgs.ApiKey == "" { + cliArgs.ApiKey = envArgs.ApiKey + } + + return cliArgs, nil +} diff --git a/main_test.go b/main_test.go new file mode 100644 index 0000000..dddbc36 --- /dev/null +++ b/main_test.go @@ -0,0 +1,42 @@ +package main + +import ( + "fmt" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCoalesceConfiguration_Key_CLIOverride(t *testing.T) { + // Set up to restore the value later + currentEnvVar := os.Getenv("OPENAI_API_KEY") + defer os.Setenv("OPENAI_API_KEY", currentEnvVar) + + // Set up an override + err := os.Setenv("OPENAI_API_KEY", "dummy_key") + require.NoError(t, err) + + args := &argT{ApiKey: "overridden_key"} + result, _ := coalesceConfiguration(args) + + assert.Equal(t, "overridden_key", result.ApiKey) +} + +func TestProcessPullRequest(t *testing.T) { + // Create a mock GitHub client + mockClient := &MockGithubClient{} + + // Create mock argT with URL + args := &argT{ + URL: "https://github.com/user/repo/pull/123", + } + + diffs, err := processPullRequest(args.URL, mockClient) + + // Check if there's no error returned + assert.NoError(t, err) + + fmt.Printf("%v", diffs) +} diff --git a/openai.go b/openai.go new file mode 100644 index 0000000..9304646 --- /dev/null +++ b/openai.go @@ -0,0 +1,61 @@ +package main + +import ( + "context" + "fmt" + + "github.com/sashabaranov/go-openai" +) + +type CompletionServiceInterface interface { + GetCompletion(diff, model, apiKey string) (string, error) +} + +type OpenAICompletionService struct{} + +func (s *OpenAICompletionService) GetCompletion(diff, gptModel, apiKey string) (string, error) { + prompt := `You are an experienced software developer conducting a code review on a Git diff. Your expertise spans + various programming languages and development best practices. Please review the attached Git diff with the + following considerations in mind: + +1. **Technical Accuracy**: Identify any bugs, coding errors, or security vulnerabilities. +2. **Best Practices**: Evaluate adherence to language-specific best practices, including code style and patterns. +3. **Performance and Scalability**: Highlight any performance issues and assess the code's scalability. +4. **Readability and Clarity**: Assess the code's readability, including its structure and commenting. +5. **Maintainability**: Consider the ease of future modifications and support. +6. **Testability**: Evaluate the test coverage and quality of tests. +7. **Contextual Fit**: Judge how the changes fit within the broader project scope and goals. + +Provide actionable feedback, suggesting improvements and alternatives where applicable. Include code samples in code +blocks. Your review should be empathetic and constructive, focusing on helping the author improve the code. +Format your review in markdown, ensuring readability with line wrapping before 60 characters. + +In your review, consider the impact of your feedback on team dynamics and the development process. Aim for a +balance between technical rigor and fostering a positive and collaborative team environment. + +Output the review in markdown format + +### Git Diff:` + "```\n%s\n```" + + fullPrompt := fmt.Sprintf(prompt, diff) + + // OpenAI + client := openai.NewClient(apiKey) + resp, err := client.CreateChatCompletion( + context.Background(), + openai.ChatCompletionRequest{ + Model: gptModel, + Messages: []openai.ChatCompletionMessage{ + { + Role: openai.ChatMessageRoleUser, + Content: fullPrompt, + }, + }, + }, + ) + if err != nil { + return "", err + } + + return resp.Choices[0].Message.Content, nil +}