pull-pal/vc/git.go
Moby von Briesen 10c77854a9 Create "local git client" and "openai client"
OpenAIClient can be used to interact with OpenAI's API. Untested at the
moment (except with an API key that does not yet have perms)
Local git client splits the "git" functionality (e.g. make commits,
checkout branches, etc...) away from the "github" client

I also removed a lot of duplicate code for the different commands in cmd
And created a basic "local issue" command to send a "code change
request" to the llm, and receive a code change response. Eventually, I
want this to make the corresponding local git changes, for the user to
check.

TODO: github client should only be responsible for github-specific
actions, e.g. opening a PR or listing issues/comments
2023-04-25 20:32:08 -04:00

187 lines
4.2 KiB
Go

package vc
import (
"errors"
"fmt"
"go/format"
"io/ioutil"
"os"
"path/filepath"
"strings"
"time"
"github.com/mobyvb/pull-pal/llm"
"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"
)
// LocalGitClient represents a service that interacts with a local git repository.
type LocalGitClient struct {
self Author
repo Repository
worktree *git.Worktree
}
// NewLocalGitClient initializes a local git client by checking out a repository locally.
func NewLocalGitClient( /*ctx context.Context, log *zap.Logger, */ self Author, repo Repository) (*LocalGitClient, error) {
// clone provided repository to local path
if repo.LocalPath == "" {
return nil, errors.New("local path to clone repository not provided")
}
// remove local repo if it exists already
err := os.RemoveAll(repo.LocalPath)
if err != nil {
return nil, err
}
localRepo, err := git.PlainClone(repo.LocalPath, false, &git.CloneOptions{
URL: repo.SSH(),
// URL: repo.HTTPS(),
Auth: &http.BasicAuth{
Username: self.Handle,
Password: self.Token,
},
})
if err != nil {
return nil, err
}
repo.localRepo = localRepo
return &LocalGitClient{
self: self,
repo: repo,
}, nil
}
func (gc *LocalGitClient) SwitchBranch(branchName string) (err error) {
branchRefName := plumbing.NewBranchReferenceName(branchName)
remoteName := "origin"
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)
remoteName := "origin"
// 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
}
return nil
}
func (gc *LocalGitClient) GetLocalFile(path string) (llm.File, error) {
fullPath := filepath.Join(gc.repo.LocalPath, path)
data, err := ioutil.ReadFile(fullPath)
if err != nil {
// if file doesn't exist, just return an empty file
// this means we want to prompt the llm to populate it for the first time
if errors.Is(err, os.ErrNotExist) {
return llm.File{
Path: path,
Contents: "",
}, nil
}
return llm.File{}, err
}
return llm.File{
Path: path,
Contents: string(data),
}, nil
}
func (gc *LocalGitClient) 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 *LocalGitClient) 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 *LocalGitClient) 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,
When: time.Now(),
},
})
if err != nil {
return err
}
// set worktree to nil so a new commit can be started
gc.worktree = nil
return nil
}