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