tools/eksDistroBuildToolingOpsTools/pkg/git/gogitClient.go (617 lines of code) (raw):
package git
import (
"context"
"errors"
"fmt"
"io"
"os"
"strings"
"time"
"github.com/go-git/go-billy/v5"
"github.com/go-git/go-billy/v5/memfs"
gogit "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"
"github.com/go-git/go-git/v5/storage/memory"
"github.com/aws/eks-distro-build-tooling/tools/eksDistroBuildToolingOpsTools/pkg/logger"
"github.com/aws/eks-distro-build-tooling/tools/eksDistroBuildToolingOpsTools/pkg/retrier"
)
const (
gitTimeout = 600 * time.Second
maxRetries = 5
backOffPeriod = 5 * time.Second
emptyRepoError = "remote repository is empty"
)
type GogitClient struct {
Auth transport.AuthMethod
Client GoGit
Retrier *retrier.Retrier
RepoDirectory *string
RepoUrl string
InMemory bool
}
type Opt func(*GogitClient)
func NewClient(opts ...Opt) Client {
c := &GogitClient{
Client: &goGit{},
Retrier: retrier.NewWithMaxRetries(maxRetries, backOffPeriod),
}
for _, opt := range opts {
opt(c)
}
return c
}
func WithAuth(auth transport.AuthMethod) Opt {
return func(c *GogitClient) {
c.Auth = auth
}
}
func WithRepositoryUrl(repoUrl string) Opt {
return func(c *GogitClient) {
c.RepoUrl = repoUrl
}
}
func WithRepositoryDirectory(repoDir string) Opt {
return func(c *GogitClient) {
c.RepoDirectory = &repoDir
}
}
func WithInMemoryFilesystem() Opt {
return func(c *GogitClient) {
c.InMemory = true
}
}
func (g *GogitClient) Clone(ctx context.Context) error {
if g.RepoDirectory != nil && !g.InMemory {
_, err := g.Client.Clone(ctx, *g.RepoDirectory, g.RepoUrl, g.Auth)
if err != nil && strings.Contains(err.Error(), emptyRepoError) {
return &RepositoryIsEmptyError{
Repository: *g.RepoDirectory,
}
}
return nil
}
_, err := g.Client.CloneInMemory(ctx, g.RepoUrl, g.Auth)
if err != nil && strings.Contains(err.Error(), emptyRepoError) {
return &RepositoryIsEmptyError{
Repository: g.RepoUrl,
}
}
return err
}
func (g *GogitClient) Add(filename string) error {
logger.V(4).Info("Opening directory", "directory", g.RepoDirectory)
r, err := g.Client.OpenRepo()
if err != nil {
return err
}
logger.V(4).Info("Opening working tree")
w, err := g.Client.OpenWorktree(r)
if err != nil {
return err
}
logger.V(4).Info("Tracking specified files", "file", filename)
err = g.Client.AddGlob(filename, w)
return err
}
func (g *GogitClient) Remove(filename string) error {
logger.V(4).Info("Opening directory", "directory", g.RepoDirectory)
r, err := g.Client.OpenRepo()
if err != nil {
return err
}
logger.V(4).Info("Opening working tree")
w, err := g.Client.OpenWorktree(r)
if err != nil {
return err
}
logger.V(4).Info("Removing specified files", "file", filename)
_, err = g.Client.Remove(filename, w)
return err
}
type CommitOpt func(signature *object.Signature)
func WithUser(user string) CommitOpt {
return func(o *object.Signature) {
o.Name = user
}
}
func WithEmail(email string) CommitOpt {
return func(o *object.Signature) {
o.Email = email
}
}
func (g *GogitClient) Commit(message string, opts ...CommitOpt) error {
logger.V(4).Info("Opening directory", "directory", g.RepoDirectory)
r, err := g.Client.OpenRepo()
if err != nil {
logger.Info("Failed while attempting to open repo")
return err
}
logger.V(4).Info("Opening working tree")
w, err := g.Client.OpenWorktree(r)
if err != nil {
return err
}
logger.V(4).Info("Generating Commit object...")
commitSignature := &object.Signature{
When: time.Now(),
}
for _, opt := range opts {
opt(commitSignature)
}
commit, err := g.Client.Commit(message, commitSignature, w)
if err != nil {
return err
}
logger.V(4).Info("Committing Object to local repo", "repo", g.RepoDirectory)
finalizedCommit, err := g.Client.CommitObject(r, commit)
if err != nil {
return err
}
logger.Info("Finalized commit and committed to local repository", "hash", finalizedCommit.Hash)
return err
}
func (g *GogitClient) Push(ctx context.Context) error {
logger.V(4).Info("Pushing to remote", "repo", g.RepoDirectory)
r, err := g.Client.OpenRepo()
if err != nil {
return fmt.Errorf("err pushing: %v", err)
}
err = g.Client.PushWithContext(ctx, r, g.Auth)
if err != nil {
return fmt.Errorf("pushing: %v", err)
}
return err
}
func (g *GogitClient) Pull(ctx context.Context, branch string) error {
logger.V(4).Info("Pulling from remote", "repo", g.RepoDirectory, "remote", gogit.DefaultRemoteName)
r, err := g.Client.OpenRepo()
if err != nil {
return fmt.Errorf("pulling from remote: %v", err)
}
w, err := g.Client.OpenWorktree(r)
if err != nil {
return fmt.Errorf("pulling from remote: %v", err)
}
branchRef := plumbing.NewBranchReferenceName(branch)
err = g.Client.PullWithContext(ctx, w, g.Auth, branchRef)
if errors.Is(err, gogit.NoErrAlreadyUpToDate) {
logger.V(4).Info("Local repo already up-to-date", "repo", g.RepoDirectory, "remote", gogit.DefaultRemoteName)
return &RepositoryUpToDateError{}
}
if err != nil {
return fmt.Errorf("pulling from remote: %v", err)
}
ref, err := g.Client.Head(r)
if err != nil {
return fmt.Errorf("pulling from remote: %v", err)
}
commit, err := g.Client.CommitObject(r, ref.Hash())
if err != nil {
return fmt.Errorf("accessing latest commit after pulling from remote: %v", err)
}
logger.V(4).Info("Successfully pulled from remote", "repo", g.RepoDirectory, "remote", gogit.DefaultRemoteName, "latest commit", commit.Hash)
return nil
}
func (g *GogitClient) Init() error {
r, err := g.Client.Init(*g.RepoDirectory)
if err != nil {
return err
}
if _, err = g.Client.Create(r, g.RepoUrl); err != nil {
return fmt.Errorf("initializing repository: %v", err)
}
return nil
}
func (g *GogitClient) Branch(name string) error {
r, err := g.Client.OpenRepo()
if err != nil {
return fmt.Errorf("creating branch %s: %v", name, err)
}
localBranchRef := plumbing.NewBranchReferenceName(name)
branchOpts := &config.Branch{
Name: name,
Remote: gogit.DefaultRemoteName,
Merge: localBranchRef,
Rebase: "true",
}
err = g.Client.CreateBranch(r, branchOpts)
branchExistsLocally := errors.Is(err, gogit.ErrBranchExists)
if err != nil && !branchExistsLocally {
return fmt.Errorf("creating branch %s: %v", name, err)
}
if branchExistsLocally {
logger.V(4).Info("Branch already exists locally", "branch", name)
}
if !branchExistsLocally {
logger.V(4).Info("Branch does not exist locally", "branch", name)
headref, err := g.Client.Head(r)
if err != nil {
return fmt.Errorf("creating branch %s: %v", name, err)
}
h := headref.Hash()
err = g.Client.SetRepositoryReference(r, plumbing.NewHashReference(localBranchRef, h))
if err != nil {
return fmt.Errorf("creating branch %s: %v", name, err)
}
}
w, err := g.Client.OpenWorktree(r)
if err != nil {
return fmt.Errorf("creating branch %s: %v", name, err)
}
err = g.Client.Checkout(w, &gogit.CheckoutOptions{
Branch: plumbing.ReferenceName(localBranchRef.String()),
Force: true,
})
if err != nil {
return fmt.Errorf("creating branch %s: %v", name, err)
}
err = g.pullIfRemoteExists(r, w, name, localBranchRef)
if err != nil {
return fmt.Errorf("creating branch %s: %v", name, err)
}
return nil
}
func (g *GogitClient) ValidateRemoteExists(ctx context.Context) error {
logger.V(4).Info("Validating git setup", "repoUrl", g.RepoUrl)
remote := g.Client.NewRemote(g.RepoUrl, gogit.DefaultRemoteName)
// Check if we are able to make a connection to the remote by attempting to list refs
_, err := g.Client.ListWithContext(ctx, remote, g.Auth)
if err != nil {
return fmt.Errorf("connecting with remote %v for repository: %v", gogit.DefaultRemoteName, err)
}
return nil
}
func (g *GogitClient) Status() error {
repo, err := g.Client.OpenRepo()
if err != nil {
logger.Error(err, "Opening repo")
return err
}
wt, err := repo.Worktree()
if err != nil {
logger.Error(err, "Accessing worktree")
return err
}
s, err := wt.Status()
if err != nil {
logger.Error(err, "git status")
return err
}
logger.V(4).Info("git status", "status", s)
return nil
}
func (g *GogitClient) pullIfRemoteExists(r *gogit.Repository, w *gogit.Worktree, branchName string, localBranchRef plumbing.ReferenceName) error {
err := g.Retrier.Retry(func() error {
remoteExists, err := g.remoteBranchExists(r, localBranchRef)
if err != nil {
return fmt.Errorf("checking if remote branch exists %s: %v", branchName, err)
}
if remoteExists {
err = g.Client.PullWithContext(context.Background(), w, g.Auth, localBranchRef)
if err != nil && !errors.Is(err, gogit.NoErrAlreadyUpToDate) && !errors.Is(err, gogit.ErrRemoteNotFound) {
return fmt.Errorf("pulling from remote when checking out existing branch %s: %v", branchName, err)
}
}
return nil
})
return err
}
func (g *GogitClient) remoteBranchExists(r *gogit.Repository, localBranchRef plumbing.ReferenceName) (bool, error) {
reflist, err := g.Client.ListRemotes(r, g.Auth)
if err != nil {
if strings.Contains(err.Error(), emptyRepoError) {
return false, nil
}
return false, fmt.Errorf("listing remotes: %v", err)
}
lb := localBranchRef.String()
for _, ref := range reflist {
if ref.Name().String() == lb {
return true, nil
}
}
return false, nil
}
func (g *GogitClient) CreateFile(filename string, contents []byte) error {
repo, err := g.Client.OpenRepo()
if err != nil {
logger.Error(err, "Opening repo")
return err
}
wt, err := repo.Worktree()
if err != nil {
logger.Error(err, "Accessing worktree")
return err
}
file, err := wt.Filesystem.Create(filename)
if err != nil {
logger.Error(err, "file creation", filename)
}
_, err = file.Write(contents)
if err != nil {
logger.Error(err, "writing to file", filename, "contents", contents)
return err
}
if err := file.Close(); err != nil {
return err
}
logger.V(4).Info("New file created", "file", filename)
return nil
}
func (g *GogitClient) MoveFile(curPath, dstPath string) error {
repo, err := g.Client.OpenRepo()
if err != nil {
logger.Error(err, "Opening repo")
return err
}
wt, err := repo.Worktree()
if err != nil {
logger.Error(err, "Accessing worktree")
return err
}
if err := wt.Filesystem.Rename(curPath, dstPath); err != nil {
logger.Error(err, "moving file", "current", curPath, "destination", dstPath)
return err
}
logger.V(4).Info("File moved", "file", dstPath)
return nil
}
func (g *GogitClient) CopyFile(curPath, dstPath string) error {
repo, err := g.Client.OpenRepo()
if err != nil {
logger.Error(err, "Opening repo")
return err
}
wt, err := repo.Worktree()
if err != nil {
logger.Error(err, "Accessing worktree")
return err
}
curFile, err := wt.Filesystem.Open(curPath)
if err != nil {
logger.Error(err, "Opening file", curPath)
return err
}
dstFile, err := wt.Filesystem.Create(dstPath)
if err != nil {
logger.Error(err, "creating file", dstFile)
return err
}
if _, err = io.Copy(curFile, dstFile); err != nil {
logger.Error(err, "copying file", "original", curFile, "destination", dstFile)
return err
}
if err := curFile.Close(); err != nil {
return err
}
if err := dstFile.Close(); err != nil {
return err
}
logger.V(4).Info("File copied", "destination file", dstFile)
return nil
}
func (g *GogitClient) DeleteFile(filename string) error {
repo, err := g.Client.OpenRepo()
if err != nil {
logger.Error(err, "Opening repo")
return err
}
wt, err := repo.Worktree()
if err != nil {
logger.Error(err, "Accessing worktree")
return err
}
file, err := wt.Filesystem.Create(filename)
if err != nil {
logger.Error(err, "file creation", filename)
}
if err := file.Close(); err != nil {
return err
}
logger.V(4).Info("New file created", "file", filename)
return nil
}
func (g *GogitClient) ModifyFile(filename string, contents []byte) error {
return g.CreateFile(filename, contents)
}
func (g *GogitClient) ReadFile(filename string) (string, error) {
repo, err := g.Client.OpenRepo()
if err != nil {
logger.Error(err, "Opening repo")
return "", err
}
ref, err := g.Client.Head(repo)
if err != nil {
logger.Error(err, "repo ref")
return "", err
}
commit, err := repo.CommitObject(ref.Hash())
if err != nil {
logger.Error(err, "commit")
return "", err
}
tree, err := repo.TreeObject(commit.TreeHash)
if err != nil {
logger.Error(err, "tree")
return "", err
}
file, err := tree.File(filename)
if err != nil {
logger.Error(err, "finding filename", "filename", filename)
return "", err
}
return file.Contents()
}
func (g *GogitClient) ReadFiles(foldername string) (map[string]string, error) {
repo, err := g.Client.OpenRepo()
if err != nil {
logger.Error(err, "Opening repo")
return nil, err
}
ref, err := g.Client.Head(repo)
if err != nil {
return nil, err
}
commit, err := repo.CommitObject(ref.Hash())
if err != nil {
return nil, err
}
tree, err := repo.TreeObject(commit.TreeHash)
if err != nil {
return nil, err
}
files := make(map[string]string)
err = tree.Files().ForEach(func(f *object.File) error {
if strings.Contains(f.Name, foldername) {
p, err := f.Contents()
if err != nil {
return err
}
files[f.Name] = p
}
return nil
})
if err != nil {
return nil, fmt.Errorf("reading files from folder: %s, %v", foldername, err)
}
return files, nil
}
type GoGit interface {
AddGlob(f string, w *gogit.Worktree) error
Checkout(w *gogit.Worktree, opts *gogit.CheckoutOptions) error
Clone(ctx context.Context, dir string, repoUrl string, auth transport.AuthMethod) (*gogit.Repository, error)
CloneInMemory(ctx context.Context, repourl string, auth transport.AuthMethod) (*gogit.Repository, error)
Commit(m string, sig *object.Signature, w *gogit.Worktree) (plumbing.Hash, error)
CommitObject(r *gogit.Repository, h plumbing.Hash) (*object.Commit, error)
Create(r *gogit.Repository, url string) (*gogit.Remote, error)
CreateBranch(r *gogit.Repository, config *config.Branch) error
Head(r *gogit.Repository) (*plumbing.Reference, error)
NewRemote(url, remoteName string) *gogit.Remote
Init(dir string) (*gogit.Repository, error)
OpenRepo() (*gogit.Repository, error)
OpenWorktree(r *gogit.Repository) (*gogit.Worktree, error)
PushWithContext(ctx context.Context, r *gogit.Repository, auth transport.AuthMethod) error
PullWithContext(ctx context.Context, w *gogit.Worktree, auth transport.AuthMethod, ref plumbing.ReferenceName) error
ListRemotes(r *gogit.Repository, auth transport.AuthMethod) ([]*plumbing.Reference, error)
ListWithContext(ctx context.Context, r *gogit.Remote, auth transport.AuthMethod) ([]*plumbing.Reference, error)
Remove(f string, w *gogit.Worktree) (plumbing.Hash, error)
SetRepositoryReference(r *gogit.Repository, p *plumbing.Reference) error
WithRepositoryDirectory(dir string)
}
type goGit struct {
storer *memory.Storage
worktreeFilesystem billy.Filesystem
repositoryDirectory string
}
func (gg *goGit) WithRepositoryDirectory(dir string) {
gg.repositoryDirectory = dir
}
func (gg *goGit) CloneInMemory(ctx context.Context, repourl string, auth transport.AuthMethod) (*gogit.Repository, error) {
ctx, cancel := context.WithTimeout(ctx, gitTimeout)
defer cancel()
if gg.storer == nil {
gg.storer = memory.NewStorage()
}
return gogit.CloneContext(ctx, gg.storer, gg.worktreeFilesystem, &gogit.CloneOptions{
Auth: auth,
URL: repourl,
Progress: os.Stdout,
})
}
func (gg *goGit) Clone(ctx context.Context, dir string, repourl string, auth transport.AuthMethod) (*gogit.Repository, error) {
ctx, cancel := context.WithTimeout(ctx, gitTimeout)
defer cancel()
return gogit.PlainCloneContext(ctx, dir, false, &gogit.CloneOptions{
Auth: auth,
URL: repourl,
Progress: os.Stdout,
})
}
func (gg *goGit) OpenRepo() (*gogit.Repository, error) {
if gg.storer == nil && gg.repositoryDirectory != "" {
return gogit.PlainOpen(gg.repositoryDirectory)
}
if gg.worktreeFilesystem == nil {
gg.worktreeFilesystem = memfs.New()
}
return gogit.Open(gg.storer, gg.worktreeFilesystem)
}
func (gg *goGit) OpenWorktree(r *gogit.Repository) (*gogit.Worktree, error) {
return r.Worktree()
}
func (gg *goGit) AddGlob(f string, w *gogit.Worktree) error {
return w.AddGlob(f)
}
func (gg *goGit) Commit(m string, sig *object.Signature, w *gogit.Worktree) (plumbing.Hash, error) {
return w.Commit(m, &gogit.CommitOptions{
Author: sig,
})
}
func (gg *goGit) CommitObject(r *gogit.Repository, h plumbing.Hash) (*object.Commit, error) {
return r.CommitObject(h)
}
func (gg *goGit) PushWithContext(ctx context.Context, r *gogit.Repository, auth transport.AuthMethod) error {
ctx, cancel := context.WithTimeout(ctx, gitTimeout)
defer cancel()
return r.PushContext(ctx, &gogit.PushOptions{
Auth: auth,
})
}
func (gg *goGit) PullWithContext(ctx context.Context, w *gogit.Worktree, auth transport.AuthMethod, ref plumbing.ReferenceName) error {
ctx, cancel := context.WithTimeout(ctx, gitTimeout)
defer cancel()
return w.PullContext(ctx, &gogit.PullOptions{RemoteName: gogit.DefaultRemoteName, Auth: auth, ReferenceName: ref})
}
func (gg *goGit) Head(r *gogit.Repository) (*plumbing.Reference, error) {
return r.Head()
}
func (gg *goGit) Init(dir string) (*gogit.Repository, error) {
return gogit.PlainInit(dir, false)
}
func (ggc *goGit) NewRemote(url, remoteName string) *gogit.Remote {
return gogit.NewRemote(memory.NewStorage(), &config.RemoteConfig{
Name: remoteName,
URLs: []string{url},
})
}
func (gg *goGit) Checkout(worktree *gogit.Worktree, opts *gogit.CheckoutOptions) error {
return worktree.Checkout(opts)
}
func (gg *goGit) Create(r *gogit.Repository, url string) (*gogit.Remote, error) {
return r.CreateRemote(&config.RemoteConfig{
Name: gogit.DefaultRemoteName,
URLs: []string{url},
})
}
func (gg *goGit) CreateBranch(repo *gogit.Repository, config *config.Branch) error {
return repo.CreateBranch(config)
}
func (gg *goGit) ListRemotes(r *gogit.Repository, auth transport.AuthMethod) ([]*plumbing.Reference, error) {
remote, err := r.Remote(gogit.DefaultRemoteName)
if err != nil {
if errors.Is(err, gogit.ErrRemoteNotFound) {
return []*plumbing.Reference{}, nil
}
return nil, err
}
refList, err := remote.List(&gogit.ListOptions{Auth: auth})
if err != nil {
return nil, err
}
return refList, nil
}
func (gg *goGit) Remove(f string, w *gogit.Worktree) (plumbing.Hash, error) {
return w.Remove(f)
}
func (gg *goGit) ListWithContext(ctx context.Context, r *gogit.Remote, auth transport.AuthMethod) ([]*plumbing.Reference, error) {
refList, err := r.ListContext(ctx, &gogit.ListOptions{Auth: auth})
if err != nil {
return nil, err
}
return refList, nil
}
func (gg *goGit) SetRepositoryReference(r *gogit.Repository, p *plumbing.Reference) error {
return r.Storer.SetReference(p)
}