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  }