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