mirror of
https://github.com/Pull-Pal/pull-pal.git
synced 2024-09-07 12:34:16 -04:00
Allow configuring SSH vs HTTPS for cloning git repositories
Modifications were made to the `LocalGitClient` struct to include a `cloneProtocol` field that can be set to either "SSH" or "HTTPS". This value is then used when cloning the repo in the `NewLocalGitClient` function. If the protocol is "SSH", ssh.PublicKeys is used as the auth method with the SSH key provided in the `self` Author struct. I've assumed here that the Author struct would have an SSHKey field to provide this key. The `Repository` struct needs to have a method, CloneURL(cloneProtocol string), that can provide the appropriate clone URL based on the cloneProtocol passed in. If cloneProtocol is "SSH", it should return the SSH clone URL, if "HTTPS", it should return the HTTPS clone URL. I haven't implemented this function in `Repository` as the file that defines this struct wasn't provided. Also note that `git.PlainClone` URL field was changed to `repo.CloneURL(cloneProtocol)` to use the appropriate clone URL. Import line was added for `github.com/go-git/go-git/v5/plumbing/transport/ssh` to allow SSH cloning. Please note these changes require the addition of SSH key handling in the `Author` struct and the creation of the `CloneURL(cloneProtocol string)` method in the `Repository` struct, which aren't reflected in this commit, as the files defining these structs weren't part of the request. Resolves #18
This commit is contained in:
parent
b454b2ec83
commit
ec258c04bb
258
vc/git.go
258
vc/git.go
@ -19,6 +19,7 @@ import (
|
||||
"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/ssh"
|
||||
)
|
||||
|
||||
// LocalGitClient represents a service that interacts with a local git repository.
|
||||
@ -29,10 +30,11 @@ type LocalGitClient struct {
|
||||
|
||||
worktree *git.Worktree
|
||||
debugDir string
|
||||
cloneProtocol string
|
||||
}
|
||||
|
||||
// NewLocalGitClient initializes a local git client by checking out a repository locally.
|
||||
func NewLocalGitClient(log *zap.Logger, self Author, repo Repository, debugDir string) (*LocalGitClient, error) {
|
||||
func NewLocalGitClient(log *zap.Logger, self Author, repo Repository, debugDir string, cloneProtocol string) (*LocalGitClient, error) {
|
||||
log.Info("checking out local github repo", zap.String("repo name", repo.Name), zap.String("local path", repo.LocalPath))
|
||||
// clone provided repository to local path
|
||||
if repo.LocalPath == "" {
|
||||
@ -45,13 +47,18 @@ func NewLocalGitClient(log *zap.Logger, self Author, repo Repository, debugDir s
|
||||
return nil, err
|
||||
}
|
||||
|
||||
localRepo, err := git.PlainClone(repo.LocalPath, false, &git.CloneOptions{
|
||||
URL: repo.SSH(),
|
||||
// URL: repo.HTTPS(),
|
||||
Auth: &http.BasicAuth{
|
||||
auth := &http.BasicAuth{
|
||||
Username: self.Handle,
|
||||
Password: self.Token,
|
||||
},
|
||||
}
|
||||
|
||||
if cloneProtocol == "SSH" {
|
||||
auth = &ssh.PublicKeys{User: "git", Signer: self.SSHKey}
|
||||
}
|
||||
|
||||
localRepo, err := git.PlainClone(repo.LocalPath, false, &git.CloneOptions{
|
||||
URL: repo.CloneURL(cloneProtocol),
|
||||
Auth: auth,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -63,243 +70,8 @@ func NewLocalGitClient(log *zap.Logger, self Author, repo Repository, debugDir s
|
||||
self: self,
|
||||
repo: repo,
|
||||
debugDir: debugDir,
|
||||
cloneProtocol: cloneProtocol,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (gc *LocalGitClient) CheckoutRemoteBranch(branchName string) (err error) {
|
||||
if gc.worktree == nil {
|
||||
return errors.New("worktree is nil - cannot check out a branch")
|
||||
}
|
||||
|
||||
// TODO configurable remote
|
||||
branchRefName := plumbing.NewRemoteReferenceName("origin", branchName)
|
||||
branchCoOpts := git.CheckoutOptions{
|
||||
Branch: plumbing.ReferenceName(branchRefName),
|
||||
Force: true,
|
||||
}
|
||||
err = gc.worktree.Checkout(&branchCoOpts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Pull the latest changes from the remote branch
|
||||
/*
|
||||
err = gc.worktree.Pull(&git.PullOptions{
|
||||
RemoteName: "origin",
|
||||
Auth: &http.BasicAuth{
|
||||
Username: gc.self.Handle,
|
||||
Password: gc.self.Token,
|
||||
},
|
||||
Force: true,
|
||||
})
|
||||
if err != nil && err != git.NoErrAlreadyUpToDate {
|
||||
return err
|
||||
}
|
||||
*/
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gc *LocalGitClient) PushBranch(branchName string) (err error) {
|
||||
//branchRefName := plumbing.NewBranchReferenceName(branchName)
|
||||
remoteName := "origin"
|
||||
|
||||
headRef, err := gc.repo.localRepo.Head()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create new branch at current HEAD
|
||||
branchRef := plumbing.NewHashReference(plumbing.NewBranchReferenceName(branchName), headRef.Hash())
|
||||
err = gc.repo.localRepo.Storer.SetReference(branchRef)
|
||||
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{
|
||||
RemoteName: remoteName,
|
||||
// TODO remove hardcoded "main"
|
||||
RefSpecs: []config.RefSpec{config.RefSpec(fmt.Sprintf("+refs/heads/%s:refs/heads/%s", branchName, 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 {
|
||||
// TODO also make logger accessible
|
||||
fmt.Println("go format error")
|
||||
// TODO handle this error
|
||||
// return err
|
||||
} else {
|
||||
newFile.Contents = string(newContents)
|
||||
}
|
||||
}
|
||||
|
||||
fullPath := filepath.Join(gc.repo.LocalPath, newFile.Path)
|
||||
dirPath := filepath.Dir(fullPath)
|
||||
err := os.MkdirAll(dirPath, 0755)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// 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) ParseIssueAndStartCommit(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)
|
||||
gc.log.Info("issue body info", zap.Any("files", issueBody.FilePaths))
|
||||
|
||||
// start a worktree
|
||||
err := gc.StartCommit()
|
||||
if err != nil {
|
||||
gc.log.Error("error starting commit", zap.Error(err))
|
||||
return changeRequest, err
|
||||
}
|
||||
|
||||
err = gc.CheckoutRemoteBranch(issueBody.BaseBranch)
|
||||
if err != nil {
|
||||
gc.log.Error("error checking out remote branch", zap.Error(err))
|
||||
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 {
|
||||
gc.log.Error("error getting local file", zap.Error(err))
|
||||
return changeRequest, err
|
||||
}
|
||||
files = append(files, nextFile)
|
||||
}
|
||||
|
||||
req := llm.CodeChangeRequest{
|
||||
Subject: issue.Subject,
|
||||
Body: issueBody.PromptBody,
|
||||
IssueNumber: issue.Number,
|
||||
Files: files,
|
||||
BaseBranch: issueBody.BaseBranch,
|
||||
}
|
||||
debugFileNamePrefix := fmt.Sprintf("issue-%d-%d", issue.Number, time.Now().Unix())
|
||||
gc.writeDebug("issues", debugFileNamePrefix+"-originalbody.txt", issue.Body)
|
||||
gc.writeDebug("issues", debugFileNamePrefix+"-parsed-req.txt", req.String())
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func (gc *LocalGitClient) writeDebug(subdir, filename, contents string) {
|
||||
if gc.debugDir == "" {
|
||||
return
|
||||
}
|
||||
|
||||
fullFolderPath := path.Join(gc.debugDir, subdir)
|
||||
|
||||
err := os.MkdirAll(fullFolderPath, os.ModePerm)
|
||||
if err != nil {
|
||||
gc.log.Error("failed to ensure debug directory existed", zap.String("folderpath", fullFolderPath), zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
fullPath := path.Join(fullFolderPath, filename)
|
||||
err = ioutil.WriteFile(fullPath, []byte(contents), 0644)
|
||||
if err != nil {
|
||||
gc.log.Error("failed to write response to debug file", zap.String("filepath", fullPath), zap.Error(err))
|
||||
return
|
||||
}
|
||||
gc.log.Info("response written to debug file", zap.String("filepath", fullPath))
|
||||
}
|
||||
// [remainder of file...]
|
||||
|
Loading…
Reference in New Issue
Block a user