Implement "local repo" functionality in github client

* Retrieving the current state (i.e. contents) of file at path in local
  repo
* Starting a commit
* Updating the contents (or creating) file at path in local repo and
  updating the "started" commit
* Finishing a commit
* Pushing commit to new remote branch (as part of creating a PR)
* Removing local repository on "shutdown"
This commit is contained in:
Moby von Briesen 2023-04-22 16:59:59 -04:00
parent 6c42bbd7b8
commit 3aa79401d0
2 changed files with 147 additions and 2 deletions

View File

@ -52,4 +52,14 @@ type VCClient interface {
// UpdateCodeChangeRequest updates an existing code change request on the version control server. // UpdateCodeChangeRequest updates an existing code change request on the version control server.
// UpdateCodeChangeRequest(id string, res llm.CodeChangeResponse) // UpdateCodeChangeRequest(id string, res llm.CodeChangeResponse)
// TODO: add/read comments to/from issues and code change requests // TODO: add/read comments to/from issues and code change requests
// GetLocalFile gets the current representation of the file at the provided path from the local git repo.
GetLocalFile(path string) (llm.File, error)
// StartCommit initiates a commit process, after which files can be modified and added to the commit.
StartCommit() error
// ReplaceOrAddLocalFile updates or adds a file in the locally cloned repo, and applies these changes to the current git worktree.
ReplaceOrAddLocalFile(newFile llm.File) error
// FinishCommit completes a commit, after which a code change request can be opened or updated.
FinishCommit(message string) error
// Close tears down the VCClient.
Close() error
} }

View File

