diff --git a/cli/catalogue/catalogue.go b/cli/catalogue/catalogue.go index 0b1dcecb..52613c5c 100644 --- a/cli/catalogue/catalogue.go +++ b/cli/catalogue/catalogue.go @@ -8,6 +8,7 @@ import ( "coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/pkg/autocomplete" + "coopcloud.tech/abra/pkg/catalogue" "coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/formatter" gitPkg "coopcloud.tech/abra/pkg/git" @@ -97,6 +98,10 @@ keys configured on your account. internal.ValidateRecipe(c, true) } + if err := catalogue.EnsureUpToDate(); err != nil { + logrus.Fatal(err) + } + repos, err := recipe.ReadReposMetadata() if err != nil { logrus.Fatal(err) diff --git a/pkg/catalogue/catalogue.go b/pkg/catalogue/catalogue.go new file mode 100644 index 00000000..e7fe816e --- /dev/null +++ b/pkg/catalogue/catalogue.go @@ -0,0 +1,83 @@ +package catalogue + +import ( + "fmt" + "os" + "path" + "strings" + + "coopcloud.tech/abra/pkg/config" + gitPkg "coopcloud.tech/abra/pkg/git" + "github.com/go-git/go-git/v5" + "github.com/sirupsen/logrus" +) + +// EnsureCatalogue ensures that the catalogue is cloned locally & present. +func EnsureCatalogue() error { + catalogueDir := path.Join(config.ABRA_DIR, "catalogue") + if _, err := os.Stat(catalogueDir); err != nil && os.IsNotExist(err) { + url := fmt.Sprintf("%s/%s.git", config.REPOS_BASE_URL, config.CATALOGUE_JSON_REPO_NAME) + if err := gitPkg.Clone(catalogueDir, url); err != nil { + return err + } + + logrus.Debugf("cloned catalogue repository to %s", catalogueDir) + } + + return nil +} + +// EnsureUpToDate ensures that the local catalogue has no unstaged changes as +// is up to date. This is useful to run before doing catalogue generation. +func EnsureUpToDate() error { + isClean, err := gitPkg.IsClean(config.CATALOGUE_DIR) + if err != nil { + return err + } + + if !isClean { + msg := "%s has locally unstaged changes? please commit/remove your changes before proceeding" + return fmt.Errorf(msg, config.CATALOGUE_DIR) + } + + repo, err := git.PlainOpen(config.CATALOGUE_DIR) + if err != nil { + return err + } + + remotes, err := repo.Remotes() + if err != nil { + return err + } + + if len(remotes) == 0 { + msg := "cannot ensure %s is up-to-date, no git remotes configured" + logrus.Debugf(msg, config.CATALOGUE_DIR) + return nil + } + + worktree, err := repo.Worktree() + if err != nil { + return err + } + + branch, err := gitPkg.CheckoutDefaultBranch(repo, config.CATALOGUE_DIR) + if err != nil { + return err + } + + opts := &git.PullOptions{ + Force: true, + ReferenceName: branch, + } + + if err := worktree.Pull(opts); err != nil { + if !strings.Contains(err.Error(), "already up-to-date") { + return err + } + } + + logrus.Debugf("fetched latest git changes for %s", config.CATALOGUE_DIR) + + return nil +} diff --git a/pkg/config/env.go b/pkg/config/env.go index f6d0e9a2..44e16efb 100644 --- a/pkg/config/env.go +++ b/pkg/config/env.go @@ -19,6 +19,7 @@ var SERVERS_DIR = path.Join(ABRA_DIR, "servers") var RECIPES_DIR = path.Join(ABRA_DIR, "recipes") var VENDOR_DIR = path.Join(ABRA_DIR, "vendor") var BACKUP_DIR = path.Join(ABRA_DIR, "backups") +var CATALOGUE_DIR = path.Join(ABRA_DIR, "catalogue") var RECIPES_JSON = path.Join(ABRA_DIR, "catalogue", "recipes.json") var REPOS_BASE_URL = "https://git.coopcloud.tech/coop-cloud" var CATALOGUE_JSON_REPO_NAME = "recipes-catalogue-json" diff --git a/pkg/git/branch.go b/pkg/git/branch.go index 9bc5bbb2..6edebee7 100644 --- a/pkg/git/branch.go +++ b/pkg/git/branch.go @@ -1,14 +1,16 @@ package git import ( + "fmt" + "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing" + "github.com/sirupsen/logrus" ) -// Check if a branch exists in a repo. -// Use this and not repository.Branch(), because the latter does not -// actually check for existing branches. -// See https://github.com/go-git/go-git/issues/518 +// Check if a branch exists in a repo. Use this and not repository.Branch(), +// because the latter does not actually check for existing branches. See +// https://github.com/go-git/go-git/issues/518 for more. func HasBranch(repository *git.Repository, name string) bool { var exist bool if iter, err := repository.Branches(); err == nil { @@ -24,7 +26,7 @@ func HasBranch(repository *git.Repository, name string) bool { return exist } -// GetCurrentBranch retrieves the current branch of a repository +// GetCurrentBranch retrieves the current branch of a repository. func GetCurrentBranch(repository *git.Repository) (string, error) { branchRefs, err := repository.Branches() if err != nil { @@ -52,3 +54,45 @@ func GetCurrentBranch(repository *git.Repository) (string, error) { return currentBranchName, nil } + +// GetDefaultBranch retrieves the default branch of a repository. +func GetDefaultBranch(repo *git.Repository, repoPath string) (plumbing.ReferenceName, error) { + branch := "master" + + if !HasBranch(repo, "master") { + if !HasBranch(repo, "main") { + return "", fmt.Errorf("failed to select default branch in %s", repoPath) + } + branch = "main" + } + + return plumbing.ReferenceName(fmt.Sprintf("refs/heads/%s", branch)), nil +} + +// CheckoutDefaultBranch checks out the default branch of a repository. +func CheckoutDefaultBranch(repo *git.Repository, repoPath string) (plumbing.ReferenceName, error) { + branch, err := GetDefaultBranch(repo, repoPath) + if err != nil { + return plumbing.ReferenceName(""), err + } + + worktree, err := repo.Worktree() + if err != nil { + return plumbing.ReferenceName(""), err + } + + checkOutOpts := &git.CheckoutOptions{ + Create: false, + Force: true, + Branch: branch, + } + + if err := worktree.Checkout(checkOutOpts); err != nil { + logrus.Debugf("failed to check out %s in %s", branch, repoPath) + return branch, err + } + + logrus.Debugf("successfully checked out %v in %s", branch, repoPath) + + return branch, nil +} diff --git a/pkg/recipe/recipe.go b/pkg/recipe/recipe.go index fabdb18c..072d69b7 100644 --- a/pkg/recipe/recipe.go +++ b/pkg/recipe/recipe.go @@ -12,6 +12,7 @@ import ( "strings" "time" + "coopcloud.tech/abra/pkg/catalogue" "coopcloud.tech/abra/pkg/compose" "coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/formatter" @@ -359,11 +360,21 @@ func EnsureLatest(recipeName string) error { return err } - branch, err := GetDefaultBranch(repo, recipeName) + meta, err := GetRecipeMeta(recipeName) if err != nil { return err } + var branch plumbing.ReferenceName + if meta.DefaultBranch != "" { + branch = plumbing.ReferenceName(fmt.Sprintf("refs/heads/%s", meta.DefaultBranch)) + } else { + branch, err = gitPkg.GetDefaultBranch(repo, recipeDir) + if err != nil { + return err + } + } + checkOutOpts := &git.CheckoutOptions{ Create: false, Force: true, @@ -575,7 +586,8 @@ func EnsureUpToDate(recipeName string) error { } if !isClean { - return fmt.Errorf("%s (%s) has locally unstaged changes? please commit/remove your changes before proceeding", recipeName, recipeDir) + msg := "%s (%s) has locally unstaged changes? please commit/remove your changes before proceeding" + return fmt.Errorf(msg, recipeName, recipeDir) } repo, err := git.PlainOpen(recipeDir) @@ -598,7 +610,7 @@ func EnsureUpToDate(recipeName string) error { return err } - branch, err := CheckoutDefaultBranch(repo, recipeName) + branch, err := gitPkg.CheckoutDefaultBranch(repo, recipeDir) if err != nil { return err } @@ -619,55 +631,6 @@ func EnsureUpToDate(recipeName string) error { return nil } -func GetDefaultBranch(repo *git.Repository, recipeName string) (plumbing.ReferenceName, error) { - recipeDir := path.Join(config.RECIPES_DIR, recipeName) - - meta, _ := GetRecipeMeta(recipeName) - if meta.DefaultBranch != "" { - return plumbing.ReferenceName(fmt.Sprintf("refs/heads/%s", meta.DefaultBranch)), nil - } - - branch := "master" - if !gitPkg.HasBranch(repo, "master") { - if !gitPkg.HasBranch(repo, "main") { - return "", fmt.Errorf("failed to select default branch in %s", recipeDir) - } - branch = "main" - } - - return plumbing.ReferenceName(fmt.Sprintf("refs/heads/%s", branch)), nil -} - -func CheckoutDefaultBranch(repo *git.Repository, recipeName string) (plumbing.ReferenceName, error) { - recipeDir := path.Join(config.RECIPES_DIR, recipeName) - - branch, err := GetDefaultBranch(repo, recipeName) - if err != nil { - return plumbing.ReferenceName(""), err - } - - worktree, err := repo.Worktree() - if err != nil { - return plumbing.ReferenceName(""), err - } - - checkOutOpts := &git.CheckoutOptions{ - Create: false, - Force: true, - Branch: branch, - } - - if err := worktree.Checkout(checkOutOpts); err != nil { - recipeDir := path.Join(config.RECIPES_DIR, recipeName) - logrus.Debugf("failed to check out %s in %s", branch, recipeDir) - return branch, err - } - - logrus.Debugf("successfully checked out %v in %s", branch, recipeDir) - - return branch, nil -} - type CatalogueOfflineError struct { msg string } @@ -717,7 +680,7 @@ func recipeCatalogueFSIsLatest() (bool, error) { func ReadRecipeCatalogue() (RecipeCatalogue, error) { recipes := make(RecipeCatalogue) - if err := EnsureCatalogue(); err != nil { + if err := catalogue.EnsureCatalogue(); err != nil { return nil, err } @@ -1024,7 +987,7 @@ func GetRecipeVersions(recipeName string) (RecipeVersions, error) { return versions, err } - _, err = CheckoutDefaultBranch(repo, recipeName) + _, err = gitPkg.CheckoutDefaultBranch(repo, recipeDir) if err != nil { return versions, err } @@ -1048,18 +1011,3 @@ func GetRecipeCatalogueVersions(recipeName string, catl RecipeCatalogue) ([]stri return versions, nil } - -// EnsureCatalogue ensures that the catalogue is cloned locally & present. -func EnsureCatalogue() error { - catalogueDir := path.Join(config.ABRA_DIR, "catalogue") - if _, err := os.Stat(catalogueDir); err != nil && os.IsNotExist(err) { - url := fmt.Sprintf("%s/%s.git", config.REPOS_BASE_URL, config.CATALOGUE_JSON_REPO_NAME) - if err := gitPkg.Clone(catalogueDir, url); err != nil { - return err - } - - logrus.Debugf("cloned catalogue repository to %s", catalogueDir) - } - - return nil -}