gitutils_test.go 11 KB

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