gitutils.go 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  1. package git
  2. import (
  3. "fmt"
  4. "io/ioutil"
  5. "net/http"
  6. "net/url"
  7. "os"
  8. "os/exec"
  9. "path/filepath"
  10. "strings"
  11. "github.com/docker/docker/pkg/symlink"
  12. "github.com/docker/docker/pkg/urlutil"
  13. "github.com/pkg/errors"
  14. )
  15. type gitRepo struct {
  16. remote string
  17. ref string
  18. subdir string
  19. }
  20. // Clone clones a repository into a newly created directory which
  21. // will be under "docker-build-git"
  22. func Clone(remoteURL string) (string, error) {
  23. repo, err := parseRemoteURL(remoteURL)
  24. if err != nil {
  25. return "", err
  26. }
  27. fetch := fetchArgs(repo.remote, repo.ref)
  28. root, err := ioutil.TempDir("", "docker-build-git")
  29. if err != nil {
  30. return "", err
  31. }
  32. if out, err := gitWithinDir(root, "init"); err != nil {
  33. return "", errors.Wrapf(err, "failed to init repo at %s: %s", root, out)
  34. }
  35. // Add origin remote for compatibility with previous implementation that
  36. // used "git clone" and also to make sure local refs are created for branches
  37. if out, err := gitWithinDir(root, "remote", "add", "origin", repo.remote); err != nil {
  38. return "", errors.Wrapf(err, "failed add origin repo at %s: %s", repo.remote, out)
  39. }
  40. if output, err := gitWithinDir(root, fetch...); err != nil {
  41. return "", errors.Wrapf(err, "error fetching: %s", output)
  42. }
  43. return checkoutGit(root, repo.ref, repo.subdir)
  44. }
  45. func parseRemoteURL(remoteURL string) (gitRepo, error) {
  46. repo := gitRepo{}
  47. if !isGitTransport(remoteURL) {
  48. remoteURL = "https://" + remoteURL
  49. }
  50. var fragment string
  51. if strings.HasPrefix(remoteURL, "git@") {
  52. // git@.. is not an URL, so cannot be parsed as URL
  53. parts := strings.SplitN(remoteURL, "#", 2)
  54. repo.remote = parts[0]
  55. if len(parts) == 2 {
  56. fragment = parts[1]
  57. }
  58. repo.ref, repo.subdir = getRefAndSubdir(fragment)
  59. } else {
  60. u, err := url.Parse(remoteURL)
  61. if err != nil {
  62. return repo, err
  63. }
  64. repo.ref, repo.subdir = getRefAndSubdir(u.Fragment)
  65. u.Fragment = ""
  66. repo.remote = u.String()
  67. }
  68. return repo, nil
  69. }
  70. func getRefAndSubdir(fragment string) (ref string, subdir string) {
  71. refAndDir := strings.SplitN(fragment, ":", 2)
  72. ref = "master"
  73. if len(refAndDir[0]) != 0 {
  74. ref = refAndDir[0]
  75. }
  76. if len(refAndDir) > 1 && len(refAndDir[1]) != 0 {
  77. subdir = refAndDir[1]
  78. }
  79. return
  80. }
  81. func fetchArgs(remoteURL string, ref string) []string {
  82. args := []string{"fetch", "--recurse-submodules=yes"}
  83. shallow := true
  84. if urlutil.IsURL(remoteURL) {
  85. res, err := http.Head(fmt.Sprintf("%s/info/refs?service=git-upload-pack", remoteURL))
  86. if err != nil || res.Header.Get("Content-Type") != "application/x-git-upload-pack-advertisement" {
  87. shallow = false
  88. }
  89. }
  90. if shallow {
  91. args = append(args, "--depth", "1")
  92. }
  93. return append(args, "origin", ref)
  94. }
  95. func checkoutGit(root, ref, subdir string) (string, error) {
  96. // Try checking out by ref name first. This will work on branches and sets
  97. // .git/HEAD to the current branch name
  98. if output, err := gitWithinDir(root, "checkout", ref); err != nil {
  99. // If checking out by branch name fails check out the last fetched ref
  100. if _, err2 := gitWithinDir(root, "checkout", "FETCH_HEAD"); err2 != nil {
  101. return "", errors.Wrapf(err, "error checking out %s: %s", ref, output)
  102. }
  103. }
  104. if subdir != "" {
  105. newCtx, err := symlink.FollowSymlinkInScope(filepath.Join(root, subdir), root)
  106. if err != nil {
  107. return "", errors.Wrapf(err, "error setting git context, %q not within git root", subdir)
  108. }
  109. fi, err := os.Stat(newCtx)
  110. if err != nil {
  111. return "", err
  112. }
  113. if !fi.IsDir() {
  114. return "", errors.Errorf("error setting git context, not a directory: %s", newCtx)
  115. }
  116. root = newCtx
  117. }
  118. return root, nil
  119. }
  120. func gitWithinDir(dir string, args ...string) ([]byte, error) {
  121. a := []string{"--work-tree", dir, "--git-dir", filepath.Join(dir, ".git")}
  122. return git(append(a, args...)...)
  123. }
  124. func git(args ...string) ([]byte, error) {
  125. return exec.Command("git", args...).CombinedOutput()
  126. }
  127. // isGitTransport returns true if the provided str is a git transport by inspecting
  128. // the prefix of the string for known protocols used in git.
  129. func isGitTransport(str string) bool {
  130. return urlutil.IsURL(str) || strings.HasPrefix(str, "git://") || strings.HasPrefix(str, "git@")
  131. }