github.com/kaisenlinux/docker.io@v0.0.0-20230510090727-ea55db55fac7/engine/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 isolateConfig bool 21 } 22 23 // CloneOption changes the behaviour of Clone(). 24 type CloneOption func(*gitRepo) 25 26 // WithIsolatedConfig disables reading the user or system gitconfig files when 27 // performing Git operations. 28 func WithIsolatedConfig(v bool) CloneOption { 29 return func(gr *gitRepo) { 30 gr.isolateConfig = v 31 } 32 } 33 34 // Clone clones a repository into a newly created directory which 35 // will be under "docker-build-git" 36 func Clone(remoteURL string, opts ...CloneOption) (string, error) { 37 repo, err := parseRemoteURL(remoteURL) 38 39 if err != nil { 40 return "", err 41 } 42 43 for _, opt := range opts { 44 opt(&repo) 45 } 46 47 return repo.clone() 48 } 49 50 func (repo gitRepo) clone() (checkoutDir string, err error) { 51 fetch := fetchArgs(repo.remote, repo.ref) 52 53 root, err := os.MkdirTemp("", "docker-build-git") 54 if err != nil { 55 return "", err 56 } 57 58 defer func() { 59 if err != nil { 60 os.RemoveAll(root) 61 } 62 }() 63 64 if out, err := repo.gitWithinDir(root, "init"); err != nil { 65 return "", errors.Wrapf(err, "failed to init repo at %s: %s", root, out) 66 } 67 68 // Add origin remote for compatibility with previous implementation that 69 // used "git clone" and also to make sure local refs are created for branches 70 if out, err := repo.gitWithinDir(root, "remote", "add", "origin", repo.remote); err != nil { 71 return "", errors.Wrapf(err, "failed add origin repo at %s: %s", repo.remote, out) 72 } 73 74 if output, err := repo.gitWithinDir(root, fetch...); err != nil { 75 return "", errors.Wrapf(err, "error fetching: %s", output) 76 } 77 78 checkoutDir, err = repo.checkout(root) 79 if err != nil { 80 return "", err 81 } 82 83 cmd := exec.Command("git", "submodule", "update", "--init", "--recursive", "--depth=1") 84 cmd.Dir = root 85 output, err := cmd.CombinedOutput() 86 if err != nil { 87 return "", errors.Wrapf(err, "error initializing submodules: %s", output) 88 } 89 90 return checkoutDir, nil 91 } 92 93 func parseRemoteURL(remoteURL string) (gitRepo, error) { 94 repo := gitRepo{} 95 96 if !isGitTransport(remoteURL) { 97 remoteURL = "https://" + remoteURL 98 } 99 100 var fragment string 101 if strings.HasPrefix(remoteURL, "git@") { 102 // git@.. is not an URL, so cannot be parsed as URL 103 parts := strings.SplitN(remoteURL, "#", 2) 104 105 repo.remote = parts[0] 106 if len(parts) == 2 { 107 fragment = parts[1] 108 } 109 repo.ref, repo.subdir = getRefAndSubdir(fragment) 110 } else { 111 u, err := url.Parse(remoteURL) 112 if err != nil { 113 return repo, err 114 } 115 116 repo.ref, repo.subdir = getRefAndSubdir(u.Fragment) 117 u.Fragment = "" 118 repo.remote = u.String() 119 } 120 121 if strings.HasPrefix(repo.ref, "-") { 122 return gitRepo{}, errors.Errorf("invalid refspec: %s", repo.ref) 123 } 124 125 return repo, nil 126 } 127 128 func getRefAndSubdir(fragment string) (ref string, subdir string) { 129 refAndDir := strings.SplitN(fragment, ":", 2) 130 ref = "master" 131 if len(refAndDir[0]) != 0 { 132 ref = refAndDir[0] 133 } 134 if len(refAndDir) > 1 && len(refAndDir[1]) != 0 { 135 subdir = refAndDir[1] 136 } 137 return 138 } 139 140 func fetchArgs(remoteURL string, ref string) []string { 141 args := []string{"fetch"} 142 143 if supportsShallowClone(remoteURL) { 144 args = append(args, "--depth", "1") 145 } 146 147 return append(args, "origin", "--", ref) 148 } 149 150 // Check if a given git URL supports a shallow git clone, 151 // i.e. it is a non-HTTP server or a smart HTTP server. 152 func supportsShallowClone(remoteURL string) bool { 153 if scheme := getScheme(remoteURL); scheme == "http" || scheme == "https" { 154 // Check if the HTTP server is smart 155 156 // Smart servers must correctly respond to a query for the git-upload-pack service 157 serviceURL := remoteURL + "/info/refs?service=git-upload-pack" 158 159 // Try a HEAD request and fallback to a Get request on error 160 res, err := http.Head(serviceURL) // #nosec G107 161 if err != nil || res.StatusCode != http.StatusOK { 162 res, err = http.Get(serviceURL) // #nosec G107 163 if err == nil { 164 res.Body.Close() 165 } 166 if err != nil || res.StatusCode != http.StatusOK { 167 // request failed 168 return false 169 } 170 } 171 172 if res.Header.Get("Content-Type") != "application/x-git-upload-pack-advertisement" { 173 // Fallback, not a smart server 174 return false 175 } 176 return true 177 } 178 // Non-HTTP protocols always support shallow clones 179 return true 180 } 181 182 func (repo gitRepo) checkout(root string) (string, error) { 183 // Try checking out by ref name first. This will work on branches and sets 184 // .git/HEAD to the current branch name 185 if output, err := repo.gitWithinDir(root, "checkout", repo.ref); err != nil { 186 // If checking out by branch name fails check out the last fetched ref 187 if _, err2 := repo.gitWithinDir(root, "checkout", "FETCH_HEAD"); err2 != nil { 188 return "", errors.Wrapf(err, "error checking out %s: %s", repo.ref, output) 189 } 190 } 191 192 if repo.subdir != "" { 193 newCtx, err := symlink.FollowSymlinkInScope(filepath.Join(root, repo.subdir), root) 194 if err != nil { 195 return "", errors.Wrapf(err, "error setting git context, %q not within git root", repo.subdir) 196 } 197 198 fi, err := os.Stat(newCtx) 199 if err != nil { 200 return "", err 201 } 202 if !fi.IsDir() { 203 return "", errors.Errorf("error setting git context, not a directory: %s", newCtx) 204 } 205 root = newCtx 206 } 207 208 return root, nil 209 } 210 211 func (repo gitRepo) gitWithinDir(dir string, args ...string) ([]byte, error) { 212 args = append([]string{"-c", "protocol.file.allow=never"}, args...) // Block sneaky repositories from using repos from the filesystem as submodules. 213 cmd := exec.Command("git", args...) 214 cmd.Dir = dir 215 // Disable unsafe remote protocols. 216 cmd.Env = append(os.Environ(), "GIT_PROTOCOL_FROM_USER=0") 217 218 if repo.isolateConfig { 219 cmd.Env = append(cmd.Env, 220 "GIT_CONFIG_NOSYSTEM=1", // Disable reading from system gitconfig. 221 "HOME=/dev/null", // Disable reading from user gitconfig. 222 ) 223 } 224 225 return cmd.CombinedOutput() 226 } 227 228 // isGitTransport returns true if the provided str is a git transport by inspecting 229 // the prefix of the string for known protocols used in git. 230 func isGitTransport(str string) bool { 231 if strings.HasPrefix(str, "git@") { 232 return true 233 } 234 235 switch getScheme(str) { 236 case "git", "http", "https", "ssh": 237 return true 238 } 239 240 return false 241 } 242 243 // getScheme returns addresses' scheme in lowercase, or an empty 244 // string in case address is an invalid URL. 245 func getScheme(address string) string { 246 u, err := url.Parse(address) 247 if err != nil { 248 return "" 249 } 250 return u.Scheme 251 }