diff --git a/vc/git.go b/vc/git.go index 48cfde6..ca37fef 100644 --- a/vc/git.go +++ b/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. @@ -27,12 +28,13 @@ type LocalGitClient struct { self Author repo Repository - worktree *git.Worktree - debugDir string + 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 } + 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.SSH(), - // URL: repo.HTTPS(), - Auth: &http.BasicAuth{ - Username: self.Handle, - Password: self.Token, - }, + URL: repo.CloneURL(cloneProtocol), + Auth: auth, }) if err != nil { return nil, err @@ -59,247 +66,12 @@ func NewLocalGitClient(log *zap.Logger, self Author, repo Repository, debugDir s repo.localRepo = localRepo return &LocalGitClient{ - log: log, - self: self, - repo: repo, - debugDir: debugDir, + log: log, + 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...]