cmd/hub/git/pull.go (243 lines of code) (raw):

// Copyright (c) 2022 EPAM Systems, Inc. // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. package git import ( "errors" "fmt" "io" "log" "os" "path/filepath" "strings" "github.com/epam/hubctl/cmd/hub/config" "github.com/epam/hubctl/cmd/hub/manifest" "github.com/epam/hubctl/cmd/hub/util" ) func PullManifest(manifestFilename string, baseDir string, reset, recurse, optimizeGitRemotes, asSubtree bool) { components, repos, manifests := pullManifest(manifestFilename, baseDir, reset, recurse, optimizeGitRemotes, asSubtree, make([]string, 0), make([]LocalGitRepo, 0), make([]string, 0)) if len(repos) == 0 { log.Printf("No Git sources found in %s", strings.Join(manifests, ", ")) } else if config.Verbose { log.Printf("Components sourced from Git: %s", strings.Join(components, ", ")) if config.Debug { printLocalGitRepos(repos) } } } func pullManifest(manifestFilename string, baseDir string, reset, recurse, optimizeGitRemotes, asSubtree bool, components []string, repos []LocalGitRepo, manifests []string) ([]string, []LocalGitRepo, []string) { stackManifest, rest, _, err := manifest.ParseManifest([]string{manifestFilename}) if err != nil { log.Fatalf("Unable to pull %s: %v", manifestFilename, err) } if len(rest) > 0 { log.Printf("Stack manifest %s contains multiple YAML documents - using first document only", manifestFilename) } baseDirCurrent := baseDir stackBaseDir := util.StripDotDirs(filepath.Dir(manifestFilename)) if baseDirCurrent == "" { baseDirCurrent = stackBaseDir } if config.Debug { log.Printf("Base directory for sources is `%s`", baseDirCurrent) } order, err := manifest.GenerateLifecycleOrder(stackManifest) if err != nil { log.Fatal(err) } stackManifest.Lifecycle.Order = order stackName := stackManifest.Meta.Name if i := strings.Index(stackName, ":"); i > 0 { stackName = stackName[0:i] } components, repos, err = getGit(stackManifest.Meta.Source.Git, baseDirCurrent, stackName, stackName, reset, optimizeGitRemotes, false, components, repos) if err != nil { log.Fatalf("%v", err) } for _, component := range stackManifest.Components { components, repos, err = getGit(component.Source.Git, baseDirCurrent, manifest.ComponentSourceDirFromRef(&component, stackBaseDir, baseDirCurrent), // TODO proper dir manifest.ComponentQualifiedNameFromRef(&component), reset, optimizeGitRemotes, asSubtree, components, repos) if err != nil { if config.Force { util.Warn("%v", err) } else { log.Fatalf("%v", err) } } } manifests = append(manifests, manifestFilename) if recurse && stackManifest.Meta.FromStack != "" { fromStackManifestFilename := filepath.Join(stackManifest.Meta.FromStack, "hub.yaml") if config.Debug { log.Printf("Recursing into %s", fromStackManifestFilename) } components, repos, manifests = pullManifest(fromStackManifestFilename, baseDir, reset, recurse, optimizeGitRemotes, asSubtree, components, repos, manifests) } return components, repos, manifests } func getGit(source manifest.Git, baseDir string, relDir string, componentName string, reset, optimizeGitRemotes, asSubtree bool, components []string, repos []LocalGitRepo) ([]string, []LocalGitRepo, error) { if source.Remote == "" || util.Contains(components, componentName) { return components, repos, nil } if source.LocalDir != "" { relDir = source.LocalDir } dir := relDir if !filepath.IsAbs(relDir) { relDir = filepath.Join(baseDir, relDir) var err error dir, err = filepath.Abs(relDir) if err != nil { return components, repos, fmt.Errorf("Error determining absolute path to pull into %s: %v", relDir, err) } } if config.Debug { log.Printf("Component `%s` Git repo dir is `%s`", componentName, dir) } if dirInRepoList(dir, repos) { return components, repos, fmt.Errorf("Directory %s used twice to pull Git repo", dir) } clone, err := emptyDir(dir, !asSubtree) if err != nil { return components, repos, err } remote := source.Remote remoteVerbose := fmt.Sprintf("`%s`", source.Remote) if optimizeGitRemotes && maybeRemote(remote) { remote = findLocalClone(repos, source.Remote, source.Ref) if remote != source.Remote { remoteVerbose = fmt.Sprintf("`%s` (%s)", remote, source.Remote) if config.Debug { log.Printf("Optimized component `%s` origin from `%s` to `%s`", componentName, source.Remote, remote) } } } if clone { if config.Verbose { log.Printf("Cloning from %s", remoteVerbose) } if asSubtree { return components, repos, errors.New("not implemented") } else { err = Clone(remote, source.Ref, dir) if err != nil { return components, repos, err } } } else { if config.Verbose { log.Printf("Updating from %s", remoteVerbose) } if asSubtree { // ensure remote with name = remote-<component name> // fetch source.Ref as _remote-<component name>/<Ref> remote branch // remember current branch // checkout _remote-<component name>/<Ref> as _remote-<component name>-<Ref> // split source.SubDir into _split-<component name> // pop to current branch // subtree merge into `dir` from _split-<component name> // delete _split-<component name> // delete _remote-<component name>-<Ref> // in case of error - show error, then note user to: // - return to current branch // - delete _split and _remote branches return components, repos, errors.New("not implemented") } else { if reset { // TODO: same way as git stash --include-untracked return components, repos, errors.New("not implemented") } err = Pull(source.Ref, dir) if err != nil { return components, repos, fmt.Errorf("Unable to pull Git repo %s into `%s`: %v", remoteVerbose, dir, err) } } } headName, _, err := HeadInfo(dir) if err != nil { util.Warn("%v", err) } return append(components, componentName), append(repos, LocalGitRepo{ Remote: source.Remote, OptimizedRemote: remote, Ref: source.Ref, HeadRef: headName, SubDir: source.SubDir, AbsDir: dir, }), nil } var dirMode = os.FileMode(0755) func emptyDir(dir string, removeContentIfForced bool) (bool, error) { dirInfo, err := os.Stat(dir) if err != nil { if !util.NoSuchFile(err) { return false, fmt.Errorf("Unable to stat `%s`: %v", dir, err) } return true, nil } if !dirInfo.IsDir() { if config.Force { err = os.Remove(dir) if err != nil { return false, fmt.Errorf("Unable to force remove `%s`: %v", dir, err) } } else { return false, fmt.Errorf("Pull target `%s` is not a directory, add -f / --force to override", dir) } } gitDir := filepath.Join(dir, ".git") gitInfo, err := os.Stat(gitDir) if err != nil { if !util.NoSuchFile(err) { return false, fmt.Errorf("Unable to stat `%s`: %v", dir, err) } dirFD, err := os.Open(dir) if err != nil { return false, fmt.Errorf("Unable to open dir `%s`: %v", dir, err) } fileNames, err := dirFD.Readdirnames(1) if err != nil && err != io.EOF { return false, fmt.Errorf("Unable to read dir `%s`: %v", dir, err) } if config.Trace { log.Printf("Dir content: %v", fileNames) } if len(fileNames) > 0 { if !removeContentIfForced { return false, nil } if config.Force { err := os.RemoveAll(dir) if err != nil { return false, fmt.Errorf("Unable to force clean dir `%s`: %v", dir, err) } os.Mkdir(dir, dirMode) } else { return false, fmt.Errorf("Pull target `%s` is not an empty directory, add -f / --force to override", dir) } } dirFD.Close() return true, nil } else { if !gitInfo.IsDir() { return false, fmt.Errorf("Pull target `%s` is not a Git repo", dir) } } return false, nil } func maybeRemote(origin string) bool { return strings.Contains(origin, ":") } func findLocalClone(repos []LocalGitRepo, remote string, ref string) string { for _, repo := range repos { if remote == repo.Remote && ref == repo.Ref { return repo.AbsDir } } return remote } func dirInRepoList(dir string, repos []LocalGitRepo) bool { for _, repo := range repos { if dir == repo.AbsDir { return true } } return false }