From f268e5893b14df893258ec039fcc187cf9492948 Mon Sep 17 00:00:00 2001 From: p4u1 Date: Mon, 8 Jul 2024 11:00:50 +0200 Subject: [PATCH] refactor(recipe): move functions that operate on the git repo to new file --- pkg/recipe/git.go | 264 +++++++++++++++++++++++++++++++++++++++++++ pkg/recipe/recipe.go | 249 ---------------------------------------- 2 files changed, 264 insertions(+), 249 deletions(-) create mode 100644 pkg/recipe/git.go diff --git a/pkg/recipe/git.go b/pkg/recipe/git.go new file mode 100644 index 00000000..ba1b0972 --- /dev/null +++ b/pkg/recipe/git.go @@ -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 +} diff --git a/pkg/recipe/recipe.go b/pkg/recipe/recipe.go index 21b1df00..0d0d2ba4 100644 --- a/pkg/recipe/recipe.go +++ b/pkg/recipe/recipe.go @@ -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