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