270 lines
		
	
	
		
			7.7 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			270 lines
		
	
	
		
			7.7 KiB
		
	
	
	
		
			Go
		
	
	
	
| /*
 | |
| Copyright 2020 The Flux authors
 | |
| 
 | |
| Licensed under the Apache License, Version 2.0 (the "License");
 | |
| you may not use this file except in compliance with the License.
 | |
| You may obtain a copy of the License at
 | |
| 
 | |
|     http://www.apache.org/licenses/LICENSE-2.0
 | |
| 
 | |
| Unless required by applicable law or agreed to in writing, software
 | |
| distributed under the License is distributed on an "AS IS" BASIS,
 | |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | |
| See the License for the specific language governing permissions and
 | |
| limitations under the License.
 | |
| */
 | |
| 
 | |
| package git
 | |
| 
 | |
| import (
 | |
| 	"context"
 | |
| 	"fmt"
 | |
| 	"sort"
 | |
| 	"time"
 | |
| 
 | |
| 	"github.com/Masterminds/semver/v3"
 | |
| 	"github.com/go-git/go-git/v5"
 | |
| 	"github.com/go-git/go-git/v5/plumbing"
 | |
| 	"github.com/go-git/go-git/v5/plumbing/object"
 | |
| 	"github.com/go-git/go-git/v5/plumbing/transport"
 | |
| 
 | |
| 	"github.com/fluxcd/pkg/version"
 | |
| 
 | |
| 	sourcev1 "github.com/fluxcd/source-controller/api/v1beta1"
 | |
| )
 | |
| 
 | |
| const (
 | |
| 	defaultOrigin = "origin"
 | |
| 	defaultBranch = "master"
 | |
| )
 | |
| 
 | |
