Skip to content

Commit

Permalink
Fetch remote branch before switching to it
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
squaremo committed Apr 3, 2021
1 parent ddd0a8d commit 40fb66a
Showing 1 changed file with 81 additions and 17 deletions.
98 changes: 81 additions & 17 deletions controllers/imageupdateautomation_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
}
}
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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,
})
Expand Down Expand Up @@ -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
Expand All @@ -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)
}
Expand Down Expand Up @@ -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)
}
Expand Down

0 comments on commit 40fb66a

Please sign in to comment.