Skip to content

Commit 0b63def

Browse files
committed
Use libgit2 for clone, fetch, push
source-controller/pkg/git does shallow clones when using the go-git implementation, and apparently this causes problems when fetching a branch that has been merged at the origin: #164 So far as I can tell, getting a shallow clone breaks the automation, no matter whether go-git or libgit2 is used for operations after cloning. So: just use libgit2 for cloning, which means non-shallow clones; and, for fetch and push, since there's no functional difference between the implementations for those. Signed-off-by: Michael Bridgen <[email protected]>
1 parent f656b84 commit 0b63def

File tree

3 files changed

+104
-118
lines changed

3 files changed

+104
-118
lines changed

controllers/git_error_test.go

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -74,29 +74,3 @@ func TestLibgit2ErrorUnchanged(t *testing.T) {
7474
t.Errorf("expected %q, got %q", expectedReformat, reformattedMessage)
7575
}
7676
}
77-
78-
func TestGoGitErrorReplace(t *testing.T) {
79-
// this is what go-git uses as the error message is if the remote
80-
// sends a blank first line
81-
unknownMessage := `unknown error: remote: `
82-
err := errors.New(unknownMessage)
83-
err = gogitPushError(err)
84-
reformattedMessage := err.Error()
85-
if reformattedMessage == unknownMessage {
86-
t.Errorf("expected rewritten error, got %q", reformattedMessage)
87-
}
88-
}
89-
90-
func TestGoGitErrorUnchanged(t *testing.T) {
91-
// this is (roughly) what GitHub sends if the deploy key doesn't
92-
// have write access; go-git passes this on verbatim
93-
regularMessage := `remote: ERROR: deploy key does not have write access`
94-
expectedReformat := regularMessage
95-
err := errors.New(regularMessage)
96-
err = gogitPushError(err)
97-
reformattedMessage := err.Error()
98-
// test that it's been rewritten, without checking the exact content
99-
if len(reformattedMessage) > len(expectedReformat) {
100-
t.Errorf("expected %q, got %q", expectedReformat, reformattedMessage)
101-
}
102-
}

controllers/imageupdateautomation_controller.go

