github.com/Prakhar-Agarwal-byte/moby@v0.0.0-20231027092010-a14e3e8ab87e/builder/remotecontext/git/gitutils_test.go (about) 1 package git // import "github.com/Prakhar-Agarwal-byte/moby/builder/remotecontext/git" 2 3 import ( 4 "bytes" 5 "fmt" 6 "net/http" 7 "net/http/cgi" 8 "net/http/httptest" 9 "net/url" 10 "os" 11 "os/exec" 12 "path/filepath" 13 "runtime" 14 "strings" 15 "testing" 16 17 "github.com/google/go-cmp/cmp" 18 "gotest.tools/v3/assert" 19 is "gotest.tools/v3/assert/cmp" 20 ) 21 22 func TestParseRemoteURL(t *testing.T) { 23 tests := []struct { 24 doc string 25 url string 26 expected gitRepo 27 }{ 28 { 29 doc: "git scheme uppercase, no url-fragment", 30 url: "GIT://github.com/user/repo.git", 31 expected: gitRepo{ 32 remote: "git://github.com/user/repo.git", 33 ref: "master", 34 }, 35 }, 36 { 37 doc: "git scheme, no url-fragment", 38 url: "git://github.com/user/repo.git", 39 expected: gitRepo{ 40 remote: "git://github.com/user/repo.git", 41 ref: "master", 42 }, 43 }, 44 { 45 doc: "git scheme, with url-fragment", 46 url: "git://github.com/user/repo.git#mybranch:mydir/mysubdir/", 47 expected: gitRepo{ 48 remote: "git://github.com/user/repo.git", 49 ref: "mybranch", 50 subdir: "mydir/mysubdir/", 51 }, 52 }, 53 { 54 doc: "https scheme, no url-fragment", 55 url: "https://github.com/user/repo.git", 56 expected: gitRepo{ 57 remote: "https://github.com/user/repo.git", 58 ref: "master", 59 }, 60 }, 61 { 62 doc: "https scheme, with url-fragment", 63 url: "https://github.com/user/repo.git#mybranch:mydir/mysubdir/", 64 expected: gitRepo{ 65 remote: "https://github.com/user/repo.git", 66 ref: "mybranch", 67 subdir: "mydir/mysubdir/", 68 }, 69 }, 70 { 71 doc: "git@, no url-fragment", 72 url: "git@github.com:user/repo.git", 73 expected: gitRepo{ 74 remote: "git@github.com:user/repo.git", 75 ref: "master", 76 }, 77 }, 78 { 79 doc: "git@, with url-fragment", 80 url: "git@github.com:user/repo.git#mybranch:mydir/mysubdir/", 81 expected: gitRepo{ 82 remote: "git@github.com:user/repo.git", 83 ref: "mybranch", 84 subdir: "mydir/mysubdir/", 85 }, 86 }, 87 { 88 doc: "ssh, no url-fragment", 89 url: "ssh://github.com/user/repo.git", 90 expected: gitRepo{ 91 remote: "ssh://github.com/user/repo.git", 92 ref: "master", 93 }, 94 }, 95 { 96 doc: "ssh, with url-fragment", 97 url: "ssh://github.com/user/repo.git#mybranch:mydir/mysubdir/", 98 expected: gitRepo{ 99 remote: "ssh://github.com/user/repo.git", 100 ref: "mybranch", 101 subdir: "mydir/mysubdir/", 102 }, 103 }, 104 { 105 doc: "ssh, with url-fragment and user", 106 url: "ssh://foo%40barcorp.com@github.com/user/repo.git#mybranch:mydir/mysubdir/", 107 expected: gitRepo{ 108 remote: "ssh://foo%40barcorp.com@github.com/user/repo.git", 109 ref: "mybranch", 110 subdir: "mydir/mysubdir/", 111 }, 112 }, 113 } 114 115 for _, tc := range tests { 116 tc := tc 117 t.Run(tc.doc, func(t *testing.T) { 118 repo, err := parseRemoteURL(tc.url) 119 assert.NilError(t, err) 120 assert.Check(t, is.DeepEqual(tc.expected, repo, cmp.AllowUnexported(gitRepo{}))) 121 }) 122 } 123 } 124 125 func TestCloneArgsSmartHttp(t *testing.T) { 126 mux := http.NewServeMux() 127 server := httptest.NewServer(mux) 128 serverURL, _ := url.Parse(server.URL) 129 130 serverURL.Path = "/repo.git" 131 132 mux.HandleFunc("/repo.git/info/refs", func(w http.ResponseWriter, r *http.Request) { 133 q := r.URL.Query().Get("service") 134 w.Header().Set("Content-Type", fmt.Sprintf("application/x-%s-advertisement", q)) 135 }) 136 137 args := fetchArgs(serverURL.String(), "master") 138 exp := []string{"fetch", "--depth", "1", "origin", "--", "master"} 139 assert.Check(t, is.DeepEqual(exp, args)) 140 } 141 142 func TestCloneArgsDumbHttp(t *testing.T) { 143 mux := http.NewServeMux() 144 server := httptest.NewServer(mux) 145 serverURL, _ := url.Parse(server.URL) 146 147 serverURL.Path = "/repo.git" 148 149 mux.HandleFunc("/repo.git/info/refs", func(w http.ResponseWriter, r *http.Request) { 150 w.Header().Set("Content-Type", "text/plain") 151 }) 152 153 args := fetchArgs(serverURL.String(), "master") 154 exp := []string{"fetch", "origin", "--", "master"} 155 assert.Check(t, is.DeepEqual(exp, args)) 156 } 157 158 func TestCloneArgsGit(t *testing.T) { 159 args := fetchArgs("git://github.com/Prakhar-Agarwal-byte/moby", "master") 160 exp := []string{"fetch", "--depth", "1", "origin", "--", "master"} 161 assert.Check(t, is.DeepEqual(exp, args)) 162 } 163 164 func gitGetConfig(name string) string { 165 b, err := gitRepo{}.gitWithinDir("", "config", "--get", name) 166 if err != nil { 167 // since we are interested in empty or non empty string, 168 // we can safely ignore the err here. 169 return "" 170 } 171 return strings.TrimSpace(string(b)) 172 } 173 174 func TestCheckoutGit(t *testing.T) { 175 root := t.TempDir() 176 177 gitpath, err := exec.LookPath("git") 178 assert.NilError(t, err) 179 gitversion, _ := exec.Command(gitpath, "version").CombinedOutput() 180 t.Logf("%s", gitversion) // E.g. "git version 2.30.2" 181 182 // Serve all repositories under root using the Smart HTTP protocol so 183 // they can be cloned. The Dumb HTTP protocol is incompatible with 184 // shallow cloning but we unconditionally shallow-clone submodules, and 185 // we explicitly disable the file protocol. 186 // (Another option would be to use `git daemon` and the Git protocol, 187 // but that listens on a fixed port number which is a recipe for 188 // disaster in CI. Funnily enough, `git daemon --port=0` works but there 189 // is no easy way to discover which port got picked!) 190 191 // Associate git-http-backend logs with the current (sub)test. 192 // Incompatible with parallel subtests. 193 currentSubtest := t 194 githttp := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 195 var logs bytes.Buffer 196 (&cgi.Handler{ 197 Path: gitpath, 198 Args: []string{"http-backend"}, 199 Dir: root, 200 Env: []string{ 201 "GIT_PROJECT_ROOT=" + root, 202 "GIT_HTTP_EXPORT_ALL=1", 203 }, 204 Stderr: &logs, 205 }).ServeHTTP(w, r) 206 if logs.Len() == 0 { 207 return 208 } 209 for { 210 line, err := logs.ReadString('\n') 211 currentSubtest.Log("git-http-backend: " + line) 212 if err != nil { 213 break 214 } 215 } 216 }) 217 server := httptest.NewServer(&githttp) 218 defer server.Close() 219 220 autocrlf := gitGetConfig("core.autocrlf") 221 if !(autocrlf == "true" || autocrlf == "false" || 222 autocrlf == "input" || autocrlf == "") { 223 t.Logf("unknown core.autocrlf value: \"%s\"", autocrlf) 224 } 225 eol := "\n" 226 if autocrlf == "true" { 227 eol = "\r\n" 228 } 229 230 must := func(out []byte, err error) { 231 t.Helper() 232 if len(out) > 0 { 233 t.Logf("%s", out) 234 } 235 assert.NilError(t, err) 236 } 237 238 gitDir := filepath.Join(root, "repo") 239 must(gitRepo{}.gitWithinDir(root, "-c", "init.defaultBranch=master", "init", gitDir)) 240 must(gitRepo{}.gitWithinDir(gitDir, "config", "user.email", "test@docker.com")) 241 must(gitRepo{}.gitWithinDir(gitDir, "config", "user.name", "Docker test")) 242 assert.NilError(t, os.WriteFile(filepath.Join(gitDir, "Dockerfile"), []byte("FROM scratch"), 0o644)) 243 244 subDir := filepath.Join(gitDir, "subdir") 245 assert.NilError(t, os.Mkdir(subDir, 0o755)) 246 assert.NilError(t, os.WriteFile(filepath.Join(subDir, "Dockerfile"), []byte("FROM scratch\nEXPOSE 5000"), 0o644)) 247 248 if runtime.GOOS != "windows" { 249 assert.NilError(t, os.Symlink("../subdir", filepath.Join(gitDir, "parentlink"))) 250 assert.NilError(t, os.Symlink("/subdir", filepath.Join(gitDir, "absolutelink"))) 251 } 252 253 must(gitRepo{}.gitWithinDir(gitDir, "add", "-A")) 254 must(gitRepo{}.gitWithinDir(gitDir, "commit", "-am", "First commit")) 255 must(gitRepo{}.gitWithinDir(gitDir, "checkout", "-b", "test")) 256 257 assert.NilError(t, os.WriteFile(filepath.Join(gitDir, "Dockerfile"), []byte("FROM scratch\nEXPOSE 3000"), 0o644)) 258 assert.NilError(t, os.WriteFile(filepath.Join(subDir, "Dockerfile"), []byte("FROM busybox\nEXPOSE 5000"), 0o644)) 259 260 must(gitRepo{}.gitWithinDir(gitDir, "add", "-A")) 261 must(gitRepo{}.gitWithinDir(gitDir, "commit", "-am", "Branch commit")) 262 must(gitRepo{}.gitWithinDir(gitDir, "checkout", "master")) 263 264 // set up submodule 265 subrepoDir := filepath.Join(root, "subrepo") 266 must(gitRepo{}.gitWithinDir(root, "-c", "init.defaultBranch=master", "init", subrepoDir)) 267 must(gitRepo{}.gitWithinDir(subrepoDir, "config", "user.email", "test@docker.com")) 268 must(gitRepo{}.gitWithinDir(subrepoDir, "config", "user.name", "Docker test")) 269 270 assert.NilError(t, os.WriteFile(filepath.Join(subrepoDir, "subfile"), []byte("subcontents"), 0o644)) 271 272 must(gitRepo{}.gitWithinDir(subrepoDir, "add", "-A")) 273 must(gitRepo{}.gitWithinDir(subrepoDir, "commit", "-am", "Subrepo initial")) 274 275 must(gitRepo{}.gitWithinDir(gitDir, "submodule", "add", server.URL+"/subrepo", "sub")) 276 must(gitRepo{}.gitWithinDir(gitDir, "add", "-A")) 277 must(gitRepo{}.gitWithinDir(gitDir, "commit", "-am", "With submodule")) 278 279 type singleCase struct { 280 frag string 281 exp string 282 fail bool 283 submodule bool 284 } 285 286 cases := []singleCase{ 287 {"", "FROM scratch", false, true}, 288 {"master", "FROM scratch", false, true}, 289 {":subdir", "FROM scratch" + eol + "EXPOSE 5000", false, false}, 290 {":nosubdir", "", true, false}, // missing directory error 291 {":Dockerfile", "", true, false}, // not a directory error 292 {"master:nosubdir", "", true, false}, 293 {"master:subdir", "FROM scratch" + eol + "EXPOSE 5000", false, false}, 294 {"master:../subdir", "", true, false}, 295 {"test", "FROM scratch" + eol + "EXPOSE 3000", false, false}, 296 {"test:", "FROM scratch" + eol + "EXPOSE 3000", false, false}, 297 {"test:subdir", "FROM busybox" + eol + "EXPOSE 5000", false, false}, 298 } 299 300 if runtime.GOOS != "windows" { 301 // Windows GIT (2.7.1 x64) does not support parentlink/absolutelink. Sample output below 302 // git --work-tree .\repo --git-dir .\repo\.git add -A 303 // error: readlink("absolutelink"): Function not implemented 304 // error: unable to index file absolutelink 305 // fatal: adding files failed 306 cases = append(cases, singleCase{frag: "master:absolutelink", exp: "FROM scratch" + eol + "EXPOSE 5000", fail: false}) 307 cases = append(cases, singleCase{frag: "master:parentlink", exp: "FROM scratch" + eol + "EXPOSE 5000", fail: false}) 308 } 309 310 for _, c := range cases { 311 t.Run(c.frag, func(t *testing.T) { 312 currentSubtest = t 313 ref, subdir := getRefAndSubdir(c.frag) 314 r, err := gitRepo{remote: server.URL + "/repo", ref: ref, subdir: subdir}.clone() 315 316 if c.fail { 317 assert.Check(t, is.ErrorContains(err, "")) 318 return 319 } 320 assert.NilError(t, err) 321 defer os.RemoveAll(r) 322 if c.submodule { 323 b, err := os.ReadFile(filepath.Join(r, "sub/subfile")) 324 assert.NilError(t, err) 325 assert.Check(t, is.Equal("subcontents", string(b))) 326 } else { 327 _, err := os.Stat(filepath.Join(r, "sub/subfile")) 328 assert.Assert(t, is.ErrorContains(err, "")) 329 assert.Assert(t, os.IsNotExist(err)) 330 } 331 332 b, err := os.ReadFile(filepath.Join(r, "Dockerfile")) 333 assert.NilError(t, err) 334 assert.Check(t, is.Equal(c.exp, string(b))) 335 }) 336 } 337 } 338 339 func TestValidGitTransport(t *testing.T) { 340 gitUrls := []string{ 341 "git://github.com/Prakhar-Agarwal-byte/moby", 342 "git@github.com:docker/docker.git", 343 "git@bitbucket.org:atlassianlabs/atlassian-docker.git", 344 "https://github.com/Prakhar-Agarwal-byte/moby.git", 345 "http://github.com/Prakhar-Agarwal-byte/moby.git", 346 "http://github.com/Prakhar-Agarwal-byte/moby.git#branch", 347 "http://github.com/Prakhar-Agarwal-byte/moby.git#:dir", 348 } 349 incompleteGitUrls := []string{ 350 "github.com/Prakhar-Agarwal-byte/moby", 351 } 352 353 for _, url := range gitUrls { 354 if !isGitTransport(url) { 355 t.Fatalf("%q should be detected as valid Git prefix", url) 356 } 357 } 358 359 for _, url := range incompleteGitUrls { 360 if isGitTransport(url) { 361 t.Fatalf("%q should not be detected as valid Git prefix", url) 362 } 363 } 364 } 365 366 func TestGitInvalidRef(t *testing.T) { 367 gitUrls := []string{ 368 "git://github.com/moby/moby#--foo bar", 369 "git@github.com/moby/moby#--upload-pack=sleep;:", 370 "git@g.com:a/b.git#-B", 371 "git@g.com:a/b.git#with space", 372 } 373 374 for _, url := range gitUrls { 375 _, err := Clone(url) 376 assert.Assert(t, err != nil) 377 assert.Check(t, is.Contains(strings.ToLower(err.Error()), "invalid refspec")) 378 } 379 }