Skip to content

Commit c4fefd4

Browse files
committed
refactor review
1 parent 319feef commit c4fefd4

File tree

6 files changed

+231
-125
lines changed

6 files changed

+231
-125
lines changed

cmd/review/config.go

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package main
2+
3+
var opts struct {
4+
GithubToken string `long:"gh-token" env:"GITHUB_TOKEN" description:"GitHub token" required:"true"`
5+
OpenAIToken string `long:"openai-token" env:"OPENAI_TOKEN" description:"OpenAI token" required:"true"`
6+
Owner string `long:"owner" env:"OWNER" description:"GitHub owner" required:"true"`
7+
Repo string `long:"repo" env:"REPO" description:"GitHub repo" required:"true"`
8+
PRNumber int `long:"pr-number" env:"PR_NUMBER" description:"Pull request number" required:"true"`
9+
Test bool `long:"test" env:"TEST" description:"Test mode"`
10+
}

cmd/review/main.go

+8-124
Original file line numberDiff line numberDiff line change
@@ -2,30 +2,17 @@ package main
22

33
import (
44
"context"
5-
"encoding/json"
6-
"errors"
75
"fmt"
86
"os"
97
"os/signal"
108
"syscall"
119

12-
"github.com/google/go-github/v51/github"
1310
"github.com/jessevdk/go-flags"
14-
"github.com/sashabaranov/go-openai"
1511

1612
ghClient "github.com/ravilushqa/gpt-pullrequest-updater/github"
1713
oAIClient "github.com/ravilushqa/gpt-pullrequest-updater/openai"
1814
)
1915

