This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / knotserver / git / merge_test.go
17 kB 754 lines
1package git 2 3import ( 4 "os" 5 "path/filepath" 6 "strings" 7 "testing" 8 9 "github.com/go-git/go-git/v5" 10 "github.com/go-git/go-git/v5/config" 11 "github.com/go-git/go-git/v5/plumbing" 12 "github.com/go-git/go-git/v5/plumbing/object" 13 "github.com/stretchr/testify/assert" 14 "github.com/stretchr/testify/require" 15) 16 17type Helper struct { 18 t *testing.T 19 tempDir string 20 repo *GitRepo 21} 22 23func helper(t *testing.T) *Helper { 24 tempDir, err := os.MkdirTemp("", "git-merge-test-*") 25 require.NoError(t, err) 26 27 return &Helper{ 28 t: t, 29 tempDir: tempDir, 30 } 31} 32 33func (h *Helper) cleanup() { 34 if h.tempDir != "" { 35 os.RemoveAll(h.tempDir) 36 } 37} 38 39// initRepo initializes a git repository with an initial commit 40func (h *Helper) initRepo() *GitRepo { 41 repoPath := filepath.Join(h.tempDir, "test-repo") 42 43 // initialize repository 44 r, err := git.PlainInit(repoPath, false) 45 require.NoError(h.t, err) 46 47 // configure git user 48 cfg, err := r.Config() 49 require.NoError(h.t, err) 50 cfg.User.Name = "Test User" 51 cfg.User.Email = "test@example.com" 52 err = r.SetConfig(cfg) 53 require.NoError(h.t, err) 54 55 // create initial commit with a file 56 w, err := r.Worktree() 57 require.NoError(h.t, err) 58 59 // create initial file 60 initialFile := filepath.Join(repoPath, "README.md") 61 err = os.WriteFile(initialFile, []byte("# Test Repository\n\nInitial content.\n"), 0644) 62 require.NoError(h.t, err) 63 64 _, err = w.Add("README.md") 65 require.NoError(h.t, err) 66 67 _, err = w.Commit("Initial commit", &git.CommitOptions{ 68 Author: &object.Signature{ 69 Name: "Test User", 70 Email: "test@example.com", 71 }, 72 }) 73 require.NoError(h.t, err) 74 75 gitRepo, err := PlainOpen(repoPath) 76 require.NoError(h.t, err) 77 78 h.repo = gitRepo 79 return gitRepo 80} 81 82// addFile creates a file in the repository 83func (h *Helper) addFile(filename, content string) { 84 filePath := filepath.Join(h.repo.path, filename) 85 dir := filepath.Dir(filePath) 86 87 err := os.MkdirAll(dir, 0755) 88 require.NoError(h.t, err) 89 90 err = os.WriteFile(filePath, []byte(content), 0644) 91 require.NoError(h.t, err) 92} 93 94// commitFile adds and commits a file 95func (h *Helper) commitFile(filename, content, message string) plumbing.Hash { 96 h.addFile(filename, content) 97 98 w, err := h.repo.r.Worktree() 99 require.NoError(h.t, err) 100 101 _, err = w.Add(filename) 102 require.NoError(h.t, err) 103 104 hash, err := w.Commit(message, &git.CommitOptions{ 105 Author: &object.Signature{ 106 Name: "Test User", 107 Email: "test@example.com", 108 }, 109 }) 110 require.NoError(h.t, err) 111 112 return hash 113} 114 115// readFile reads a file from the repository 116func (h *Helper) readFile(filename string) string { 117 content, err := os.ReadFile(filepath.Join(h.repo.path, filename)) 118 require.NoError(h.t, err) 119 return string(content) 120} 121 122// fileExists checks if a file exists in the repository 123func (h *Helper) fileExists(filename string) bool { 124 _, err := os.Stat(filepath.Join(h.repo.path, filename)) 125 return err == nil 126} 127 128func TestApplyPatch_Success(t *testing.T) { 129 h := helper(t) 130 defer h.cleanup() 131 132 repo := h.initRepo() 133 134 // modify README.md 135 patch := `diff --git a/README.md b/README.md 136index 1234567..abcdefg 100644 137--- a/README.md 138+++ b/README.md 139@@ -1,3 +1,3 @@ 140 # Test Repository 141 142-Initial content. 143+Modified content. 144` 145 146 patchFile, err := createTemp(patch) 147 require.NoError(t, err) 148 defer os.Remove(patchFile) 149 150 opts := MergeOptions{ 151 CommitMessage: "Apply test patch", 152 CommitterName: "Test Committer", 153 CommitterEmail: "committer@example.com", 154 FormatPatch: false, 155 } 156 157 err = repo.applyPatch(patch, patchFile, opts) 158 assert.NoError(t, err) 159 160 // verify the file was modified 161 content := h.readFile("README.md") 162 assert.Contains(t, content, "Modified content.") 163} 164 165func TestApplyPatch_AddNewFile(t *testing.T) { 166 h := helper(t) 167 defer h.cleanup() 168 169 repo := h.initRepo() 170 171 // add a new file 172 patch := `diff --git a/newfile.txt b/newfile.txt 173new file mode 100644 174index 0000000..ce01362 175--- /dev/null 176+++ b/newfile.txt 177@@ -0,0 +1 @@ 178+hello 179` 180 181 patchFile, err := createTemp(patch) 182 require.NoError(t, err) 183 defer os.Remove(patchFile) 184 185 opts := MergeOptions{ 186 CommitMessage: "Add new file", 187 CommitterName: "Test Committer", 188 CommitterEmail: "committer@example.com", 189 FormatPatch: false, 190 } 191 192 err = repo.applyPatch(patch, patchFile, opts) 193 assert.NoError(t, err) 194 195 assert.True(t, h.fileExists("newfile.txt")) 196 content := h.readFile("newfile.txt") 197 assert.Equal(t, "hello\n", content) 198} 199 200func TestApplyPatch_DoesNotCommitPatchFile(t *testing.T) { 201 h := helper(t) 202 defer h.cleanup() 203 204 repo := h.initRepo() 205 206 patch := `diff --git a/scallop.txt b/scallop.txt 207new file mode 100644 208index 0000000..ce01362 209--- /dev/null 210+++ b/scallop.txt 211@@ -0,0 +1 @@ 212+hello 213` 214 215 patchFile, err := createTempIn(repo.path, patch) 216 require.NoError(t, err) 217 defer os.Remove(patchFile) 218 219 opts := MergeOptions{ 220 CommitMessage: "Add scallop.txt", 221 CommitterName: "nel", 222 CommitterEmail: "nel@nel.pet", 223 FormatPatch: false, 224 } 225 226 err = repo.applyPatch(patch, patchFile, opts) 227 require.NoError(t, err) 228 229 refreshed, err := PlainOpen(repo.path) 230 require.NoError(t, err) 231 232 head, err := refreshed.r.Head() 233 require.NoError(t, err) 234 235 commit, err := refreshed.r.CommitObject(head.Hash()) 236 require.NoError(t, err) 237 238 tree, err := commit.Tree() 239 require.NoError(t, err) 240 241 _, err = tree.File("scallop.txt") 242 assert.NoError(t, err, "patched file should be committed") 243 244 _, err = tree.File(filepath.Base(patchFile)) 245 assert.ErrorIs(t, err, object.ErrFileNotFound, "temporary patch file must not be committed") 246} 247 248func TestApplyPatch_DeleteFile(t *testing.T) { 249 h := helper(t) 250 defer h.cleanup() 251 252 repo := h.initRepo() 253 254 // add a file 255 h.commitFile("deleteme.txt", "content to delete\n", "Add file to delete") 256 257 // delete the file 258 patch := `diff --git a/deleteme.txt b/deleteme.txt 259deleted file mode 100644 260index 1234567..0000000 261--- a/deleteme.txt 262+++ /dev/null 263@@ -1 +0,0 @@ 264-content to delete 265` 266 267 patchFile, err := createTemp(patch) 268 require.NoError(t, err) 269 defer os.Remove(patchFile) 270 271 opts := MergeOptions{ 272 CommitMessage: "Delete file", 273 CommitterName: "Test Committer", 274 CommitterEmail: "committer@example.com", 275 FormatPatch: false, 276 } 277 278 err = repo.applyPatch(patch, patchFile, opts) 279 assert.NoError(t, err) 280 281 assert.False(t, h.fileExists("deleteme.txt")) 282} 283 284func TestApplyPatch_WithAuthor(t *testing.T) { 285 h := helper(t) 286 defer h.cleanup() 287 288 repo := h.initRepo() 289 290 patch := `diff --git a/README.md b/README.md 291index 1234567..abcdefg 100644 292--- a/README.md 293+++ b/README.md 294@@ -1,3 +1,4 @@ 295 # Test Repository 296 297 Initial content. 298+New line. 299` 300 301 patchFile, err := createTemp(patch) 302 require.NoError(t, err) 303 defer os.Remove(patchFile) 304 305 opts := MergeOptions{ 306 CommitMessage: "Patch with author", 307 AuthorName: "Patch Author", 308 AuthorEmail: "author@example.com", 309 CommitterName: "Test Committer", 310 CommitterEmail: "committer@example.com", 311 FormatPatch: false, 312 } 313 314 err = repo.applyPatch(patch, patchFile, opts) 315 assert.NoError(t, err) 316 317 head, err := repo.r.Head() 318 require.NoError(t, err) 319 320 commit, err := repo.r.CommitObject(head.Hash()) 321 require.NoError(t, err) 322 323 assert.Equal(t, "Patch Author", commit.Author.Name) 324 assert.Equal(t, "author@example.com", commit.Author.Email) 325} 326 327func TestApplyPatch_MissingFile(t *testing.T) { 328 h := helper(t) 329 defer h.cleanup() 330 331 repo := h.initRepo() 332 333 // patch that modifies a non-existent file 334 patch := `diff --git a/nonexistent.txt b/nonexistent.txt 335index 1234567..abcdefg 100644 336--- a/nonexistent.txt 337+++ b/nonexistent.txt 338@@ -1 +1 @@ 339-old content 340+new content 341` 342 343 patchFile, err := createTemp(patch) 344 require.NoError(t, err) 345 defer os.Remove(patchFile) 346 347 opts := MergeOptions{ 348 CommitMessage: "Should fail", 349 CommitterName: "Test Committer", 350 CommitterEmail: "committer@example.com", 351 FormatPatch: false, 352 } 353 354 err = repo.applyPatch(patch, patchFile, opts) 355 assert.Error(t, err) 356 assert.Contains(t, err.Error(), "patch application failed") 357} 358 359func TestApplyPatch_Conflict(t *testing.T) { 360 h := helper(t) 361 defer h.cleanup() 362 363 repo := h.initRepo() 364 365 // modify the file to create a conflict 366 h.commitFile("README.md", "# Test Repository\n\nDifferent content.\n", "Modify README") 367 368 // patch that expects different content 369 patch := `diff --git a/README.md b/README.md 370index 1234567..abcdefg 100644 371--- a/README.md 372+++ b/README.md 373@@ -1,3 +1,3 @@ 374 # Test Repository 375 376-Initial content. 377+Modified content. 378` 379 380 patchFile, err := createTemp(patch) 381 require.NoError(t, err) 382 defer os.Remove(patchFile) 383 384 opts := MergeOptions{ 385 CommitMessage: "Should conflict", 386 CommitterName: "Test Committer", 387 CommitterEmail: "committer@example.com", 388 FormatPatch: false, 389 } 390 391 err = repo.applyPatch(patch, patchFile, opts) 392 assert.Error(t, err) 393} 394 395func TestApplyPatch_MissingDirectory(t *testing.T) { 396 h := helper(t) 397 defer h.cleanup() 398 399 repo := h.initRepo() 400 401 // patch that adds a file in a non-existent directory 402 patch := `diff --git a/subdir/newfile.txt b/subdir/newfile.txt 403new file mode 100644 404index 0000000..ce01362 405--- /dev/null 406+++ b/subdir/newfile.txt 407@@ -0,0 +1 @@ 408+content 409` 410 411 patchFile, err := createTemp(patch) 412 require.NoError(t, err) 413 defer os.Remove(patchFile) 414 415 opts := MergeOptions{ 416 CommitMessage: "Add file in subdir", 417 CommitterName: "Test Committer", 418 CommitterEmail: "committer@example.com", 419 FormatPatch: false, 420 } 421 422 // git apply should create the directory automatically 423 err = repo.applyPatch(patch, patchFile, opts) 424 assert.NoError(t, err) 425 426 // Verify the file and directory were created 427 assert.True(t, h.fileExists("subdir/newfile.txt")) 428} 429 430func TestApplyMailbox_Single(t *testing.T) { 431 h := helper(t) 432 defer h.cleanup() 433 434 repo := h.initRepo() 435 436 // format-patch mailbox format 437 patch := `From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 438From: Patch Author <author@example.com> 439Date: Mon, 1 Jan 2024 12:00:00 +0000 440Subject: [PATCH] Add new feature 441 442This is a test patch. 443--- 444 newfile.txt | 1 + 445 1 file changed, 1 insertion(+) 446 create mode 100644 newfile.txt 447 448diff --git a/newfile.txt b/newfile.txt 449new file mode 100644 450index 0000000..ce01362 451--- /dev/null 452+++ b/newfile.txt 453@@ -0,0 +1 @@ 454+hello 455-- 4562.40.0 457` 458 459 err := repo.applyMailbox(patch) 460 assert.NoError(t, err) 461 462 assert.True(t, h.fileExists("newfile.txt")) 463 content := h.readFile("newfile.txt") 464 assert.Equal(t, "hello\n", content) 465} 466 467func TestApplyMailbox_Multiple(t *testing.T) { 468 h := helper(t) 469 defer h.cleanup() 470 471 repo := h.initRepo() 472 473 // multiple patches in mailbox format 474 patch := `From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 475From: Patch Author <author@example.com> 476Date: Mon, 1 Jan 2024 12:00:00 +0000 477Subject: [PATCH 1/2] Add first file 478 479--- 480 file1.txt | 1 + 481 1 file changed, 1 insertion(+) 482 create mode 100644 file1.txt 483 484diff --git a/file1.txt b/file1.txt 485new file mode 100644 486index 0000000..ce01362 487--- /dev/null 488+++ b/file1.txt 489@@ -0,0 +1 @@ 490+first 491-- 4922.40.0 493 494From 1111111111111111111111111111111111111111 Mon Sep 17 00:00:00 2001 495From: Patch Author <author@example.com> 496Date: Mon, 1 Jan 2024 12:01:00 +0000 497Subject: [PATCH 2/2] Add second file 498 499--- 500 file2.txt | 1 + 501 1 file changed, 1 insertion(+) 502 create mode 100644 file2.txt 503 504diff --git a/file2.txt b/file2.txt 505new file mode 100644 506index 0000000..ce01362 507--- /dev/null 508+++ b/file2.txt 509@@ -0,0 +1 @@ 510+second 511-- 5122.40.0 513` 514 515 err := repo.applyMailbox(patch) 516 assert.NoError(t, err) 517 518 assert.True(t, h.fileExists("file1.txt")) 519 assert.True(t, h.fileExists("file2.txt")) 520 521 content1 := h.readFile("file1.txt") 522 assert.Equal(t, "first\n", content1) 523 524 content2 := h.readFile("file2.txt") 525 assert.Equal(t, "second\n", content2) 526} 527 528func TestApplyMailbox_Conflict(t *testing.T) { 529 h := helper(t) 530 defer h.cleanup() 531 532 repo := h.initRepo() 533 534 h.commitFile("README.md", "# Test Repository\n\nConflicting content.\n", "Create conflict") 535 536 patch := `From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 537From: Patch Author <author@example.com> 538Date: Mon, 1 Jan 2024 12:00:00 +0000 539Subject: [PATCH] Modify README 540 541--- 542 README.md | 2 +- 543 1 file changed, 1 insertion(+), 1 deletion(-) 544 545diff --git a/README.md b/README.md 546index 1234567..abcdefg 100644 547--- a/README.md 548+++ b/README.md 549@@ -1,3 +1,3 @@ 550 # Test Repository 551 552-Initial content. 553+Different content. 554-- 5552.40.0 556` 557 558 err := repo.applyMailbox(patch) 559 assert.Error(t, err) 560 561 var mergeErr *ErrMerge 562 assert.ErrorAs(t, err, &mergeErr) 563} 564 565func TestParseGitApplyErrors(t *testing.T) { 566 tests := []struct { 567 name string 568 errorOutput string 569 expectedCount int 570 expectedReason string 571 }{ 572 { 573 name: "file already exists", 574 errorOutput: `error: path/to/file.txt: already exists in working directory`, 575 expectedCount: 1, 576 expectedReason: "file already exists", 577 }, 578 { 579 name: "file does not exist", 580 errorOutput: `error: path/to/file.txt: does not exist in working tree`, 581 expectedCount: 1, 582 expectedReason: "file does not exist", 583 }, 584 { 585 name: "patch does not apply", 586 errorOutput: `error: patch failed: file.txt:10 587error: file.txt: patch does not apply`, 588 expectedCount: 1, 589 expectedReason: "patch does not apply", 590 }, 591 { 592 name: "multiple conflicts", 593 errorOutput: `error: patch failed: file1.txt:5 594error: file1.txt:5: some error 595error: patch failed: file2.txt:10 596error: file2.txt:10: another error`, 597 expectedCount: 2, 598 }, 599 } 600 601 for _, tt := range tests { 602 t.Run(tt.name, func(t *testing.T) { 603 conflicts := parseGitApplyErrors(tt.errorOutput) 604 assert.Len(t, conflicts, tt.expectedCount) 605 606 if tt.expectedReason != "" && len(conflicts) > 0 { 607 assert.Equal(t, tt.expectedReason, conflicts[0].Reason) 608 } 609 }) 610 } 611} 612 613func TestErrMerge_Error(t *testing.T) { 614 tests := []struct { 615 name string 616 err ErrMerge 617 expectedMsg string 618 }{ 619 { 620 name: "with conflicts", 621 err: ErrMerge{ 622 Message: "test merge failed", 623 HasConflict: true, 624 Conflicts: []ConflictInfo{ 625 {Filename: "file1.txt", Reason: "conflict 1"}, 626 {Filename: "file2.txt", Reason: "conflict 2"}, 627 }, 628 }, 629 expectedMsg: "merge failed due to conflicts: test merge failed (2 conflicts)", 630 }, 631 { 632 name: "with other error", 633 err: ErrMerge{ 634 Message: "command failed", 635 OtherError: assert.AnError, 636 }, 637 expectedMsg: "merge failed: command failed:", 638 }, 639 { 640 name: "message only", 641 err: ErrMerge{ 642 Message: "simple failure", 643 }, 644 expectedMsg: "merge failed: simple failure", 645 }, 646 } 647 648 for _, tt := range tests { 649 t.Run(tt.name, func(t *testing.T) { 650 errMsg := tt.err.Error() 651 assert.Contains(t, errMsg, tt.expectedMsg) 652 }) 653 } 654} 655 656func TestMergeWithOptions_Integration(t *testing.T) { 657 h := helper(t) 658 defer h.cleanup() 659 660 // create a repository first with initial content 661 workRepoPath := filepath.Join(h.tempDir, "work-repo") 662 workRepo, err := git.PlainInit(workRepoPath, false) 663 require.NoError(t, err) 664 665 // configure git user 666 cfg, err := workRepo.Config() 667 require.NoError(t, err) 668 cfg.User.Name = "Test User" 669 cfg.User.Email = "test@example.com" 670 err = workRepo.SetConfig(cfg) 671 require.NoError(t, err) 672 673 // Create initial commit 674 w, err := workRepo.Worktree() 675 require.NoError(t, err) 676 677 err = os.WriteFile(filepath.Join(workRepoPath, "README.md"), []byte("# Initial\n"), 0644) 678 require.NoError(t, err) 679 680 _, err = w.Add("README.md") 681 require.NoError(t, err) 682 683 _, err = w.Commit("Initial commit", &git.CommitOptions{ 684 Author: &object.Signature{ 685 Name: "Test User", 686 Email: "test@example.com", 687 }, 688 }) 689 require.NoError(t, err) 690 691 // create a bare repository (like production) 692 bareRepoPath := filepath.Join(h.tempDir, "bare-repo") 693 err = InitBare(bareRepoPath, "main") 694 require.NoError(t, err) 695 696 // add bare repo as remote and push to it 697 _, err = workRepo.CreateRemote(&config.RemoteConfig{ 698 Name: "origin", 699 URLs: []string{"file://" + bareRepoPath}, 700 }) 701 require.NoError(t, err) 702 703 err = workRepo.Push(&git.PushOptions{ 704 RemoteName: "origin", 705 RefSpecs: []config.RefSpec{"refs/heads/master:refs/heads/main"}, 706 }) 707 require.NoError(t, err) 708 709 // now merge a patch into the bare repo 710 gitRepo, err := PlainOpen(bareRepoPath) 711 require.NoError(t, err) 712 713 patch := `diff --git a/feature.txt b/feature.txt 714new file mode 100644 715index 0000000..5e1c309 716--- /dev/null 717+++ b/feature.txt 718@@ -0,0 +1 @@ 719+Hello World 720` 721 722 opts := MergeOptions{ 723 CommitMessage: "Add feature", 724 CommitterName: "Test Committer", 725 CommitterEmail: "committer@example.com", 726 FormatPatch: false, 727 } 728 729 err = gitRepo.MergeWithOptions(patch, "main", opts) 730 assert.NoError(t, err) 731 732 // Clone again and verify the changes were merged 733 verifyRepoPath := filepath.Join(h.tempDir, "verify-repo") 734 verifyRepo, err := git.PlainClone(verifyRepoPath, false, &git.CloneOptions{ 735 URL: "file://" + bareRepoPath, 736 }) 737 require.NoError(t, err) 738 739 // check that feature.txt exists 740 featureFile := filepath.Join(verifyRepoPath, "feature.txt") 741 assert.FileExists(t, featureFile) 742 743 content, err := os.ReadFile(featureFile) 744 require.NoError(t, err) 745 assert.Equal(t, "Hello World\n", string(content)) 746 747 // verify commit message 748 head, err := verifyRepo.Head() 749 require.NoError(t, err) 750 751 commit, err := verifyRepo.CommitObject(head.Hash()) 752 require.NoError(t, err) 753 assert.Equal(t, "Add feature", strings.TrimSpace(commit.Message)) 754}