@ -6,12 +6,21 @@ import (
"encoding/hex" "encoding/hex"
"errors" "errors"
"fmt" "fmt"
"go/format"
"io/ioutil"
"os"
"path/filepath"
"strconv" "strconv"
"strings"
"github.com/mobyvb/pull-pal/llm"
"github.com/go-git/go-git/v5" "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" "github.com/go-git/go-git/v5/plumbing/transport/http"
"github.com/google/go-github/github" "github.com/google/go-github/github"
"github.com/mobyvb/pull-pal/llm"
"go.uber.org/zap" "go.uber.org/zap"
"golang.org/x/oauth2" "golang.org/x/oauth2"
) )
@ -24,6 +33,8 @@ type GithubClient struct {
client *github.Client client *github.Client
self Author self Author
repo Repository repo Repository
worktree *git.Worktree
} }
// NewGithubClient initializes a Github client and checks out a repository locally. // NewGithubClient initializes a Github client and checks out a repository locally.
@ -69,14 +80,16 @@ func NewGithubClient(ctx context.Context, log *zap.Logger, self Author, repo Rep
}, nil }, nil
} }
// OpenCodeChangeRequest opens a new PR on Github based on the provided LLM request and response. // OpenCodeChangeRequest pushes to a new remote branch and opens a PR on Github.
func (gc *GithubClient) OpenCodeChangeRequest(req llm.CodeChangeRequest, res llm.CodeChangeResponse) (id, url string, err error) { func (gc *GithubClient) OpenCodeChangeRequest(req llm.CodeChangeRequest, res llm.CodeChangeResponse) (id, url string, err error) {
// TODO handle gc.ctx canceled // TODO handle gc.ctx canceled
gc.log.Debug("Creating a new pull request...") gc.log.Debug("Creating a new pull request...")
title := req.Subject title := req.Subject
branchName := randomBranchName() branchName := randomBranchName()
branchRefName := plumbing.NewBranchReferenceName(branchName)
baseBranch := "main" baseBranch := "main"
remoteName := "origin"
body := res.Notes body := res.Notes
body += fmt.Sprintf("\n\nResolves #%s", req.IssueID) body += fmt.Sprintf("\n\nResolves #%s", req.IssueID)
issue, err := strconv.Atoi(req.IssueID) issue, err := strconv.Atoi(req.IssueID)
@ -85,6 +98,44 @@ func (gc *GithubClient) OpenCodeChangeRequest(req llm.CodeChangeRequest, res llm
return "", "", err return "", "", err
} }
// Create new local branch
headRef, err := gc.repo.localRepo.Head()
if err != nil {
return "", "", err
}
err = gc.repo.localRepo.CreateBranch(&config.Branch{
Name: branchName,
Remote: remoteName,
Merge: branchRefName,
})
if err != nil {
return "", "", err
}
// Update the branch to point to the new commit
err = gc.repo.localRepo.Storer.SetReference(plumbing.NewHashReference(branchRefName, headRef.Hash()))
if err != nil {
return "", "", err
}
// Push the new branch to the remote repository
remote, err := gc.repo.localRepo.Remote(remoteName)
if err != nil {
return "", "", err
}
err = remote.Push(&git.PushOptions{
RefSpecs: []config.RefSpec{config.RefSpec(fmt.Sprintf("%s:refs/heads/%s", branchRefName, branchName))},
Auth: &http.BasicAuth{
Username: gc.self.Handle,
Password: gc.self.Token,
},
})
if err != nil {
return "", "", err
}
// Finally, open a pull request from the new branch.
pr, _, err := gc.client.PullRequests.Create(gc.ctx, gc.repo.Owner.Handle, gc.repo.Name, &github.NewPullRequest{ pr, _, err := gc.client.PullRequests.Create(gc.ctx, gc.repo.Owner.Handle, gc.repo.Name, &github.NewPullRequest{
Title: &title, Title: &title,
Head: &branchName, Head: &branchName,
@ -136,3 +187,87 @@ func (gc *GithubClient) ListOpenIssues() ([]Issue, error) {
return toReturn, nil return toReturn, nil
} }
// GetLocalFile gets the current representation of the file at the provided path from the local git repo.
func (gc *GithubClient) GetLocalFile(path string) (llm.File, error) {
fullPath := filepath.Join(gc.repo.LocalPath, path)
data, err := ioutil.ReadFile(fullPath)
if err != nil {
return llm.File{}, err
}
return llm.File{
Path: path,
Contents: string(data),
}, nil
}
// StartCommit creates a new worktree associated with this Github client.
func (gc *GithubClient) StartCommit() error {
if gc.worktree != nil {
return errors.New("worktree is not nil - cannot start a new commit")
}
worktree, err := gc.repo.localRepo.Worktree()
if err != nil {
return err
}
gc.worktree = worktree
return nil
}
// ReplaceOrAddLocalFile updates or adds a file in the locally cloned repo, and applies these changes to the current git worktree.
func (gc *GithubClient) ReplaceOrAddLocalFile(newFile llm.File) error {
if gc.worktree == nil {
return errors.New("worktree is nil - StartCommit must be called")
}
// TODO format non-go files as well
if strings.HasSuffix(newFile.Path, ".go") {
newContents, err := format.Source([]byte(newFile.Contents))
if err != nil {
return err
}
newFile.Contents = string(newContents)
}
fullPath := filepath.Join(gc.repo.LocalPath, newFile.Path)
err := ioutil.WriteFile(fullPath, []byte(newFile.Contents), 0644)
if err != nil {
return err
}
_, err = gc.worktree.Add(newFile.Path)
return err
}
// FinishCommit completes a commit, after which a code change request can be opened or updated.
func (gc *GithubClient) FinishCommit(message string) error {
if gc.worktree == nil {
return errors.New("worktree is nil - StartCommit must be called")
}
_, err := gc.worktree.Commit(message, &git.CommitOptions{
Author: &object.Signature{
Name: gc.self.Handle,
Email: gc.self.Email,
},
})
return err
}
func (gc *GithubClient) Close() error {
// Remove local repository
if gc.repo.LocalPath != "" {
err := os.RemoveAll(gc.repo.LocalPath)
return err
}
return nil
}