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