github.com/openshift/source-to-image@v1.4.1-0.20240516041539-bf52fc02204e/pkg/scm/git/git.go (about)

     1  package git
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"fmt"
     7  	"os"
     8  	"os/exec"
     9  	"path/filepath"
    10  	"regexp"
    11  	"strconv"
    12  	"strings"
    13  
    14  	"github.com/openshift/source-to-image/pkg/util/cmd"
    15  	"github.com/openshift/source-to-image/pkg/util/cygpath"
    16  	"github.com/openshift/source-to-image/pkg/util/fs"
    17  	utillog "github.com/openshift/source-to-image/pkg/util/log"
    18  )
    19  
    20  var log = utillog.StderrLog
    21  var lsTreeRegexp = regexp.MustCompile("([0-7]{6}) [^ ]+ [0-9a-f]{40}\t(.*)")
    22  
    23  // Git is an interface used by main STI code to extract/checkout git repositories
    24  type Git interface {
    25  	Clone(source *URL, target string, opts CloneConfig) error
    26  	Checkout(repo, ref string) error
    27  	SubmoduleUpdate(repo string, init, recursive bool) error
    28  	LsTree(repo, ref string, recursive bool) ([]os.FileInfo, error)
    29  	GetInfo(string) *SourceInfo
    30  }
    31  
    32  // New returns a new instance of the default implementation of the Git interface
    33  func New(fs fs.FileSystem, runner cmd.CommandRunner) Git {
    34  	return &stiGit{
    35  		FileSystem:    fs,
    36  		CommandRunner: runner,
    37  	}
    38  }
    39  
    40  type stiGit struct {
    41  	fs.FileSystem
    42  	cmd.CommandRunner
    43  }
    44  
    45  func cloneConfigToArgs(opts CloneConfig) []string {
    46  	result := []string{}
    47  	if opts.Quiet {
    48  		result = append(result, "--quiet")
    49  	}
    50  	if opts.Recursive {
    51  		result = append(result, "--recursive")
    52  	}
    53  	return result
    54  }
    55  
    56  // followGitSubmodule looks at a .git /file/ and tries to retrieve from inside
    57  // it the gitdir value, which is supposed to indicate the location of the
    58  // corresponding .git /directory/.  Note: the gitdir value should point directly
    59  // to the corresponding .git directory even in the case of nested submodules.
    60  func followGitSubmodule(fs fs.FileSystem, gitPath string) (string, error) {
    61  	f, err := os.Open(gitPath)
    62  	if err != nil {
    63  		return "", err
    64  	}
    65  	defer f.Close()
    66  
    67  	sc := bufio.NewScanner(f)
    68  	if sc.Scan() {
    69  		s := sc.Text()
    70  
    71  		if strings.HasPrefix(s, "gitdir: ") {
    72  			newGitPath := s[8:]
    73  
    74  			if !filepath.IsAbs(newGitPath) {
    75  				newGitPath = filepath.Join(filepath.Dir(gitPath), newGitPath)
    76  			}
    77  
    78  			fi, err := fs.Stat(newGitPath)
    79  			if err != nil && !os.IsNotExist(err) {
    80  				return "", err
    81  			}
    82  			if os.IsNotExist(err) || !fi.IsDir() {
    83  				return "", fmt.Errorf("gitdir link in .git file %q is invalid", gitPath)
    84  			}
    85  			return newGitPath, nil
    86  		}
    87  	}
    88  
    89  	return "", fmt.Errorf("unable to parse .git file %q", gitPath)
    90  }
    91  
    92  // IsLocalNonBareGitRepository returns true if dir hosts a non-bare git
    93  // repository, i.e. it contains a ".git" subdirectory or file (submodule case).
    94  func IsLocalNonBareGitRepository(fs fs.FileSystem, dir string) (bool, error) {
    95  	_, err := fs.Stat(filepath.Join(dir, ".git"))
    96  	if os.IsNotExist(err) {
    97  		return false, nil
    98  	}
    99  	if err != nil {
   100  		return false, err
   101  	}
   102  	return true, nil
   103  }
   104  
   105  // LocalNonBareGitRepositoryIsEmpty returns true if the non-bare git repository
   106  // at dir has no refs or objects.  It also handles the case of dir being a
   107  // checked out git submodule.
   108  func LocalNonBareGitRepositoryIsEmpty(fs fs.FileSystem, dir string) (bool, error) {
   109  	gitPath := filepath.Join(dir, ".git")
   110  
   111  	fi, err := fs.Stat(gitPath)
   112  	if err != nil {
   113  		return false, err
   114  	}
   115  
   116  	if !fi.IsDir() {
   117  		gitPath, err = followGitSubmodule(fs, gitPath)
   118  		if err != nil {
   119  			return false, err
   120  		}
   121  	}
   122  
   123  	// Search for any file in .git/{objects,refs}.  We don't just search the
   124  	// base .git directory because of the hook samples that are normally
   125  	// generated with `git init`
   126  	found := false
   127  	for _, dir := range []string{"objects", "refs"} {
   128  		err := fs.Walk(filepath.Join(gitPath, dir), func(path string, info os.FileInfo, err error) error {
   129  			if err != nil {
   130  				return err
   131  			}
   132  
   133  			if !info.IsDir() {
   134  				found = true
   135  			}
   136  
   137  			if found {
   138  				return filepath.SkipDir
   139  			}
   140  
   141  			return nil
   142  		})
   143  
   144  		if err != nil {
   145  			return false, err
   146  		}
   147  
   148  		if found {
   149  			return false, nil
   150  		}
   151  	}
   152  
   153  	return true, nil
   154  }
   155  
   156  // HasGitBinary checks if the 'git' binary is available on the system
   157  func HasGitBinary() bool {
   158  	_, err := exec.LookPath("git")
   159  	return err == nil
   160  }
   161  
   162  // Clone clones a git repository to a specific target directory.
   163  func (h *stiGit) Clone(src *URL, target string, c CloneConfig) error {
   164  	var err error
   165  
   166  	source := *src
   167  
   168  	if cygpath.UsingCygwinGit {
   169  		if source.IsLocal() {
   170  			source.URL.Path, err = cygpath.ToSlashCygwin(source.LocalPath())
   171  			if err != nil {
   172  				return err
   173  			}
   174  		}
   175  
   176  		target, err = cygpath.ToSlashCygwin(target)
   177  		if err != nil {
   178  			return err
   179  		}
   180  	}
   181  
   182  	cloneArgs := append([]string{"clone"}, cloneConfigToArgs(c)...)
   183  	cloneArgs = append(cloneArgs, []string{source.StringNoFragment(), target}...)
   184  	stderr := &bytes.Buffer{}
   185  	opts := cmd.CommandOpts{Stderr: stderr}
   186  	err = h.RunWithOptions(opts, "git", cloneArgs...)
   187  	if err != nil {
   188  		log.Errorf("Clone failed: source %s, target %s, with output %q", source, target, stderr.String())
   189  		return err
   190  	}
   191  	return nil
   192  }
   193  
   194  // Checkout checks out a specific branch reference of a given git repository
   195  func (h *stiGit) Checkout(repo, ref string) error {
   196  	opts := cmd.CommandOpts{
   197  		Stdout: os.Stdout,
   198  		Stderr: os.Stderr,
   199  		Dir:    repo,
   200  	}
   201  	if log.Is(1) {
   202  		return h.RunWithOptions(opts, "git", "checkout", "--quiet", ref)
   203  	}
   204  	return h.RunWithOptions(opts, "git", "checkout", ref)
   205  }
   206  
   207  // SubmoduleInit initializes/clones submodules
   208  func (h *stiGit) SubmoduleInit(repo string) error {
   209  	opts := cmd.CommandOpts{
   210  		Stdout: os.Stdout,
   211  		Stderr: os.Stderr,
   212  		Dir:    repo,
   213  	}
   214  	return h.RunWithOptions(opts, "git", "submodule", "init")
   215  }
   216  
   217  // SubmoduleUpdate checks out submodules to their correct version.
   218  // Optionally also inits submodules, optionally operates recursively.
   219  func (h *stiGit) SubmoduleUpdate(repo string, init, recursive bool) error {
   220  	updateArgs := []string{"submodule", "update"}
   221  	if init {
   222  		updateArgs = append(updateArgs, "--init")
   223  	}
   224  	if recursive {
   225  		updateArgs = append(updateArgs, "--recursive")
   226  	}
   227  
   228  	opts := cmd.CommandOpts{
   229  		Stdout: os.Stdout,
   230  		Stderr: os.Stderr,
   231  		Dir:    repo,
   232  	}
   233  	return h.RunWithOptions(opts, "git", updateArgs...)
   234  }
   235  
   236  // LsTree returns a slice of os.FileInfo objects populated with the paths and
   237  // file modes of files known to Git.  This is used on Windows systems where the
   238  // executable mode metadata is lost on git checkout.
   239  func (h *stiGit) LsTree(repo, ref string, recursive bool) ([]os.FileInfo, error) {
   240  	args := []string{"ls-tree", ref}
   241  	if recursive {
   242  		args = append(args, "-r")
   243  	}
   244  
   245  	opts := cmd.CommandOpts{
   246  		Dir: repo,
   247  	}
   248  
   249  	r, err := h.StartWithStdoutPipe(opts, "git", args...)
   250  	if err != nil {
   251  		return nil, err
   252  	}
   253  
   254  	submodules := []string{}
   255  	rv := []os.FileInfo{}
   256  	scanner := bufio.NewScanner(r)
   257  	for scanner.Scan() {
   258  		text := scanner.Text()
   259  		m := lsTreeRegexp.FindStringSubmatch(text)
   260  		if m == nil {
   261  			return nil, fmt.Errorf("unparsable response %q from git ls-files", text)
   262  		}
   263  		mode, _ := strconv.ParseInt(m[1], 8, 0)
   264  		path := m[2]
   265  		if recursive && mode == 0160000 { // S_IFGITLINK
   266  			submodules = append(submodules, filepath.Join(repo, path))
   267  			continue
   268  		}
   269  		rv = append(rv, &fs.FileInfo{FileMode: os.FileMode(mode), FileName: path})
   270  	}
   271  	err = scanner.Err()
   272  	if err != nil {
   273  		h.Wait()
   274  		return nil, err
   275  	}
   276  
   277  	err = h.Wait()
   278  	if err != nil {
   279  		return nil, err
   280  	}
   281  
   282  	for _, submodule := range submodules {
   283  		rrv, err := h.LsTree(submodule, "HEAD", recursive)
   284  		if err != nil {
   285  			return nil, err
   286  		}
   287  		rv = append(rv, rrv...)
   288  	}
   289  
   290  	return rv, nil
   291  }
   292  
   293  // GetInfo retrieves the information about the source code and commit
   294  func (h *stiGit) GetInfo(repo string) *SourceInfo {
   295  	git := func(arg ...string) string {
   296  		command := exec.Command("git", arg...)
   297  		command.Dir = repo
   298  		out, err := command.CombinedOutput()
   299  		if err != nil {
   300  			log.V(1).Infof("Error executing 'git %#v': %s (%v)", arg, out, err)
   301  			return ""
   302  		}
   303  		return strings.TrimSpace(string(out))
   304  	}
   305  	return &SourceInfo{
   306  		Location:       git("config", "--get", "remote.origin.url"),
   307  		Ref:            git("rev-parse", "--abbrev-ref", "HEAD"),
   308  		CommitID:       git("rev-parse", "--verify", "HEAD"),
   309  		AuthorName:     git("--no-pager", "show", "-s", "--format=%an", "HEAD"),
   310  		AuthorEmail:    git("--no-pager", "show", "-s", "--format=%ae", "HEAD"),
   311  		CommitterName:  git("--no-pager", "show", "-s", "--format=%cn", "HEAD"),
   312  		CommitterEmail: git("--no-pager", "show", "-s", "--format=%ce", "HEAD"),
   313  		Date:           git("--no-pager", "show", "-s", "--format=%ad", "HEAD"),
   314  		Message:        git("--no-pager", "show", "-s", "--format=%<(80,trunc)%s", "HEAD"),
   315  	}
   316  }