Lines changed: 26 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ import (
3434

3535
"github.com/ProtonMail/go-crypto/openpgp"
3636
securejoin "github.com/cyphar/filepath-securejoin"
37-
"github.com/go-git/go-git/v5/config"
3837
"github.com/go-git/go-git/v5/plumbing"
3938
"github.com/go-git/go-git/v5/plumbing/object"
4039
"github.com/go-logr/logr"
@@ -214,15 +213,15 @@ func (r *ImageUpdateAutomationReconciler) Reconcile(ctx context.Context, req ctr
214213
}
215214

216215
var repo *gogit.Repository
217-
if repo, err = cloneInto(ctx, access, ref, tmp, origin.Spec.GitImplementation); err != nil {
216+
if repo, err = cloneInto(ctx, access, ref, tmp); err != nil {
218217
return failWithError(err)
219218
}
220219

221220
// When there's a push spec, the pushed-to branch is where commits
222221
// shall be made
223222

224223
if gitSpec.Push != nil {
225-
if err := fetch(ctx, tmp, repo, pushBranch, access, origin.Spec.GitImplementation); err != nil && err != errRemoteBranchMissing {
224+
if err := fetch(ctx, tmp, pushBranch, access); err != nil && err != errRemoteBranchMissing {
226225
return failWithError(err)
227226
}
228227
if err = switchBranch(repo, pushBranch); err != nil {
@@ -307,7 +306,7 @@ func (r *ImageUpdateAutomationReconciler) Reconcile(ctx context.Context, req ctr
307306
return failWithError(err)
308307
}
309308
} else {
310-
if err := push(ctx, tmp, repo, pushBranch, access, origin.Spec.GitImplementation); err != nil {
309+
if err := push(ctx, tmp, pushBranch, access); err != nil {
311310
return failWithError(err)
312311
}
313312

@@ -424,6 +423,10 @@ func (r *ImageUpdateAutomationReconciler) automationsForImagePolicy(obj client.O
424423

425424
// --- git ops
426425

426+
// Note: libgit2 is always used for network operations; for cloning,
427+
// it will do a non-shallow clone, and for anything else, it doesn't
428+
// matter what is used.
429+
427430
type repoAccess struct {
428431
auth *git.Auth
429432
url string
@@ -433,19 +436,21 @@ func (r *ImageUpdateAutomationReconciler) getRepoAccess(ctx context.Context, rep
433436
var access repoAccess
434437
access.auth = &git.Auth{}
435438
access.url = repository.Spec.URL
436-
authStrat, err := gitstrat.AuthSecretStrategyForURL(access.url, git.CheckoutOptions{GitImplementation: repository.Spec.GitImplementation})
439+
440+
authStrat, err := gitstrat.AuthSecretStrategyForURL(access.url, git.CheckoutOptions{GitImplementation: sourcev1.LibGit2Implementation})
437441
if err != nil {
438442
return access, err
439443
}
440444

441445
if repository.Spec.SecretRef != nil && authStrat != nil {
446+
442447
name := types.NamespacedName{
443448
Namespace: repository.GetNamespace(),
444449
Name: repository.Spec.SecretRef.Name,
445450
}
446451

447452
var secret corev1.Secret
448-
err := r.Client.Get(ctx, name, &secret)
453+
err = r.Client.Get(ctx, name, &secret)
449454
if err != nil {
450455
err = fmt.Errorf("auth secret error: %w", err)
451456
return access, err
@@ -468,11 +473,10 @@ func (r repoAccess) remoteCallbacks() libgit2.RemoteCallbacks {
468473
}
469474

470475
// cloneInto clones the upstream repository at the `ref` given (which
471-
// can be `nil`), using the git library indicated by `impl`. It
472-
// returns a `*gogit.Repository` regardless of the git library, since
473-
// that is used for committing changes.
474-
func cloneInto(ctx context.Context, access repoAccess, ref *sourcev1.GitRepositoryRef, path, impl string) (*gogit.Repository, error) {
475-
checkoutStrat, err := gitstrat.CheckoutStrategyForRef(ref, git.CheckoutOptions{GitImplementation: impl})
476+
// can be `nil`). It returns a `*gogit.Repository` since that is used
477+
// for committing changes.
478+
func cloneInto(ctx context.Context, access repoAccess, ref *sourcev1.GitRepositoryRef, path string) (*gogit.Repository, error) {
479+
checkoutStrat, err := gitstrat.CheckoutStrategyForRef(ref, git.CheckoutOptions{GitImplementation: sourcev1.LibGit2Implementation})
476480
if err == nil {
477481
_, _, err = checkoutStrat.Checkout(ctx, path, access.url, access.auth)
478482
}
@@ -490,18 +494,12 @@ func switchBranch(repo *gogit.Repository, pushBranch string) error {
490494
localBranch := plumbing.NewBranchReferenceName(pushBranch)
491495

492496
// is the branch already present?
493-
_, err := repo.Reference(localBranch, false)
497+
_, err := repo.Reference(localBranch, true)
498+
var create bool
494499
switch {
495500
case err == plumbing.ErrReferenceNotFound:
496501
// make a new branch, starting at HEAD
497-
head, err := repo.Head()
498-
if err != nil {
499-
return err
500-
}
501-
branchRef := plumbing.NewHashReference(localBranch, head.Hash())
502-
if err = repo.Storer.SetReference(branchRef); err != nil {
503-
return err
504-
}
502+
create = true
505503
case err != nil:
506504
return err
507505
default:
@@ -516,6 +514,7 @@ func switchBranch(repo *gogit.Repository, pushBranch string) error {
516514

517515
return tree.Checkout(&gogit.CheckoutOptions{
518516
Branch: localBranch,
517+
Create: create,
519518
})
520519
}
521520

@@ -608,23 +607,12 @@ var errRemoteBranchMissing = errors.New("remote branch missing")
608607
// returns errRemoteBranchMissing (this is to work in sympathy with
609608
// `switchBranch`, which will create the branch if it doesn't
610609
// exist). For any other problem it will return the error.
611-
func fetch(ctx context.Context, path string, repo *gogit.Repository, branch string, access repoAccess, impl string) error {
610+
func fetch(ctx context.Context, path string, branch string, access repoAccess) error {
612611
refspec := fmt.Sprintf("refs/heads/%s:refs/heads/%s", branch, branch)
613-
switch impl {
614-
case sourcev1.LibGit2Implementation:
615-
lg2repo, err := libgit2.OpenRepository(path)
616-
if err != nil {
617-
return err
618-
}
619-
return fetchLibgit2(lg2repo, refspec, access)
620-
case sourcev1.GoGitImplementation:
621-
return fetchGoGit(ctx, repo, refspec, access)
622-
default:
623-
return fmt.Errorf("unknown git implementation %q", impl)
612+
repo, err := libgit2.OpenRepository(path)
613+
if err != nil {
614+
return err
624615
}
625-
}
626-
627-
func fetchLibgit2(repo *libgit2.Repository, refspec string, access repoAccess) error {
628616
origin, err := repo.Remotes.Lookup(originRemote)
629617
if err != nil {
630618
return err
@@ -641,69 +629,15 @@ func fetchLibgit2(repo *libgit2.Repository, refspec string, access repoAccess) e
641629
return err
642630
}
643631

644-
func fetchGoGit(ctx context.Context, repo *gogit.Repository, refspec string, access repoAccess) error {
645-
err := repo.FetchContext(ctx, &gogit.FetchOptions{
646-
RemoteName: originRemote,
647-
RefSpecs: []config.RefSpec{config.RefSpec(refspec)},
648-
Auth: access.auth.AuthMethod,
649-
})
650-
if err == gogit.NoErrAlreadyUpToDate {
651-
return nil
652-
}
653-
if _, ok := err.(gogit.NoMatchingRefSpecError); ok {
654-
return errRemoteBranchMissing
655-
}
656-
return err
657-
}
658-
659632
// push pushes the branch given to the origin using the git library
660633
// indicated by `impl`. It's passed both the path to the repo and a
661634
// gogit.Repository value, since the latter may as well be used if the
662635
// implementation is GoGit.
663-
func push(ctx context.Context, path string, repo *gogit.Repository, branch string, access repoAccess, impl string) error {
664-
switch impl {
665-
case sourcev1.LibGit2Implementation:
666-
lg2repo, err := libgit2.OpenRepository(path)
667-
if err != nil {
668-
return err
669-
}
670-
return pushLibgit2(lg2repo, access, branch)
671-
case sourcev1.GoGitImplementation:
672-
return pushGoGit(ctx, repo, access, branch)
673-
default:
674-
return fmt.Errorf("unknown git implementation %q", impl)
675-
}
676-
}
677-
678-
func pushGoGit(ctx context.Context, repo *gogit.Repository, access repoAccess, branch string) error {
679-
refspec := config.RefSpec(fmt.Sprintf("refs/heads/%s:refs/heads/%s", branch, branch))
680-
err := repo.PushContext(ctx, &gogit.PushOptions{
681-
RemoteName: originRemote,
682-
Auth: access.auth.AuthMethod,
683-
RefSpecs: []config.RefSpec{refspec},
684-
})
685-
return gogitPushError(err)
686-
}
687-
688-
func gogitPushError(err error) error {
689-
if err == nil {
690-
return nil
691-
}
692-
switch strings.TrimSpace(err.Error()) {
693-
case "unknown error: remote:":
694-
// this unhelpful error arises because go-git takes the first
695-
// line of the output on stderr, and for some git providers
696-
// (GitLab, at least) the output has a blank line at the
697-
// start. The rest of stderr is thrown away, so we can't get
698-
// the actual error; but at least we know what was being
699-
// attempted, and the likely cause.
700-
return fmt.Errorf("push rejected; check git secret has write access")
701-
default:
636+
func push(ctx context.Context, path, branch string, access repoAccess) error {
637+
repo, err := libgit2.OpenRepository(path)
638+
if err != nil {
702639
return err
703640
}
704-
}
705-
706-
func pushLibgit2(repo *libgit2.Repository, access repoAccess, branch string) error {
707641
origin, err := repo.Remotes.Lookup(originRemote)
708642
if err != nil {
709643
return err

controllers/update_test.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -782,6 +782,29 @@ Images:
782782
Expect(head.String()).NotTo(Equal(headHash))
783783
})
784784

785+
It("still pushes to the push branch after it's merged", func() {
786+
// observe the first commit
787+
waitForNewHead(localRepo, pushBranch)
788+
head, err := localRepo.Reference(plumbing.NewRemoteReferenceName(originRemote, pushBranch), true)
789+
headHash := head.String()
790+
Expect(err).NotTo(HaveOccurred())
791+
792+
// merge the push branch into checkout branch, and push the merge commit
793+
// upstream.
794+
// waitForNewHead() leaves the repo at the head of the branch given, i.e., the
795+
// push branch), so we have to check out the "main" branch first.
796+
Expect(checkoutBranch(localRepo, branch)).To(Succeed())
797+
mergeBranchIntoHead(localRepo, pushBranch)
798+
799+
// update the policy and expect another commit in the push branch
800+
policy.Status.LatestImage = "helloworld:v1.3.0"
801+
Expect(k8sClient.Status().Update(context.TODO(), policy)).To(Succeed())
802+
waitForNewHead(localRepo, pushBranch)
803+
head, err = localRepo.Reference(plumbing.NewRemoteReferenceName(originRemote, pushBranch), true)
804+
Expect(err).NotTo(HaveOccurred())
805+
Expect(head.String()).NotTo(Equal(headHash))
806+
})
807+
785808
AfterEach(func() {
786809
Expect(k8sClient.Delete(context.Background(), update)).To(Succeed())
787810
})
@@ -1044,6 +1067,7 @@ func waitForNewHead(repo *git.Repository, branch string) {
10441067
// remote, so it is a detached head.
10451068
Expect(working.Reset(&git.ResetOptions{
10461069
Commit: remoteHead.Hash(),
1070+
Mode: git.HardReset,
10471071
})).To(Succeed())
10481072
}
10491073

@@ -1128,3 +1152,57 @@ func initGitRepo(gitServer *gittestserver.GitServer, fixture, branch, repository
11281152
RefSpecs: []config.RefSpec{"refs/heads/*:refs/heads/*"},
11291153
})
11301154
}
1155+
1156+
func checkoutBranch(repo *git.Repository, branch string) error {
1157+
working, err := repo.Worktree()
1158+
if err != nil {
1159+
return err
1160+
}
1161+
// check that there's no local changes, as a sanity check
1162+
status, err := working.Status()
1163+
if err != nil {
1164+
return err
1165+
}
1166+
if len(status) > 0 {
1167+
for path := range status {
1168+
println(path, "is changed")
1169+
}
1170+
} // the checkout next will fail if there are changed files
1171+
1172+
if err = working.Checkout(&git.CheckoutOptions{
1173+
Branch: plumbing.NewBranchReferenceName(branch),
1174+
Create: false,
1175+
}); err != nil {
1176+
return err
1177+
}
1178+
return nil
1179+
}
1180+
1181+
// This merges the push branch into HEAD, and pushes upstream. This is
1182+
// to simulate e.g., a PR being merged.
1183+
func mergeBranchIntoHead(repo *git.Repository, pushBranch string) {
1184+
// hash of head
1185+
headRef, err := repo.Head()
1186+
Expect(err).NotTo(HaveOccurred())
1187+
pushBranchRef, err := repo.Reference(plumbing.NewRemoteReferenceName(originRemote, pushBranch), false)
1188+
Expect(err).NotTo(HaveOccurred())
1189+
1190+
// You need the worktree to be able to create a commit
1191+
worktree, err := repo.Worktree()
1192+
Expect(err).NotTo(HaveOccurred())
1193+
_, err = worktree.Commit(fmt.Sprintf("Merge %s", pushBranch), &git.CommitOptions{
1194+
Author: &object.Signature{
1195+
Name: "Testbot",
1196+
1197+
When: time.Now(),
1198+
},
1199+
Parents: []plumbing.Hash{headRef.Hash(), pushBranchRef.Hash()},
1200+
})
1201+
Expect(err).NotTo(HaveOccurred())
1202+
1203+
// push upstream
1204+
err = repo.Push(&git.PushOptions{
1205+
RemoteName: originRemote,
1206+
})
1207+
Expect(err).NotTo(HaveOccurred())
1208+
}

0 commit comments

Comments
 (0)