Fetch remote branch before switching to it

For the "push to branch" feature, the controller must either switch to
the branch given, or create it starting at the checked-out HEAD. The
func `switchBranch` encapsulates this decision -- but it assumes that
if the branch exists at the remote, it will have been fetched when
cloning, and this is not always true. In particular, cloning with
go-git avoids fetching all refs:

    https://github.com/fluxcd/source-controller/blob/v0.11.0/pkg/git/gogit/checkout.go

This commit adds a step to fetch the remote branch to a local branch,
before attempting to switch to the local branch. This makes
`switchBranch` a little simpler, and doesn't rely on any refs having
been fetched ahead of time.

Signed-off-by: Michael Bridgen <michael@weave.works>
This commit is contained in:
Michael Bridgen 2021-04-03 15:11:20 +01:00
parent ddd0a8d8ed
commit 40fb66a217
1 changed files with 81 additions and 17 deletions

View File

@ -33,6 +33,7 @@ import (
libgit2 "github.com/libgit2/git2go/v31"
securejoin "github.com/cyphar/filepath-securejoin"
"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-logr/logr"
@ -188,7 +189,11 @@ func (r *ImageUpdateAutomationReconciler) Reconcile(ctx context.Context, req ctr
// When there's a push spec, the pushed-to branch is where commits
// shall be made
if auto.Spec.Push != nil {
if err := switchBranch(repo, auto.Spec.Push.Branch); err != nil {
pushBranch := auto.Spec.Push.Branch
if err := fetch(ctx, tmp, repo, pushBranch, access, origin.Spec.GitImplementation); err != nil && err != errRemoteBranchMissing {
return failWithError(err)
}
if err = switchBranch(repo, pushBranch); err != nil {
return failWithError(err)
}
}
@ -367,7 +372,6 @@ func (r *ImageUpdateAutomationReconciler) automationsForImagePolicy(obj client.O
reqs[i].NamespacedName.Name = autoList.Items[i].GetName()
reqs[i].NamespacedName.Namespace = autoList.Items[i].GetNamespace()
}
println("[DEBUG] enqueuing autos for image policy", obj.GetName(), obj.GetNamespace(), len(reqs))
return reqs
}
@ -409,6 +413,13 @@ func (r *ImageUpdateAutomationReconciler) getRepoAccess(ctx context.Context, rep
return access, nil
}
func (r repoAccess) remoteCallbacks() libgit2.RemoteCallbacks {
return libgit2.RemoteCallbacks{
CertificateCheckCallback: r.auth.CertCallback,
CredentialsCallback: r.auth.CredCallback,
}
}
// cloneInto clones the upstream repository at the `branch` given,
// using the git library indicated by `impl`. It returns a
// `*gogit.Repository` regardless of the git library, since that is
@ -431,11 +442,10 @@ func cloneInto(ctx context.Context, access repoAccess, branch, path, impl string
// 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 {
remoteBranch := plumbing.NewRemoteReferenceName(originRemote, pushBranch)
localBranch := plumbing.NewBranchReferenceName(pushBranch)
// is the remote branch already present?
branchHead, err := repo.Reference(remoteBranch, false)
// is the branch already present?
_, err := repo.Reference(localBranch, false)
switch {
case err == plumbing.ErrReferenceNotFound:
// make a new branch, starting at HEAD
@ -450,17 +460,15 @@ func switchBranch(repo *gogit.Repository, pushBranch string) error {
case err != nil:
return err
default:
// make a local branch that references the remote branch
branchRef := plumbing.NewHashReference(localBranch, branchHead.Hash())
if err = repo.Storer.SetReference(branchRef); err != nil {
return err
}
// local branch found, great
break
}
tree, err := repo.Worktree()
if err != nil {
return err
}
return tree.Checkout(&gogit.CheckoutOptions{
Branch: localBranch,
})
@ -540,6 +548,62 @@ func (r *ImageUpdateAutomationReconciler) getSigningEntity(ctx context.Context,
return entities[0], nil
}
var errRemoteBranchMissing = errors.New("remote branch missing")
// fetch gets the remote branch given and updates the local branch
// head of the same name, so it can be switched to. If the fetch
// completes, it returns nil; if the remote branch is missing, it
// returns errRemoteBranchMissing (this is to work in sympathy with
// `switchBranch`, which will create the branch if it doesn't
// exist). For any other problem it will return the error.
func fetch(ctx context.Context, path string, repo *gogit.Repository, branch string, access repoAccess, impl string) error {
refspec := fmt.Sprintf("refs/heads/%s:refs/heads/%s", branch, branch)
switch impl {
case sourcev1.LibGit2Implementation:
lg2repo, err := libgit2.OpenRepository(path)
if err != nil {
return err
}
return fetchLibgit2(lg2repo, refspec, access)
case sourcev1.GoGitImplementation:
return fetchGoGit(ctx, repo, refspec, access)
default:
return fmt.Errorf("unknown git implementation %q", impl)
}
}
func fetchLibgit2(repo *libgit2.Repository, refspec string, access repoAccess) error {
origin, err := repo.Remotes.Lookup(originRemote)
if err != nil {
return err
}
err = origin.Fetch(
[]string{refspec},
&libgit2.FetchOptions{
RemoteCallbacks: access.remoteCallbacks(),
}, "",
)
if err != nil && libgit2.IsErrorCode(err, libgit2.ErrorCodeNotFound) {
return errRemoteBranchMissing
}
return err
}
func fetchGoGit(ctx context.Context, repo *gogit.Repository, refspec string, access repoAccess) error {
err := repo.FetchContext(ctx, &gogit.FetchOptions{
RemoteName: originRemote,
RefSpecs: []config.RefSpec{config.RefSpec(refspec)},
Auth: access.auth.AuthMethod,
})
if err == gogit.NoErrAlreadyUpToDate {
return nil
}
if _, ok := err.(gogit.NoMatchingRefSpecError); ok {
return errRemoteBranchMissing
}
return err
}
// 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
@ -553,15 +617,18 @@ func push(ctx context.Context, path string, repo *gogit.Repository, branch strin
}
return pushLibgit2(lg2repo, access, branch)
case sourcev1.GoGitImplementation:
return pushGoGit(ctx, repo, access)
return pushGoGit(ctx, repo, access, branch)
default:
return fmt.Errorf("unknown git implementation %q", impl)
}
}
func pushGoGit(ctx context.Context, repo *gogit.Repository, access repoAccess) error {
func pushGoGit(ctx context.Context, repo *gogit.Repository, access repoAccess, branch string) error {
refspec := config.RefSpec(fmt.Sprintf("refs/heads/%s:refs/heads/%s", branch, branch))
err := repo.PushContext(ctx, &gogit.PushOptions{
Auth: access.auth.AuthMethod,
RemoteName: originRemote,
Auth: access.auth.AuthMethod,
RefSpecs: []config.RefSpec{refspec},
})
return gogitPushError(err)
}
@ -590,10 +657,7 @@ func pushLibgit2(repo *libgit2.Repository, access repoAccess, branch string) err
return err
}
err = origin.Push([]string{fmt.Sprintf("refs/heads/%s:refs/heads/%s", branch, branch)}, &libgit2.PushOptions{
RemoteCallbacks: libgit2.RemoteCallbacks{
CertificateCheckCallback: access.auth.CertCallback,
CredentialsCallback: access.auth.CredCallback,
},
RemoteCallbacks: access.remoteCallbacks(),
})
return libgit2PushError(err)
}