20-
var opts struct {
21-
GithubToken string `long:"gh-token" env:"GITHUB_TOKEN" description:"GitHub token" required:"true"`
22-
OpenAIToken string `long:"openai-token" env:"OPENAI_TOKEN" description:"OpenAI token" required:"true"`
23-
Owner string `long:"owner" env:"OWNER" description:"GitHub owner" required:"true"`
24-
Repo string `long:"repo" env:"REPO" description:"GitHub repo" required:"true"`
25-
PRNumber int `long:"pr-number" env:"PR_NUMBER" description:"Pull request number" required:"true"`
26-
Test bool `long:"test" env:"TEST" description:"Test mode"`
27-
}
28-
2916
func main() {
3017
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
3118
defer cancel()
@@ -56,122 +43,19 @@ func run(ctx context.Context) error {
5643
return fmt.Errorf("error getting commits: %w", err)
5744
}
5845

59-
var comments []*github.PullRequestComment
60-
61-
for i, file := range diff.Files {
62-
patch := file.GetPatch()
63-
fmt.Printf("processing file: %s %d/%d\n", file.GetFilename(), i+1, len(diff.Files))
64-
if patch == "" || file.GetStatus() == "removed" || file.GetStatus() == "renamed" {
65-
continue
66-
}
67-
68-
if len(patch) > 3000 {
69-
fmt.Println("Patch is too long, truncating")
70-
patch = fmt.Sprintf("%s...", patch[:3000])
71-
}
72-
completion, err := openAIClient.ChatCompletion(ctx, []openai.ChatCompletionMessage{
73-
{
74-
Role: openai.ChatMessageRoleUser,
75-
Content: oAIClient.PromptReview,
76-
},
77-
{
78-
Role: openai.ChatMessageRoleUser,
79-
Content: patch,
80-
},
81-
})
82-
83-
if err != nil {
84-
return fmt.Errorf("error getting completion: %w", err)
85-
}
86-
87-
if opts.Test {
88-
fmt.Println("Completion:", completion)
89-
}
90-
91-
review, err := extractJSON(completion)
92-
if err != nil {
93-
fmt.Println("Error extracting JSON:", err)
94-
continue
95-
}
96-
97-
if review.Quality == Good {
98-
fmt.Println("Review is good")
99-
continue
100-
}
101-
for _, issue := range review.Issues {
102-
body := fmt.Sprintf("[%s] %s", issue.Type, issue.Description)
103-
comment := &github.PullRequestComment{
104-
CommitID: diff.Commits[len(diff.Commits)-1].SHA,
105-
Path: file.Filename,
106-
Body: &body,
107-
Position: &issue.Line,
108-
}
109-
comments = append(comments, comment)
110-
}
111-
112-
if opts.Test {
113-
continue
114-
}
115-
116-
for i, c := range comments {
117-
fmt.Printf("creating comment: %s %d/%d\n", *c.Path, i+1, len(comments))
118-
if _, err := githubClient.CreatePullRequestComment(ctx, opts.Owner, opts.Repo, opts.PRNumber, c); err != nil {
119-
return fmt.Errorf("error creating comment: %w", err)
120-
}
121-
}
122-
}
123-
return nil
124-
}
125-
126-
type Review struct {
127-
Quality Quality `json:"quality"`
128-
Issues []struct {
129-
Type string `json:"type"`
130-
Line int `json:"line"`
131-
Description string `json:"description"`
132-
} `json:"issues"`
133-
}
134-
135-
type Quality string
136-
137-
const (
138-
Good Quality = "good"
139-
Bad Quality = "bad"
140-
Neutral Quality = "neutral"
141-
)
142-
143-
func extractJSON(input string) (*Review, error) {
144-
var jsonObj *Review
145-
146-
// find the start and end positions of the JSON object
147-
start := 0
148-
end := len(input)
149-
for i, c := range input {
150-
if c == '{' {
151-
start = i
152-
break
153-
}
154-
if i == len(input)-1 {
155-
return nil, errors.New("invalid JSON object")
156-
}
46+
comments, err := processFiles(ctx, openAIClient, diff)
47+
if err != nil {
48+
return err
15749
}
158-
for i := len(input) - 1; i >= 0; i-- {
159-
if input[i] == '}' {
160-
end = i + 1
161-
break
162-
}
16350

164-
if i == 0 {
165-
return nil, errors.New("invalid JSON object")
166-
}
51+
if opts.Test {
52+
fmt.Printf("Comments: %v \n", comments)
16753
}
16854

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

176-
return jsonObj, nil
60+
return nil
17761
}

cmd/review/review.go

+99
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/google/go-github/v51/github"
8+
"github.com/sashabaranov/go-openai"
9+
10+
ghClient "github.com/ravilushqa/gpt-pullrequest-updater/github"
11+
oAIClient "github.com/ravilushqa/gpt-pullrequest-updater/openai"
12+
)
13+
14+
type Review struct {
15+
Quality Quality `json:"quality"`
16+
Issues []Issue `json:"issues"`
17+
}
18+
19+
type Issue struct {
20+
Type string `json:"type"`
21+
Line int `json:"line"`
22+
Description string `json:"description"`
23+
}
24+
25+
type Quality string
26+
27+
const (
28+
Good Quality = "good"
29+
Bad Quality = "bad"
30+
Neutral Quality = "neutral"
31+
)
32+
33+
func processFiles(ctx context.Context, openAIClient *oAIClient.Client, diff *github.CommitsComparison) ([]*github.PullRequestComment, error) {
34+
var comments []*github.PullRequestComment
35+
36+
for i, file := range diff.Files {
37+
patch := file.GetPatch()
38+
fmt.Printf("processing file: %s %d/%d\n", file.GetFilename(), i+1, len(diff.Files))
39+
if patch == "" || file.GetStatus() == "removed" || file.GetStatus() == "renamed" {
40+
continue
41+
}
42+
43+
if len(patch) > 3000 {
44+
fmt.Println("Patch is too long, truncating")
45+
patch = fmt.Sprintf("%s...", patch[:3000])
46+
}
47+
completion, err := openAIClient.ChatCompletion(ctx, []openai.ChatCompletionMessage{
48+
{
49+
Role: openai.ChatMessageRoleUser,
50+
Content: oAIClient.PromptReview,
51+
},
52+
{
53+
Role: openai.ChatMessageRoleUser,
54+
Content: patch,
55+
},
56+
})
57+
58+
if err != nil {
59+
return nil, fmt.Errorf("error getting completion: %w", err)
60+
}
61+
62+
if opts.Test {
63+
fmt.Println("Completion:", completion)
64+
}
65+
66+
review, err := extractReviewFromString(completion)
67+
if err != nil {
68+
fmt.Println("Error extracting JSON:", err)
69+
continue
70+
}
71+
72+
if review.Quality == Good {
73+
fmt.Println("Review is good")
74+
continue
75+
}
76+
for _, issue := range review.Issues {
77+
body := fmt.Sprintf("[%s] %s", issue.Type, issue.Description)
78+
comment := &github.PullRequestComment{
79+
CommitID: diff.Commits[len(diff.Commits)-1].SHA,
80+
Path: file.Filename,
81+
Body: &body,
82+
Position: &issue.Line,
83+
}
84+
comments = append(comments, comment)
85+
}
86+
}
87+
88+
return comments, nil
89+
}
90+
91+
func createComments(ctx context.Context, githubClient *ghClient.Client, comments []*github.PullRequestComment) error {
92+
for i, c := range comments {
93+
fmt.Printf("creating comment: %s %d/%d\n", *c.Path, i+1, len(comments))
94+
if _, err := githubClient.CreatePullRequestComment(ctx, opts.Owner, opts.Repo, opts.PRNumber, c); err != nil {
95+
return fmt.Errorf("error creating comment: %w", err)
96+
}
97+
}
98+
return nil
99+
}

cmd/review/utils.go

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package main
2+
3+
import (
4+
"encoding/json"
5+
"errors"
6+
)
7+
8+
func extractReviewFromString(input string) (*Review, error) {
9+
var jsonObj *Review
10+
11+
// find the start and end positions of the JSON object
12+
start := 0
13+
end := len(input)
14+
for i, c := range input {
15+
if c == '{' {
16+
start = i
17+
break
18+
}
19+
if i == len(input)-1 {
20+
return nil, errors.New("invalid JSON object")
21+
}
22+
}
23+
for i := len(input) - 1; i >= 0; i-- {
24+
if input[i] == '}' {
25+
end = i + 1
26+
break
27+
}
28+
29+
if i == 0 {
30+
return nil, errors.New("invalid JSON object")
31+
}
32+
}
33+
34+
// extract the JSON object from the input
35+
jsonStr := input[start:end]
36+
err := json.Unmarshal([]byte(jsonStr), &jsonObj)
37+
if err != nil {
38+
return nil, errors.New("invalid JSON object")
39+
}
40+
41+
return jsonObj, nil
42+
}

cmd/review/utils_test.go

+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package main
2+
3+
import (
4+
"reflect"
5+
"testing"
6+
)
7+
8+
func Test_extractReviewFromString(t *testing.T) {
9+
tests := []struct {
10+
name string
11+
input string
12+
want *Review
13+
wantErr bool
14+
}{
15+
{
16+
name: "correctly parses a review",
17+
input: `{ "quality": "good", "issues": [] }`,
18+
want: &Review{
19+
Quality: Good,
20+
Issues: []Issue{},
21+
},
22+
wantErr: false,
23+
},
24+
{
25+
name: "correctly parses a review with issues",
26+
input: `{ "quality": "good", "issues": [{ "type": "typo", "line": 1, "description": "typo" }] }`,
27+
want: &Review{
28+
Quality: Good,
29+
Issues: []Issue{{Type: "typo", Line: 1, Description: "typo"}},
30+
},
31+
wantErr: false,
32+
},
33+
{
34+
name: "correctly parses a review with multiple issues",
35+
input: `{ "quality": "good", "issues": [{ "type": "typo", "line": 1, "description": "typo" }, { "type": "typo", "line": 2, "description": "typo" }] }`,
36+
want: &Review{
37+
Quality: Good,
38+
Issues: []Issue{{Type: "typo", Line: 1, Description: "typo"}, {Type: "typo", Line: 2, Description: "typo"}},
39+
},
40+
wantErr: false,
41+
},
42+
{
43+
name: "correctly parses a review with prefix and suffix",
44+
input: `Review: { "quality": "good", "issues": [] } Done`,
45+
want: &Review{
46+
Quality: Good,
47+
Issues: []Issue{},
48+
},
49+
},
50+
{
51+
name: "correctly parses a review with prefix and suffix and issues",
52+
input: `Review: { "quality": "good", "issues": [{ "type": "typo", "line": 1, "description": "typo" }] } Done`,
53+
want: &Review{
54+
Quality: Good,
55+
Issues: []Issue{{Type: "typo", Line: 1, Description: "typo"}},
56+
},
57+
},
58+
}
59+
for _, tt := range tests {
60+
t.Run(tt.name, func(t *testing.T) {
61+
got, err := extractReviewFromString(tt.input)
62+
if (err != nil) != tt.wantErr {
63+
t.Errorf("extractReviewFromString() error = %v, wantErr %v", err, tt.wantErr)
64+
return
65+
}
66+
if !reflect.DeepEqual(got, tt.want) {
67+
t.Errorf("extractReviewFromString() got = %v, want %v", got, tt.want)
68+
}
69+
})
70+
}
71+
}

0 commit comments

Comments
 (0)