github.com/kubesphere/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 }