mirror of
https://github.com/openshift/source-to-image.git
synced 2026-02-06 15:45:00 +01:00
* pkg/util/glog -> pkg/util/log * var glog replaced with var log * Removed unnecessary flag.Parse call (fixed with klog)
317 lines
8.1 KiB
Go
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"),
|
|
}
|
|
}
|