github.com/GoogleContainerTools/skaffold@v1.39.18/pkg/skaffold/git/gitutil.go (about) 1 // code modified from https://github.com/GoogleContainerTools/kpt/blob/master/internal/gitutil/gitutil.go 2 3 // Copyright 2019 Google LLC 4 // 5 // Licensed under the Apache License, Version 2.0 (the "License"); 6 // you may not use this file except in compliance with the License. 7 // You may obtain a copy of the License at 8 // 9 // http://www.apache.org/licenses/LICENSE-2.0 10 // 11 // Unless required by applicable law or agreed to in writing, software 12 // distributed under the License is distributed on an "AS IS" BASIS, 13 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 // See the License for the specific language governing permissions and 15 // limitations under the License. 16 17 package git 18 19 import ( 20 "context" 21 "crypto/sha256" 22 "encoding/base64" 23 "encoding/json" 24 "fmt" 25 "os" 26 "os/exec" 27 "path/filepath" 28 "strings" 29 30 "github.com/mitchellh/go-homedir" 31 32 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/config" 33 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/constants" 34 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest" 35 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/util" 36 ) 37 38 // SyncRepo syncs the target git repository with skaffold's local cache and returns the path to the repository root directory. 39 var SyncRepo = syncRepo 40 var findGit = func() (string, error) { return exec.LookPath("git") } 41 42 // defaultRef returns the default ref as "master" if master branch exists in 43 // remote repository, falls back to "main" if master branch doesn't exist 44 func defaultRef(ctx context.Context, repo string) (string, error) { 45 masterRef := "master" 46 mainRef := "main" 47 masterExists, err := branchExists(ctx, repo, masterRef) 48 if err != nil { 49 return "", err 50 } 51 mainExists, err := branchExists(ctx, repo, mainRef) 52 if err != nil { 53 return "", err 54 } 55 if masterExists { 56 return masterRef, nil 57 } else if mainExists { 58 return mainRef, nil 59 } 60 return "", fmt.Errorf("failed to get default branch for repo %s", repo) 61 } 62 63 // BranchExists checks if branch is present in the input repo 64 func branchExists(ctx context.Context, repo, branch string) (bool, error) { 65 gitProgram, err := findGit() 66 if err != nil { 67 return false, err 68 } 69 out, err := util.RunCmdOut(ctx, exec.Command(gitProgram, "ls-remote", "--heads", repo, branch)) 70 if err != nil { 71 // stdErr contains the error message for os related errors, git permission errors 72 // and if repo doesn't exist 73 return false, fmt.Errorf("failed to lookup %s branch for repo %s: %w", branch, repo, err) 74 } 75 // stdOut contains the branch information if the branch is present in remote repo 76 // stdOut is empty if the repo doesn't have the input branch 77 if strings.TrimSpace(string(out)) != "" { 78 return true, nil 79 } 80 return false, nil 81 } 82 83 // getRepoDir returns the cache directory name for a remote repo 84 func getRepoDir(g latest.GitInfo) (string, error) { 85 inputs := []string{g.Repo, g.Ref} 86 hasher := sha256.New() 87 enc := json.NewEncoder(hasher) 88 if err := enc.Encode(inputs); err != nil { 89 return "", err 90 } 91 92 return base64.URLEncoding.EncodeToString(hasher.Sum(nil))[:32], nil 93 } 94 95 // GetRepoCacheDir returns the directory for the remote git repo cache 96 func GetRepoCacheDir(opts config.SkaffoldOptions) (string, error) { 97 if opts.RepoCacheDir != "" { 98 return opts.RepoCacheDir, nil 99 } 100 101 // cache location unspecified, use ~/.skaffold/repos 102 home, err := homedir.Dir() 103 if err != nil { 104 return "", fmt.Errorf("retrieving home directory: %w", err) 105 } 106 return filepath.Join(home, constants.DefaultSkaffoldDir, "repos"), nil 107 } 108 109 func syncRepo(ctx context.Context, g latest.GitInfo, opts config.SkaffoldOptions) (string, error) { 110 skaffoldCacheDir, err := GetRepoCacheDir(opts) 111 r := gitCmd{Dir: skaffoldCacheDir} 112 if err != nil { 113 return "", fmt.Errorf("failed to clone repo %s: %w", g.Repo, err) 114 } 115 if err := os.MkdirAll(skaffoldCacheDir, 0700); err != nil { 116 return "", fmt.Errorf( 117 "failed to clone repo %s: trouble creating cache directory: %w", g.Repo, err) 118 } 119 120 ref := g.Ref 121 if ref == "" { 122 ref, err = defaultRef(ctx, g.Repo) 123 if err != nil { 124 return "", fmt.Errorf("failed to clone repo %s: trouble getting default branch: %w", g.Repo, err) 125 } 126 } 127 128 hash, err := getRepoDir(g) 129 if err != nil { 130 return "", fmt.Errorf("failed to clone git repo: unable to create directory name: %w", err) 131 } 132 repoCacheDir := filepath.Join(skaffoldCacheDir, hash) 133 if _, err := os.Stat(repoCacheDir); os.IsNotExist(err) { 134 if opts.SyncRemoteCache.CloneDisabled() { 135 return "", SyncDisabledErr(g, repoCacheDir) 136 } 137 if _, err := r.Run(ctx, "clone", g.Repo, fmt.Sprintf("./%s", hash), "--branch", ref, "--depth", "1"); err != nil { 138 return "", fmt.Errorf("failed to clone repo: %w", err) 139 } 140 } else { 141 r.Dir = repoCacheDir 142 // check remote is defined 143 if remotes, err := r.Run(ctx, "remote", "-v"); err != nil { 144 return "", fmt.Errorf("failed to clone repo %s: trouble checking repository remote; run 'git clone <REPO>; stat <DIR/SUBDIR>' to verify credentials: %w", g.Repo, err) 145 } else if len(remotes) == 0 { 146 return "", fmt.Errorf("failed to clone repo %s: remote not set for existing clone", g.Repo) 147 } 148 149 // if sync property is false, then skip fetching latest from remote and resetting the branch. 150 if g.Sync != nil && !*g.Sync { 151 return repoCacheDir, nil 152 } 153 154 // if sync is turned off via flag `--sync-remote-cache`, then skip fetching latest from remote and resetting the branch. 155 if opts.SyncRemoteCache.FetchDisabled() { 156 return repoCacheDir, nil 157 } 158 159 if _, err = r.Run(ctx, "fetch", "origin", ref); err != nil { 160 return "", fmt.Errorf("failed to clone repo %s: unable to find any matching refs %s; run 'git clone <REPO>; stat <DIR/SUBDIR>' to verify credentials: %w", g.Repo, ref, err) 161 } 162 163 // Sync option is either nil or true, so we are resetting the repo 164 if _, err := r.Run(ctx, "reset", "--hard", fmt.Sprintf("origin/%s", ref)); err != nil { 165 return "", fmt.Errorf("failed to clone repo %s: trouble resetting branch to origin/%s; run 'git clone <REPO>; stat <DIR/SUBDIR>' to verify credentials: %w", g.Repo, ref, err) 166 } 167 } 168 return repoCacheDir, nil 169 } 170 171 // gitCmd runs git commands in a git repo. 172 type gitCmd struct { 173 // Dir is the directory the commands are run in. 174 Dir string 175 } 176 177 // Run runs a git command. 178 // Omit the 'git' part of the command. 179 func (g *gitCmd) Run(ctx context.Context, args ...string) ([]byte, error) { 180 p, err := findGit() 181 if err != nil { 182 return nil, fmt.Errorf("no 'git' program on path: %w", err) 183 } 184 185 cmd := exec.Command(p, args...) 186 cmd.Dir = g.Dir 187 return util.RunCmdOut(ctx, cmd) 188 }