Skip to content

Commit 7f280aa

Browse files
committed
init
0 parents  commit 7f280aa

File tree

3 files changed

+278
-0
lines changed

3 files changed

+278
-0
lines changed

go.mod

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
module ar
2+
3+
go 1.19
4+
5+
require (
6+
github.com/jessevdk/go-flags v1.5.0
7+
github.com/sashabaranov/go-openai v1.7.0
8+
)
9+
10+
require golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4 // indirect

go.sum

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc=
2+
github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
3+
github.com/sashabaranov/go-openai v1.7.0 h1:D1dBXoZhtf/aKNu6WFf0c7Ah2NM30PZ/3Mqly6cZ7fk=
4+
github.com/sashabaranov/go-openai v1.7.0/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
5+
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4 h1:EZ2mChiOa8udjfp6rRmswTbtZN/QzUQp4ptM4rnjHvc=
6+
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

main.go

Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"encoding/json"
7+
"fmt"
8+
"io"
9+
"net/http"
10+
"os"
11+
"strings"
12+
13+
"github.com/jessevdk/go-flags"
14+
"github.com/sashabaranov/go-openai"
15+
)
16+
17+
var opts struct {
18+
GithubToken string `long:"gh-token" env:"GITHUB_TOKEN" description:"GitHub token" required:"true"`
19+
OpenAIToken string `long:"openai-token" env:"OPENAI_TOKEN" description:"OpenAI token" required:"true"`
20+
Owner string `long:"owner" env:"OWNER" description:"GitHub owner" required:"true"`
21+
Repo string `long:"repo" env:"REPO" description:"GitHub repo" required:"true"`
22+
PRNumber int `long:"pr-number" env:"PR_NUMBER" description:"Pull request number" required:"true"`
23+
Test bool `long:"test" env:"TEST" description:"Test mode"`
24+
}
25+
26+
func main() {
27+
if _, err := flags.Parse(&opts); err != nil {
28+
if err.(*flags.Error).Type != flags.ErrHelp {
29+
fmt.Printf("Error parsing flags: %v \n", err)
30+
}
31+
os.Exit(0)
32+
}
33+
openaiClient := openai.NewClient(opts.OpenAIToken)
34+
35+
title, err := getPullRequestTitle(opts.GithubToken, opts.Owner, opts.Repo, opts.PRNumber)
36+
if err != nil {
37+
return
38+
}
39+
40+
jiraLink := generateJiraLinkByTitle(title)
41+
42+
diff, err := getDiffContent(opts.GithubToken, opts.Owner, opts.Repo, opts.PRNumber)
43+
if err != nil {
44+
fmt.Printf("Error fetching diff content: %v\n", err)
45+
return
46+
}
47+
filesDiff, err := ParseGitDiffAndSplitPerFile(diff)
48+
if err != nil {
49+
return
50+
}
51+
52+
var messages []openai.ChatCompletionMessage
53+
prompt := fmt.Sprintf(
54+
"Generate a GitHub pull request description based on the following changes " +
55+
"without basic prefix in markdown format with ###Description and ###Changes blocks:\n",
56+
)
57+
messages = append(messages, openai.ChatCompletionMessage{
58+
Role: openai.ChatMessageRoleUser,
59+
Content: prompt,
60+
})
61+
for _, fileDiff := range filesDiff {
62+
prompt := fmt.Sprintf("File %s:\n%s\n%s\n", getFilenameFromDiffHeader(fileDiff.Header), fileDiff.Header, fileDiff.Diff)
63+
messages = append(messages, openai.ChatCompletionMessage{
64+
Role: openai.ChatMessageRoleUser,
65+
Content: prompt,
66+
})
67+
}
68+
chatGPTDescription, err := generatePRDescription(openaiClient, messages)
69+
if err != nil {
70+
fmt.Printf("Error generating pull request description: %v\n", err)
71+
return
72+
}
73+
74+
description := fmt.Sprintf("## Jira\n%s\n%s", jiraLink, chatGPTDescription)
75+
if opts.Test {
76+
fmt.Println(description)
77+
os.Exit(0)
78+
}
79+
// Update the pull request with the generated description
80+
err = updatePullRequestDescription(opts.GithubToken, opts.Owner, opts.Repo, opts.PRNumber, description)
81+
if err != nil {
82+
fmt.Printf("Error updating pull request description: %v\n", err)
83+
return
84+
}
85+
86+
fmt.Println("Pull request description updated successfully")
87+
}
88+
89+
func updatePullRequestDescription(token string, o string, r string, number int, description string) error {
90+
url := fmt.Sprintf("https://api.github.com/repos/%s/%s/pulls/%d", o, r, number)
91+
92+
data := map[string]string{
93+
"body": description,
94+
}
95+
96+
payload, err := json.Marshal(data)
97+
if err != nil {
98+
return err
99+
}
100+
101+
req, err := http.NewRequest("PATCH", url, bytes.NewBuffer(payload))
102+
if err != nil {
103+
return err
104+
}
105+
106+
req.Header.Set("Authorization", fmt.Sprintf("token %s", token))
107+
req.Header.Set("Accept", "application/vnd.github.v3+json")
108+
req.Header.Set("Content-Type", "application/json")
109+
110+
client := &http.Client{}
111+
resp, err := client.Do(req)
112+
if err != nil {
113+
return err
114+
}
115+
defer resp.Body.Close()
116+
117+
if resp.StatusCode != http.StatusOK {
118+
return fmt.Errorf("failed to update pull request description. Status code: %d", resp.StatusCode)
119+
}
120+
121+
return nil
122+
}
123+
124+
func getDiffContent(token, owner, repo string, prNumber int) (string, error) {
125+
url := fmt.Sprintf("https://api.github.com/repos/%s/%s/pulls/%d", owner, repo, prNumber)
126+
method := "GET"
127+
128+
client := &http.Client{}
129+
req, err := http.NewRequest(method, url, nil)
130+
131+
if err != nil {
132+
fmt.Println(err)
133+
return "", err
134+
}
135+
req.Header.Add("Accept", "application/vnd.github.v3.diff")
136+
req.Header.Add("Authorization", fmt.Sprintf("token %s", token))
137+
//req.Header.Add("Cookie", "logged_in=no")
138+
139+
res, err := client.Do(req)
140+
if err != nil {
141+
return "", err
142+
}
143+
defer res.Body.Close()
144+
145+
body, err := io.ReadAll(res.Body)
146+
if err != nil {
147+
return "", err
148+
}
149+
150+
return string(body), nil
151+
}
152+
153+
func generatePRDescription(client *openai.Client, messages []openai.ChatCompletionMessage) (string, error) {
154+
resp, err := client.CreateChatCompletion(
155+
context.Background(),
156+
openai.ChatCompletionRequest{
157+
Model: openai.GPT3Dot5Turbo,
158+
Messages: messages,
159+
},
160+
)
161+
162+
if err != nil {
163+
fmt.Printf("ChatCompletion error: %v\n", err)
164+
return "", err
165+
}
166+
167+
return resp.Choices[0].Message.Content, nil
168+
}
169+
170+
// FileDiff represents a single file diff.
171+
type FileDiff struct {
172+
Header string
173+
Diff string
174+
}
175+
176+
// ParseGitDiffAndSplitPerFile parses a git diff and splits it into a slice of FileDiff.
177+
func ParseGitDiffAndSplitPerFile(diff string) ([]FileDiff, error) {
178+
lines := strings.Split(diff, "\n")
179+
var fileDiffs []FileDiff
180+
181+
inFileDiff := false
182+
var currentFileDiff FileDiff
183+
for _, line := range lines {
184+
if strings.HasPrefix(line, "diff --git") {
185+
if inFileDiff {
186+
fileDiffs = append(fileDiffs, currentFileDiff)
187+
}
188+
currentFileDiff = FileDiff{Header: line}
189+
inFileDiff = true
190+
} else if inFileDiff {
191+
currentFileDiff.Diff += line + "\n"
192+
}
193+
}
194+
if inFileDiff {
195+
fileDiffs = append(fileDiffs, currentFileDiff)
196+
}
197+
198+
return fileDiffs, nil
199+
}
200+
201+
func getFilenameFromDiffHeader(diffHeader string) string {
202+
// Split the diff header into individual lines
203+
lines := strings.Split(diffHeader, "\n")
204+
205+
// Extract the filename from the "diff --git" line
206+
gitDiffLine := lines[0]
207+
parts := strings.Split(gitDiffLine, " ")
208+
oldFileName := strings.TrimPrefix(parts[2], "a/")
209+
newFileName := strings.TrimPrefix(parts[3], "b/")
210+
211+
// Return the new filename if it exists, otherwise return the old filename
212+
if newFileName != "/dev/null" {
213+
return newFileName
214+
} else {
215+
return oldFileName
216+
}
217+
}
218+
219+
func getPullRequestTitle(token, owner, repo string, prNumber int) (string, error) {
220+
url := fmt.Sprintf("https://api.github.com/repos/%s/%s/pulls/%d", owner, repo, prNumber)
221+
222+
req, err := http.NewRequest("GET", url, nil)
223+
if err != nil {
224+
return "", err
225+
}
226+
227+
req.Header.Set("Authorization", fmt.Sprintf("token %s", token))
228+
req.Header.Set("Accept", "application/vnd.github.v3+json")
229+
230+
client := &http.Client{}
231+
resp, err := client.Do(req)
232+
if err != nil {
233+
return "", err
234+
}
235+
defer resp.Body.Close()
236+
237+
if resp.StatusCode != http.StatusOK {
238+
return "", fmt.Errorf("Failed to fetch pull request details. Status code: %d", resp.StatusCode)
239+
}
240+
241+
var pr struct {
242+
Title string `json:"title"`
243+
}
244+
245+
err = json.NewDecoder(resp.Body).Decode(&pr)
246+
if err != nil {
247+
return "", err
248+
}
249+
250+
return pr.Title, nil
251+
}
252+
253+
func generateJiraLinkByTitle(title string) string {
254+
//NCR-1234
255+
issueKey := strings.ToUpper(strings.Split(title, " ")[0])
256+
if !strings.HasPrefix(issueKey, "NCR-") {
257+
return ""
258+
}
259+
jiraBaseURL := "https://jira.deliveryhero.com/browse/"
260+
261+
return fmt.Sprintf("[%s](%s%s)", issueKey, jiraBaseURL, issueKey)
262+
}

0 commit comments

Comments
 (0)