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 }