| func CheckoutStrategyForRef(ref *sourcev1.GitRepositoryRef) CheckoutStrategy {
 | |
| 	switch {
 | |
| 	case ref == nil:
 | |
| 		return &CheckoutBranch{branch: defaultBranch}
 | |
| 	case ref.SemVer != "":
 | |
| 		return &CheckoutSemVer{semVer: ref.SemVer}
 | |
| 	case ref.Tag != "":
 | |
| 		return &CheckoutTag{tag: ref.Tag}
 | |
| 	case ref.Commit != "":
 | |
| 		strategy := &CheckoutCommit{branch: ref.Branch, commit: ref.Commit}
 | |
| 		if strategy.branch == "" {
 | |
| 			strategy.branch = defaultBranch
 | |
| 		}
 | |
| 		return strategy
 | |
| 	case ref.Branch != "":
 | |
| 		return &CheckoutBranch{branch: ref.Branch}
 | |
| 	default:
 | |
| 		return &CheckoutBranch{branch: defaultBranch}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| type CheckoutStrategy interface {
 | |
| 	Checkout(ctx context.Context, path, url string, auth transport.AuthMethod) (*object.Commit, string, error)
 | |
| }
 | |
| 
 | |
| type CheckoutBranch struct {
 | |
| 	branch string
 | |
| }
 | |
| 
 | |
| func (c *CheckoutBranch) Checkout(ctx context.Context, path, url string, auth transport.AuthMethod) (*object.Commit, string, error) {
 | |
| 	repo, err := git.PlainCloneContext(ctx, path, false, &git.CloneOptions{
 | |
| 		URL:               url,
 | |
| 		Auth:              auth,
 | |
| 		RemoteName:        defaultOrigin,
 | |
| 		ReferenceName:     plumbing.NewBranchReferenceName(c.branch),
 | |
| 		SingleBranch:      true,
 | |
| 		NoCheckout:        false,
 | |
| 		Depth:             1,
 | |
| 		RecurseSubmodules: 0,
 | |
| 		Progress:          nil,
 | |
| 		Tags:              git.NoTags,
 | |
| 	})
 | |
| 	if err != nil {
 | |
| 		return nil, "", fmt.Errorf("unable to clone '%s', error: %w", url, err)
 | |
| 	}
 | |
| 	head, err := repo.Head()
 | |
| 	if err != nil {
 | |
| 		return nil, "", fmt.Errorf("git resolve HEAD error: %w", err)
 | |
| 	}
 | |
| 	commit, err := repo.CommitObject(head.Hash())
 | |
| 	if err != nil {
 | |
| 		return nil, "", fmt.Errorf("git commit '%s' not found: %w", head.Hash(), err)
 | |
| 	}
 | |
| 	return commit, fmt.Sprintf("%s/%s", c.branch, head.Hash().String()), nil
 | |
| }
 | |
| 
 | |
| type CheckoutTag struct {
 | |
| 	tag string
 | |
| }
 | |
| 
 | |
| func (c *CheckoutTag) Checkout(ctx context.Context, path, url string, auth transport.AuthMethod) (*object.Commit, string, error) {
 | |
| 	repo, err := git.PlainCloneContext(ctx, path, false, &git.CloneOptions{
 | |
| 		URL:               url,
 | |
| 		Auth:              auth,
 | |
| 		RemoteName:        defaultOrigin,
 | |
| 		ReferenceName:     plumbing.NewTagReferenceName(c.tag),
 | |
| 		SingleBranch:      true,
 | |
| 		NoCheckout:        false,
 | |
| 		Depth:             1,
 | |
| 		RecurseSubmodules: 0,
 | |
| 		Progress:          nil,
 | |
| 		Tags:              git.NoTags,
 | |
| 	})
 | |
| 	if err != nil {
 | |
| 		return nil, "", fmt.Errorf("unable to clone '%s', error: %w", url, err)
 | |
| 	}
 | |
| 	head, err := repo.Head()
 | |
| 	if err != nil {
 | |
| 		return nil, "", fmt.Errorf("git resolve HEAD error: %w", err)
 | |
| 	}
 | |
| 	commit, err := repo.CommitObject(head.Hash())
 | |
| 	if err != nil {
 | |
| 		return nil, "", fmt.Errorf("git commit '%s' not found: %w", head.Hash(), err)
 | |
| 	}
 | |
| 	return commit, fmt.Sprintf("%s/%s", c.tag, head.Hash().String()), nil
 | |
| }
 | |
| 
 | |
| type CheckoutCommit struct {
 | |
| 	branch string
 | |
| 	commit string
 | |
| }
 | |
| 
 | |
| func (c *CheckoutCommit) Checkout(ctx context.Context, path, url string, auth transport.AuthMethod) (*object.Commit, string, error) {
 | |
| 	repo, err := git.PlainCloneContext(ctx, path, false, &git.CloneOptions{
 | |
| 		URL:               url,
 | |
| 		Auth:              auth,
 | |
| 		RemoteName:        defaultOrigin,
 | |
| 		ReferenceName:     plumbing.NewBranchReferenceName(c.branch),
 | |
| 		SingleBranch:      true,
 | |
| 		NoCheckout:        false,
 | |
| 		RecurseSubmodules: 0,
 | |
| 		Progress:          nil,
 | |
| 		Tags:              git.NoTags,
 | |
| 	})
 | |
| 	if err != nil {
 | |
| 		return nil, "", fmt.Errorf("unable to clone '%s', error: %w", url, err)
 | |
| 	}
 | |
| 	w, err := repo.Worktree()
 | |
| 	if err != nil {
 | |
| 		return nil, "", fmt.Errorf("git worktree error: %w", err)
 | |
| 	}
 | |
| 	commit, err := repo.CommitObject(plumbing.NewHash(c.commit))
 | |
| 	if err != nil {
 | |
| 		return nil, "", fmt.Errorf("git commit '%s' not found: %w", c.commit, err)
 | |
| 	}
 | |
| 	err = w.Checkout(&git.CheckoutOptions{
 | |
| 		Hash:  commit.Hash,
 | |
| 		Force: true,
 | |
| 	})
 | |
| 	if err != nil {
 | |
| 		return nil, "", fmt.Errorf("git checkout error: %w", err)
 | |
| 	}
 | |
| 	return commit, fmt.Sprintf("%s/%s", c.branch, commit.Hash.String()), nil
 | |
| }
 | |
| 
 | |
| type CheckoutSemVer struct {
 | |
| 	semVer string
 | |
| }
 | |
| 
 | |
| func (c *CheckoutSemVer) Checkout(ctx context.Context, path, url string, auth transport.AuthMethod) (*object.Commit, string, error) {
 | |
| 	verConstraint, err := semver.NewConstraint(c.semVer)
 | |
| 	if err != nil {
 | |
| 		return nil, "", fmt.Errorf("semver parse range error: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	repo, err := git.PlainCloneContext(ctx, path, false, &git.CloneOptions{
 | |
| 		URL:               url,
 | |
| 		Auth:              auth,
 | |
| 		RemoteName:        defaultOrigin,
 | |
| 		NoCheckout:        false,
 | |
| 		Depth:             1,
 | |
| 		RecurseSubmodules: 0,
 | |
| 		Progress:          nil,
 | |
| 		Tags:              git.AllTags,
 | |
| 	})
 | |
| 	if err != nil {
 | |
| 		return nil, "", fmt.Errorf("unable to clone '%s', error: %w", url, err)
 | |
| 	}
 | |
| 
 | |
| 	repoTags, err := repo.Tags()
 | |
| 	if err != nil {
 | |
| 		return nil, "", fmt.Errorf("git list tags error: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	tags := make(map[string]string)
 | |
| 	tagTimestamps := make(map[string]time.Time)
 | |
| 	_ = repoTags.ForEach(func(t *plumbing.Reference) error {
 | |
| 		revision := plumbing.Revision(t.Name().String())
 | |
| 		hash, err := repo.ResolveRevision(revision)
 | |
| 		if err != nil {
 | |
| 			return fmt.Errorf("unable to resolve tag revision: %w", err)
 | |
| 		}
 | |
| 		commit, err := repo.CommitObject(*hash)
 | |
| 		if err != nil {
 | |
| 			return fmt.Errorf("unable to resolve commit of a tag revision: %w", err)
 | |
| 		}
 | |
| 		tagTimestamps[t.Name().Short()] = commit.Committer.When
 | |
| 
 | |
| 		tags[t.Name().Short()] = t.Strings()[1]
 | |
| 		return nil
 | |
| 	})
 | |
| 
 | |
| 	var matchedVersions semver.Collection
 | |
| 	for tag, _ := range tags {
 | |
| 		v, err := version.ParseVersion(tag)
 | |
| 		if err != nil {
 | |
| 			continue
 | |
| 		}
 | |
| 		if !verConstraint.Check(v) {
 | |
| 			continue
 | |
| 		}
 | |
| 		matchedVersions = append(matchedVersions, v)
 | |
| 	}
 | |
| 	if len(matchedVersions) == 0 {
 | |
| 		return nil, "", fmt.Errorf("no match found for semver: %s", c.semVer)
 | |
| 	}
 | |
| 
 | |
| 	// Sort versions
 | |
| 	sort.SliceStable(matchedVersions, func(i, j int) bool {
 | |
| 		left := matchedVersions[i]
 | |
| 		right := matchedVersions[j]
 | |
| 
 | |
| 		if !left.Equal(right) {
 | |
| 			return left.LessThan(right)
 | |
| 		}
 | |
| 
 | |
| 		// Having tag target timestamps at our disposal, we further try to sort
 | |
| 		// versions into a chronological order. This is especially important for
 | |
| 		// versions that differ only by build metadata, because it is not considered
 | |
| 		// a part of the comparable version in Semver
 | |
| 		return tagTimestamps[left.String()].Before(tagTimestamps[right.String()])
 | |
| 	})
 | |
| 	v := matchedVersions[len(matchedVersions)-1]
 | |
| 	t := v.Original()
 | |
| 
 | |
| 	w, err := repo.Worktree()
 | |
| 	if err != nil {
 | |
| 		return nil, "", fmt.Errorf("git worktree error: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	err = w.Checkout(&git.CheckoutOptions{
 | |
| 		Branch: plumbing.NewTagReferenceName(t),
 | |
| 	})
 | |
| 	if err != nil {
 | |
| 		return nil, "", fmt.Errorf("git checkout error: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	head, err := repo.Head()
 | |
| 	if err != nil {
 | |
| 		return nil, "", fmt.Errorf("git resolve HEAD error: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	commit, err := repo.CommitObject(head.Hash())
 | |
| 	if err != nil {
 | |
| 		return nil, "", fmt.Errorf("git commit '%s' not found: %w", head.Hash(), err)
 | |
| 	}
 | |
| 
 | |
| 	return commit, fmt.Sprintf("%s/%s", t, head.Hash().String()), nil
 | |
| }
 |