refactor(recipe): move functions that operate on the git repo to new file

This commit is contained in:
p4u1 2024-07-08 11:00:50 +02:00
parent 47013c63d6
commit f268e5893b
2 changed files with 264 additions and 249 deletions

264
pkg/recipe/git.go Normal file
View File

@ -0,0 +1,264 @@
package recipe
import (
"fmt"
"os"
"path"
"strings"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter"
gitPkg "coopcloud.tech/abra/pkg/git"
"coopcloud.tech/abra/pkg/log"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
)
// Ensure makes sure the recipe exists, is up to date and has the latest version checked out.
func (r Recipe2) Ensure(chaos bool, offline bool) error {
if err := r.EnsureExists(); err != nil {
return err
}
if !chaos {
if err := r.EnsureIsClean(); err != nil {
return err
}
if !offline {
if err := r.EnsureUpToDate(); err != nil {
log.Fatal(err)
}
}
if err := r.EnsureLatest(); err != nil {
return err
}
}
return nil
}
// EnsureExists ensures that the recipe is locally cloned
func (r Recipe2) EnsureExists() error {
recipeDir := path.Join(config.RECIPES_DIR, r.Name)
if _, err := os.Stat(recipeDir); os.IsNotExist(err) {
log.Debugf("%s does not exist, attemmpting to clone", recipeDir)
url := fmt.Sprintf("%s/%s.git", config.REPOS_BASE_URL, r.Name)
if err := gitPkg.Clone(recipeDir, url); err != nil {
return err
}
}
if err := gitPkg.EnsureGitRepo(recipeDir); err != nil {
return err
}
return nil
}
// EnsureVersion checks whether a specific version exists for a recipe.
func (r Recipe2) EnsureVersion(version string) error {
recipeDir := path.Join(config.RECIPES_DIR, r.Name)
if err := gitPkg.EnsureGitRepo(recipeDir); err != nil {
return err
}
repo, err := git.PlainOpen(recipeDir)
if err != nil {
return err
}
tags, err := repo.Tags()
if err != nil {
return nil
}
var parsedTags []string
var tagRef plumbing.ReferenceName
if err := tags.ForEach(func(ref *plumbing.Reference) (err error) {
parsedTags = append(parsedTags, ref.Name().Short())
if ref.Name().Short() == version {
tagRef = ref.Name()
}
return nil
}); err != nil {
return err
}
joinedTags := strings.Join(parsedTags, ", ")
if joinedTags != "" {
log.Debugf("read %s as tags for recipe %s", joinedTags, r.Name)
}
if tagRef.String() == "" {
return fmt.Errorf("the local copy of %s doesn't seem to have version %s available?", r.Name, version)
}
worktree, err := repo.Worktree()
if err != nil {
return err
}
opts := &git.CheckoutOptions{
Branch: tagRef,
Create: false,
Force: true,
}
if err := worktree.Checkout(opts); err != nil {
return err
}
log.Debugf("successfully checked %s out to %s in %s", r.Name, tagRef.Short(), recipeDir)
return nil
}
// EnsureIsClean makes sure that the recipe repository has no unstaged changes.
func (r Recipe2) EnsureIsClean() error {
recipeDir := path.Join(config.RECIPES_DIR, r.Name)
isClean, err := gitPkg.IsClean(recipeDir)
if err != nil {
return fmt.Errorf("unable to check git clean status in %s: %s", recipeDir, err)
}
if !isClean {
msg := "%s (%s) has locally unstaged changes? please commit/remove your changes before proceeding"
return fmt.Errorf(msg, r.Name, recipeDir)
}
return nil
}
// EnsureLatest makes sure the latest commit is checked out for the local recipe repository
func (r Recipe2) EnsureLatest() error {
recipeDir := path.Join(config.RECIPES_DIR, r.Name)
if err := gitPkg.EnsureGitRepo(recipeDir); err != nil {
return err
}
repo, err := git.PlainOpen(recipeDir)
if err != nil {
return err
}
worktree, err := repo.Worktree()
if err != nil {
return err
}
branch, err := gitPkg.GetDefaultBranch(repo, recipeDir)
if err != nil {
return err
}
checkOutOpts := &git.CheckoutOptions{
Create: false,
Force: true,
Branch: plumbing.ReferenceName(branch),
}
if err := worktree.Checkout(checkOutOpts); err != nil {
log.Debugf("failed to check out %s in %s", branch, recipeDir)
return err
}
return nil
}
// EnsureUpToDate ensures that the local repo is synced to the remote
func (r Recipe2) EnsureUpToDate() error {
recipeDir := path.Join(config.RECIPES_DIR, r.Name)
repo, err := git.PlainOpen(recipeDir)
if err != nil {
return fmt.Errorf("unable to open %s: %s", recipeDir, err)
}
remotes, err := repo.Remotes()
if err != nil {
return fmt.Errorf("unable to read remotes in %s: %s", recipeDir, err)
}
if len(remotes) == 0 {
log.Debugf("cannot ensure %s is up-to-date, no git remotes configured", r.Name)
return nil
}
worktree, err := repo.Worktree()
if err != nil {
return fmt.Errorf("unable to open git work tree in %s: %s", recipeDir, err)
}
branch, err := gitPkg.CheckoutDefaultBranch(repo, recipeDir)
if err != nil {
return fmt.Errorf("unable to check out default branch in %s: %s", recipeDir, err)
}
fetchOpts := &git.FetchOptions{Tags: git.AllTags}
if err := repo.Fetch(fetchOpts); err != nil {
if !strings.Contains(err.Error(), "already up-to-date") {
return fmt.Errorf("unable to fetch tags in %s: %s", recipeDir, err)
}
}
opts := &git.PullOptions{
Force: true,
ReferenceName: branch,
SingleBranch: true,
}
if err := worktree.Pull(opts); err != nil {
if !strings.Contains(err.Error(), "already up-to-date") {
return fmt.Errorf("unable to git pull in %s: %s", recipeDir, err)
}
}
log.Debugf("fetched latest git changes for %s", r.Name)
return nil
}
// ChaosVersion constructs a chaos mode recipe version.
func (r Recipe2) ChaosVersion() (string, error) {
var version string
head, err := gitPkg.GetRecipeHead(r.Name)
if err != nil {
return version, err
}
version = formatter.SmallSHA(head.String())
recipeDir := path.Join(config.RECIPES_DIR, r.Name)
isClean, err := gitPkg.IsClean(recipeDir)
if err != nil {
return version, err
}
if !isClean {
version = fmt.Sprintf("%s + unstaged changes", version)
}
return version, nil
}
// Push pushes the latest changes to a SSH URL remote. You need to have your
// local SSH configuration for git.coopcloud.tech working for this to work
func (r Recipe2) Push(dryRun bool) error {
repo, err := git.PlainOpen(r.Dir)
if err != nil {
return err
}
if err := gitPkg.CreateRemote(repo, "origin-ssh", r.SSHURL, dryRun); err != nil {
return err
}
if err := gitPkg.Push(r.Dir, "origin-ssh", true, dryRun); err != nil {
return err
}
return nil
}

