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