kubesphere.io/s2irun@v3.2.1+incompatible/pkg/scm/git/git.go (about)

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