gitutils.go 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  1. package git // import "github.com/docker/docker/builder/remotecontext/git"
  2. import (
  3. "io/ioutil"
  4. "net/http"
  5. "net/url"
  6. "os"
  7. "os/exec"
  8. "path/filepath"
  9. "strings"
  10. "github.com/moby/sys/symlink"
  11. "github.com/pkg/errors"
  12. )
  13. type gitRepo struct {
  14. remote string
  15. ref string
  16. subdir string
  17. }
  18. // Clone clones a repository into a newly created directory which
  19. // will be under "docker-build-git"
  20. func Clone(remoteURL string) (string, error) {
  21. repo, err := parseRemoteURL(remoteURL)
  22. if err != nil {
  23. return "", err
  24. }
  25. return cloneGitRepo(repo)
  26. }
  27. func cloneGitRepo(repo gitRepo) (checkoutDir string, err error) {
  28. fetch := fetchArgs(repo.remote, repo.ref)
  29. root, err := ioutil.TempDir("", "docker-build-git")
  30. if err != nil {
  31. return "", err
  32. }
  33. defer func() {
  34. if err != nil {
  35. os.RemoveAll(root)
  36. }
  37. }()
  38. if out, err := gitWithinDir(root, "init"); err != nil {
  39. return "", errors.Wrapf(err, "failed to init repo at %s: %s", root, out)
  40. }
  41. // Add origin remote for compatibility with previous implementation that
  42. // used "git clone" and also to make sure local refs are created for branches
  43. if out, err := gitWithinDir(root, "remote", "add", "origin", repo.remote); err != nil {
  44. return "", errors.Wrapf(err, "failed add origin repo at %s: %s", repo.remote, out)
  45. }
  46. if output, err := gitWithinDir(root, fetch...); err != nil {
  47. return "", errors.Wrapf(err, "error fetching: %s", output)
  48. }
  49. checkoutDir, err = checkoutGit(root, repo.ref, repo.subdir)
  50. if err != nil {
  51. return "", err
  52. }
  53. cmd := exec.Command("git", "submodule", "update", "--init", "--recursive", "--depth=1")
  54. cmd.Dir = root
  55. output, err := cmd.CombinedOutput()
  56. if err != nil {
  57. return "", errors.Wrapf(err, "error initializing submodules: %s", output)
  58. }
  59. return checkoutDir, nil
  60. }
  61. func parseRemoteURL(remoteURL string) (gitRepo, error) {
  62. repo := gitRepo{}
  63. if !isGitTransport(remoteURL) {
  64. remoteURL = "https://" + remoteURL
  65. }
  66. var fragment string
  67. if strings.HasPrefix(remoteURL, "git@") {
  68. // git@.. is not an URL, so cannot be parsed as URL
  69. parts := strings.SplitN(remoteURL, "#", 2)
  70. repo.remote = parts[0]
  71. if len(parts) == 2 {
  72. fragment = parts[1]
  73. }
  74. repo.ref, repo.subdir = getRefAndSubdir(fragment)
  75. } else {
  76. u, err := url.Parse(remoteURL)
  77. if err != nil {
  78. return repo, err
  79. }
  80. repo.ref, repo.subdir = getRefAndSubdir(u.Fragment)
  81. u.Fragment = ""
  82. repo.remote = u.String()
  83. }
  84. if strings.HasPrefix(repo.ref, "-") {
  85. return gitRepo{}, errors.Errorf("invalid refspec: %s", repo.ref)
  86. }
  87. return repo, nil
  88. }
  89. func getRefAndSubdir(fragment string) (ref string, subdir string) {
  90. refAndDir := strings.SplitN(fragment, ":", 2)
  91. ref = "master"
  92. if len(refAndDir[0]) != 0 {
  93. ref = refAndDir[0]
  94. }
  95. if len(refAndDir) > 1 && len(refAndDir[1]) != 0 {
  96. subdir = refAndDir[1]
  97. }
  98. return
  99. }
  100. func fetchArgs(remoteURL string, ref string) []string {
  101. args := []string{"fetch"}
  102. if supportsShallowClone(remoteURL) {
  103. args = append(args, "--depth", "1")
  104. }
  105. return append(args, "origin", "--", ref)
  106. }
  107. // Check if a given git URL supports a shallow git clone,
  108. // i.e. it is a non-HTTP server or a smart HTTP server.
  109. func supportsShallowClone(remoteURL string) bool {
  110. if scheme := getScheme(remoteURL); scheme == "http" || scheme == "https" {
  111. // Check if the HTTP server is smart
  112. // Smart servers must correctly respond to a query for the git-upload-pack service
  113. serviceURL := remoteURL + "/info/refs?service=git-upload-pack"
  114. // Try a HEAD request and fallback to a Get request on error
  115. res, err := http.Head(serviceURL) // #nosec G107
  116. if err != nil || res.StatusCode != http.StatusOK {
  117. res, err = http.Get(serviceURL) // #nosec G107
  118. if err == nil {
  119. res.Body.Close()
  120. }
  121. if err != nil || res.StatusCode != http.StatusOK {
  122. // request failed
  123. return false
  124. }
  125. }
  126. if res.Header.Get("Content-Type") != "application/x-git-upload-pack-advertisement" {
  127. // Fallback, not a smart server
  128. return false
  129. }
  130. return true
  131. }
  132. // Non-HTTP protocols always support shallow clones
  133. return true
  134. }
  135. func checkoutGit(root, ref, subdir string) (string, error) {
  136. // Try checking out by ref name first. This will work on branches and sets
  137. // .git/HEAD to the current branch name
  138. if output, err := gitWithinDir(root, "checkout", ref); err != nil {
  139. // If checking out by branch name fails check out the last fetched ref
  140. if _, err2 := gitWithinDir(root, "checkout", "FETCH_HEAD"); err2 != nil {
  141. return "", errors.Wrapf(err, "error checking out %s: %s", ref, output)
  142. }
  143. }
  144. if subdir != "" {
  145. newCtx, err := symlink.FollowSymlinkInScope(filepath.Join(root, subdir), root)
  146. if err != nil {
  147. return "", errors.Wrapf(err, "error setting git context, %q not within git root", subdir)
  148. }
  149. fi, err := os.Stat(newCtx)
  150. if err != nil {
  151. return "", err
  152. }
  153. if !fi.IsDir() {
  154. return "", errors.Errorf("error setting git context, not a directory: %s", newCtx)
  155. }
  156. root = newCtx
  157. }
  158. return root, nil
  159. }
  160. func gitWithinDir(dir string, args ...string) ([]byte, error) {
  161. a := []string{"--work-tree", dir, "--git-dir", filepath.Join(dir, ".git")}
  162. return git(append(a, args...)...)
  163. }
  164. func git(args ...string) ([]byte, error) {
  165. return exec.Command("git", args...).CombinedOutput()
  166. }
  167. // isGitTransport returns true if the provided str is a git transport by inspecting
  168. // the prefix of the string for known protocols used in git.
  169. func isGitTransport(str string) bool {
  170. if strings.HasPrefix(str, "git@") {
  171. return true
  172. }
  173. switch getScheme(str) {
  174. case "git", "http", "https", "ssh":
  175. return true
  176. }
  177. return false
  178. }
  179. // getScheme returns addresses' scheme in lowercase, or an empty
  180. // string in case address is an invalid URL.
  181. func getScheme(address string) string {
  182. u, err := url.Parse(address)
  183. if err != nil {
  184. return ""
  185. }
  186. return u.Scheme
  187. }