github.com/wmuizelaar/kpt@v0.0.0-20221018115725-bd564717b2ed/internal/testutil/testutil.go (about)

     1  // Copyright 2019 Google LLC
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package testutil
    16  
    17  import (
    18  	"bytes"
    19  	"fmt"
    20  	"os"
    21  	"os/exec"
    22  	"path/filepath"
    23  	"reflect"
    24  	"runtime"
    25  	"strings"
    26  	"testing"
    27  
    28  	"github.com/GoogleContainerTools/kpt/internal/gitutil"
    29  	"github.com/GoogleContainerTools/kpt/internal/pkg"
    30  	"github.com/GoogleContainerTools/kpt/internal/printer/fake"
    31  	"github.com/GoogleContainerTools/kpt/internal/util/addmergecomment"
    32  	"github.com/GoogleContainerTools/kpt/internal/util/git"
    33  	kptfilev1 "github.com/GoogleContainerTools/kpt/pkg/api/kptfile/v1"
    34  	"github.com/GoogleContainerTools/kpt/pkg/kptfile/kptfileutil"
    35  	toposort "github.com/philopon/go-toposort"
    36  	"github.com/stretchr/testify/assert"
    37  	assertnow "gotest.tools/assert"
    38  	"sigs.k8s.io/kustomize/kyaml/copyutil"
    39  	"sigs.k8s.io/kustomize/kyaml/errors"
    40  	"sigs.k8s.io/kustomize/kyaml/filesys"
    41  	"sigs.k8s.io/kustomize/kyaml/sets"
    42  	"sigs.k8s.io/kustomize/kyaml/yaml"
    43  )
    44  
    45  const TmpDirPrefix = "test-kpt"
    46  
    47  const (
    48  	Dataset1            = "dataset1"
    49  	Dataset2            = "dataset2"
    50  	Dataset3            = "dataset3"
    51  	Dataset4            = "dataset4" // Dataset4 is replica of Dataset2 with different setter values
    52  	Dataset5            = "dataset5" // Dataset5 is replica of Dataset2 with additional non KRM files
    53  	Dataset6            = "dataset6" // Dataset6 contains symlinks
    54  	DatasetMerged       = "datasetmerged"
    55  	DiffOutput          = "diff_output"
    56  	UpdateMergeConflict = "updateMergeConflict"
    57  )
    58  
    59  // TestGitRepo manages a local git repository for testing
    60  type TestGitRepo struct {
    61  	T *testing.T
    62  
    63  	// RepoDirectory is the temp directory of the git repo
    64  	RepoDirectory string
    65  
    66  	// DatasetDirectory is the directory of the testdata files
    67  	DatasetDirectory string
    68  
    69  	// RepoName is the name of the repository
    70  	RepoName string
    71  
    72  	// Commits keeps track of the commit shas for the changes
    73  	// to the repo.
    74  	Commits []string
    75  }
    76  
    77  var AssertNoError = assertnow.NilError
    78  
    79  var KptfileSet = diffSet(kptfilev1.KptFileName)
    80  
    81  func diffSet(path string) sets.String {
    82  	s := sets.String{}
    83  	s.Insert(path)
    84  	return s
    85  }
    86  
    87  // AssertEqual verifies the contents of a source package matches the contents of the
    88  // destination package it was fetched to.
    89  // Excludes comparing the .git directory in the source package.
    90  //
    91  // While the sourceDir can be the TestGitRepo, because tests change the TestGitRepo
    92  // may have been changed after the destDir was copied, it is often better to explicitly
    93  // use a set of golden files as the sourceDir rather than the original TestGitRepo
    94  // that was copied.
    95  func (g *TestGitRepo) AssertEqual(t *testing.T, sourceDir, destDir string, addMergeCommentsToSource bool) bool {
    96  	diff, err := Diff(sourceDir, destDir, addMergeCommentsToSource)
    97  	if !assert.NoError(t, err) {
    98  		return false
    99  	}
   100  	diff = diff.Difference(KptfileSet)
   101  	return assert.Empty(t, diff.List())
   102  }
   103  
   104  // KptfileAwarePkgEqual compares two packages (including any subpackages)
   105  // and has special handling of Kptfiles to handle fields that contain
   106  // values which cannot easily be specified in the golden package.
   107  func KptfileAwarePkgEqual(t *testing.T, pkg1, pkg2 string, addMergeCommentsToSource bool) bool {
   108  	diff, err := Diff(pkg1, pkg2, addMergeCommentsToSource)
   109  	if !assert.NoError(t, err) {
   110  		return false
   111  	}
   112  
   113  	// TODO(mortent): See if we can avoid this. We just need to make sure
   114  	// we can compare Kptfiles without any formatting issues.
   115  	for _, s := range diff.List() {
   116  		if !strings.HasSuffix(s, kptfilev1.KptFileName) {
   117  			continue
   118  		}
   119  
   120  		pkg1Path := filepath.Join(pkg1, s)
   121  		pkg1KfExists := kptfileExists(t, pkg1Path)
   122  
   123  		pkg2Path := filepath.Join(pkg2, s)
   124  		pkg2KfExists := kptfileExists(t, pkg2Path)
   125  
   126  		if !pkg1KfExists || !pkg2KfExists {
   127  			continue
   128  		}
   129  
   130  		// Read the Kptfiles and set the Commit field to an empty
   131  		// string before we compare.
   132  		pkg1kf, err := pkg.ReadKptfile(filesys.FileSystemOrOnDisk{}, filepath.Dir(pkg1Path))
   133  		if !assert.NoError(t, err) {
   134  			t.FailNow()
   135  		}
   136  		pkg2kf, err := pkg.ReadKptfile(filesys.FileSystemOrOnDisk{}, filepath.Dir(pkg2Path))
   137  		if !assert.NoError(t, err) {
   138  			t.FailNow()
   139  		}
   140  
   141  		equal, err := kptfileutil.Equal(pkg1kf, pkg2kf)
   142  		if !assert.NoError(t, err) {
   143  			t.FailNow()
   144  		}
   145  		// If the two files are considered equal after we have compared
   146  		// them with Kptfile-specific rules, we remove the path from the
   147  		// diff set.
   148  		if equal {
   149  			diff = diff.Difference(diffSet(s))
   150  		}
   151  	}
   152  	return assert.Empty(t, diff.List())
   153  }
   154  
   155  func kptfileExists(t *testing.T, path string) bool {
   156  	_, err := os.Stat(path)
   157  	if err != nil && !os.IsNotExist(err) {
   158  		assert.NoError(t, err)
   159  		t.FailNow()
   160  	}
   161  	return !os.IsNotExist(err)
   162  }
   163  
   164  // Diff returns a list of files that differ between the source and destination.
   165  //
   166  // Diff is guaranteed to return a non-empty set if any files differ, but
   167  // this set is not guaranteed to contain all differing files.
   168  func Diff(sourceDir, destDir string, addMergeCommentsToSource bool) (sets.String, error) {
   169  	// get set of filenames in the package source
   170  	var newSourceDir string
   171  	if addMergeCommentsToSource {
   172  		dir, clean, err := addmergecomment.ProcessWithCleanup(sourceDir)
   173  		defer clean()
   174  		if err != nil {
   175  			return sets.String{}, err
   176  		}
   177  		newSourceDir = dir
   178  	}
   179  	if newSourceDir != "" {
   180  		sourceDir = newSourceDir
   181  	}
   182  	upstreamFiles := sets.String{}
   183  	err := filepath.Walk(sourceDir, func(path string, info os.FileInfo, err error) error {
   184  		if err != nil {
   185  			return err
   186  		}
   187  
   188  		// skip git repo if it exists
   189  		if strings.Contains(path, ".git") {
   190  			return nil
   191  		}
   192  
   193  		upstreamFiles.Insert(strings.TrimPrefix(strings.TrimPrefix(path, sourceDir), string(filepath.Separator)))
   194  		return nil
   195  	})
   196  	if err != nil {
   197  		return sets.String{}, err
   198  	}
   199  
   200  	// get set of filenames in the cloned package
   201  	localFiles := sets.String{}
   202  	err = filepath.Walk(destDir, func(path string, info os.FileInfo, err error) error {
   203  		if err != nil {
   204  			return err
   205  		}
   206  
   207  		// skip git repo if it exists
   208  		if strings.Contains(path, ".git") {
   209  			return nil
   210  		}
   211  
   212  		localFiles.Insert(strings.TrimPrefix(strings.TrimPrefix(path, destDir), string(filepath.Separator)))
   213  		return nil
   214  	})
   215  	if err != nil {
   216  		return sets.String{}, err
   217  	}
   218  
   219  	// verify the source and cloned packages have the same set of filenames
   220  	diff := upstreamFiles.SymmetricDifference(localFiles)
   221  
   222  	// verify file contents match
   223  	for _, f := range upstreamFiles.Intersection(localFiles).List() {
   224  		fi, err := os.Stat(filepath.Join(destDir, f))
   225  		if err != nil {
   226  			return diff, err
   227  		}
   228  		if fi.Mode().IsDir() {
   229  			// already checked that this directory exists in the local files
   230  			continue
   231  		}
   232  
   233  		// compare upstreamFiles
   234  		b1, err := os.ReadFile(filepath.Join(destDir, f))
   235  		if err != nil {
   236  			return diff, err
   237  		}
   238  		b2, err := os.ReadFile(filepath.Join(sourceDir, f))
   239  		if err != nil {
   240  			return diff, err
   241  		}
   242  
   243  		s1 := strings.TrimSpace(strings.TrimPrefix(string(b1), trimPrefix))
   244  		s2 := strings.TrimSpace(strings.TrimPrefix(string(b2), trimPrefix))
   245  		if s1 != s2 {
   246  			fmt.Println(copyutil.PrettyFileDiff(s1, s2))
   247  			diff.Insert(f)
   248  		}
   249  	}
   250  	// return the differing files
   251  	return diff, nil
   252  }
   253  
   254  const trimPrefix = `# Copyright 2019 Google LLC
   255  #
   256  # Licensed under the Apache License, Version 2.0 (the "License");
   257  # you may not use this file except in compliance with the License.
   258  # You may obtain a copy of the License at
   259  #
   260  #      http://www.apache.org/licenses/LICENSE-2.0
   261  #
   262  # Unless required by applicable law or agreed to in writing, software
   263  # distributed under the License is distributed on an "AS IS" BASIS,
   264  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   265  # See the License for the specific language governing permissions and
   266  # limitations under the License.`
   267  
   268  // AssertKptfile verifies the contents of the KptFile matches the provided value.
   269  func (g *TestGitRepo) AssertKptfile(t *testing.T, cloned string, kpkg kptfilev1.KptFile) bool {
   270  	// read the actual generated KptFile
   271  	b, err := os.ReadFile(filepath.Join(cloned, kptfilev1.KptFileName))
   272  	if !assert.NoError(t, err) {
   273  		return false
   274  	}
   275  	var res bytes.Buffer
   276  	d := yaml.NewEncoder(&res)
   277  	if !assert.NoError(t, d.Encode(kpkg)) {
   278  		return false
   279  	}
   280  	return assert.Equal(t, res.String(), string(b))
   281  }
   282  
   283  // CheckoutBranch checks out the git branch in the repo
   284  func (g *TestGitRepo) CheckoutBranch(branch string, create bool) error {
   285  	return checkoutBranch(g.RepoDirectory, branch, create)
   286  }
   287  
   288  // DeleteBranch deletes the git branch in the repo
   289  func (g *TestGitRepo) DeleteBranch(branch string) error {
   290  	// checkout the branch
   291  	cmd := exec.Command("git", []string{"branch", "-D", branch}...)
   292  	cmd.Dir = g.RepoDirectory
   293  	_, err := cmd.Output()
   294  	if err != nil {
   295  		return err
   296  	}
   297  
   298  	return nil
   299  }
   300  
   301  // Commit performs a git commit and returns the SHA for the newly
   302  // created commit.
   303  func (g *TestGitRepo) Commit(message string) (string, error) {
   304  	return commit(g.RepoDirectory, message)
   305  }
   306  
   307  func (g *TestGitRepo) GetCommit() (string, error) {
   308  	cmd := exec.Command("git", "rev-parse", "--verify", "HEAD")
   309  	cmd.Dir = g.RepoDirectory
   310  	b, err := cmd.Output()
   311  	if err != nil {
   312  		return "", err
   313  	}
   314  	return strings.TrimSpace(string(b)), nil
   315  }
   316  
   317  // RemoveAll deletes the test git repo
   318  func (g *TestGitRepo) RemoveAll() error {
   319  	err := os.RemoveAll(g.RepoDirectory)
   320  	return err
   321  }
   322  
   323  // ReplaceData replaces the data with a new source
   324  func (g *TestGitRepo) ReplaceData(data string) error {
   325  	if !filepath.IsAbs(data) {
   326  		data = filepath.Join(g.DatasetDirectory, data)
   327  	}
   328  
   329  	return replaceData(g.RepoDirectory, data)
   330  }
   331  
   332  // CustomUpdate executes the provided update function and passes in the
   333  // path to the directory of the repository.
   334  func (g *TestGitRepo) CustomUpdate(f func(string) error) error {
   335  	return f(g.RepoDirectory)
   336  }
   337  
   338  // SetupTestGitRepo initializes a new git repository and populates it with data from a source
   339  func (g *TestGitRepo) SetupTestGitRepo(name string, data []Content, repos map[string]*TestGitRepo) error {
   340  	defaultBranch := "main"
   341  	if len(data) > 0 && len(data[0].Branch) > 0 {
   342  		defaultBranch = data[0].Branch
   343  	}
   344  
   345  	err := g.createEmptyGitRepo(defaultBranch)
   346  	if err != nil {
   347  		return err
   348  	}
   349  
   350  	// configure the path to the test dataset
   351  	ds, err := GetTestDataPath()
   352  	if err != nil {
   353  		return err
   354  	}
   355  	g.DatasetDirectory = ds
   356  
   357  	return UpdateGitDir(g.T, name, g, data, repos)
   358  }
   359  
   360  func (g *TestGitRepo) createEmptyGitRepo(defaultBranch string) error {
   361  	dir, err := os.MkdirTemp("", fmt.Sprintf("%s-upstream-", TmpDirPrefix))
   362  	if err != nil {
   363  		return err
   364  	}
   365  	g.RepoDirectory = dir
   366  	g.RepoName = filepath.Base(g.RepoDirectory)
   367  
   368  	cmd := exec.Command("git", "init",
   369  		fmt.Sprintf("--initial-branch=%s", defaultBranch))
   370  	cmd.Dir = dir
   371  	stdoutStderr, err := cmd.CombinedOutput()
   372  	if err != nil {
   373  		fmt.Fprintf(os.Stderr, "%s", stdoutStderr)
   374  		return err
   375  	}
   376  	_, err = g.Commit("initial commit")
   377  	return err
   378  }
   379  
   380  func GetTestDataPath() (string, error) {
   381  	filename, err := getTestUtilGoFilePath()
   382  	if err != nil {
   383  		return "", err
   384  	}
   385  	ds, err := filepath.Abs(filepath.Join(filepath.Dir(filename), "testdata"))
   386  	if err != nil {
   387  		return "", err
   388  	}
   389  	return ds, nil
   390  }
   391  
   392  func getTestUtilGoFilePath() (string, error) {
   393  	_, filename, _, ok := runtime.Caller(1)
   394  	if !ok {
   395  		return "", errors.Errorf("failed to testutil package location")
   396  	}
   397  	return filename, nil
   398  }
   399  
   400  // Tag initializes tags the git repository
   401  func (g *TestGitRepo) Tag(tagName string) error {
   402  	return tag(g.RepoDirectory, tagName)
   403  }
   404  
   405  // SetupRepoAndWorkspace handles setting up a default repo to clone, and a workspace to clone into.
   406  // returns a cleanup function to remove the git repo and workspace.
   407  func SetupRepoAndWorkspace(t *testing.T, content Content) (*TestGitRepo, *TestWorkspace, func()) {
   408  	repos, workspace, cleanup := SetupReposAndWorkspace(t, map[string][]Content{
   409  		Upstream: {
   410  			content,
   411  		},
   412  	})
   413  
   414  	g := repos[Upstream]
   415  	return g, workspace, cleanup
   416  }
   417  
   418  // SetupReposAndWorkspace handles setting up a set of repos as specified by
   419  // the reposContent and a workspace to clone into. It returns a cleanup function
   420  // that will remove the repos.
   421  func SetupReposAndWorkspace(t *testing.T, reposContent map[string][]Content) (map[string]*TestGitRepo, *TestWorkspace, func()) {
   422  	repos, repoCleanup := SetupRepos(t, reposContent)
   423  	w, workspaceCleanup := SetupWorkspace(t)
   424  	return repos, w, func() {
   425  		repoCleanup()
   426  		workspaceCleanup()
   427  	}
   428  }
   429  
   430  // SetupWorkspace creates a local workspace which kpt packages can be cloned
   431  // into. It returns a cleanup function that will remove the workspace.
   432  func SetupWorkspace(t *testing.T) (*TestWorkspace, func()) {
   433  	// setup the directory to clone to
   434  	w := &TestWorkspace{}
   435  	err := w.SetupTestWorkspace()
   436  	assert.NoError(t, err)
   437  
   438  	gr, err := gitutil.NewLocalGitRunner(w.WorkspaceDirectory)
   439  	if !assert.NoError(t, err) {
   440  		t.FailNow()
   441  	}
   442  
   443  	rr, err := gr.Run(fake.CtxWithDefaultPrinter(), "init")
   444  	if !assert.NoError(t, err) {
   445  		assert.FailNowf(t, "%s %s", rr.Stdout, rr.Stderr)
   446  	}
   447  	return w, func() {
   448  		_ = w.RemoveAll()
   449  	}
   450  }
   451  
   452  // AddKptfileToWorkspace writes the provided Kptfile to the workspace
   453  // and makes a commit.
   454  func AddKptfileToWorkspace(t *testing.T, w *TestWorkspace, kf *kptfilev1.KptFile) {
   455  	err := os.MkdirAll(w.FullPackagePath(), 0700)
   456  	if !assert.NoError(t, err) {
   457  		t.FailNow()
   458  	}
   459  
   460  	err = kptfileutil.WriteFile(w.FullPackagePath(), kf)
   461  	if !assert.NoError(t, err) {
   462  		t.FailNow()
   463  	}
   464  
   465  	gitRunner, err := gitutil.NewLocalGitRunner(w.WorkspaceDirectory)
   466  	if !assert.NoError(t, err) {
   467  		t.FailNow()
   468  	}
   469  	_, err = gitRunner.Run(fake.CtxWithDefaultPrinter(), "add", ".")
   470  	if !assert.NoError(t, err) {
   471  		t.FailNow()
   472  	}
   473  	_, err = w.Commit("added Kptfile")
   474  	if !assert.NoError(t, err) {
   475  		t.FailNow()
   476  	}
   477  }
   478  
   479  // SetupRepos creates repos and returns a mapping from name to TestGitRepos.
   480  // This only creates the first version of each repo as given by the first item
   481  // in the repoContent slice.
   482  func SetupRepos(t *testing.T, repoContent map[string][]Content) (map[string]*TestGitRepo, func()) {
   483  	repos := make(map[string]*TestGitRepo)
   484  
   485  	cleanupFunc := func() {
   486  		for _, rp := range repos {
   487  			_ = os.RemoveAll(rp.RepoDirectory)
   488  		}
   489  	}
   490  
   491  	ordering, err := findRepoOrdering(repoContent)
   492  	if !assert.NoError(t, err) {
   493  		t.FailNow()
   494  	}
   495  
   496  	for _, name := range ordering {
   497  		data := repoContent[name]
   498  		if len(data) < 1 {
   499  			continue
   500  		}
   501  		tgr := &TestGitRepo{T: t}
   502  		repos[name] = tgr
   503  		if err := tgr.SetupTestGitRepo(name, data[:1], repos); err != nil {
   504  			return repos, cleanupFunc
   505  		}
   506  	}
   507  	return repos, cleanupFunc
   508  }
   509  
   510  // UpdateRepos updates the existing repos with any additional Content
   511  // items in the repoContent slice.
   512  func UpdateRepos(t *testing.T, repos map[string]*TestGitRepo, repoContent map[string][]Content) error {
   513  	ordering, err := findRepoOrdering(repoContent)
   514  	if !assert.NoError(t, err) {
   515  		t.FailNow()
   516  	}
   517  
   518  	for _, name := range ordering {
   519  		data := repoContent[name]
   520  		if len(data) < 1 {
   521  			continue
   522  		}
   523  
   524  		r := repos[name]
   525  		err := UpdateGitDir(t, name, r, data[1:], repos)
   526  		if err != nil {
   527  			return err
   528  		}
   529  	}
   530  	return nil
   531  }
   532  
   533  // findRepoOrdering orders the repos based on their dependencies. So if repo
   534  // A includes repo B as a subpackage, we can create repo B before we create
   535  // repo B. This is done with a topological sort. If there are any circular
   536  // dependencies between the repos, it will return an error.
   537  func findRepoOrdering(repoContent map[string][]Content) ([]string, error) {
   538  	var repoNames []string
   539  	for n := range repoContent {
   540  		repoNames = append(repoNames, n)
   541  	}
   542  
   543  	topo := toposort.NewGraph(len(repoNames))
   544  	topo.AddNodes(repoNames...)
   545  	// Keep track of which edges have been added to topo. The library doesn't
   546  	// handle the same edge added multiple times.
   547  	added := make(map[string]string)
   548  	for n, contents := range repoContent {
   549  		for _, c := range contents {
   550  			if c.Pkg == nil {
   551  				continue
   552  			}
   553  			pkg := c.Pkg
   554  			refRepos := pkg.AllReferencedRepos()
   555  			for _, refRepo := range refRepos {
   556  				if v, ok := added[refRepo]; ok && v == n {
   557  					continue
   558  				}
   559  				topo.AddEdge(refRepo, n)
   560  				added[refRepo] = n
   561  			}
   562  		}
   563  	}
   564  	ordering, ok := topo.Toposort()
   565  	if !ok {
   566  		return nil, fmt.Errorf("unable to sort repo references. Cycles are not allowed")
   567  	}
   568  	return ordering, nil
   569  }
   570  
   571  func checkoutBranch(repo string, branch string, create bool) error {
   572  	var args []string
   573  	if create {
   574  		args = []string{"checkout", "-b", branch}
   575  	} else {
   576  		args = []string{"checkout", branch}
   577  	}
   578  
   579  	// checkout the branch
   580  	cmd := exec.Command("git", args...)
   581  	cmd.Dir = repo
   582  	_, err := cmd.CombinedOutput()
   583  	if err != nil {
   584  		return err
   585  	}
   586  
   587  	return nil
   588  }
   589  
   590  // nolint:gocyclo
   591  func replaceData(repo, data string) error {
   592  	// If the path is absolute we assume it is the full path to the
   593  	// testdata. If it is relative, we assume it refers to one of the
   594  	// test data sets.
   595  	if !filepath.IsAbs(data) {
   596  		ds, err := GetTestDataPath()
   597  		if err != nil {
   598  			return err
   599  		}
   600  		data = filepath.Join(ds, data)
   601  	}
   602  	// Walk the data directory and copy over all files. We have special
   603  	// handling of the Kptfile to make sure we don't lose the Upstream data.
   604  	if err := filepath.Walk(data, func(path string, info os.FileInfo, err error) error {
   605  		if err != nil {
   606  			return err
   607  		}
   608  		rel, err := filepath.Rel(data, path)
   609  		if err != nil {
   610  			return err
   611  		}
   612  
   613  		_, err = os.Stat(filepath.Join(repo, rel))
   614  		if err != nil && !os.IsNotExist(err) {
   615  			return err
   616  		}
   617  
   618  		// If the file/directory doesn't exist in the repo folder, we just
   619  		// copy it over.
   620  		if os.IsNotExist(err) {
   621  			switch {
   622  			case info.Mode()&os.ModeSymlink != 0:
   623  				path, err := os.Readlink(path)
   624  				if err != nil {
   625  					return err
   626  				}
   627  				return os.Symlink(path, filepath.Join(repo, rel))
   628  			case info.IsDir():
   629  				return os.Mkdir(filepath.Join(repo, rel), 0700)
   630  			default:
   631  				return copyutil.SyncFile(path, filepath.Join(repo, rel))
   632  			}
   633  		}
   634  
   635  		// If it is a directory and we know it already exists, we don't need
   636  		// to do anything.
   637  		if info.IsDir() {
   638  			return nil
   639  		}
   640  
   641  		// For Kptfiles we want to keep the Upstream section if the Kptfile
   642  		// in the data directory doesn't already include one.
   643  		if filepath.Base(path) == "Kptfile" {
   644  			dataKptfile, err := pkg.ReadKptfile(filesys.FileSystemOrOnDisk{}, filepath.Dir(path))
   645  			if err != nil {
   646  				return err
   647  			}
   648  			repoKptfileDir := filepath.Dir(filepath.Join(repo, rel))
   649  			repoKptfile, err := pkg.ReadKptfile(filesys.FileSystemOrOnDisk{}, repoKptfileDir)
   650  			if err != nil {
   651  				return err
   652  			}
   653  			// Only copy over the Upstream section from the existing
   654  			// Kptfile if other values hasn't been provided.
   655  			if dataKptfile.Upstream == nil || reflect.DeepEqual(dataKptfile.Upstream, kptfilev1.Upstream{}) {
   656  				dataKptfile.Upstream = repoKptfile.Upstream
   657  			}
   658  			if dataKptfile.UpstreamLock == nil || reflect.DeepEqual(dataKptfile.UpstreamLock, kptfilev1.UpstreamLock{}) {
   659  				dataKptfile.UpstreamLock = repoKptfile.UpstreamLock
   660  			}
   661  			dataKptfile.Name = repoKptfile.Name
   662  			err = kptfileutil.WriteFile(repoKptfileDir, dataKptfile)
   663  			if err != nil {
   664  				return err
   665  			}
   666  		} else {
   667  			err := copyutil.SyncFile(path, filepath.Join(repo, rel))
   668  			if err != nil {
   669  				return err
   670  			}
   671  		}
   672  		return nil
   673  	}); err != nil {
   674  		return err
   675  	}
   676  
   677  	// We then walk the repo folder and remove and files/directories that
   678  	// exists in the repo, but doesn't exist in the data directory.
   679  	if err := filepath.Walk(repo, func(path string, info os.FileInfo, err error) error {
   680  		if os.IsNotExist(err) {
   681  			return nil
   682  		}
   683  		if err != nil {
   684  			return err
   685  		}
   686  		// Find the relative path of the file/directory
   687  		rel, err := filepath.Rel(repo, path)
   688  		if err != nil {
   689  			return err
   690  		}
   691  		// We skip anything that is inside the .git folder
   692  		if strings.HasPrefix(rel, ".git") {
   693  			return nil
   694  		}
   695  
   696  		// Never delete the Kptfile in the root package.
   697  		if rel == kptfilev1.KptFileName {
   698  			return nil
   699  		}
   700  
   701  		// Check if a file/directory exists at the path relative path within the
   702  		// data directory
   703  		dataCopy := filepath.Join(data, rel)
   704  		_, err = os.Stat(dataCopy)
   705  		if err != nil && !os.IsNotExist(err) {
   706  			return err
   707  		}
   708  
   709  		// If the file/directory doesn't exist in the data folder, we remove
   710  		// them from the repo folder.
   711  		if os.IsNotExist(err) {
   712  			if info.IsDir() {
   713  				if err := os.RemoveAll(path); err != nil {
   714  					return err
   715  				}
   716  			} else {
   717  				if err := os.Remove(path); err != nil {
   718  					return err
   719  				}
   720  			}
   721  		}
   722  		return nil
   723  	}); err != nil {
   724  		return err
   725  	}
   726  
   727  	// Add the changes to git.
   728  	cmd := exec.Command("git", "add", ".")
   729  	cmd.Dir = repo
   730  	_, err := cmd.CombinedOutput()
   731  	return err
   732  }
   733  
   734  func commit(repo, message string) (string, error) {
   735  	cmd := exec.Command("git", "commit", "-m", message, "--allow-empty")
   736  	cmd.Dir = repo
   737  	stdoutStderr, err := cmd.CombinedOutput()
   738  	if err != nil {
   739  		fmt.Fprintf(os.Stderr, "%s", stdoutStderr)
   740  		return "", err
   741  	}
   742  
   743  	sha, err := git.LookupCommit(repo)
   744  	if err != nil {
   745  		return "", err
   746  	}
   747  
   748  	return sha, nil
   749  }
   750  
   751  func tag(repo, tag string) error {
   752  	cmd := exec.Command("git", "tag", tag)
   753  	cmd.Dir = repo
   754  	b, err := cmd.Output()
   755  	if err != nil {
   756  		fmt.Fprintf(os.Stderr, "%s\n", b)
   757  		return err
   758  	}
   759  
   760  	return nil
   761  }
   762  
   763  type TestWorkspace struct {
   764  	WorkspaceDirectory string
   765  	PackageDir         string
   766  }
   767  
   768  // FullPackagePath returns the full path to the roor package in the
   769  // local workspace.
   770  func (w *TestWorkspace) FullPackagePath() string {
   771  	return filepath.Join(w.WorkspaceDirectory, w.PackageDir)
   772  }
   773  
   774  func (w *TestWorkspace) SetupTestWorkspace() error {
   775  	var err error
   776  	w.WorkspaceDirectory, err = os.MkdirTemp("", "test-kpt-local-")
   777  	return err
   778  }
   779  
   780  func (w *TestWorkspace) RemoveAll() error {
   781  	return os.RemoveAll(w.WorkspaceDirectory)
   782  }
   783  
   784  // CheckoutBranch checks out the git branch in the repo
   785  func (w *TestWorkspace) CheckoutBranch(branch string, create bool) error {
   786  	return checkoutBranch(w.WorkspaceDirectory, branch, create)
   787  }
   788  
   789  // ReplaceData replaces the data with a new source
   790  func (w *TestWorkspace) ReplaceData(data string) error {
   791  	return replaceData(filepath.Join(w.WorkspaceDirectory, w.PackageDir), data)
   792  }
   793  
   794  // CustomUpdate executes the provided update function and passes in the
   795  // path to the directory of the repository.
   796  func (w *TestWorkspace) CustomUpdate(f func(string) error) error {
   797  	return f(w.WorkspaceDirectory)
   798  }
   799  
   800  // Commit performs a git commit
   801  func (w *TestWorkspace) Commit(message string) (string, error) {
   802  	return commit(w.WorkspaceDirectory, message)
   803  }
   804  
   805  // Tag initializes tags the git repository
   806  func (w *TestWorkspace) Tag(tagName string) error {
   807  	return tag(w.WorkspaceDirectory, tagName)
   808  }
   809  
   810  func PrintPackage(paths ...string) error {
   811  	path := filepath.Join(paths...)
   812  	return filepath.Walk(path, func(path string, info os.FileInfo, err error) error {
   813  		if err != nil {
   814  			return err
   815  		}
   816  		if strings.Contains(path, "/.git") {
   817  			return nil
   818  		}
   819  		fmt.Println(path)
   820  		return nil
   821  	})
   822  }
   823  
   824  func PrintFile(paths ...string) error {
   825  	path := filepath.Join(paths...)
   826  	b, err := os.ReadFile(path)
   827  	if err != nil {
   828  		return err
   829  	}
   830  	fmt.Println(string(b))
   831  	return nil
   832  }
   833  
   834  func Chdir(t *testing.T, path string) func() {
   835  	cwd, err := os.Getwd()
   836  	if !assert.NoError(t, err) {
   837  		t.FailNow()
   838  	}
   839  	revertFunc := func() {
   840  		if err := os.Chdir(cwd); err != nil {
   841  			panic(err)
   842  		}
   843  	}
   844  	err = os.Chdir(path)
   845  	if !assert.NoError(t, err) {
   846  		defer revertFunc()
   847  		t.FailNow()
   848  	}
   849  	return revertFunc
   850  }
   851  
   852  // ConfigureTestKptCache sets up a temporary directory for the kpt git
   853  // cache, sets the env variable so it will be used for tests, and cleans
   854  // up the directory afterwards.
   855  func ConfigureTestKptCache(m *testing.M) int {
   856  	cacheDir, err := os.MkdirTemp("", "kpt-test-cache-repos-")
   857  	if err != nil {
   858  		panic(fmt.Errorf("error creating temp dir for cache: %w", err))
   859  	}
   860  	defer func() {
   861  		_ = os.RemoveAll(cacheDir)
   862  	}()
   863  	if err := os.Setenv(gitutil.RepoCacheDirEnv, cacheDir); err != nil {
   864  		panic(fmt.Errorf("error setting repo cache env variable: %w", err))
   865  	}
   866  	return m.Run()
   867  }
   868  
   869  var EmptyReposInfo = &ReposInfo{}
   870  
   871  func ToReposInfo(repos map[string]*TestGitRepo) *ReposInfo {
   872  	return &ReposInfo{
   873  		repos: repos,
   874  	}
   875  }
   876  
   877  type ReposInfo struct {
   878  	repos map[string]*TestGitRepo
   879  }
   880  
   881  func (ri *ReposInfo) ResolveRepoRef(repoRef string) (string, bool) {
   882  	repo, found := ri.repos[repoRef]
   883  	if !found {
   884  		return "", false
   885  	}
   886  	return repo.RepoDirectory, true
   887  }
   888  
   889  func (ri *ReposInfo) ResolveCommitIndex(repoRef string, index int) (string, bool) {
   890  	repo, found := ri.repos[repoRef]
   891  	if !found {
   892  		return "", false
   893  	}
   894  	commits := repo.Commits
   895  	if len(commits) <= index {
   896  		return "", false
   897  	}
   898  	return commits[index], true
   899  }