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 }