From eea53db3a27d1c35b841c6794f4a428137cfddfa Mon Sep 17 00:00:00 2001 From: Moby von Briesen Date: Wed, 3 May 2023 20:05:34 -0400 Subject: [PATCH] now it works somewhat reliably --- cmd/debug.go | 32 +++++++++++++++++++++++++ llm/common.go | 16 +++++++++++-- llm/openai.go | 2 +- main.go | 5 ++++ pullpal/common.go | 59 +++++++++++++++++++++++++++++++++-------------- pullpal/debug.go | 47 +++++++++++++++++++++++++++++++++++++ vc/git.go | 47 ++++++++++++++++++++++++++++--------- vc/github.go | 33 ++++++++++++++++++++++++++ 8 files changed, 210 insertions(+), 31 deletions(-) create mode 100644 cmd/debug.go create mode 100644 pullpal/debug.go diff --git a/cmd/debug.go b/cmd/debug.go new file mode 100644 index 0000000..bd38088 --- /dev/null +++ b/cmd/debug.go @@ -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) +} diff --git a/llm/common.go b/llm/common.go index e3b7c5e..8d123f1 100644 --- a/llm/common.go +++ b/llm/common.go @@ -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) diff --git a/llm/openai.go b/llm/openai.go index 4233933..a2dc394 100644 --- a/llm/openai.go +++ b/llm/openai.go @@ -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 } diff --git a/main.go b/main.go index e9881bd..7d89a99 100644 --- a/main.go +++ b/main.go @@ -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 \n \n \n Pull Pal\n \n \n \n
\n

Introducing Pull Pal!

\n

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!

\n

Sign up now to start automating your workflow and get more done in less time.

\n
\n \n \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() } diff --git a/pullpal/common.go b/pullpal/common.go index ebbb720..20f1e20 100644 --- a/pullpal/common.go +++ b/pullpal/common.go @@ -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 diff --git a/pullpal/debug.go b/pullpal/debug.go new file mode 100644 index 0000000..2388e0c --- /dev/null +++ b/pullpal/debug.go @@ -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 +} diff --git a/vc/git.go b/vc/git.go index 2e8a521..0c2fbe7 100644 --- a/vc/git.go +++ b/vc/git.go @@ -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) diff --git a/vc/github.go b/vc/github.go index 1ec6231..afaff29 100644 --- a/vc/github.go +++ b/vc/github.go @@ -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{}