Move issue parsing code into the vc package

Write some tests to verify the issue parsing code
add additional option for specifying a base branch to use
This commit is contained in:
Moby von Briesen 2023-05-11 23:53:17 -04:00
parent ab7521477a
commit 7053a0b693
7 changed files with 201 additions and 49 deletions

3
go.mod
View File

@ -8,6 +8,7 @@ require (
github.com/sashabaranov/go-openai v1.9.0
github.com/spf13/cobra v1.7.0
github.com/spf13/viper v1.15.0
github.com/stretchr/testify v1.8.1
go.uber.org/zap v1.24.0
golang.org/x/oauth2 v0.7.0
)
@ -17,6 +18,7 @@ require (
github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 // indirect
github.com/acomagu/bufpipe v1.0.4 // indirect
github.com/cloudflare/circl v1.1.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/go-git/gcfg v1.5.0 // indirect
@ -32,6 +34,7 @@ require (
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/pelletier/go-toml/v2 v2.0.6 // indirect
github.com/pjbgf/sha1cd v0.3.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/sergi/go-diff v1.1.0 // indirect
github.com/skeema/knownhosts v1.1.0 // indirect
github.com/spf13/afero v1.9.3 // indirect

View File

@ -19,10 +19,11 @@ const (
// CodeChangeRequest contains all necessary information for generating a prompt for a LLM.
type CodeChangeRequest struct {
Files []File
Subject string
Body string
IssueID string
Files []File
Subject string
Body string
IssueID string
BaseBranch string
}
// CodeChangeResponse contains data derived from an LLM response to a prompt generated via a CodeChangeRequest.

View File

@ -5,7 +5,6 @@ import (
"errors"
"fmt"
"strconv"
"strings"
"time"
"github.com/mobyvb/pull-pal/llm"
@ -74,6 +73,7 @@ func (p *PullPal) Run() error {
issue := issues[0]
err = p.handleIssue(issue)
if err != nil {
// TODO leave comment if error (make configurable)
p.log.Error("error handling issue", zap.Error(err))
}
}
@ -95,6 +95,7 @@ func (p *PullPal) Run() error {
comment := comments[0]
err = p.handleComment(comment)
if err != nil {
// TODO leave comment if error (make configurable)
p.log.Error("error handling comment", zap.Error(err))
}
}
@ -112,7 +113,7 @@ func (p *PullPal) handleIssue(issue vc.Issue) error {
return err
}
err = p.ghClient.CommentOnIssue(issueNumber, "on it")
err = p.ghClient.CommentOnIssue(issueNumber, "working on it")
if err != nil {
p.log.Error("error commenting on issue", zap.Error(err))
return err
@ -125,51 +126,14 @@ func (p *PullPal) handleIssue(issue vc.Issue) error {
}
}
// remove file list from issue body
// TODO do this better and probably somewhere else
parts := strings.Split(issue.Body, "Files:")
issue.Body = parts[0]
fileList := []string{}
if len(parts) > 1 {
fileList = strings.Split(parts[1], ",")
}
// get file contents from local git repository
files := []llm.File{}
for _, path := range fileList {
path = strings.TrimSpace(path)
nextFile, err := p.localGitClient.GetLocalFile(path)
if err != nil {
return err
}
files = append(files, nextFile)
}
changeRequest := llm.CodeChangeRequest{
Subject: issue.Subject,
Body: issue.Body,
IssueID: issue.ID,
Files: files,
changeRequest, err := p.localGitClient.ParseIssueAndStartCommit(issue)
if err != nil {
return err
}
changeResponse, err := p.openAIClient.EvaluateCCR(p.ctx, "", changeRequest)
if err != nil {
return err
}
// create commit with file changes
err = p.localGitClient.StartCommit()
if err != nil {
return err
}
// todo remove hardcoded main
p.log.Info("checking out main branch")
err = p.localGitClient.CheckoutRemoteBranch("main")
if err != nil {
p.log.Info("error checking out main branch", zap.Error(err))
return err
}
newBranchName := fmt.Sprintf("fix-%s", changeRequest.IssueID)
@ -197,7 +161,7 @@ func (p *PullPal) handleIssue(issue vc.Issue) error {
// open code change request
// TODO don't hardcode main branch, make configurable
_, url, err := p.ghClient.OpenCodeChangeRequest(changeRequest, changeResponse, newBranchName, "main")
_, url, err := p.ghClient.OpenCodeChangeRequest(changeRequest, changeResponse, newBranchName)
if err != nil {
return err
}

View File

@ -2,6 +2,7 @@ package vc
import (
"fmt"
"strings"
"github.com/go-git/go-git/v5"
)
@ -81,3 +82,46 @@ func (repo Repository) SSH() string {
func (repo Repository) HTTPS() string {
return fmt.Sprintf("https://%s/%s/%s.git", repo.HostDomain, repo.Owner.Handle, repo.Name)
}
type IssueBody struct {
PromptBody string
FilePaths []string
BaseBranch string
}
func ParseIssueBody(body string) IssueBody {
issueBody := IssueBody{
BaseBranch: "main",
}
divider := "---"
parts := strings.Split(body, divider)
issueBody.PromptBody = strings.TrimSpace(parts[0])
// if there was nothing to split, no additional configuration was provided
if len(parts) <= 1 {
return issueBody
}
configStr := parts[1]
configLines := strings.Split(configStr, "\n")
for _, line := range configLines {
lineParts := strings.Split(line, ":")
if len(lineParts) < 2 {
continue
}
key := strings.ToLower(strings.TrimSpace(lineParts[0]))
if key == "base" {
issueBody.BaseBranch = strings.TrimSpace(lineParts[1])
continue
}
if key == "files" {
filePaths := strings.Split(lineParts[1], ",")
for _, p := range filePaths {
issueBody.FilePaths = append(issueBody.FilePaths, strings.TrimSpace(p))
}
continue
}
}
return issueBody
}

99
vc/common_test.go Normal file
View File

@ -0,0 +1,99 @@
package vc_test
import (
"testing"
"github.com/mobyvb/pull-pal/vc"
"github.com/stretchr/testify/require"
)
func TestParseIssueBody(t *testing.T) {
var testCases = []struct {
testcase string
body string
parsed vc.IssueBody
}{
{
"simple issue",
`
add an html file
`,
vc.IssueBody{
PromptBody: "add an html file",
BaseBranch: "main",
},
},
{
"issue with explicit file list",
`
add an html file
and also a go file
read a readme file too
---
FiLeS: index.html, README.md ,main.go
`,
vc.IssueBody{
PromptBody: "add an html file\nand also a go file\nread a readme file too",
BaseBranch: "main",
FilePaths: []string{"index.html", "README.md", "main.go"},
},
},
{
"issue with a custom base branch",
`
add an html file
---
base: some-base-branch
`,
vc.IssueBody{
PromptBody: "add an html file",
BaseBranch: "some-base-branch",
},
},
{
"issue with an explicit base branch and file list",
`
add an html file
---
base: some-base-branch
files: index.html, main.go
`,
vc.IssueBody{
PromptBody: "add an html file",
BaseBranch: "some-base-branch",
FilePaths: []string{"index.html", "main.go"},
},
},
{
"issue with garbage in config section",
`
add an html file
---
asdf:
files: index.html, main.go
: asdfsadf
base: some-base-branch
asdfjljldsfj
nonexistentoption: asdf
`,
vc.IssueBody{
PromptBody: "add an html file",
BaseBranch: "some-base-branch",
FilePaths: []string{"index.html", "main.go"},
},
},
}
for _, tt := range testCases {
t.Log("testing case:", tt.testcase)
parsed := vc.ParseIssueBody(tt.body)
require.Equal(t, tt.parsed.PromptBody, parsed.PromptBody)
require.Equal(t, tt.parsed.BaseBranch, parsed.BaseBranch)
require.Equal(t, len(tt.parsed.FilePaths), len(parsed.FilePaths))
for i, p := range tt.parsed.FilePaths {
require.Equal(t, p, parsed.FilePaths[i])
}
}
}

View File

@ -219,3 +219,44 @@ func (gc *LocalGitClient) FinishCommit(message string) error {
return nil
}
// ParseIssueAndStartCommit parses the information provided in the issue to check out the appropriate branch,
// get the contents of the files mentioned in the issue, and initialize the worktree.
func (gc *LocalGitClient) ParseIssue(issue Issue) (llm.CodeChangeRequest, error) {
var changeRequest llm.CodeChangeRequest
if gc.worktree != nil {
return changeRequest, errors.New("worktree is active - some other work is incomplete")
}
issueBody := ParseIssueBody(issue.Body)
// start a worktree
err := gc.StartCommit()
if err != nil {
return changeRequest, err
}
err = gc.CheckoutRemoteBranch(issueBody.BaseBranch)
if err != nil {
return changeRequest, err
}
// get file contents from local git repository
files := []llm.File{}
for _, path := range issueBody.FilePaths {
nextFile, err := gc.GetLocalFile(path)
if err != nil {
return changeRequest, err
}
files = append(files, nextFile)
}
return llm.CodeChangeRequest{
Subject: issue.Subject,
Body: issueBody.PromptBody,
IssueID: issue.ID,
Files: files,
BaseBranch: issueBody.BaseBranch,
}, nil
}

View File

@ -48,7 +48,7 @@ func NewGithubClient(ctx context.Context, log *zap.Logger, self Author, repo Rep
}
// OpenCodeChangeRequest pushes to a new remote branch and opens a PR on Github.
func (gc *GithubClient) OpenCodeChangeRequest(req llm.CodeChangeRequest, res llm.CodeChangeResponse, fromBranch, toBranch string) (id, url string, err error) {
func (gc *GithubClient) OpenCodeChangeRequest(req llm.CodeChangeRequest, res llm.CodeChangeResponse, fromBranch string) (id, url string, err error) {
// TODO handle gc.ctx canceled
title := req.Subject
@ -63,7 +63,7 @@ func (gc *GithubClient) OpenCodeChangeRequest(req llm.CodeChangeRequest, res llm
pr, _, err := gc.client.PullRequests.Create(gc.ctx, gc.repo.Owner.Handle, gc.repo.Name, &github.NewPullRequest{
Title: &title,
Head: &fromBranch,
Base: &toBranch,
Base: &req.BaseBranch,
Body: &body,
})
if err != nil {