github.com/rawahars/moby@v24.0.4+incompatible/builder/remotecontext/git/gitutils_test.go (about)

     1  package git // import "github.com/docker/docker/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/docker/docker", "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"), 0644))
   243  
   244  	subDir := filepath.Join(gitDir, "subdir")
   245  	assert.NilError(t, os.Mkdir(subDir, 0755))
   246  	assert.NilError(t, os.WriteFile(filepath.Join(subDir, "Dockerfile"), []byte("FROM scratch\nEXPOSE 5000"), 0644))
   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"), 0644))
   258  	assert.NilError(t, os.WriteFile(filepath.Join(subDir, "Dockerfile"), []byte("FROM busybox\nEXPOSE 5000"), 0644))
   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"), 0644))
   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/docker/docker",
   342  		"git@github.com:docker/docker.git",
   343  		"git@bitbucket.org:atlassianlabs/atlassian-docker.git",
   344  		"https://github.com/docker/docker.git",
   345  		"http://github.com/docker/docker.git",
   346  		"http://github.com/docker/docker.git#branch",
   347  		"http://github.com/docker/docker.git#:dir",
   348  	}
   349  	incompleteGitUrls := []string{
   350  		"github.com/docker/docker",
   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  }