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 }