1
0
mirror of https://github.com/openshift/source-to-image.git synced 2026-02-05 21:44:56 +01:00
Files
source-to-image/pkg/scm/git/git.go
Adam Kaplan 1fad0a5e69 Refactor glog to log/klog in code
* pkg/util/glog -> pkg/util/log
* var glog replaced with var log
* Removed unnecessary flag.Parse call (fixed with klog)
2019-07-15 15:04:15 -04:00

317 lines
8.1 KiB
Go

package git
import (
"bufio"
"bytes"
"fmt"
"os"
"os/exec"
"path/filepath"
"regexp"
"strconv"
"strings"
"github.com/openshift/source-to-image/pkg/util/cmd"
"github.com/openshift/source-to-image/pkg/util/cygpath"
"github.com/openshift/source-to-image/pkg/util/fs"
utillog "github.com/openshift/source-to-image/pkg/util/log"
)
var log = utillog.StderrLog
var lsTreeRegexp = regexp.MustCompile("([0-7]{6}) [^ ]+ [0-9a-f]{40}\t(.*)")
// Git is an interface used by main STI code to extract/checkout git repositories
type Git interface {
Clone(source *URL, target string, opts CloneConfig) error
Checkout(repo, ref string) error
SubmoduleUpdate(repo string, init, recursive bool) error
LsTree(repo, ref string, recursive bool) ([]os.FileInfo, error)
GetInfo(string) *SourceInfo
}
// New returns a new instance of the default implementation of the Git interface
func New(fs fs.FileSystem, runner cmd.CommandRunner) Git {
return &stiGit{
FileSystem: fs,
CommandRunner: runner,
}
}
type stiGit struct {
fs.FileSystem
cmd.CommandRunner
}
func cloneConfigToArgs(opts CloneConfig) []string {
result := []string{}
if opts.Quiet {
result = append(result, "--quiet")
}
if opts.Recursive {
result = append(result, "--recursive")
}
return result
}
// followGitSubmodule looks at a .git /file/ and tries to retrieve from inside
// it the gitdir value, which is supposed to indicate the location of the
// corresponding .git /directory/. Note: the gitdir value should point directly
// to the corresponding .git directory even in the case of nested submodules.
func followGitSubmodule(fs fs.FileSystem, gitPath string) (string, error) {
f, err := os.Open(gitPath)
if err != nil {
return "", err
}
defer f.Close()
sc := bufio.NewScanner(f)
if sc.Scan() {
s := sc.Text()
if strings.HasPrefix(s, "gitdir: ") {
newGitPath := s[8:]
if !filepath.IsAbs(newGitPath) {
newGitPath = filepath.Join(filepath.Dir(gitPath), newGitPath)
}
fi, err := fs.Stat(newGitPath)
if err != nil && !os.IsNotExist(err) {
return "", err
}
if os.IsNotExist(err) || !fi.IsDir() {
return "", fmt.Errorf("gitdir link in .git file %q is invalid", gitPath)
}
return newGitPath, nil
}
}
return "", fmt.Errorf("unable to parse .git file %q", gitPath)
}
// IsLocalNonBareGitRepository returns true if dir hosts a non-bare git
// repository, i.e. it contains a ".git" subdirectory or file (submodule case).
func IsLocalNonBareGitRepository(fs fs.FileSystem, dir string) (bool, error) {
_, err := fs.Stat(filepath.Join(dir, ".git"))
if os.IsNotExist(err) {
return false, nil
}
if err != nil {
return false, err
}
return true, nil
}
// LocalNonBareGitRepositoryIsEmpty returns true if the non-bare git repository
// at dir has no refs or objects. It also handles the case of dir being a
// checked out git submodule.
func LocalNonBareGitRepositoryIsEmpty(fs fs.FileSystem, dir string) (bool, error) {
gitPath := filepath.Join(dir, ".git")
fi, err := fs.Stat(gitPath)
if err != nil {
return false, err
}
if !fi.IsDir() {
gitPath, err = followGitSubmodule(fs, gitPath)
if err != nil {
return false, err
}
}
// Search for any file in .git/{objects,refs}. We don't just search the
// base .git directory because of the hook samples that are normally
// generated with `git init`
found := false
for _, dir := range []string{"objects", "refs"} {
err := fs.Walk(filepath.Join(gitPath, dir), func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() {
found = true
}
if found {
return filepath.SkipDir
}
return nil
})
if err != nil {
return false, err
}
if found {
return false, nil
}
}
return true, nil
}
// HasGitBinary checks if the 'git' binary is available on the system
func HasGitBinary() bool {
_, err := exec.LookPath("git")
return err == nil
}
// Clone clones a git repository to a specific target directory.
func (h *stiGit) Clone(src *URL, target string, c CloneConfig) error {
var err error
source := *src
if cygpath.UsingCygwinGit {
if source.IsLocal() {
source.URL.Path, err = cygpath.ToSlashCygwin(source.LocalPath())
if err != nil {
return err
}
}
target, err = cygpath.ToSlashCygwin(target)
if err != nil {
return err
}
}
cloneArgs := append([]string{"clone"}, cloneConfigToArgs(c)...)
cloneArgs = append(cloneArgs, []string{source.StringNoFragment(), target}...)
stderr := &bytes.Buffer{}
opts := cmd.CommandOpts{Stderr: stderr}
err = h.RunWithOptions(opts, "git", cloneArgs...)
if err != nil {
log.Errorf("Clone failed: source %s, target %s, with output %q", source, target, stderr.String())
return err
}
return nil
}
// Checkout checks out a specific branch reference of a given git repository
func (h *stiGit) Checkout(repo, ref string) error {
opts := cmd.CommandOpts{
Stdout: os.Stdout,
Stderr: os.Stderr,
Dir: repo,
}
if log.Is(1) {
return h.RunWithOptions(opts, "git", "checkout", "--quiet", ref)
}
return h.RunWithOptions(opts, "git", "checkout", ref)
}
// SubmoduleInit initializes/clones submodules
func (h *stiGit) SubmoduleInit(repo string) error {
opts := cmd.CommandOpts{
Stdout: os.Stdout,
Stderr: os.Stderr,
Dir: repo,
}
return h.RunWithOptions(opts, "git", "submodule", "init")
}
// SubmoduleUpdate checks out submodules to their correct version.
// Optionally also inits submodules, optionally operates recursively.
func (h *stiGit) SubmoduleUpdate(repo string, init, recursive bool) error {
updateArgs := []string{"submodule", "update"}
if init {
updateArgs = append(updateArgs, "--init")
}
if recursive {
updateArgs = append(updateArgs, "--recursive")
}
opts := cmd.CommandOpts{
Stdout: os.Stdout,
Stderr: os.Stderr,
Dir: repo,
}
return h.RunWithOptions(opts, "git", updateArgs...)
}
// LsTree returns a slice of os.FileInfo objects populated with the paths and
// file modes of files known to Git. This is used on Windows systems where the
// executable mode metadata is lost on git checkout.
func (h *stiGit) LsTree(repo, ref string, recursive bool) ([]os.FileInfo, error) {
args := []string{"ls-tree", ref}
if recursive {
args = append(args, "-r")
}
opts := cmd.CommandOpts{
Dir: repo,
}
r, err := h.StartWithStdoutPipe(opts, "git", args...)
if err != nil {
return nil, err
}
submodules := []string{}
rv := []os.FileInfo{}
scanner := bufio.NewScanner(r)
for scanner.Scan() {
text := scanner.Text()
m := lsTreeRegexp.FindStringSubmatch(text)
if m == nil {
return nil, fmt.Errorf("unparsable response %q from git ls-files", text)
}
mode, _ := strconv.ParseInt(m[1], 8, 0)
path := m[2]
if recursive && mode == 0160000 { // S_IFGITLINK
submodules = append(submodules, filepath.Join(repo, path))
continue
}
rv = append(rv, &fs.FileInfo{FileMode: os.FileMode(mode), FileName: path})
}
err = scanner.Err()
if err != nil {
h.Wait()
return nil, err
}
err = h.Wait()
if err != nil {
return nil, err
}
for _, submodule := range submodules {
rrv, err := h.LsTree(submodule, "HEAD", recursive)
if err != nil {
return nil, err
}
rv = append(rv, rrv...)
}
return rv, nil
}
// GetInfo retrieves the information about the source code and commit
func (h *stiGit) GetInfo(repo string) *SourceInfo {
git := func(arg ...string) string {
command := exec.Command("git", arg...)
command.Dir = repo
out, err := command.CombinedOutput()
if err != nil {
log.V(1).Infof("Error executing 'git %#v': %s (%v)", arg, out, err)
return ""
}
return strings.TrimSpace(string(out))
}
return &SourceInfo{
Location: git("config", "--get", "remote.origin.url"),
Ref: git("rev-parse", "--abbrev-ref", "HEAD"),
CommitID: git("rev-parse", "--verify", "HEAD"),
AuthorName: git("--no-pager", "show", "-s", "--format=%an", "HEAD"),
AuthorEmail: git("--no-pager", "show", "-s", "--format=%ae", "HEAD"),
CommitterName: git("--no-pager", "show", "-s", "--format=%cn", "HEAD"),
CommitterEmail: git("--no-pager", "show", "-s", "--format=%ce", "HEAD"),
Date: git("--no-pager", "show", "-s", "--format=%ad", "HEAD"),
Message: git("--no-pager", "show", "-s", "--format=%<(80,trunc)%s", "HEAD"),
}
}