now it works somewhat reliably

This commit is contained in:
Moby von Briesen 2023-05-03 20:05:34 -04:00
parent e4918fcb36
commit eea53db3a2
8 changed files with 210 additions and 31 deletions

32
cmd/debug.go Normal file
View File

@ -0,0 +1,32 @@
package cmd
import (
"fmt"
"github.com/spf13/cobra"
)
var debugGitCmd = &cobra.Command{
Use: "debug-git",
Short: "debug git functionality",
Run: func(cmd *cobra.Command, args []string) {
cfg := getConfig()
p, err := getPullPal(cmd.Context(), cfg)
if err != nil {
fmt.Println("error creating new pull pal", err)
return
}
fmt.Println("Successfully initialized pull pal")
err = p.DebugGit()
if err != nil {
fmt.Println("err debugging git", err)
return
}
},
}
func init() {
rootCmd.AddCommand(debugGitCmd)
}

View File

@ -74,8 +74,14 @@ func (res CodeChangeResponse) String() string {
func ParseCodeChangeResponse(llmResponse string) CodeChangeResponse {
sections := strings.Split(llmResponse, "Notes:")
filesSection := sections[0]
notes := strings.TrimSpace(sections[1])
filesSection := ""
if len(sections) > 0 {
filesSection = sections[0]
}
notes := ""
if len(sections) > 1 {
notes = strings.TrimSpace(sections[1])
}
files := parseFiles(filesSection)
@ -88,6 +94,9 @@ func ParseCodeChangeResponse(llmResponse string) CodeChangeResponse {
// parseFiles process the "files" subsection of the LLM's response. It is a helper for GetCodeChangeResponse.
func parseFiles(filesSection string) []File {
fileStringList := strings.Split(filesSection, "name:")
if len(fileStringList) < 2 {
return []File{}
}
// first item in the list is just gonna be "Files:"
fileStringList = fileStringList[1:]
@ -99,6 +108,9 @@ func parseFiles(filesSection string) []File {
fileList := make([]File, len(fileStringList))
for i, f := range fileStringList {
fileParts := strings.Split(f, "contents:")
if len(fileParts) < 2 {
continue
}
path := replacer.Replace(fileParts[0])
path = strings.TrimSpace(path)

View File

@ -41,7 +41,7 @@ func (oc *OpenAIClient) EvaluateCCR(ctx context.Context, req CodeChangeRequest)
// TODO use different choices/different options in different branches/worktrees?
choice := resp.Choices[0].Message.Content
oc.log.Debug("got response from llm", zap.String("output", choice))
oc.log.Info("got response from llm", zap.String("output", choice))
return ParseCodeChangeResponse(choice), nil
}

View File

@ -3,5 +3,10 @@ package main
import "github.com/mobyvb/pull-pal/cmd"
func main() {
/*
response := "Files:\n\n - name: main.go\n contents:\n ```go\n package main\n\n import (\n \"fmt\"\n \"log\"\n \"net/http\"\n )\n\n func main() {\n http.Handle(\"/\", http.FileServer(http.Dir(\"./\")))\n fmt.Println(\"Server listening on :7777\")\n log.Fatal(http.ListenAndServe(\":7777\", nil))\n }\n ```\n\n - name: index.html\n contents:\n ```html\n <!DOCTYPE html>\n <html>\n <head>\n <title>Pull Pal</title>\n <style>\n body {\n font-family: sans-serif;\n }\n .content {\n background-color: #f2f2f2;\n border-radius: 10px;\n box-shadow: 2px 2px 10px rgba(0,0,0,0.2);\n padding: 20px;\n margin: 20px;\n }\n h1 {\n color: #3399cc;\n background-color: #f2f2f2;\n text-align: center;\n padding: 10px;\n border-radius: 10px;\n box-shadow: 2px 2px 10px rgba(0,0,0,0.2);\n }\n </style>\n </head>\n <body>\n <div class=\"content\">\n <h1>Introducing Pull Pal!</h1>\n <p>Pull Pal is a digital assistant that can monitor your Github repositories and create pull requests using the power of artificial intelligence. Say goodbye to manual pull request creation like it's 2005!</p>\n <p>Sign up now to start automating your workflow and get more done in less time. </p>\n </div>\n </body>\n </html>\n ```\n\nNotes:\n- Added a basic HTTP server in main.go which serves index.html from the root path on port 7777\n- Added basic HTML structure and CSS styling to index.html, using sans-serif font and a soft color scheme with blue as the main accent color\n- Added a content container with an off-white background, rounded corners, and a box shadow, and a centered heading with blue font color and an off-white background to draw attention to the product name and value proposition"
res := llm.ParseCodeChangeResponse(response)
fmt.Println(res.String())
*/
cmd.Execute()
}

View File

@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"io/ioutil"
"strconv"
"strings"
"time"
@ -63,17 +64,35 @@ func (p *PullPal) Run() error {
issues, err := p.ghClient.ListOpenIssues(p.listIssueOptions)
if err != nil {
p.log.Error("error listing issues", zap.Error(err))
continue
return err
}
if len(issues) == 0 {
// todo don't sleep
p.log.Info("no issues found. sleeping for 5 mins")
time.Sleep(5 * time.Minute)
p.log.Info("no issues found. sleeping for 30 seconds")
time.Sleep(30 * time.Second)
continue
}
issue := issues[0]
issueNumber, err := strconv.Atoi(issue.ID)
if err != nil {
p.log.Error("error converting issue ID to int", zap.Error(err))
return err
}
err = p.ghClient.CommentOnIssue(issueNumber, "on it")
if err != nil {
p.log.Error("error commenting on issue", zap.Error(err))
return err
}
for _, label := range p.listIssueOptions.Labels {
err = p.ghClient.RemoveLabelFromIssue(issueNumber, label)
if err != nil {
p.log.Error("error removing labels from issue", zap.Error(err))
return err
}
}
// remove file list from issue body
// TODO do this better and probably somewhere else
@ -115,38 +134,43 @@ func (p *PullPal) Run() error {
//codeChangeResponse := llm.ParseCodeChangeResponse(llmResponse)
// create commit with file changes
newBranchName := fmt.Sprintf("fix-%s", changeRequest.IssueID)
err = p.localGitClient.SwitchBranch(newBranchName)
if err != nil {
p.log.Error("error switching branch", zap.Error(err))
continue
}
err = p.localGitClient.StartCommit()
//err = p.ghClient.StartCommit()
if err != nil {
p.log.Error("error starting commit", zap.Error(err))
continue
return err
}
newBranchName := fmt.Sprintf("fix-%s", changeRequest.IssueID)
/*
err = p.localGitClient.SwitchBranch(newBranchName)
if err != nil {
p.log.Error("error switching branch", zap.Error(err))
return err
}
*/
for _, f := range changeResponse.Files {
p.log.Info("replacing or adding file", zap.String("path", f.Path), zap.String("contents", f.Contents))
err = p.localGitClient.ReplaceOrAddLocalFile(f)
// err = p.ghClient.ReplaceOrAddLocalFile(f)
if err != nil {
p.log.Error("error replacing or adding file", zap.Error(err))
continue
return err
}
}
commitMessage := changeRequest.Subject + "\n\n" + changeResponse.Notes + "\n\nResolves: #" + changeRequest.IssueID
p.log.Info("about to create commit", zap.String("message", commitMessage))
err = p.localGitClient.FinishCommit(commitMessage)
if err != nil {
p.log.Error("error finshing commit", zap.Error(err))
continue
p.log.Error("error finishing commit", zap.Error(err))
// TODO figure out why sometimes finish commit returns "already up-to-date error"
// return err
}
err = p.localGitClient.PushBranch(newBranchName)
if err != nil {
p.log.Error("error finshing commit", zap.Error(err))
continue
p.log.Error("error pushing branch", zap.Error(err))
return err
}
// open code change request
@ -154,11 +178,12 @@ func (p *PullPal) Run() error {
_, url, err := p.ghClient.OpenCodeChangeRequest(changeRequest, changeResponse, newBranchName, "main")
if err != nil {
p.log.Error("error opening PR", zap.Error(err))
return err
}
p.log.Info("successfully created PR", zap.String("URL", url))
p.log.Info("going to sleep for five mins")
time.Sleep(5 * time.Minute)
p.log.Info("going to sleep for thirty seconds")
time.Sleep(30 * time.Second)
}
return nil

47
pullpal/debug.go Normal file
View File

@ -0,0 +1,47 @@
package pullpal
import (
"fmt"
"github.com/mobyvb/pull-pal/llm"
"go.uber.org/zap"
)
func (p *PullPal) DebugGit() error {
p.log.Info("Starting Pull Pal git debug")
// create commit with file changes
err := p.localGitClient.StartCommit()
//err = p.ghClient.StartCommit()
if err != nil {
p.log.Error("error starting commit", zap.Error(err))
return err
}
newBranchName := fmt.Sprintf("debug-branch")
for _, f := range []string{"a", "b"} {
err = p.localGitClient.ReplaceOrAddLocalFile(llm.File{
Path: f,
Contents: "hello",
})
if err != nil {
p.log.Error("error replacing or adding file", zap.Error(err))
return err
}
}
commitMessage := "debug commit message"
err = p.localGitClient.FinishCommit(commitMessage)
if err != nil {
p.log.Error("error finishing commit", zap.Error(err))
return err
}
err = p.localGitClient.PushBranch(newBranchName)
if err != nil {
p.log.Error("error pushing branch", zap.Error(err))
return err
}
return nil
}

View File

@ -14,7 +14,6 @@ import (
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/config"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/object"
"github.com/go-git/go-git/v5/plumbing/transport/http"
)
@ -59,24 +58,44 @@ func NewLocalGitClient( /*ctx context.Context, log *zap.Logger, */ self Author,
}, nil
}
/*
func (gc *LocalGitClient) SwitchBranch(branchName string) (err error) {
branchRefName := plumbing.NewBranchReferenceName(branchName)
remoteName := "origin"
if gc.worktree == nil {
return errors.New("worktree is nil - cannot check out a branch")
}
err = gc.repo.localRepo.CreateBranch(&config.Branch{
Name: branchName,
Remote: remoteName,
Merge: branchRefName,
branchRefName := plumbing.NewBranchReferenceName(branchName)
// remoteName := "origin"
err = gc.repo.localRepo.Fetch(&git.FetchOptions{
RefSpecs: []config.RefSpec{"refs/*:refs/*", "HEAD:refs/heads/HEAD"},
})
if err != nil {
return err
}
err = gc.worktree.Checkout(&git.CheckoutOptions{
Branch: branchRefName,
Force: true,
})
if err != nil {
return err
}
err = gc.repo.localRepo.CreateBranch(&config.Branch{
Name: branchName,
Remote: remoteName,
Merge: branchRefName,
})
if err != nil {
return err
}
return nil
}
*/
func (gc *LocalGitClient) PushBranch(branchName string) (err error) {
branchRefName := plumbing.NewBranchReferenceName(branchName)
//branchRefName := plumbing.NewBranchReferenceName(branchName)
remoteName := "origin"
// Push the new branch to the remote repository
@ -86,7 +105,9 @@ func (gc *LocalGitClient) PushBranch(branchName string) (err error) {
}
err = remote.Push(&git.PushOptions{
RefSpecs: []config.RefSpec{config.RefSpec(fmt.Sprintf("%s:refs/heads/%s", branchRefName, branchName))},
RemoteName: remoteName,
// TODO remove hardcoded "main"
RefSpecs: []config.RefSpec{config.RefSpec(fmt.Sprintf("+refs/heads/%s:refs/heads/%s", "main", branchName))},
Auth: &http.BasicAuth{
Username: gc.self.Handle,
Password: gc.self.Token,
@ -146,9 +167,13 @@ func (gc *LocalGitClient) ReplaceOrAddLocalFile(newFile llm.File) error {
if strings.HasSuffix(newFile.Path, ".go") {
newContents, err := format.Source([]byte(newFile.Contents))
if err != nil {
return err
// TODO also make logger accessible
fmt.Println("go format error")
// TODO handle this error
// return err
} else {
newFile.Contents = string(newContents)
}
newFile.Contents = string(newContents)
}
fullPath := filepath.Join(gc.repo.LocalPath, newFile.Path)

View File

@ -208,6 +208,39 @@ func (gc *GithubClient) ListOpenIssues(options ListIssueOptions) ([]Issue, error
return toReturn, nil
}
// CommentOnIssue adds a comment to the issue provided.
func (gc *GithubClient) CommentOnIssue(issueNumber int, comment string) error {
ghComment := &github.IssueComment{
Body: github.String(comment),
}
_, _, err := gc.client.Issues.CreateComment(gc.ctx, gc.repo.Owner.Handle, gc.repo.Name, issueNumber, ghComment)
return err
}
// RemoveLabelFromIssue removes the provided label from an issue if that label is applied.
func (gc *GithubClient) RemoveLabelFromIssue(issueNumber int, label string) error {
hasLabel := false
labels, _, err := gc.client.Issues.ListLabelsByIssue(gc.ctx, gc.repo.Owner.Handle, gc.repo.Name, issueNumber, nil)
if err != nil {
return err
}
for _, l := range labels {
if l.GetName() == label {
hasLabel = true
break
}
}
if hasLabel {
_, err = gc.client.Issues.RemoveLabelForIssue(gc.ctx, gc.repo.Owner.Handle, gc.repo.Name, issueNumber, label)
return err
}
return nil
}
// ListOpenComments lists unresolved comments in the Github repository.
func (gc *GithubClient) ListOpenComments(options ListCommentOptions) ([]Comment, error) {
toReturn := []Comment{}