Skip to content

Commit 1a52581

Browse files
author
Paulo Gomes
committed
Remove direct dependency to go-git
Signed-off-by: Paulo Gomes <[email protected]>
1 parent 121577c commit 1a52581

File tree

4 files changed

+518
-346
lines changed

4 files changed

+518
-346
lines changed

controllers/git_test.go

Lines changed: 17 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -8,44 +8,39 @@ import (
88
"testing"
99
"time"
1010

11-
"github.com/go-git/go-billy/v5/memfs"
12-
gogit "github.com/go-git/go-git/v5"
13-
"github.com/go-git/go-git/v5/plumbing/object"
14-
"github.com/go-git/go-git/v5/storage/memory"
1511
"github.com/go-logr/logr"
12+
libgit2 "github.com/libgit2/git2go/v33"
1613

1714
"github.com/fluxcd/pkg/gittestserver"
1815
)
1916

20-
func populateRepoFromFixture(repo *gogit.Repository, fixture string) error {
21-
working, err := repo.Worktree()
17+
func populateRepoFromFixture(repo *libgit2.Repository, fixture string) error {
18+
absFixture, err := filepath.Abs(fixture)
2219
if err != nil {
2320
return err
2421
}
25-
fs := working.Filesystem
26-
27-
if err = filepath.Walk(fixture, func(path string, info os.FileInfo, err error) error {
22+
if err := filepath.Walk(absFixture, func(path string, info os.FileInfo, err error) error {
2823
if err != nil {
2924
return err
3025
}
3126
if info.IsDir() {
32-
return fs.MkdirAll(fs.Join(path[len(fixture):]), info.Mode())
27+
return os.MkdirAll(filepath.Join(path[len(fixture):]), info.Mode())
3328
}
3429
// copy symlinks as-is, so I can test what happens with broken symlinks
3530
if info.Mode()&os.ModeSymlink > 0 {
3631
target, err := os.Readlink(path)
3732
if err != nil {
3833
return err
3934
}
40-
return fs.Symlink(target, path[len(fixture):])
35+
return os.Symlink(target, path[len(fixture):])
4136
}
4237

4338
fileBytes, err := os.ReadFile(path)
4439
if err != nil {
4540
return err
4641
}
4742

48-
ff, err := fs.Create(path[len(fixture):])
43+
ff, err := os.Create(path[len(fixture):])
4944
if err != nil {
5045
return err
5146
}
@@ -57,34 +52,31 @@ func populateRepoFromFixture(repo *gogit.Repository, fixture string) error {
5752
return err
5853
}
5954

60-
_, err = working.Add(".")
61-
if err != nil {
62-
return err
55+
sig := &libgit2.Signature{
56+
Name: "Testbot",
57+
58+
When: time.Now(),
6359
}
6460

65-
if _, err = working.Commit("Initial revision from "+fixture, &gogit.CommitOptions{
66-
Author: &object.Signature{
67-
Name: "Testbot",
68-
69-
When: time.Now(),
70-
},
71-
}); err != nil {
61+
if _, err := commitWorkDir(repo, "main", "Initial revision from "+fixture, sig); err != nil {
7262
return err
7363
}
7464

7565
return nil
7666
}
7767

7868
func TestRepoForFixture(t *testing.T) {
79-
repo, err := gogit.Init(memory.NewStorage(), memfs.New())
69+
tmp, err := os.MkdirTemp("", "flux-test")
8070
if err != nil {
8171
t.Fatal(err)
8272
}
73+
defer os.RemoveAll(tmp)
8374

84-
err = populateRepoFromFixture(repo, "testdata/pathconfig")
75+
repo, err := initGitRepoPlain("testdata/pathconfig", tmp)
8576
if err != nil {
8677
t.Error(err)
8778
}
79+
repo.Free()
8880
}
8981

9082
func TestIgnoreBrokenSymlink(t *testing.T) {
@@ -95,11 +87,7 @@ func TestIgnoreBrokenSymlink(t *testing.T) {
9587
}
9688
defer os.RemoveAll(tmp)
9789

98-
repo, err := gogit.PlainInit(tmp, false)
99-
if err != nil {
100-
t.Fatal(err)
101-
}
102-
err = populateRepoFromFixture(repo, "testdata/brokenlink")
90+
repo, err := initGitRepoPlain("testdata/brokenlink", tmp)
10391
if err != nil {
10492
t.Fatal(err)
10593
}

controllers/imageupdateautomation_controller.go

Lines changed: 136 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,11 @@ import (
2929
"time"
3030

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

3534
"github.com/ProtonMail/go-crypto/openpgp"
35+
"github.com/ProtonMail/go-crypto/openpgp/packet"
3636
securejoin "github.com/cyphar/filepath-securejoin"
37-
"github.com/go-git/go-git/v5/plumbing"
38-
"github.com/go-git/go-git/v5/plumbing/object"
3937
"github.com/go-logr/logr"
4038
corev1 "k8s.io/api/core/v1"
4139
apimeta "k8s.io/apimachinery/pkg/api/meta"
@@ -253,10 +251,11 @@ func (r *ImageUpdateAutomationReconciler) Reconcile(ctx context.Context, req ctr
253251
// Use the git operations timeout for the repo.
254252
cloneCtx, cancel := context.WithTimeout(ctx, origin.Spec.Timeout.Duration)
255253
defer cancel()
256-
var repo *gogit.Repository
254+
var repo *libgit2.Repository
257255
if repo, err = cloneInto(cloneCtx, access, ref, tmp); err != nil {
258256
return failWithError(err)
259257
}
258+
defer repo.Free()
260259

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

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

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

535-
return gogit.PlainOpen(path)
534+
return libgit2.OpenRepository(path)
536535
}
537536

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

544-
// is the branch already present?
545-
_, err := repo.Reference(localBranch, true)
546-
var create bool
547-
switch {
548-
case err == plumbing.ErrReferenceNotFound:
549-
// make a new branch, starting at HEAD
550-
create = true
551-
case err != nil:
548+
_, err = repo.CreateBranch(pushBranch, head, false)
552549
return err
553-
default:
554-
// local branch found, great
555-
break
556550
}
557551

558-
tree, err := repo.Worktree()
552+
return nil
553+
}
554+
555+
func headCommit(repo *libgit2.Repository) (*libgit2.Commit, error) {
556+
head, err := repo.Head()
559557
if err != nil {
560-
return err
558+
return nil, err
561559
}
562-
563-
return tree.Checkout(&gogit.CheckoutOptions{
564-
Branch: localBranch,
565-
Create: create,
566-
})
560+
defer head.Free()
561+
c, err := repo.LookupCommit(head.Target())
562+
if err != nil {
563+
return nil, err
564+
}
565+
return c, nil
567566
}
568567

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

571-
func commitChangedManifests(tracelog logr.Logger, repo *gogit.Repository, absRepoPath string, ent *openpgp.Entity, author *object.Signature, message string) (string, error) {
572-
working, err := repo.Worktree()
570+
func commitChangedManifests(tracelog logr.Logger, repo *libgit2.Repository, absRepoPath string, ent *openpgp.Entity, sig *libgit2.Signature, message string) (string, error) {
571+
sl, err := repo.StatusList(&libgit2.StatusOptions{
572+
Show: libgit2.StatusShowIndexAndWorkdir,
573+
})
573574
if err != nil {
574575
return "", err
575576
}
576-
status, err := working.Status()
577+
defer sl.Free()
578+
579+
count, err := sl.EntryCount()
577580
if err != nil {
578581
return "", err
579582
}
580583

581-
// go-git has [a bug](https://github.com/go-git/go-git/issues/253)
582-
// whereby it thinks broken symlinks to absolute paths are
583-
// modified. There's no circumstance in which we want to commit a
584-
// change to a broken symlink: so, detect and skip those.
585-
var changed bool
586-
for file, _ := range status {
587-
abspath := filepath.Join(absRepoPath, file)
588-
info, err := os.Lstat(abspath)
589-
if err != nil {
590-
return "", fmt.Errorf("checking if %s is a symlink: %w", file, err)
591-
}
592-
if info.Mode()&os.ModeSymlink > 0 {
593-
// symlinks are OK; broken symlinks are probably a result
594-
// of the bug mentioned above, but not of interest in any
595-
// case.
596-
if _, err := os.Stat(abspath); os.IsNotExist(err) {
597-
tracelog.Info("apparently broken symlink found; ignoring", "path", abspath)
598-
continue
584+
if count == 0 {
585+
return "", errNoChanges
586+
}
587+
588+
var parentC []*libgit2.Commit
589+
head, err := headCommit(repo)
590+
if err == nil {
591+
defer head.Free()
592+
parentC = append(parentC, head)
593+
}
594+
595+
index, err := repo.Index()
596+
if err != nil {
597+
return "", err
598+
}
599+
defer index.Free()
600+
601+
// add to index any files that are not within .git/
602+
if err = filepath.Walk(repo.Workdir(),
603+
func(path string, info os.FileInfo, err error) error {
604+
if err != nil {
605+
return err
606+
}
607+
rel, err := filepath.Rel(repo.Workdir(), path)
608+
if err != nil {
609+
return err
610+
}
611+
f, err := os.Stat(path)
612+
if err != nil {
613+
return err
599614
}
615+
if f.IsDir() || strings.HasPrefix(rel, ".git") || rel == "." {
616+
return nil
617+
}
618+
if err := index.AddByPath(rel); err != nil {
619+
tracelog.Info("adding file", "file", rel)
620+
return err
621+
}
622+
return nil
623+
}); err != nil {
624+
return "", err
625+
}
626+
627+
if err := index.Write(); err != nil {
628+
return "", err
629+
}
630+
631+
treeID, err := index.WriteTree()
632+
if err != nil {
633+
return "", err
634+
}
635+
636+
tree, err := repo.LookupTree(treeID)
637+
if err != nil {
638+
return "", err
639+
}
640+
defer tree.Free()
641+
642+
commitID, err := repo.CreateCommit("HEAD", sig, sig, message, tree, parentC...)
643+
if err != nil {
644+
return "", err
645+
}
646+
647+
// return unsigned commit if pgp entity is not provided
648+
if ent == nil {
649+
return commitID.String(), nil
650+
}
651+
652+
commit, err := repo.LookupCommit(commitID)
653+
if err != nil {
654+
return "", err
655+
}
656+
657+
signedCommitID, err := commit.WithSignatureUsing(func(commitContent string) (string, string, error) {
658+
cipherText := new(bytes.Buffer)
659+
err := openpgp.ArmoredDetachSignText(cipherText, ent, strings.NewReader(commitContent), &packet.Config{})
660+
if err != nil {
661+
return "", "", errors.New("error signing payload")
600662
}
601-
tracelog.Info("adding file", "file", file)
602-
working.Add(file)
603-
changed = true
663+
664+
return cipherText.String(), "", nil
665+
})
666+
if err != nil {
667+
return "", err
604668
}
669+
signedCommit, err := repo.LookupCommit(signedCommitID)
670+
if err != nil {
671+
return "", err
672+
}
673+
defer signedCommit.Free()
605674

606-
if !changed {
607-
return "", errNoChanges
675+
newHead, err := repo.Head()
676+
if err != nil {
677+
return "", err
608678
}
679+
defer newHead.Free()
609680

610-
var rev plumbing.Hash
611-
if rev, err = working.Commit(message, &gogit.CommitOptions{
612-
Author: author,
613-
SignKey: ent,
614-
}); err != nil {
681+
_, err = repo.References.Create(
682+
newHead.Name(),
683+
signedCommit.Id(),
684+
true,
685+
"repoint to signed commit",
686+
)
687+
if err != nil {
615688
return "", err
616689
}
617690

618-
return rev.String(), nil
691+
return signedCommitID.String(), nil
619692
}
620693

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

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

0 commit comments

Comments
 (0)