github.com/docker/engine@v22.0.0-20211208180946-d456264580cf+incompatible/builder/remotecontext/git/gitutils_test.go (about)

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