| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593 |
- package git
- import (
- "os"
- "path/filepath"
- "strings"
- "testing"
- )
- // headHash returns the current HEAD commit hash of a repository.
- func headHash(t *testing.T, manager *Manager, repoPath string) string {
- t.Helper()
- status, err := manager.Status(repoPath)
- if err != nil {
- t.Fatalf("Status() returned error: %v", err)
- }
- if status.Head == nil {
- t.Fatalf("HEAD is unborn, want a commit")
- }
- return status.Head.Hash
- }
- func TestCheckoutCommitDetachesHead(t *testing.T) {
- manager := newTestManager(t)
- repoPath := newTestRepo(t, manager)
- first := commitFile(t, manager, repoPath, "a.txt", "one\n", "first")
- commitFile(t, manager, repoPath, "a.txt", "two\n", "second")
- if err := manager.CheckoutCommit(repoPath, first); err != nil {
- t.Fatalf("CheckoutCommit() returned error: %v", err)
- }
- status, err := manager.Status(repoPath)
- if err != nil {
- t.Fatalf("Status() returned error: %v", err)
- }
- if !status.Detached {
- t.Errorf("Detached = false after checking out a commit, want true")
- }
- if status.Head.Hash != first {
- t.Errorf("HEAD = %q, want %q", status.Head.Hash, first)
- }
- //The working tree must reflect the older commit
- content, _ := os.ReadFile(filepath.Join(repoPath, "a.txt"))
- if string(content) != "one\n" {
- t.Errorf("working tree = %q, want the first commit's content", string(content))
- }
- }
- func TestCheckoutCommitRefusesDirtyTree(t *testing.T) {
- manager := newTestManager(t)
- repoPath := newTestRepo(t, manager)
- first := commitFile(t, manager, repoPath, "a.txt", "one\n", "first")
- commitFile(t, manager, repoPath, "a.txt", "two\n", "second")
- writeFile(t, repoPath, "a.txt", "uncommitted\n")
- if err := manager.CheckoutCommit(repoPath, first); err == nil {
- t.Errorf("CheckoutCommit() with a dirty tree = nil error, want a refusal")
- }
- }
- func TestResetToCommitModes(t *testing.T) {
- tests := []struct {
- name string
- mode string
- wantWorktree string // file content after reset
- wantChangeSeen bool // is the reverted change reported as pending?
- }{
- //soft/mixed keep the newer file on disk, so the difference reappears as
- //an uncommitted change; hard rewinds the file too.
- {name: "soft", mode: "soft", wantWorktree: "two\n", wantChangeSeen: true},
- {name: "mixed", mode: "mixed", wantWorktree: "two\n", wantChangeSeen: true},
- {name: "hard", mode: "hard", wantWorktree: "one\n", wantChangeSeen: false},
- }
- for _, test := range tests {
- t.Run(test.name, func(t *testing.T) {
- manager := newTestManager(t)
- repoPath := newTestRepo(t, manager)
- first := commitFile(t, manager, repoPath, "a.txt", "one\n", "first")
- commitFile(t, manager, repoPath, "a.txt", "two\n", "second")
- if err := manager.ResetToCommit(repoPath, first, test.mode); err != nil {
- t.Fatalf("ResetToCommit(%s) returned error: %v", test.mode, err)
- }
- if got := headHash(t, manager, repoPath); got != first {
- t.Errorf("HEAD = %q, want the first commit %q", got, first)
- }
- content, _ := os.ReadFile(filepath.Join(repoPath, "a.txt"))
- if string(content) != test.wantWorktree {
- t.Errorf("working tree = %q, want %q", string(content), test.wantWorktree)
- }
- status, _ := manager.Status(repoPath)
- if status.Clean == test.wantChangeSeen {
- t.Errorf("Clean = %v, want %v", status.Clean, !test.wantChangeSeen)
- }
- })
- }
- }
- func TestResetHardRefusesDirtyTree(t *testing.T) {
- manager := newTestManager(t)
- repoPath := newTestRepo(t, manager)
- first := commitFile(t, manager, repoPath, "a.txt", "one\n", "first")
- commitFile(t, manager, repoPath, "a.txt", "two\n", "second")
- writeFile(t, repoPath, "b.txt", "unsaved work\n")
- if err := manager.ResetToCommit(repoPath, first, "hard"); err == nil {
- t.Errorf("hard reset with a dirty tree = nil error, want a refusal")
- }
- }
- func TestResetToCommitValidation(t *testing.T) {
- manager := newTestManager(t)
- repoPath := newTestRepo(t, manager)
- hash := commitFile(t, manager, repoPath, "a.txt", "one\n", "first")
- if err := manager.ResetToCommit(repoPath, hash, "sideways"); err == nil {
- t.Errorf("ResetToCommit() with an unknown mode = nil error, want an error")
- }
- if err := manager.ResetToCommit(repoPath, "abc1234", "mixed"); err == nil {
- t.Errorf("ResetToCommit() with a short hash = nil error, want an error")
- }
- }
- func TestCreateBranchAt(t *testing.T) {
- manager := newTestManager(t)
- repoPath := newTestRepo(t, manager)
- first := commitFile(t, manager, repoPath, "a.txt", "one\n", "first")
- commitFile(t, manager, repoPath, "a.txt", "two\n", "second")
- if err := manager.CreateBranchAt(repoPath, "legacy", first); err != nil {
- t.Fatalf("CreateBranchAt() returned error: %v", err)
- }
- status, err := manager.Status(repoPath)
- if err != nil {
- t.Fatalf("Status() returned error: %v", err)
- }
- if status.Branch != "legacy" {
- t.Errorf("current branch = %q, want %q", status.Branch, "legacy")
- }
- if status.Head.Hash != first {
- t.Errorf("new branch points at %q, want the first commit %q", status.Head.Hash, first)
- }
- }
- func TestCreateBranchAtRejectsExistingName(t *testing.T) {
- manager := newTestManager(t)
- repoPath := newTestRepo(t, manager)
- hash := commitFile(t, manager, repoPath, "a.txt", "one\n", "first")
- branches, _ := manager.Branches(repoPath)
- existing := branches[0].Name
- if err := manager.CreateBranchAt(repoPath, existing, hash); err == nil {
- t.Errorf("CreateBranchAt() with an existing name = nil error, want an error")
- }
- if err := manager.CreateBranchAt(repoPath, "bad name", hash); err == nil {
- t.Errorf("CreateBranchAt() with an invalid name = nil error, want an error")
- }
- }
- func TestCreateTagLightweightAndAnnotated(t *testing.T) {
- manager := newTestManager(t)
- repoPath := newTestRepo(t, manager)
- hash := commitFile(t, manager, repoPath, "a.txt", "one\n", "first")
- if err := manager.CreateTag(repoPath, "v1.0", hash, "", "T", "t@e.com"); err != nil {
- t.Fatalf("CreateTag() lightweight returned error: %v", err)
- }
- if err := manager.CreateTag(repoPath, "v1.0-annotated", hash, "first release", "T", "t@e.com"); err != nil {
- t.Fatalf("CreateTag() annotated returned error: %v", err)
- }
- tags, err := manager.TagsForCommit(repoPath, hash)
- if err != nil {
- t.Fatalf("TagsForCommit() returned error: %v", err)
- }
- if len(tags) != 2 {
- t.Fatalf("TagsForCommit() = %v, want two tags", tags)
- }
- found := map[string]bool{}
- for _, tag := range tags {
- found[tag] = true
- }
- if !found["v1.0"] || !found["v1.0-annotated"] {
- t.Errorf("TagsForCommit() = %v, want both the lightweight and annotated tags", tags)
- }
- }
- func TestCreateTagValidation(t *testing.T) {
- manager := newTestManager(t)
- repoPath := newTestRepo(t, manager)
- hash := commitFile(t, manager, repoPath, "a.txt", "one\n", "first")
- if err := manager.CreateTag(repoPath, "v1.0", hash, "", "T", "t@e.com"); err != nil {
- t.Fatalf("CreateTag() returned error: %v", err)
- }
- tests := []struct {
- name string
- tag string
- }{
- {name: "duplicate", tag: "v1.0"},
- {name: "empty", tag: ""},
- {name: "with space", tag: "v 1.0"},
- {name: "with tilde", tag: "v~1"},
- }
- for _, test := range tests {
- t.Run(test.name, func(t *testing.T) {
- if err := manager.CreateTag(repoPath, test.tag, hash, "", "T", "t@e.com"); err == nil {
- t.Errorf("CreateTag(%q) = nil error, want an error", test.tag)
- }
- })
- }
- }
- func TestTagsForCommitEmpty(t *testing.T) {
- manager := newTestManager(t)
- repoPath := newTestRepo(t, manager)
- hash := commitFile(t, manager, repoPath, "a.txt", "one\n", "first")
- tags, err := manager.TagsForCommit(repoPath, hash)
- if err != nil {
- t.Fatalf("TagsForCommit() returned error: %v", err)
- }
- if len(tags) != 0 {
- t.Errorf("TagsForCommit() = %v, want none", tags)
- }
- }
- func TestRevertLatestCommit(t *testing.T) {
- manager := newTestManager(t)
- repoPath := newTestRepo(t, manager)
- commitFile(t, manager, repoPath, "a.txt", "original\n", "first")
- commitFile(t, manager, repoPath, "a.txt", "changed\n", "second")
- second := headHash(t, manager, repoPath)
- revertHash, err := manager.RevertCommit(repoPath, second, &CommitRequest{
- Name: "Test User",
- Email: "test@arozos.local",
- })
- if err != nil {
- t.Fatalf("RevertCommit() returned error: %v", err)
- }
- if revertHash == "" {
- t.Fatalf("RevertCommit() returned an empty hash")
- }
- //The file must be back to its state before the reverted commit
- content, _ := os.ReadFile(filepath.Join(repoPath, "a.txt"))
- if string(content) != "original\n" {
- t.Errorf("after revert a.txt = %q, want %q", string(content), "original\n")
- }
- status, _ := manager.Status(repoPath)
- if !status.Clean {
- t.Errorf("working tree is not clean after revert: %+v", status.Changes)
- }
- if status.Head.Subject != `Revert "second"` {
- t.Errorf("revert commit subject = %q, want %q", status.Head.Subject, `Revert "second"`)
- }
- }
- func TestRevertAddedFileDeletesIt(t *testing.T) {
- manager := newTestManager(t)
- repoPath := newTestRepo(t, manager)
- commitFile(t, manager, repoPath, "keep.txt", "keep\n", "first")
- commitFile(t, manager, repoPath, "added.txt", "new file\n", "add a file")
- second := headHash(t, manager, repoPath)
- if _, err := manager.RevertCommit(repoPath, second, &CommitRequest{Name: "T", Email: "t@e.com"}); err != nil {
- t.Fatalf("RevertCommit() returned error: %v", err)
- }
- if _, err := os.Stat(filepath.Join(repoPath, "added.txt")); !os.IsNotExist(err) {
- t.Errorf("reverting the commit that added the file did not remove it")
- }
- if _, err := os.Stat(filepath.Join(repoPath, "keep.txt")); err != nil {
- t.Errorf("revert removed an unrelated file: %v", err)
- }
- }
- func TestRevertOlderUntouchedCommit(t *testing.T) {
- manager := newTestManager(t)
- repoPath := newTestRepo(t, manager)
- commitFile(t, manager, repoPath, "a.txt", "aaa\n", "add a")
- addACommit := headHash(t, manager, repoPath)
- commitFile(t, manager, repoPath, "b.txt", "bbb\n", "add b unrelated")
- //Reverting the older commit that added a.txt is clean because nothing has
- //touched a.txt since.
- if _, err := manager.RevertCommit(repoPath, addACommit, &CommitRequest{Name: "T", Email: "t@e.com"}); err != nil {
- t.Fatalf("RevertCommit() of an older untouched commit returned error: %v", err)
- }
- if _, err := os.Stat(filepath.Join(repoPath, "a.txt")); !os.IsNotExist(err) {
- t.Errorf("a.txt should be gone after reverting the commit that added it")
- }
- //The unrelated later file must be untouched
- content, _ := os.ReadFile(filepath.Join(repoPath, "b.txt"))
- if string(content) != "bbb\n" {
- t.Errorf("b.txt = %q, want it left alone", string(content))
- }
- }
- func TestRevertRefusesWhenFileChangedSince(t *testing.T) {
- manager := newTestManager(t)
- repoPath := newTestRepo(t, manager)
- commitFile(t, manager, repoPath, "a.txt", "v1\n", "first")
- target := headHash(t, manager, repoPath)
- commitFile(t, manager, repoPath, "a.txt", "v2\n", "second touches the same file")
- //a.txt was changed again after the target commit, so a clean revert is
- //impossible and the operation must refuse rather than corrupt the file.
- if _, err := manager.RevertCommit(repoPath, target, &CommitRequest{Name: "T", Email: "t@e.com"}); err == nil {
- t.Errorf("RevertCommit() of a commit whose file changed since = nil error, want a refusal")
- }
- //Nothing should have been written
- status, _ := manager.Status(repoPath)
- if !status.Clean {
- t.Errorf("a refused revert left the tree dirty: %+v", status.Changes)
- }
- }
- func TestRevertRefusesDirtyTree(t *testing.T) {
- manager := newTestManager(t)
- repoPath := newTestRepo(t, manager)
- commitFile(t, manager, repoPath, "a.txt", "one\n", "first")
- hash := headHash(t, manager, repoPath)
- writeFile(t, repoPath, "a.txt", "uncommitted\n")
- if _, err := manager.RevertCommit(repoPath, hash, &CommitRequest{Name: "T", Email: "t@e.com"}); err == nil {
- t.Errorf("RevertCommit() with a dirty tree = nil error, want a refusal")
- }
- }
- func TestCherryPickAppliesToAnotherBranch(t *testing.T) {
- manager := newTestManager(t)
- repoPath := newTestRepo(t, manager)
- base := commitFile(t, manager, repoPath, "a.txt", "base\n", "base")
- //A feature commit that adds a file, on a branch off the base
- if err := manager.Checkout(repoPath, "feature", true); err != nil {
- t.Fatalf("Checkout(create) returned error: %v", err)
- }
- commitFile(t, manager, repoPath, "feature.txt", "feature work\n", "add feature")
- featureCommit := headHash(t, manager, repoPath)
- //Back to the base branch, cherry-pick the feature commit
- if err := manager.CheckoutCommit(repoPath, base); err != nil {
- t.Fatalf("CheckoutCommit(base) returned error: %v", err)
- }
- if err := manager.Checkout(repoPath, "main-line", true); err != nil {
- t.Fatalf("Checkout(create main-line) returned error: %v", err)
- }
- pickHash, err := manager.CherryPickCommit(repoPath, featureCommit, &CommitRequest{Name: "T", Email: "t@e.com"})
- if err != nil {
- t.Fatalf("CherryPickCommit() returned error: %v", err)
- }
- if pickHash == "" {
- t.Fatalf("CherryPickCommit() returned an empty hash")
- }
- content, err := os.ReadFile(filepath.Join(repoPath, "feature.txt"))
- if err != nil {
- t.Fatalf("the cherry-picked file is missing: %v", err)
- }
- if string(content) != "feature work\n" {
- t.Errorf("feature.txt = %q, want the cherry-picked content", string(content))
- }
- //The original commit message is preserved
- status, _ := manager.Status(repoPath)
- if status.Head.Subject != "add feature" {
- t.Errorf("cherry-pick subject = %q, want %q", status.Head.Subject, "add feature")
- }
- }
- func TestCherryPickPreservesOriginalAuthor(t *testing.T) {
- manager := newTestManager(t)
- repoPath := newTestRepo(t, manager)
- base := commitFile(t, manager, repoPath, "a.txt", "base\n", "base")
- if err := manager.Checkout(repoPath, "feature", true); err != nil {
- t.Fatalf("Checkout(create) returned error: %v", err)
- }
- writeFile(t, repoPath, "feature.txt", "work\n")
- if _, err := manager.Commit(repoPath, &CommitRequest{
- Message: "feature commit",
- Files: []string{"feature.txt"},
- Name: "Original Author",
- Email: "original@example.com",
- }); err != nil {
- t.Fatalf("Commit() returned error: %v", err)
- }
- featureCommit := headHash(t, manager, repoPath)
- if err := manager.CheckoutCommit(repoPath, base); err != nil {
- t.Fatalf("CheckoutCommit(base) returned error: %v", err)
- }
- if err := manager.Checkout(repoPath, "target", true); err != nil {
- t.Fatalf("Checkout(create) returned error: %v", err)
- }
- if _, err := manager.CherryPickCommit(repoPath, featureCommit, &CommitRequest{
- Name: "Picker",
- Email: "picker@example.com",
- }); err != nil {
- t.Fatalf("CherryPickCommit() returned error: %v", err)
- }
- status, _ := manager.Status(repoPath)
- if status.Head.AuthorName != "Original Author" {
- t.Errorf("author = %q, want the original author preserved", status.Head.AuthorName)
- }
- if status.Head.AuthorEmail != "original@example.com" {
- t.Errorf("author email = %q, want the original preserved", status.Head.AuthorEmail)
- }
- }
- func TestCherryPickRefusesConflict(t *testing.T) {
- manager := newTestManager(t)
- repoPath := newTestRepo(t, manager)
- base := commitFile(t, manager, repoPath, "shared.txt", "base\n", "base")
- //Feature edits shared.txt
- if err := manager.Checkout(repoPath, "feature", true); err != nil {
- t.Fatalf("Checkout(create) returned error: %v", err)
- }
- commitFile(t, manager, repoPath, "shared.txt", "feature edit\n", "feature edits shared")
- featureCommit := headHash(t, manager, repoPath)
- //Target branch edits shared.txt differently, so the pre-image no longer
- //matches and a clean cherry-pick is impossible.
- if err := manager.CheckoutCommit(repoPath, base); err != nil {
- t.Fatalf("CheckoutCommit(base) returned error: %v", err)
- }
- if err := manager.Checkout(repoPath, "target", true); err != nil {
- t.Fatalf("Checkout(create) returned error: %v", err)
- }
- commitFile(t, manager, repoPath, "shared.txt", "target edit\n", "target edits shared")
- if _, err := manager.CherryPickCommit(repoPath, featureCommit, &CommitRequest{Name: "T", Email: "t@e.com"}); err == nil {
- t.Errorf("CherryPickCommit() onto a diverged file = nil error, want a refusal")
- }
- status, _ := manager.Status(repoPath)
- if !status.Clean {
- t.Errorf("a refused cherry-pick left the tree dirty: %+v", status.Changes)
- }
- }
- func TestAmendCommitMessage(t *testing.T) {
- manager := newTestManager(t)
- repoPath := newTestRepo(t, manager)
- commitFile(t, manager, repoPath, "a.txt", "one\n", "typo in mesage")
- original := headHash(t, manager, repoPath)
- newHash, err := manager.AmendCommitMessage(repoPath, "fixed message", &CommitRequest{
- Name: "T",
- Email: "t@e.com",
- })
- if err != nil {
- t.Fatalf("AmendCommitMessage() returned error: %v", err)
- }
- if newHash == original {
- t.Errorf("amend produced the same hash %q, want a new commit object", newHash)
- }
- status, _ := manager.Status(repoPath)
- if status.Head.Subject != "fixed message" {
- t.Errorf("HEAD subject = %q, want the amended message", status.Head.Subject)
- }
- if status.Head.Hash != newHash {
- t.Errorf("HEAD = %q, want the amended commit %q", status.Head.Hash, newHash)
- }
- //The file content, i.e. the tree, must be unchanged
- content, _ := os.ReadFile(filepath.Join(repoPath, "a.txt"))
- if string(content) != "one\n" {
- t.Errorf("amend altered the tree, a.txt = %q", string(content))
- }
- //Only one commit should exist — amend replaces, not appends
- log, _ := manager.Log(repoPath, 10)
- if len(log) != 1 {
- t.Errorf("log has %d commits after amend, want 1", len(log))
- }
- }
- func TestAmendPreservesAuthorAcrossMessageChange(t *testing.T) {
- manager := newTestManager(t)
- repoPath := newTestRepo(t, manager)
- writeFile(t, repoPath, "a.txt", "one\n")
- if _, err := manager.Commit(repoPath, &CommitRequest{
- Message: "original",
- Files: []string{"a.txt"},
- Name: "First Author",
- Email: "first@example.com",
- }); err != nil {
- t.Fatalf("Commit() returned error: %v", err)
- }
- if _, err := manager.AmendCommitMessage(repoPath, "reworded", &CommitRequest{
- Name: "Someone Else",
- Email: "else@example.com",
- }); err != nil {
- t.Fatalf("AmendCommitMessage() returned error: %v", err)
- }
- status, _ := manager.Status(repoPath)
- if status.Head.AuthorName != "First Author" {
- t.Errorf("author = %q, want the original author preserved through amend", status.Head.AuthorName)
- }
- }
- func TestAmendValidation(t *testing.T) {
- manager := newTestManager(t)
- repoPath := newTestRepo(t, manager)
- commitFile(t, manager, repoPath, "a.txt", "one\n", "first")
- if _, err := manager.AmendCommitMessage(repoPath, " ", &CommitRequest{}); err == nil {
- t.Errorf("AmendCommitMessage() with a blank message = nil error, want an error")
- }
- }
- func TestAmendRefusesDetachedHead(t *testing.T) {
- manager := newTestManager(t)
- repoPath := newTestRepo(t, manager)
- first := commitFile(t, manager, repoPath, "a.txt", "one\n", "first")
- commitFile(t, manager, repoPath, "a.txt", "two\n", "second")
- if err := manager.CheckoutCommit(repoPath, first); err != nil {
- t.Fatalf("CheckoutCommit() returned error: %v", err)
- }
- if _, err := manager.AmendCommitMessage(repoPath, "new message", &CommitRequest{}); err == nil {
- t.Errorf("AmendCommitMessage() on a detached HEAD = nil error, want an error")
- }
- }
- func TestParseResetMode(t *testing.T) {
- tests := []struct {
- name string
- mode string
- wantError bool
- }{
- {name: "soft", mode: "soft"},
- {name: "mixed", mode: "mixed"},
- {name: "hard", mode: "hard"},
- {name: "empty defaults to mixed", mode: ""},
- {name: "uppercase", mode: "HARD"},
- {name: "padded", mode: " soft "},
- {name: "unknown", mode: "medium", wantError: true},
- }
- for _, test := range tests {
- t.Run(test.name, func(t *testing.T) {
- _, err := parseResetMode(test.mode)
- if test.wantError && err == nil {
- t.Errorf("parseResetMode(%q) = nil error, want an error", test.mode)
- }
- if !test.wantError && err != nil {
- t.Errorf("parseResetMode(%q) returned error: %v", test.mode, err)
- }
- })
- }
- }
- func TestHistoryActionsOnNonRepo(t *testing.T) {
- manager := newTestManager(t)
- plain := t.TempDir()
- dummy := strings.Repeat("a", 40)
- if err := manager.CheckoutCommit(plain, dummy); err != ErrNotARepo {
- t.Errorf("CheckoutCommit() on a plain folder = %v, want ErrNotARepo", err)
- }
- if err := manager.CreateTag(plain, "v1", dummy, "", "T", "t@e.com"); err != ErrNotARepo {
- t.Errorf("CreateTag() on a plain folder = %v, want ErrNotARepo", err)
- }
- if _, err := manager.TagsForCommit(plain, dummy); err != ErrNotARepo {
- t.Errorf("TagsForCommit() on a plain folder = %v, want ErrNotARepo", err)
- }
- }
|