Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor review #12

Merged
merged 2 commits into from
May 7, 2023
Merged
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
10 changes: 10 additions & 0 deletions cmd/review/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package main

var opts struct {
GithubToken string `long:"gh-token" env:"GITHUB_TOKEN" description:"GitHub token" required:"true"`
OpenAIToken string `long:"openai-token" env:"OPENAI_TOKEN" description:"OpenAI token" required:"true"`
Owner string `long:"owner" env:"OWNER" description:"GitHub owner" required:"true"`
Repo string `long:"repo" env:"REPO" description:"GitHub repo" required:"true"`
PRNumber int `long:"pr-number" env:"PR_NUMBER" description:"Pull request number" required:"true"`
Test bool `long:"test" env:"TEST" description:"Test mode"`
}
132 changes: 8 additions & 124 deletions cmd/review/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,17 @@ package main

import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"os/signal"
"syscall"

"github.com/google/go-github/v51/github"
"github.com/jessevdk/go-flags"
"github.com/sashabaranov/go-openai"

ghClient "github.com/ravilushqa/gpt-pullrequest-updater/github"
oAIClient "github.com/ravilushqa/gpt-pullrequest-updater/openai"
)

var opts struct {
GithubToken string `long:"gh-token" env:"GITHUB_TOKEN" description:"GitHub token" required:"true"`
OpenAIToken string `long:"openai-token" env:"OPENAI_TOKEN" description:"OpenAI token" required:"true"`
Owner string `long:"owner" env:"OWNER" description:"GitHub owner" required:"true"`
Repo string `long:"repo" env:"REPO" description:"GitHub repo" required:"true"`
PRNumber int `long:"pr-number" env:"PR_NUMBER" description:"Pull request number" required:"true"`
Test bool `long:"test" env:"TEST" description:"Test mode"`
}

func main() {
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer cancel()
Expand Down Expand Up @@ -56,122 +43,19 @@ func run(ctx context.Context) error {
return fmt.Errorf("error getting commits: %w", err)
}

var comments []*github.PullRequestComment

for i, file := range diff.Files {
patch := file.GetPatch()
fmt.Printf("processing file: %s %d/%d\n", file.GetFilename(), i+1, len(diff.Files))
if patch == "" || file.GetStatus() == "removed" || file.GetStatus() == "renamed" {
continue
}

if len(patch) > 3000 {
fmt.Println("Patch is too long, truncating")
patch = fmt.Sprintf("%s...", patch[:3000])
}
completion, err := openAIClient.ChatCompletion(ctx, []openai.ChatCompletionMessage{
{
Role: openai.ChatMessageRoleUser,
Content: oAIClient.PromptReview,
},
{
Role: openai.ChatMessageRoleUser,
Content: patch,
},
})

if err != nil {
return fmt.Errorf("error getting completion: %w", err)
}

if opts.Test {
fmt.Println("Completion:", completion)
}

review, err := extractJSON(completion)
if err != nil {
fmt.Println("Error extracting JSON:", err)
continue
}

if review.Quality == Good {
fmt.Println("Review is good")
continue
}
for _, issue := range review.Issues {
body := fmt.Sprintf("[%s] %s", issue.Type, issue.Description)
comment := &github.PullRequestComment{
CommitID: diff.Commits[len(diff.Commits)-1].SHA,
Path: file.Filename,
Body: &body,
Position: &issue.Line,
}
comments = append(comments, comment)
}

if opts.Test {
continue
}

for i, c := range comments {
fmt.Printf("creating comment: %s %d/%d\n", *c.Path, i+1, len(comments))
if _, err := githubClient.CreatePullRequestComment(ctx, opts.Owner, opts.Repo, opts.PRNumber, c); err != nil {
return fmt.Errorf("error creating comment: %w", err)
}
}
}
return nil
}

type Review struct {
Quality Quality `json:"quality"`
Issues []struct {
Type string `json:"type"`
Line int `json:"line"`
Description string `json:"description"`
} `json:"issues"`
}

type Quality string

const (
Good Quality = "good"
Bad Quality = "bad"
Neutral Quality = "neutral"
)

