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