View File

@ -261,255 +261,6 @@ type Recipe2 struct {
SSHURL string
}
// Ensure makes sure the recipe exists, is up to date and has the latest version checked out.
func (r Recipe2) Ensure(chaos bool, offline bool) error {
if err := r.EnsureExists(); err != nil {
return err
}
if !chaos {
if err := r.EnsureIsClean(); err != nil {
return err
}
if !offline {
if err := r.EnsureUpToDate(); err != nil {
log.Fatal(err)
}
}
if err := r.EnsureLatest(); err != nil {
return err
}
}
return nil
}
// EnsureExists ensures that the recipe is locally cloned
func (r Recipe2) EnsureExists() error {
recipeDir := path.Join(config.RECIPES_DIR, r.Name)
if _, err := os.Stat(recipeDir); os.IsNotExist(err) {
log.Debugf("%s does not exist, attemmpting to clone", recipeDir)
url := fmt.Sprintf("%s/%s.git", config.REPOS_BASE_URL, r.Name)
if err := gitPkg.Clone(recipeDir, url); err != nil {
return err
}
}
if err := gitPkg.EnsureGitRepo(recipeDir); err != nil {
return err
}
return nil
}
// EnsureVersion checks whether a specific version exists for a recipe.
func (r Recipe2) EnsureVersion(version string) error {
recipeDir := path.Join(config.RECIPES_DIR, r.Name)
if err := gitPkg.EnsureGitRepo(recipeDir); err != nil {
return err
}
repo, err := git.PlainOpen(recipeDir)
if err != nil {
return err
}
tags, err := repo.Tags()
if err != nil {
return nil
}
var parsedTags []string
var tagRef plumbing.ReferenceName
if err := tags.ForEach(func(ref *plumbing.Reference) (err error) {
parsedTags = append(parsedTags, ref.Name().Short())
if ref.Name().Short() == version {
tagRef = ref.Name()
}
return nil
}); err != nil {
return err
}
joinedTags := strings.Join(parsedTags, ", ")
if joinedTags != "" {
log.Debugf("read %s as tags for recipe %s", joinedTags, r.Name)
}
if tagRef.String() == "" {
return fmt.Errorf("the local copy of %s doesn't seem to have version %s available?", r.Name, version)
}
worktree, err := repo.Worktree()
if err != nil {
return err
}
opts := &git.CheckoutOptions{
Branch: tagRef,
Create: false,
Force: true,
}
if err := worktree.Checkout(opts); err != nil {
return err
}
log.Debugf("successfully checked %s out to %s in %s", r.Name, tagRef.Short(), recipeDir)
return nil
}
// EnsureIsClean makes sure that the recipe repository has no unstaged changes.
func (r Recipe2) EnsureIsClean() error {
recipeDir := path.Join(config.RECIPES_DIR, r.Name)
isClean, err := gitPkg.IsClean(recipeDir)
if err != nil {
return fmt.Errorf("unable to check git clean status in %s: %s", recipeDir, err)
}
if !isClean {
msg := "%s (%s) has locally unstaged changes? please commit/remove your changes before proceeding"
return fmt.Errorf(msg, r.Name, recipeDir)
}
return nil
}
// EnsureLatest makes sure the latest commit is checked out for the local recipe repository
func (r Recipe2) EnsureLatest() error {
recipeDir := path.Join(config.RECIPES_DIR, r.Name)
if err := gitPkg.EnsureGitRepo(recipeDir); err != nil {
return err
}
repo, err := git.PlainOpen(recipeDir)
if err != nil {
return err
}
worktree, err := repo.Worktree()
if err != nil {
return err
}
branch, err := gitPkg.GetDefaultBranch(repo, recipeDir)
if err != nil {
return err
}
checkOutOpts := &git.CheckoutOptions{
Create: false,
Force: true,
Branch: plumbing.ReferenceName(branch),
}
if err := worktree.Checkout(checkOutOpts); err != nil {
log.Debugf("failed to check out %s in %s", branch, recipeDir)
return err
}
return nil
}
// EnsureUpToDate ensures that the local repo is synced to the remote
func (r Recipe2) EnsureUpToDate() error {
recipeDir := path.Join(config.RECIPES_DIR, r.Name)
repo, err := git.PlainOpen(recipeDir)
if err != nil {
return fmt.Errorf("unable to open %s: %s", recipeDir, err)
}
remotes, err := repo.Remotes()
if err != nil {
return fmt.Errorf("unable to read remotes in %s: %s", recipeDir, err)
}
if len(remotes) == 0 {
log.Debugf("cannot ensure %s is up-to-date, no git remotes configured", r.Name)
return nil
}
worktree, err := repo.Worktree()
if err != nil {
return fmt.Errorf("unable to open git work tree in %s: %s", recipeDir, err)
}
branch, err := gitPkg.CheckoutDefaultBranch(repo, recipeDir)
if err != nil {
return fmt.Errorf("unable to check out default branch in %s: %s", recipeDir, err)
}
fetchOpts := &git.FetchOptions{Tags: git.AllTags}
if err := repo.Fetch(fetchOpts); err != nil {
if !strings.Contains(err.Error(), "already up-to-date") {
return fmt.Errorf("unable to fetch tags in %s: %s", recipeDir, err)
}
}
opts := &git.PullOptions{
Force: true,
ReferenceName: branch,
SingleBranch: true,
}
if err := worktree.Pull(opts); err != nil {
if !strings.Contains(err.Error(), "already up-to-date") {
return fmt.Errorf("unable to git pull in %s: %s", recipeDir, err)
}
}
log.Debugf("fetched latest git changes for %s", r.Name)
return nil
}
// ChaosVersion constructs a chaos mode recipe version.
func (r Recipe2) ChaosVersion() (string, error) {
var version string
head, err := gitPkg.GetRecipeHead(r.Name)
if err != nil {
return version, err
}
version = formatter.SmallSHA(head.String())
recipeDir := path.Join(config.RECIPES_DIR, r.Name)
isClean, err := gitPkg.IsClean(recipeDir)
if err != nil {
return version, err
}
if !isClean {
version = fmt.Sprintf("%s + unstaged changes", version)
}
return version, nil
}
// Push pushes the latest changes to a SSH URL remote. You need to have your
// local SSH configuration for git.coopcloud.tech working for this to work
func (r Recipe2) Push(dryRun bool) error {
repo, err := git.PlainOpen(r.Dir)
if err != nil {
return err
}
if err := gitPkg.CreateRemote(repo, "origin-ssh", r.SSHURL, dryRun); err != nil {
return err
}
if err := gitPkg.Push(r.Dir, "origin-ssh", true, dryRun); err != nil {
return err
}
return nil
}
// GetRecipesLocal retrieves all local recipe directories
func GetRecipesLocal() ([]string, error) {
var recipes []string