func extractJSON(input string) (*Review, error) {
var jsonObj *Review

// find the start and end positions of the JSON object
start := 0
end := len(input)
for i, c := range input {
if c == '{' {
start = i
break
}
if i == len(input)-1 {
return nil, errors.New("invalid JSON object")
}
comments, err := processFiles(ctx, openAIClient, diff)
if err != nil {
return err
}
for i := len(input) - 1; i >= 0; i-- {
if input[i] == '}' {
end = i + 1
break
}

if i == 0 {
return nil, errors.New("invalid JSON object")
}
if opts.Test {
fmt.Printf("Comments: %v \n", comments)
}

// extract the JSON object from the input
jsonStr := input[start:end]
err := json.Unmarshal([]byte(jsonStr), &jsonObj)
err = createComments(ctx, githubClient, comments)
if err != nil {
return nil, errors.New("invalid JSON object")
return fmt.Errorf("error creating comments: %w", err)
}

return jsonObj, nil
return nil
}
97 changes: 97 additions & 0 deletions cmd/review/review.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package main

import (
"context"
"fmt"

"github.com/google/go-github/v51/github"
"github.com/sashabaranov/go-openai"

ghClient "github.com/ravilushqa/gpt-pullrequest-updater/github"
oAIClient "github.com/ravilushqa/gpt-pullrequest-updater/openai"
)

type Review struct {
Quality Quality `json:"quality"`
Issues []Issue `json:"issues"`
}

type Issue struct {
Type string `json:"type"`
Line int `json:"line"`
Description string `json:"description"`
}

type Quality string

const (
Good Quality = "good"
Bad Quality = "bad"
Neutral Quality = "neutral"
)

func processFiles(ctx context.Context, openAIClient *oAIClient.Client, diff *github.CommitsComparison) ([]*github.PullRequestComment, error) {
var comments []*github.PullRequestComment

for i, file := range diff.Files {
patch := file.GetPatch()
fmt.Printf("processing file: %s %d/%d\n", file.GetFilename(), i+1, len(diff.Files))
if patch == "" || file.GetStatus() == "removed" || file.GetStatus() == "renamed" {
continue
}

if len(patch) > 3000 {
fmt.Println("Patch is too long, truncating")
patch = fmt.Sprintf("%s...", patch[:3000])
}
completion, err := openAIClient.ChatCompletion(ctx, []openai.ChatCompletionMessage{
{
Role: openai.ChatMessageRoleUser,
Content: oAIClient.PromptReview,
},
{
Role: openai.ChatMessageRoleUser,
Content: patch,
},
})

if err != nil {
return nil, fmt.Errorf("error getting completion: %w", err)
}

fmt.Println("Completion:", completion)

review, err := extractReviewFromString(completion)
if err != nil {
fmt.Println("Error extracting JSON:", err)
continue
}

if review.Quality == Good {
fmt.Println("Review is good")
continue
}
for _, issue := range review.Issues {
body := fmt.Sprintf("[%s] %s", issue.Type, issue.Description)
comment := &github.PullRequestComment{
CommitID: diff.Commits[len(diff.Commits)-1].SHA,
Path: file.Filename,
Body: &body,
Position: &issue.Line,
}
comments = append(comments, comment)
}
}

return comments, nil
}

func createComments(ctx context.Context, githubClient *ghClient.Client, comments []*github.PullRequestComment) error {
for i, c := range comments {
fmt.Printf("creating comment: %s %d/%d\n", *c.Path, i+1, len(comments))
if _, err := githubClient.CreatePullRequestComment(ctx, opts.Owner, opts.Repo, opts.PRNumber, c); err != nil {
return fmt.Errorf("error creating comment: %w", err)
}
}
return nil
}
42 changes: 42 additions & 0 deletions cmd/review/utils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package main

import (
"encoding/json"
"errors"
)

func extractReviewFromString(input string) (*Review, error) {
var jsonObj *Review

// find the start and end positions of the JSON object
start := 0
end := len(input)
for i, c := range input {
if c == '{' {
start = i
break
}
if i == len(input)-1 {
return nil, errors.New("invalid JSON object")
}
}
for i := len(input) - 1; i >= 0; i-- {
if input[i] == '}' {
end = i + 1
break
}

if i == 0 {
return nil, errors.New("invalid JSON object")
}
}

// extract the JSON object from the input
jsonStr := input[start:end]
err := json.Unmarshal([]byte(jsonStr), &jsonObj)
if err != nil {
return nil, errors.New("invalid JSON object")
}

return jsonObj, nil
}
71 changes: 71 additions & 0 deletions cmd/review/utils_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package main

import (
"reflect"
"testing"
)

func Test_extractReviewFromString(t *testing.T) {
tests := []struct {
name string
input string
want *Review
wantErr bool
}{
{
name: "correctly parses a review",
input: `{ "quality": "good", "issues": [] }`,
want: &Review{
Quality: Good,
Issues: []Issue{},
},
wantErr: false,
},
{
name: "correctly parses a review with issues",
input: `{ "quality": "good", "issues": [{ "type": "typo", "line": 1, "description": "typo" }] }`,
want: &Review{
Quality: Good,
Issues: []Issue{{Type: "typo", Line: 1, Description: "typo"}},
},
wantErr: false,
},
{
name: "correctly parses a review with multiple issues",
input: `{ "quality": "good", "issues": [{ "type": "typo", "line": 1, "description": "typo" }, { "type": "typo", "line": 2, "description": "typo" }] }`,
want: &Review{
Quality: Good,
Issues: []Issue{{Type: "typo", Line: 1, Description: "typo"}, {Type: "typo", Line: 2, Description: "typo"}},
},
wantErr: false,
},
{
name: "correctly parses a review with prefix and suffix",
input: `Review: { "quality": "good", "issues": [] } Done`,
want: &Review{
Quality: Good,
Issues: []Issue{},
},
},
{
name: "correctly parses a review with prefix and suffix and issues",
input: `Review: { "quality": "good", "issues": [{ "type": "typo", "line": 1, "description": "typo" }] } Done`,
want: &Review{
Quality: Good,
Issues: []Issue{{Type: "typo", Line: 1, Description: "typo"}},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := extractReviewFromString(tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("extractReviewFromString() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("extractReviewFromString() got = %v, want %v", got, tt.want)
}
})
}
}
Loading