sigs.k8s.io/release-sdk@v0.11.1-0.20240417074027-8061fb5e4952/git/git_test.go (about) 1 /* 2 Copyright 2020 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package git_test 18 19 import ( 20 "errors" 21 "fmt" 22 "os" 23 "os/exec" 24 "path/filepath" 25 "strconv" 26 "strings" 27 "testing" 28 "time" 29 30 gogit "github.com/go-git/go-git/v5" 31 "github.com/go-git/go-git/v5/plumbing/object" 32 "github.com/stretchr/testify/require" 33 34 "sigs.k8s.io/release-sdk/git" 35 "sigs.k8s.io/release-sdk/git/gitfakes" 36 "sigs.k8s.io/release-utils/command" 37 ) 38 39 var testAuthor = &object.Signature{ 40 Name: "John Doe", 41 Email: "john@doe.org", 42 When: time.Now(), 43 } 44 45 func newSUT() (*git.Repo, *gitfakes.FakeWorktree) { 46 repoMock := &gitfakes.FakeRepository{} 47 worktreeMock := &gitfakes.FakeWorktree{} 48 49 repo := &git.Repo{} 50 repo.SetWorktree(worktreeMock) 51 repo.SetInnerRepo(repoMock) 52 53 return repo, worktreeMock 54 } 55 56 func TestCommit(t *testing.T) { 57 repo, worktreeMock := newSUT() 58 require.Nil(t, repo.Commit("msg")) 59 require.Equal(t, worktreeMock.CommitCallCount(), 1) 60 } 61 62 func TestGetDefaultKubernetesRepoURLSuccess(t *testing.T) { 63 testcases := []struct { 64 name string 65 org string 66 useSSH bool 67 expected string 68 }{ 69 { 70 name: "default HTTPS", 71 expected: "https://github.com/kubernetes/kubernetes", 72 }, 73 } 74 75 for _, tc := range testcases { 76 t.Logf("Test case: %s", tc.name) 77 78 actual := git.GetDefaultKubernetesRepoURL() 79 require.Equal(t, tc.expected, actual) 80 } 81 } 82 83 // createTestRepository creates a test repo, cd into it and returns the path 84 func createTestRepository() (repoPath string, err error) { 85 repoPath, err = os.MkdirTemp("", "sigrelease-test-repo-*") 86 if err != nil { 87 return "", fmt.Errorf("creating a directory for test repository: %w", err) 88 } 89 if err := os.Chdir(repoPath); err != nil { 90 return "", fmt.Errorf("cd'ing into test repository: %w", err) 91 } 92 out, err := exec.Command("git", "init").Output() 93 if err != nil { 94 return "", fmt.Errorf("initializing test repository: %s: %w", out, err) 95 } 96 return repoPath, nil 97 } 98 99 func TestGetUserName(t *testing.T) { 100 const fakeUserName = "SIG Release Test User" 101 currentDir, err := os.Getwd() 102 require.Nil(t, err, "error reading the current directory") 103 defer os.Chdir(currentDir) //nolint: errcheck 104 105 // Create an empty repo and configure the users name to test 106 repoPath, err := createTestRepository() 107 require.Nil(t, err, "getting a test repo") 108 109 // Call git to configure the user's name: 110 _, err = exec.Command("git", "config", "user.name", fakeUserName).Output() 111 require.Nil(t, err, fmt.Sprintf("configuring fake user email in %s", repoPath)) 112 113 testRepo, err := git.OpenRepo(repoPath) 114 require.Nil(t, err, fmt.Sprintf("opening test repo in %s", repoPath)) 115 defer testRepo.Cleanup() //nolint: errcheck 116 117 actual, err := git.GetUserName() 118 require.Nil(t, err) 119 require.Equal(t, fakeUserName, actual) 120 require.NotEqual(t, fakeUserName, "") 121 } 122 123 func TestGetUserEmail(t *testing.T) { 124 const fakeUserEmail = "kubernetes-test@example.com" 125 currentDir, err := os.Getwd() 126 require.Nil(t, err, "error reading the current directory") 127 defer os.Chdir(currentDir) //nolint: errcheck 128 129 // Create an empty repo and configure the users name to test 130 repoPath, err := createTestRepository() 131 require.Nil(t, err, "getting a test repo") 132 133 // Call git to configure the user's name: 134 _, err = exec.Command("git", "config", "user.email", fakeUserEmail).Output() 135 require.Nil(t, err, fmt.Sprintf("configuring fake user email in %s", repoPath)) 136 137 testRepo, err := git.OpenRepo(repoPath) 138 require.Nil(t, err, fmt.Sprintf("opening test repo in %s", repoPath)) 139 defer testRepo.Cleanup() //nolint: errcheck 140 141 // Do the actual call 142 actual, err := git.GetUserEmail() 143 require.Nil(t, err) 144 require.Equal(t, fakeUserEmail, actual) 145 require.NotEqual(t, fakeUserEmail, "") 146 } 147 148 func TestGetKubernetesRepoURLSuccess(t *testing.T) { 149 testcases := []struct { 150 name string 151 org string 152 useSSH bool 153 expected string 154 }{ 155 { 156 name: "default HTTPS", 157 expected: "https://github.com/kubernetes/kubernetes", 158 }, 159 { 160 name: "ssh with custom org", 161 org: "fake-org", 162 useSSH: true, 163 expected: "git@github.com:fake-org/kubernetes", 164 }, 165 } 166 167 for _, tc := range testcases { 168 t.Logf("Test case: %s", tc.name) 169 170 actual := git.GetKubernetesRepoURL(tc.org, tc.useSSH) 171 require.Equal(t, tc.expected, actual) 172 } 173 } 174 175 func TestGetRepoURLSuccess(t *testing.T) { 176 testcases := []struct { 177 name string 178 org string 179 repo string 180 useSSH bool 181 expected string 182 }{ 183 { 184 name: "default Kubernetes HTTPS", 185 org: "kubernetes", 186 repo: "kubernetes", 187 expected: "https://github.com/kubernetes/kubernetes", 188 }, 189 { 190 name: "ssh with custom org", 191 org: "fake-org", 192 repo: "repofoo", 193 useSSH: true, 194 expected: "git@github.com:fake-org/repofoo", 195 }, 196 } 197 198 for _, tc := range testcases { 199 t.Logf("Test case: %s", tc.name) 200 201 actual := git.GetRepoURL(tc.org, tc.repo, tc.useSSH) 202 require.Equal(t, tc.expected, actual) 203 } 204 } 205 206 func TestRemotify(t *testing.T) { 207 testcases := []struct{ provided, expected string }{ 208 {provided: git.DefaultBranch, expected: git.DefaultRemote + "/" + git.DefaultBranch}, 209 {provided: "origin/ref", expected: "origin/ref"}, 210 {provided: "base/another_ref", expected: "base/another_ref"}, 211 } 212 213 for _, tc := range testcases { 214 require.Equal(t, git.Remotify(tc.provided), tc.expected) 215 } 216 } 217 218 func TestIsDirtyMockSuccess(t *testing.T) { 219 repo, _ := newSUT() 220 221 dirty, err := repo.IsDirty() 222 223 require.Nil(t, err) 224 require.False(t, dirty) 225 } 226 227 func TestIsDirtyMockSuccessDirty(t *testing.T) { 228 repo, worktreeMock := newSUT() 229 worktreeMock.StatusReturns(gogit.Status{ 230 "file": &gogit.FileStatus{ 231 Worktree: gogit.Modified, 232 }, 233 }, nil) 234 235 dirty, err := repo.IsDirty() 236 237 require.Nil(t, err) 238 require.True(t, dirty) 239 } 240 241 func TestIsDirtyMockFailureWorktreeStatus(t *testing.T) { 242 repo, worktreeMock := newSUT() 243 worktreeMock.StatusReturns(gogit.Status{}, errors.New("")) 244 245 dirty, err := repo.IsDirty() 246 247 require.NotNil(t, err) 248 require.False(t, dirty) 249 } 250 251 func TestParseRepoSlug(t *testing.T) { 252 slugTests := []struct { 253 caseName, repoSlug, orgName, repoName string 254 isValid bool 255 }{ 256 { 257 caseName: "valid slug", repoSlug: "kubernetes/release", 258 orgName: "kubernetes", repoName: "release", isValid: true, 259 }, 260 261 { 262 caseName: "slug with hyphens", repoSlug: "kubernetes/repo_with_underscores", 263 orgName: "", repoName: "", isValid: false, 264 }, 265 266 { 267 caseName: "slug with dashes", repoSlug: "kubernetes-sigs/release-notes", 268 orgName: "kubernetes-sigs", repoName: "release-notes", isValid: true, 269 }, 270 271 { 272 caseName: "slug with uppercase", repoSlug: "GoogleCloudPlatform/compute-image-tools", 273 orgName: "GoogleCloudPlatform", repoName: "compute-image-tools", isValid: true, 274 }, 275 276 { 277 caseName: "slug with invalid chars", repoSlug: "kubern#etes/not.valid", 278 orgName: "", repoName: "", isValid: false, 279 }, 280 281 { 282 caseName: "slug with extra slash", repoSlug: "kubernetes/not/valid", 283 orgName: "", repoName: "", isValid: false, 284 }, 285 286 { 287 caseName: "slug with only org", repoSlug: "kubernetes", 288 orgName: "kubernetes", repoName: "", isValid: true, 289 }, 290 } 291 292 for _, testCase := range slugTests { 293 org, repo, err := git.ParseRepoSlug(testCase.repoSlug) 294 if testCase.isValid { 295 require.Nil(t, err, testCase.caseName) 296 } else { 297 require.NotNil(t, err, testCase.caseName) 298 } 299 require.Equal(t, testCase.orgName, org, testCase.caseName) 300 require.Equal(t, testCase.repoName, repo, testCase.caseName) 301 } 302 } 303 304 func TestRetryErrors(t *testing.T) { 305 retryErrorStrings := []string{ 306 "dial tcp: lookup github.com on [::1]:53", 307 "read udp [::1]:48087->[::1]:53", 308 "read: connection refused", 309 } 310 311 nonRetryErrorStrings := []string{ 312 "could not list references on the remote repository", 313 "error checking remote branch", 314 "src refspec release-chorizo does not match", 315 } 316 317 for _, message := range retryErrorStrings { 318 err := git.NewNetworkError(errors.New(message)) 319 require.True(t, err.CanRetry(), fmt.Sprintf("Checking retriable error '%s'", message)) 320 } 321 322 for _, message := range nonRetryErrorStrings { 323 err := git.NewNetworkError(errors.New(message)) 324 require.False(t, err.CanRetry(), fmt.Sprintf("Checking non-retriable error '%s'", message)) 325 } 326 } 327 328 func TestNetworkError(t *testing.T) { 329 // Return a NetWorkError in a fun that returns a standard error 330 err := func() error { 331 return git.NewNetworkError(errors.New("This is a test error")) 332 }() 333 require.NotNil(t, err, "checking if NewNetWork error returns nil") 334 require.NotEmpty(t, err.Error(), "checking if NetworkError returns a message") 335 require.False(t, err.(git.NetworkError).CanRetry(), "checking if network error can be properly asserted") 336 } 337 338 func TestHasBranch(t *testing.T) { 339 testBranchName := "git-package-test-branch" 340 repoPath, err := createTestRepository() 341 require.Nil(t, err, "getting a test repo") 342 343 // Create a file and a test commit 344 testfile := filepath.Join(repoPath, "README.md") 345 err = os.WriteFile(testfile, []byte("# WHY SIG-RELEASE ROCKS\n\n"), os.FileMode(0o644)) 346 require.Nil(t, err, "writing test file") 347 348 err = command.NewWithWorkDir(repoPath, "git", "add", testfile).RunSuccess() 349 require.Nil(t, err, fmt.Sprintf("adding test file in %s", repoPath)) 350 351 err = command.NewWithWorkDir(repoPath, "git", "commit", "-m", "adding test file").RunSuccess() 352 require.Nil(t, err, "creating first commit") 353 354 // Call git to configure the user's name: 355 err = command.NewWithWorkDir(repoPath, "git", "branch", testBranchName).RunSuccess() 356 require.Nil(t, err, fmt.Sprintf("configuring test branch in %s", repoPath)) 357 358 // Now, open the repo and test to see if branches are there 359 testRepo, err := git.OpenRepo(repoPath) 360 require.Nil(t, err, fmt.Sprintf("opening test repo in %s", repoPath)) 361 defer testRepo.Cleanup() //nolint: errcheck 362 363 actual, err := testRepo.HasBranch(testBranchName) 364 require.Nil(t, err) 365 require.True(t, actual) 366 367 actual, err = testRepo.HasBranch(git.DefaultBranch) 368 require.Nil(t, err) 369 require.True(t, actual) 370 371 actual, err = testRepo.HasBranch("non-existing-branch") 372 require.Nil(t, err) 373 require.False(t, actual) 374 } 375 376 func TestStatus(t *testing.T) { 377 rawRepoDir, err := os.MkdirTemp("", "k8s-test-repo") 378 require.Nil(t, err) 379 _, err = gogit.PlainInit(rawRepoDir, false) 380 require.Nil(t, err) 381 382 testFile := "test-status.txt" 383 384 testRepo, err := git.OpenRepo(rawRepoDir) 385 require.Nil(t, err) 386 defer testRepo.Cleanup() //nolint: errcheck 387 388 // Get the status object 389 status, err := testRepo.Status() 390 require.Nil(t, err) 391 require.NotNil(t, status) 392 require.True(t, status.IsClean()) 393 394 // Create an untracked file 395 require.Nil(t, os.WriteFile(filepath.Join(testRepo.Dir(), testFile), []byte("Hello SIG Release"), 0o644)) 396 397 // Status should be modified now 398 status, err = testRepo.Status() 399 require.Nil(t, err) 400 require.Equal(t, fmt.Sprintf("?? %s\n", testFile), status.String()) 401 402 // Add the file, should status should be A 403 require.Nil(t, testRepo.Add(testFile)) 404 status, err = testRepo.Status() 405 require.Nil(t, err) 406 require.Equal(t, fmt.Sprintf("A %s\n", testFile), status.String()) 407 408 // Commit the file, status should be blank again 409 require.Nil(t, testRepo.Commit("Commit test file")) 410 status, err = testRepo.Status() 411 require.Nil(t, err) 412 require.Empty(t, status.String()) 413 414 // Modify the file 415 require.Nil(t, os.WriteFile(filepath.Join(testRepo.Dir(), testFile), []byte("Bye SIG Release"), 0o644)) 416 status, err = testRepo.Status() 417 require.Nil(t, err) 418 require.Equal(t, fmt.Sprintf(" M %s\n", testFile), status.String()) 419 } 420 421 func TestShowLastCommit(t *testing.T) { 422 rawRepoDir, err := os.MkdirTemp("", "k8s-test-repo") 423 require.Nil(t, err) 424 _, err = gogit.PlainInit(rawRepoDir, false) 425 require.Nil(t, err) 426 427 testFile := "test-last-commit.txt" 428 timeNow := strconv.FormatInt(time.Now().UnixNano(), 10) 429 430 testRepo, err := git.OpenRepo(rawRepoDir) 431 require.Nil(t, err) 432 defer testRepo.Cleanup() //nolint: errcheck 433 434 // Create an untracked file 435 require.Nil(t, os.WriteFile(filepath.Join(testRepo.Dir(), testFile), []byte("Hello SIG Release"), 0o644)) 436 require.Nil(t, testRepo.Add(testFile)) 437 require.Nil(t, testRepo.Commit(fmt.Sprintf("Commit test file at %s", timeNow))) 438 439 // Now get the log message back and check if it contains the time 440 lastLog, err := testRepo.ShowLastCommit() 441 require.Nil(t, err) 442 require.NotEmpty(t, lastLog) 443 require.True(t, strings.Contains(lastLog, timeNow)) 444 } 445 446 func TestFetchRemote(t *testing.T) { 447 testTagName := "test-tag" + strconv.FormatInt(time.Now().UnixNano(), 10) 448 // Create a new empty repo 449 rawRepoDir, err := os.MkdirTemp("", "k8s-test-repo") 450 require.Nil(t, err) 451 gogitRepo, err := gogit.PlainInit(rawRepoDir, false) 452 require.Nil(t, err) 453 454 // Create the foirst commit 455 wtree, err := gogitRepo.Worktree() 456 require.Nil(t, err) 457 require.Nil(t, err) 458 commitSha, err := wtree.Commit("Initial Commit", &gogit.CommitOptions{ 459 Author: testAuthor, 460 AllowEmptyCommits: true, 461 }) 462 require.Nil(t, err) 463 464 // Create a git.Repo from it 465 originRepo, err := git.OpenRepo(rawRepoDir) 466 require.Nil(t, err) 467 468 branchName, err := originRepo.CurrentBranch() 469 require.Nil(t, err) 470 defer originRepo.Cleanup() //nolint: errcheck 471 472 // Create a new clone of the original repo 473 testRepo, err := git.CloneOrOpenRepo("", rawRepoDir, false, true, nil) 474 require.Nil(t, err) 475 defer testRepo.Cleanup() //nolint: errcheck 476 477 // The initial clone must not have any tags 478 testTags, err := testRepo.TagsForBranch(branchName) 479 require.Nil(t, err) 480 require.Empty(t, testTags) 481 482 // Create a tag on the originRepo 483 _, err = gogitRepo.CreateTag(testTagName, commitSha, &gogit.CreateTagOptions{ 484 Message: testTagName, 485 Tagger: testAuthor, 486 }) 487 require.Nil(t, err) 488 489 // Now, call fetch 490 newContent, err := testRepo.FetchRemote("origin") 491 require.Nil(t, err, "Calling fetch to get a test tag") 492 require.True(t, newContent) 493 494 // Fetching again should provide no updates 495 newContent, err = testRepo.FetchRemote("origin") 496 require.Nil(t, err, "Calling fetch to get a test tag again") 497 require.False(t, newContent) 498 499 // And now we can verify the tags was successfully transferred via FetchRemote() 500 testTags, err = testRepo.TagsForBranch(branchName) 501 require.Nil(t, err) 502 require.NotEmpty(t, testTags) 503 require.ElementsMatch(t, []string{testTagName}, testTags) 504 } 505 506 func TestRebase(t *testing.T) { 507 testFile := "test-rebase.txt" 508 509 // Create a new empty repo 510 rawRepoDir, err := os.MkdirTemp("", "k8s-test-repo") 511 require.Nil(t, err) 512 gogitRepo, err := gogit.PlainInit(rawRepoDir, false) 513 require.Nil(t, err) 514 515 // Create the initial commit 516 wtree, err := gogitRepo.Worktree() 517 require.Nil(t, err) 518 _, err = wtree.Commit("Initial Commit", &gogit.CommitOptions{ 519 Author: testAuthor, 520 AllowEmptyCommits: true, 521 }) 522 require.Nil(t, err) 523 524 // Create a git.Repo from it 525 originRepo, err := git.OpenRepo(rawRepoDir) 526 require.Nil(t, err) 527 528 branchName, err := originRepo.CurrentBranch() 529 require.Nil(t, err) 530 defer originRepo.Cleanup() //nolint: errcheck 531 532 // Create a new clone of the original repo 533 testRepo, err := git.CloneOrOpenRepo("", rawRepoDir, false, true, nil) 534 require.Nil(t, err) 535 defer testRepo.Cleanup() //nolint: errcheck 536 537 // Test 1. Rebase should not fail if both repos are in sync 538 require.Nil(t, testRepo.Rebase(fmt.Sprintf("origin/%s", branchName)), "cloning synchronizaed repos") 539 540 // Test 2. Rebase should not fail with pulling changes in the remote 541 require.Nil(t, os.WriteFile(filepath.Join(rawRepoDir, testFile), []byte("Hello SIG Release"), 0o644)) 542 _, err = wtree.Add(testFile) 543 require.Nil(t, err) 544 545 _, err = wtree.Commit("Test2-Commit", &gogit.CommitOptions{ 546 Author: testAuthor, 547 }) 548 require.Nil(t, err) 549 550 // Pull the changes to the test repo 551 newContent, err := testRepo.FetchRemote("origin") 552 require.Nil(t, err) 553 require.True(t, newContent) 554 555 // Do the Rebase 556 require.Nil(t, testRepo.Rebase(fmt.Sprintf("origin/%s", branchName)), "rebasing changes from origin") 557 558 // Verify we got the commit 559 lastLog, err := testRepo.ShowLastCommit() 560 require.Nil(t, err) 561 require.True(t, strings.Contains(lastLog, "Test2-Commit")) 562 563 // Test 3: Rebase must on an invalid branch 564 require.NotNil(t, testRepo.Rebase("origin/invalidBranch"), "rebasing to invalid branch") 565 566 // Test 4: Rebase must fail on merge conflicts 567 require.Nil(t, os.WriteFile(filepath.Join(rawRepoDir, testFile), []byte("Hello again SIG Release"), 0o644)) 568 _, err = wtree.Add(testFile) 569 require.Nil(t, err) 570 571 _, err = wtree.Commit("Test4-Commit", &gogit.CommitOptions{ 572 Author: testAuthor, 573 }) 574 require.Nil(t, err) 575 576 // Commit the same file in the test repo 577 require.Nil(t, os.WriteFile(filepath.Join(testRepo.Dir(), testFile), []byte("Conflict me!"), 0o644)) 578 require.Nil(t, testRepo.Add(filepath.Join(testRepo.Dir(), testFile))) 579 require.Nil(t, testRepo.Commit("Adding file to cause conflict")) 580 581 // Now, fetch and rebase 582 newContent, err = testRepo.FetchRemote("origin") 583 require.Nil(t, err) 584 require.True(t, newContent) 585 586 err = testRepo.Rebase(fmt.Sprintf("origin/%s", branchName)) 587 require.NotNil(t, err, "testing for merge conflicts") 588 } 589 590 func TestLastCommitSha(t *testing.T) { 591 // Create a test repository 592 rawRepoDir, err := os.MkdirTemp("", "k8s-test-repo") 593 require.Nil(t, err) 594 defer os.RemoveAll(rawRepoDir) 595 _, err = gogit.PlainInit(rawRepoDir, false) 596 require.Nil(t, err) 597 598 repo, err := git.OpenRepo(rawRepoDir) 599 require.Nil(t, err) 600 601 // Create two commits in the repository 602 shas := make([]string, 2) 603 for _, i := range []int{0, 1} { 604 require.Nil(t, repo.CommitEmpty(fmt.Sprintf("Empty commit %d", i+1))) 605 shas[i], err = repo.LastCommitSha() 606 require.Nil(t, err) 607 require.NotEmpty(t, shas[i]) 608 } 609 require.Len(t, shas, 2) 610 611 // Now, checkout the first one and check we get the right hash 612 require.Nil(t, repo.Checkout("HEAD~1")) 613 614 lastCommit, err := repo.LastCommitSha() 615 require.Nil(t, err) 616 require.Equal(t, shas[0], lastCommit, "Checking HEAD~1 sha matches commit #1") 617 require.NotEqual(t, shas[1], lastCommit, "Checking HEAD~1 sha does not matches commit #2") 618 }