Skip to content

Commit

Permalink
Remove direct dependency to go-git
Browse files Browse the repository at this point in the history
Signed-off-by: Paulo Gomes <[email protected]>
  • Loading branch information
Paulo Gomes committed Mar 4, 2022
1 parent 121577c commit 1a52581
Show file tree
Hide file tree
Showing 4 changed files with 518 additions and 346 deletions.
46 changes: 17 additions & 29 deletions controllers/git_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,44 +8,39 @@ import (
"testing"
"time"

"github.com/go-git/go-billy/v5/memfs"
gogit "github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing/object"
"github.com/go-git/go-git/v5/storage/memory"
"github.com/go-logr/logr"
libgit2 "github.com/libgit2/git2go/v33"

"github.com/fluxcd/pkg/gittestserver"
)

func populateRepoFromFixture(repo *gogit.Repository, fixture string) error {
working, err := repo.Worktree()
func populateRepoFromFixture(repo *libgit2.Repository, fixture string) error {
absFixture, err := filepath.Abs(fixture)
if err != nil {
return err
}
fs := working.Filesystem

if err = filepath.Walk(fixture, func(path string, info os.FileInfo, err error) error {
if err := filepath.Walk(absFixture, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
return fs.MkdirAll(fs.Join(path[len(fixture):]), info.Mode())
return os.MkdirAll(filepath.Join(path[len(fixture):]), info.Mode())
}
// copy symlinks as-is, so I can test what happens with broken symlinks
if info.Mode()&os.ModeSymlink > 0 {
target, err := os.Readlink(path)
if err != nil {
return err
}
return fs.Symlink(target, path[len(fixture):])
return os.Symlink(target, path[len(fixture):])
}

fileBytes, err := os.ReadFile(path)
if err != nil {
return err
}

ff, err := fs.Create(path[len(fixture):])
ff, err := os.Create(path[len(fixture):])
if err != nil {
return err
}
Expand All @@ -57,34 +52,31 @@ func populateRepoFromFixture(repo *gogit.Repository, fixture string) error {
return err
}

_, err = working.Add(".")
if err != nil {
return err
sig := &libgit2.Signature{
Name: "Testbot",
Email: "[email protected]",
When: time.Now(),
}

if _, err = working.Commit("Initial revision from "+fixture, &gogit.CommitOptions{
Author: &object.Signature{
Name: "Testbot",
Email: "[email protected]",
When: time.Now(),
},
}); err != nil {
if _, err := commitWorkDir(repo, "main", "Initial revision from "+fixture, sig); err != nil {
return err
}

return nil
}

func TestRepoForFixture(t *testing.T) {
repo, err := gogit.Init(memory.NewStorage(), memfs.New())
tmp, err := os.MkdirTemp("", "flux-test")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmp)

err = populateRepoFromFixture(repo, "testdata/pathconfig")
repo, err := initGitRepoPlain("testdata/pathconfig", tmp)
if err != nil {
t.Error(err)
}
repo.Free()
}

func TestIgnoreBrokenSymlink(t *testing.T) {
Expand All @@ -95,11 +87,7 @@ func TestIgnoreBrokenSymlink(t *testing.T) {
}
defer os.RemoveAll(tmp)

repo, err := gogit.PlainInit(tmp, false)
if err != nil {
t.Fatal(err)
}
err = populateRepoFromFixture(repo, "testdata/brokenlink")
repo, err := initGitRepoPlain("testdata/brokenlink", tmp)
if err != nil {
t.Fatal(err)
}
Expand Down
199 changes: 136 additions & 63 deletions controllers/imageupdateautomation_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,11 @@ import (
"time"

"github.com/Masterminds/sprig/v3"
gogit "github.com/go-git/go-git/v5"
libgit2 "github.com/libgit2/git2go/v33"

"github.com/ProtonMail/go-crypto/openpgp"
"github.com/ProtonMail/go-crypto/openpgp/packet"
securejoin "github.com/cyphar/filepath-securejoin"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/object"
"github.com/go-logr/logr"
corev1 "k8s.io/api/core/v1"
apimeta "k8s.io/apimachinery/pkg/api/meta"
Expand Down Expand Up @@ -253,10 +251,11 @@ func (r *ImageUpdateAutomationReconciler) Reconcile(ctx context.Context, req ctr
// Use the git operations timeout for the repo.
cloneCtx, cancel := context.WithTimeout(ctx, origin.Spec.Timeout.Duration)
defer cancel()
var repo *gogit.Repository
var repo *libgit2.Repository
if repo, err = cloneInto(cloneCtx, access, ref, tmp); err != nil {
return failWithError(err)
}
defer repo.Free()

// When there's a push spec, the pushed-to branch is where commits
// shall be made
Expand Down Expand Up @@ -333,13 +332,13 @@ func (r *ImageUpdateAutomationReconciler) Reconcile(ctx context.Context, req ctr
// The status message depends on what happens next. Since there's
// more than one way to succeed, there's some if..else below, and
// early returns only on failure.
author := &object.Signature{
signature := &libgit2.Signature{
Name: gitSpec.Commit.Author.Name,
Email: gitSpec.Commit.Author.Email,
When: time.Now(),
}

if rev, err := commitChangedManifests(tracelog, repo, tmp, signingEntity, author, message); err != nil {
if rev, err := commitChangedManifests(tracelog, repo, tmp, signingEntity, signature, message); err != nil {
if err != errNoChanges {
return failWithError(err)
}
Expand Down Expand Up @@ -514,9 +513,9 @@ func (r repoAccess) remoteCallbacks(ctx context.Context) libgit2.RemoteCallbacks
}

// cloneInto clones the upstream repository at the `ref` given (which
// can be `nil`). It returns a `*gogit.Repository` since that is used
// can be `nil`). It returns a `*libgit2.Repository` since that is used
// for committing changes.
func cloneInto(ctx context.Context, access repoAccess, ref *sourcev1.GitRepositoryRef, path string) (*gogit.Repository, error) {
func cloneInto(ctx context.Context, access repoAccess, ref *sourcev1.GitRepositoryRef, path string) (*libgit2.Repository, error) {
opts := git.CheckoutOptions{}
if ref != nil {
opts.Tag = ref.Tag
Expand All @@ -532,90 +531,164 @@ func cloneInto(ctx context.Context, access repoAccess, ref *sourcev1.GitReposito
return nil, err
}

return gogit.PlainOpen(path)
return libgit2.OpenRepository(path)
}

// switchBranch switches the repo from the current branch to the
// branch given. If the branch does not exist, it is created using the
// head as the starting point.
func switchBranch(repo *gogit.Repository, pushBranch string) error {
localBranch := plumbing.NewBranchReferenceName(pushBranch)
func switchBranch(repo *libgit2.Repository, pushBranch string) error {
if err := repo.SetHead(fmt.Sprintf("refs/heads/%s", pushBranch)); err != nil {
head, err := headCommit(repo)
if err != nil {
return err
}
defer head.Free()

// is the branch already present?
_, err := repo.Reference(localBranch, true)
var create bool
switch {
case err == plumbing.ErrReferenceNotFound:
// make a new branch, starting at HEAD
create = true
case err != nil:
_, err = repo.CreateBranch(pushBranch, head, false)
return err
default:
// local branch found, great
break
}

tree, err := repo.Worktree()
return nil
}

func headCommit(repo *libgit2.Repository) (*libgit2.Commit, error) {
head, err := repo.Head()
if err != nil {
return err
return nil, err
}

return tree.Checkout(&gogit.CheckoutOptions{
Branch: localBranch,
Create: create,
})
defer head.Free()
c, err := repo.LookupCommit(head.Target())
if err != nil {
return nil, err
}
return c, nil
}

var errNoChanges error = errors.New("no changes made to working directory")

func commitChangedManifests(tracelog logr.Logger, repo *gogit.Repository, absRepoPath string, ent *openpgp.Entity, author *object.Signature, message string) (string, error) {
working, err := repo.Worktree()
func commitChangedManifests(tracelog logr.Logger, repo *libgit2.Repository, absRepoPath string, ent *openpgp.Entity, sig *libgit2.Signature, message string) (string, error) {
sl, err := repo.StatusList(&libgit2.StatusOptions{
Show: libgit2.StatusShowIndexAndWorkdir,
})
if err != nil {
return "", err
}
status, err := working.Status()
defer sl.Free()

count, err := sl.EntryCount()
if err != nil {
return "", err
}

// go-git has [a bug](https://github.com/go-git/go-git/issues/253)
// whereby it thinks broken symlinks to absolute paths are
// modified. There's no circumstance in which we want to commit a
// change to a broken symlink: so, detect and skip those.
var changed bool
for file, _ := range status {
abspath := filepath.Join(absRepoPath, file)
info, err := os.Lstat(abspath)
if err != nil {
return "", fmt.Errorf("checking if %s is a symlink: %w", file, err)
}
if info.Mode()&os.ModeSymlink > 0 {
// symlinks are OK; broken symlinks are probably a result
// of the bug mentioned above, but not of interest in any
// case.
if _, err := os.Stat(abspath); os.IsNotExist(err) {
tracelog.Info("apparently broken symlink found; ignoring", "path", abspath)
continue
if count == 0 {
return "", errNoChanges
}

var parentC []*libgit2.Commit
head, err := headCommit(repo)
if err == nil {
defer head.Free()
parentC = append(parentC, head)
}

index, err := repo.Index()
if err != nil {
return "", err
}
defer index.Free()

// add to index any files that are not within .git/
if err = filepath.Walk(repo.Workdir(),
func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
rel, err := filepath.Rel(repo.Workdir(), path)
if err != nil {
return err
}
f, err := os.Stat(path)
if err != nil {
return err
}
if f.IsDir() || strings.HasPrefix(rel, ".git") || rel == "." {
return nil
}
if err := index.AddByPath(rel); err != nil {
tracelog.Info("adding file", "file", rel)
return err
}
return nil
}); err != nil {
return "", err
}

if err := index.Write(); err != nil {
return "", err
}

treeID, err := index.WriteTree()
if err != nil {
return "", err
}

tree, err := repo.LookupTree(treeID)
if err != nil {
return "", err
}
defer tree.Free()

commitID, err := repo.CreateCommit("HEAD", sig, sig, message, tree, parentC...)
if err != nil {
return "", err
}

// return unsigned commit if pgp entity is not provided
if ent == nil {
return commitID.String(), nil
}

commit, err := repo.LookupCommit(commitID)
if err != nil {
return "", err
}

signedCommitID, err := commit.WithSignatureUsing(func(commitContent string) (string, string, error) {
cipherText := new(bytes.Buffer)
err := openpgp.ArmoredDetachSignText(cipherText, ent, strings.NewReader(commitContent), &packet.Config{})
if err != nil {
return "", "", errors.New("error signing payload")
}
tracelog.Info("adding file", "file", file)
working.Add(file)
changed = true

return cipherText.String(), "", nil
})
if err != nil {
return "", err
}
signedCommit, err := repo.LookupCommit(signedCommitID)
if err != nil {
return "", err
}
defer signedCommit.Free()

if !changed {
return "", errNoChanges
newHead, err := repo.Head()
if err != nil {
return "", err
}
defer newHead.Free()

var rev plumbing.Hash
if rev, err = working.Commit(message, &gogit.CommitOptions{
Author: author,
SignKey: ent,
}); err != nil {
_, err = repo.References.Create(
newHead.Name(),
signedCommit.Id(),
true,
"repoint to signed commit",
)
if err != nil {
return "", err
}

return rev.String(), nil
return signedCommitID.String(), nil
}

// getSigningEntity retrieves an OpenPGP entity referenced by the
Expand Down Expand Up @@ -683,8 +756,8 @@ func fetch(ctx context.Context, path string, branch string, access repoAccess) e

// push pushes the branch given to the origin using the git library
// indicated by `impl`. It's passed both the path to the repo and a
// gogit.Repository value, since the latter may as well be used if the
// implementation is GoGit.
// libgit2.Repository value, since the latter may as well be used if the
// implementation is libgit2.
func push(ctx context.Context, path, branch string, access repoAccess) error {
repo, err := libgit2.OpenRepository(path)
if err != nil {
Expand Down
Loading

0 comments on commit 1a52581

Please sign in to comment.