github.com/stffabi/git-lfs@v2.3.5-0.20180214015214-8eeaa8d88902+incompatible/test/testutils.go (about)

     1  package test
     2  
     3  // Utility functions for more complex go tests
     4  // Need to be in a separate test package so they can be imported anywhere
     5  // Also can't add _test.go suffix to exclude from main build (import doesn't work)
     6  
     7  // To avoid import cycles, append "_test" to the package statement of any test using
     8  // this package and use "import . original/package/name" to get the same visibility
     9  // as if the test was in the same package (as usual)
    10  
    11  import (
    12  	"fmt"
    13  	"io"
    14  	"io/ioutil"
    15  	"math/rand"
    16  	"os"
    17  	"os/exec"
    18  	"path/filepath"
    19  	"strings"
    20  	"sync"
    21  	"time"
    22  
    23  	"github.com/git-lfs/git-lfs/config"
    24  	"github.com/git-lfs/git-lfs/errors"
    25  	"github.com/git-lfs/git-lfs/fs"
    26  	"github.com/git-lfs/git-lfs/git"
    27  	"github.com/git-lfs/git-lfs/lfs"
    28  )
    29  
    30  type RepoType int
    31  
    32  const (
    33  	// Normal repo with working copy
    34  	RepoTypeNormal = RepoType(iota)
    35  	// Bare repo (no working copy)
    36  	RepoTypeBare = RepoType(iota)
    37  	// Repo with working copy but git dir is separate
    38  	RepoTypeSeparateDir = RepoType(iota)
    39  )
    40  
    41  var (
    42  	// Deterministic sequence of seeds for file data
    43  	fileInputSeed = rand.NewSource(0)
    44  	storageOnce   sync.Once
    45  )
    46  
    47  type RepoCreateSettings struct {
    48  	RepoType RepoType
    49  }
    50  
    51  // Callback interface (testing.T compatible)
    52  type RepoCallback interface {
    53  	// Fatalf reports error and fails
    54  	Fatalf(format string, args ...interface{})
    55  	// Errorf reports error and continues
    56  	Errorf(format string, args ...interface{})
    57  }
    58  
    59  type Repo struct {
    60  	// Path to the repo, working copy if non-bare
    61  	Path string
    62  	// Path to the git dir
    63  	GitDir string
    64  	// Paths to remotes
    65  	Remotes map[string]*Repo
    66  	// Settings used to create this repo
    67  	Settings *RepoCreateSettings
    68  	// Previous dir for pushd
    69  	popDir string
    70  	// Test callback
    71  	callback  RepoCallback
    72  	cfg       *config.Configuration
    73  	gitfilter *lfs.GitFilter
    74  	fs        *fs.Filesystem
    75  }
    76  
    77  // Change to repo dir but save current dir
    78  func (r *Repo) Pushd() {
    79  	if r.popDir != "" {
    80  		r.callback.Fatalf("Cannot Pushd twice")
    81  	}
    82  	oldwd, err := os.Getwd()
    83  	if err != nil {
    84  		r.callback.Fatalf("Can't get cwd %v", err)
    85  	}
    86  	err = os.Chdir(r.Path)
    87  	if err != nil {
    88  		r.callback.Fatalf("Can't chdir %v", err)
    89  	}
    90  	r.popDir = oldwd
    91  }
    92  
    93  func (r *Repo) Popd() {
    94  	if r.popDir != "" {
    95  		err := os.Chdir(r.popDir)
    96  		if err != nil {
    97  			r.callback.Fatalf("Can't chdir %v", err)
    98  		}
    99  		r.popDir = ""
   100  	}
   101  }
   102  
   103  func (r *Repo) Filesystem() *fs.Filesystem {
   104  	return r.fs
   105  }
   106  
   107  func (r *Repo) GitConfig() *git.Configuration {
   108  	return r.cfg.GitConfig()
   109  }
   110  
   111  func (r *Repo) GitEnv() config.Environment {
   112  	return r.cfg.Git
   113  }
   114  
   115  func (r *Repo) OSEnv() config.Environment {
   116  	return r.cfg.Os
   117  }
   118  
   119  func (r *Repo) Cleanup() {
   120  	// pop out if necessary
   121  	r.Popd()
   122  
   123  	// Make sure cwd isn't inside a path we're going to delete
   124  	oldwd, err := os.Getwd()
   125  	if err == nil {
   126  		if strings.HasPrefix(oldwd, r.Path) ||
   127  			strings.HasPrefix(oldwd, r.GitDir) {
   128  			os.Chdir(os.TempDir())
   129  		}
   130  	}
   131  
   132  	if r.GitDir != "" {
   133  		os.RemoveAll(r.GitDir)
   134  		r.GitDir = ""
   135  	}
   136  	if r.Path != "" {
   137  		os.RemoveAll(r.Path)
   138  		r.Path = ""
   139  	}
   140  	for _, remote := range r.Remotes {
   141  		remote.Cleanup()
   142  	}
   143  	r.Remotes = nil
   144  }
   145  
   146  // NewRepo creates a new git repo in a new temp dir
   147  func NewRepo(callback RepoCallback) *Repo {
   148  	return newRepo(callback, &RepoCreateSettings{
   149  		RepoType: RepoTypeNormal,
   150  	})
   151  }
   152  
   153  // newRepo creates a new git repo in a new temp dir with more control over settings
   154  func newRepo(callback RepoCallback, settings *RepoCreateSettings) *Repo {
   155  	ret := &Repo{
   156  		Settings: settings,
   157  		Remotes:  make(map[string]*Repo),
   158  		callback: callback,
   159  	}
   160  
   161  	path, err := ioutil.TempDir("", "lfsRepo")
   162  	if err != nil {
   163  		callback.Fatalf("Can't create temp dir for git repo: %v", err)
   164  	}
   165  	ret.Path = path
   166  	args := []string{"init"}
   167  	switch settings.RepoType {
   168  	case RepoTypeBare:
   169  		args = append(args, "--bare")
   170  		ret.GitDir = ret.Path
   171  	case RepoTypeSeparateDir:
   172  		gitdir, err := ioutil.TempDir("", "lfstestgitdir")
   173  		if err != nil {
   174  			ret.Cleanup()
   175  			callback.Fatalf("Can't create temp dir for git repo: %v", err)
   176  		}
   177  		args = append(args, "--separate-dir", gitdir)
   178  		ret.GitDir = gitdir
   179  	default:
   180  		ret.GitDir = filepath.Join(ret.Path, ".git")
   181  	}
   182  
   183  	ret.cfg = config.NewIn(ret.Path, ret.GitDir)
   184  	ret.fs = ret.cfg.Filesystem()
   185  	ret.gitfilter = lfs.NewGitFilter(ret.cfg)
   186  
   187  	args = append(args, path)
   188  	cmd := exec.Command("git", args...)
   189  	err = cmd.Run()
   190  	if err != nil {
   191  		ret.Cleanup()
   192  		callback.Fatalf("Unable to create git repo at %v: %v", path, err)
   193  	}
   194  
   195  	// Configure default user/email so not reliant on env
   196  	ret.Pushd()
   197  	RunGitCommand(callback, true, "config", "user.name", "Git LFS Tests")
   198  	RunGitCommand(callback, true, "config", "user.email", "git-lfs@example.com")
   199  	ret.Popd()
   200  
   201  	return ret
   202  }
   203  
   204  // WrapRepo creates a new Repo instance for an existing git repo
   205  func WrapRepo(c RepoCallback, path string) *Repo {
   206  	cfg := config.NewIn(path, "")
   207  	return &Repo{
   208  		Path:   path,
   209  		GitDir: cfg.LocalGitDir(),
   210  		Settings: &RepoCreateSettings{
   211  			RepoType: RepoTypeNormal,
   212  		},
   213  		callback:  c,
   214  		cfg:       cfg,
   215  		gitfilter: lfs.NewGitFilter(cfg),
   216  		fs:        cfg.Filesystem(),
   217  	}
   218  }
   219  
   220  // Simplistic fire & forget running of git command - returns combined output
   221  func RunGitCommand(callback RepoCallback, failureCheck bool, args ...string) string {
   222  	outp, err := exec.Command("git", args...).CombinedOutput()
   223  	if failureCheck && err != nil {
   224  		callback.Fatalf("Error running git command 'git %v': %v %v", strings.Join(args, " "), err, string(outp))
   225  	}
   226  	return string(outp)
   227  
   228  }
   229  
   230  // Input data for a single file in a commit
   231  type FileInput struct {
   232  	// Name of file (required)
   233  	Filename string
   234  	// Size of file (required)
   235  	Size int64
   236  	// Input data (optional, if provided will be source of data)
   237  	DataReader io.Reader
   238  	// Input data (optional, if provided will be source of data)
   239  	Data string
   240  }
   241  
   242  func (infile *FileInput) AddToIndex(output *CommitOutput, repo *Repo) {
   243  	inputData := infile.getFileInputReader()
   244  	pointer, err := infile.writeLFSPointer(repo, inputData)
   245  	if err != nil {
   246  		repo.callback.Errorf("%+v", err)
   247  		return
   248  	}
   249  	output.Files = append(output.Files, pointer)
   250  	RunGitCommand(repo.callback, true, "add", infile.Filename)
   251  }
   252  
   253  func (infile *FileInput) writeLFSPointer(repo *Repo, inputData io.Reader) (*lfs.Pointer, error) {
   254  	cleaned, err := repo.gitfilter.Clean(inputData, infile.Filename, infile.Size, nil)
   255  	if err != nil {
   256  		return nil, errors.Wrap(err, "creating pointer file")
   257  	}
   258  
   259  	// this only created the temp file, move to final location
   260  	tmpfile := cleaned.Filename
   261  	mediafile, err := repo.fs.ObjectPath(cleaned.Oid)
   262  	if err != nil {
   263  		return nil, errors.Wrap(err, "local media path")
   264  	}
   265  
   266  	if _, err := os.Stat(mediafile); err != nil {
   267  		if err := os.Rename(tmpfile, mediafile); err != nil {
   268  			return nil, err
   269  		}
   270  	}
   271  
   272  	// Write pointer to local filename for adding (not using clean filter)
   273  	os.MkdirAll(filepath.Dir(infile.Filename), 0755)
   274  	f, err := os.Create(infile.Filename)
   275  	if err != nil {
   276  		return nil, errors.Wrap(err, "creating pointer file")
   277  	}
   278  	_, err = cleaned.Pointer.Encode(f)
   279  	f.Close()
   280  	if err != nil {
   281  		return nil, errors.Wrap(err, "encoding pointer file")
   282  	}
   283  
   284  	return cleaned.Pointer, nil
   285  }
   286  
   287  func (infile *FileInput) getFileInputReader() io.Reader {
   288  	if infile.DataReader != nil {
   289  		return infile.DataReader
   290  	}
   291  
   292  	if len(infile.Data) > 0 {
   293  		return strings.NewReader(infile.Data)
   294  	}
   295  
   296  	// Different data for each file but deterministic
   297  	return NewPlaceholderDataReader(fileInputSeed.Int63(), infile.Size)
   298  }
   299  
   300  // Input for defining commits for test repo
   301  type CommitInput struct {
   302  	// Date that we should commit on (optional, leave blank for 'now')
   303  	CommitDate time.Time
   304  	// List of files to include in this commit
   305  	Files []*FileInput
   306  	// List of parent branches (all branches must have been created in a previous NewBranch or be master)
   307  	// Can be omitted to just use the parent of the previous commit
   308  	ParentBranches []string
   309  	// Name of a new branch we should create at this commit (optional - master not required)
   310  	NewBranch string
   311  	// Names of any tags we should create at this commit (optional)
   312  	Tags []string
   313  	// Name of committer
   314  	CommitterName string
   315  	// Email of committer
   316  	CommitterEmail string
   317  }
   318  
   319  // Output struct with details of commits created for test
   320  type CommitOutput struct {
   321  	Sha     string
   322  	Parents []string
   323  	Files   []*lfs.Pointer
   324  }
   325  
   326  func commitAtDate(atDate time.Time, committerName, committerEmail, msg string) error {
   327  	var args []string
   328  	if committerName != "" && committerEmail != "" {
   329  		args = append(args, "-c", fmt.Sprintf("user.name=%v", committerName))
   330  		args = append(args, "-c", fmt.Sprintf("user.email=%v", committerEmail))
   331  	}
   332  	args = append(args, "commit", "--allow-empty", "-m", msg)
   333  	cmd := exec.Command("git", args...)
   334  	env := os.Environ()
   335  	// set GIT_COMMITTER_DATE environment var e.g. "Fri Jun 21 20:26:41 2013 +0900"
   336  	if atDate.IsZero() {
   337  		env = append(env, "GIT_COMMITTER_DATE=")
   338  		env = append(env, "GIT_AUTHOR_DATE=")
   339  	} else {
   340  		env = append(env, fmt.Sprintf("GIT_COMMITTER_DATE=%v", git.FormatGitDate(atDate)))
   341  		env = append(env, fmt.Sprintf("GIT_AUTHOR_DATE=%v", git.FormatGitDate(atDate)))
   342  	}
   343  	cmd.Env = env
   344  	out, err := cmd.CombinedOutput()
   345  	if err != nil {
   346  		return fmt.Errorf("%v %v", err, string(out))
   347  	}
   348  	return nil
   349  }
   350  
   351  func (repo *Repo) AddCommits(inputs []*CommitInput) []*CommitOutput {
   352  	if repo.Settings.RepoType == RepoTypeBare {
   353  		repo.callback.Fatalf("Cannot use AddCommits on a bare repo; clone it & push changes instead")
   354  	}
   355  
   356  	// Change to repo working dir
   357  	oldwd, err := os.Getwd()
   358  	if err != nil {
   359  		repo.callback.Fatalf("Can't get cwd %v", err)
   360  	}
   361  	err = os.Chdir(repo.Path)
   362  	if err != nil {
   363  		repo.callback.Fatalf("Can't chdir to repo %v", err)
   364  	}
   365  	// Used to check whether we need to checkout another commit before
   366  	lastBranch := "master"
   367  	outputs := make([]*CommitOutput, 0, len(inputs))
   368  
   369  	for i, input := range inputs {
   370  		output := &CommitOutput{}
   371  		// first, are we on the correct branch
   372  		if len(input.ParentBranches) > 0 {
   373  			if input.ParentBranches[0] != lastBranch {
   374  				RunGitCommand(repo.callback, true, "checkout", input.ParentBranches[0])
   375  				lastBranch = input.ParentBranches[0]
   376  			}
   377  		}
   378  		// Is this a merge?
   379  		if len(input.ParentBranches) > 1 {
   380  			// Always take the *other* side in a merge so we adopt changes
   381  			// also don't automatically commit, we'll do that below
   382  			args := []string{"merge", "--no-ff", "--no-commit", "--strategy-option=theirs"}
   383  			args = append(args, input.ParentBranches[1:]...)
   384  			RunGitCommand(repo.callback, false, args...)
   385  		} else if input.NewBranch != "" {
   386  			RunGitCommand(repo.callback, true, "checkout", "-b", input.NewBranch)
   387  			lastBranch = input.NewBranch
   388  		}
   389  		// Any files to write?
   390  		for _, infile := range input.Files {
   391  			infile.AddToIndex(output, repo)
   392  		}
   393  		// Now commit
   394  		err = commitAtDate(input.CommitDate, input.CommitterName, input.CommitterEmail,
   395  			fmt.Sprintf("Test commit %d", i))
   396  		if err != nil {
   397  			repo.callback.Fatalf("Error committing: %v", err)
   398  		}
   399  
   400  		commit, err := git.GetCommitSummary("HEAD")
   401  		if err != nil {
   402  			repo.callback.Fatalf("Error determining commit SHA: %v", err)
   403  		}
   404  
   405  		// tags
   406  		for _, tag := range input.Tags {
   407  			// Use annotated tags, assume full release tags (also tag objects have edge cases)
   408  			RunGitCommand(repo.callback, true, "tag", "-a", "-m", "Added tag", tag)
   409  		}
   410  
   411  		output.Sha = commit.Sha
   412  		output.Parents = commit.Parents
   413  		outputs = append(outputs, output)
   414  	}
   415  
   416  	// Restore cwd
   417  	err = os.Chdir(oldwd)
   418  	if err != nil {
   419  		repo.callback.Fatalf("Can't restore old cwd %v", err)
   420  	}
   421  
   422  	return outputs
   423  }
   424  
   425  // Add a new remote (generate a path for it to live in, will be cleaned up)
   426  func (r *Repo) AddRemote(name string) *Repo {
   427  	if _, exists := r.Remotes[name]; exists {
   428  		r.callback.Fatalf("Remote %v already exists", name)
   429  	}
   430  	remote := newRepo(r.callback, &RepoCreateSettings{
   431  		RepoType: RepoTypeBare,
   432  	})
   433  	r.Remotes[name] = remote
   434  	RunGitCommand(r.callback, true, "remote", "add", name, remote.Path)
   435  	return remote
   436  }
   437  
   438  // Just a psuedo-random stream of bytes (not cryptographic)
   439  // Calls RNG a bit less often than using rand.Source directly
   440  type PlaceholderDataReader struct {
   441  	source    rand.Source
   442  	bytesLeft int64
   443  }
   444  
   445  func NewPlaceholderDataReader(seed, size int64) *PlaceholderDataReader {
   446  	return &PlaceholderDataReader{rand.NewSource(seed), size}
   447  }
   448  
   449  func (r *PlaceholderDataReader) Read(p []byte) (int, error) {
   450  	c := len(p)
   451  	i := 0
   452  	for i < c && r.bytesLeft > 0 {
   453  		// Use all 8 bytes of the 64-bit random number
   454  		val64 := r.source.Int63()
   455  		for j := 0; j < 8 && i < c && r.bytesLeft > 0; j++ {
   456  			// Duplicate this byte 16 times (faster)
   457  			for k := 0; k < 16 && r.bytesLeft > 0; k++ {
   458  				p[i] = byte(val64)
   459  				i++
   460  				r.bytesLeft--
   461  			}
   462  			// Next byte from the 8-byte number
   463  			val64 = val64 >> 8
   464  		}
   465  	}
   466  	var err error
   467  	if r.bytesLeft == 0 {
   468  		err = io.EOF
   469  	}
   470  	return i, err
   471  }
   472  
   473  // RefsByName implements sort.Interface for []*git.Ref based on name
   474  type RefsByName []*git.Ref
   475  
   476  func (a RefsByName) Len() int           { return len(a) }
   477  func (a RefsByName) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
   478  func (a RefsByName) Less(i, j int) bool { return a[i].Name < a[j].Name }
   479  
   480  // WrappedPointersByOid implements sort.Interface for []*lfs.WrappedPointer based on oid
   481  type WrappedPointersByOid []*lfs.WrappedPointer
   482  
   483  func (a WrappedPointersByOid) Len() int           { return len(a) }
   484  func (a WrappedPointersByOid) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
   485  func (a WrappedPointersByOid) Less(i, j int) bool { return a[i].Pointer.Oid < a[j].Pointer.Oid }
   486  
   487  // PointersByOid implements sort.Interface for []*lfs.Pointer based on oid
   488  type PointersByOid []*lfs.Pointer
   489  
   490  func (a PointersByOid) Len() int           { return len(a) }
   491  func (a PointersByOid) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
   492  func (a PointersByOid) Less(i, j int) bool { return a[i].Oid < a[j].Oid }