package git import ( "errors" "fmt" "github.com/go-git/go-billy/v5" "github.com/go-git/go-billy/v5/memfs" "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/object" "github.com/go-git/go-git/v5/storage/memory" "github.com/stretchr/testify/assert" "testing" ) func TestCommit(t *testing.T) { t.Parallel() t.Run("successful run", func(t *testing.T) { t.Parallel() worktreeMock := WorktreeMock{} hash, err := commitSingleFile(".", "message", "user", &worktreeMock) assert.NoError(t, err) assert.Equal(t, plumbing.Hash([20]byte{4, 5, 6}), hash) assert.Equal(t, "user", worktreeMock.author) assert.True(t, worktreeMock.commitAll) }) t.Run("error adding file", func(t *testing.T) { t.Parallel() _, err := commitSingleFile(".", "message", "user", WorktreeMockFailing{ failingAdd: true, }) assert.EqualError(t, err, "failed to add file to git: failed to add file") }) t.Run("error committing file", func(t *testing.T) { t.Parallel() _, err := commitSingleFile(".", "message", "user", WorktreeMockFailing{ failingCommit: true, }) assert.EqualError(t, err, "failed to commit file: failed to commit file") }) } func TestPushChangesToRepository(t *testing.T) { t.Parallel() t.Run("successful push", func(t *testing.T) { t.Parallel() err := pushChangesToRepository("user", "password", RepositoryMock{ test: t, }) assert.NoError(t, err) }) t.Run("error pushing", func(t *testing.T) { t.Parallel() err := pushChangesToRepository("user", "password", RepositoryMockError{}) assert.EqualError(t, err, "failed to push commit: error on push commits") }) } func TestPlainClone(t *testing.T) { t.Parallel() t.Run("successful clone", func(t *testing.T) { t.Parallel() abstractedGit := &UtilsGitMock{} _, err := plainClone("user", "password", "URL", "directory", abstractedGit) assert.NoError(t, err) assert.Equal(t, "directory", abstractedGit.path) assert.False(t, abstractedGit.isBare) assert.Equal(t, "http-basic-auth - user:*******", abstractedGit.authString) assert.Equal(t, "URL", abstractedGit.URL) }) t.Run("error on cloning", func(t *testing.T) { t.Parallel() abstractedGit := UtilsGitMockError{} _, err := plainClone("user", "password", "URL", "directory", abstractedGit) assert.EqualError(t, err, "failed to clone git: error during clone") }) } func TestPlainOpenMock(t *testing.T) { t.Parallel() t.Run("successful clone", func(t *testing.T) { t.Parallel() abstractedGit := &UtilsGitMock{} _, err := plainOpen("directory", abstractedGit) assert.NoError(t, err) assert.Equal(t, "directory", abstractedGit.path) }) t.Run("error on cloning", func(t *testing.T) { t.Parallel() abstractedGit := UtilsGitMockError{} _, err := plainOpen("directory", abstractedGit) assert.EqualError(t, err, "Unable to open git repository at 'directory': error during git plain open") }) } func TestChangeBranch(t *testing.T) { t.Parallel() t.Run("checkout existing branch", func(t *testing.T) { t.Parallel() worktreeMock := &WorktreeMock{} err := changeBranch("otherBranch", worktreeMock) assert.NoError(t, err) assert.Equal(t, string(plumbing.NewBranchReferenceName("otherBranch")), worktreeMock.checkedOutBranch) assert.False(t, worktreeMock.create) }) t.Run("empty branch raises error", func(t *testing.T) { t.Parallel() worktreeMock := &WorktreeMock{} err := changeBranch("", worktreeMock) assert.EqualError(t, err, "no branch name provided") }) t.Run("create new branch", func(t *testing.T) { t.Parallel() err := changeBranch("otherBranch", WorktreeUtilsNewBranch{}) assert.NoError(t, err) }) t.Run("error on new branch", func(t *testing.T) { t.Parallel() err := changeBranch("otherBranch", WorktreeMockFailing{ failingCheckout: true, }) assert.EqualError(t, err, "failed to checkout branch: failed to checkout branch") }) } func TestLogRange(t *testing.T) { against := func(t *testing.T, r *git.Repository, from, to string, expected []plumbing.Hash) { seen := []plumbing.Hash{} cIter, err := LogRange(r, from, to) if assert.NoError(t, err) { err = cIter.ForEach(func(c *object.Commit) error { seen = append(seen, c.ID()) return nil }) if assert.NoError(t, err) { if assert.Len(t, seen, len(expected)) { assert.Subset(t, seen, expected) } } } } prepareRepo := func() (r *git.Repository, hashes map[string]plumbing.Hash, err error) { hashes = map[string]plumbing.Hash{} // Creates a commit c := func(r *git.Repository, fs billy.Filesystem, name string) (hash plumbing.Hash, err error) { if val, ok := hashes[name]; ok { err = fmt.Errorf("Cannot create commit for name '%s'. There is already a commit available (%s) for that name", name, val) return } w, err := r.Worktree() if err != nil { return } f, err := fs.Create(fmt.Sprintf("commit%s.txt", name)) if err != nil { return } _, err = f.Write([]byte(fmt.Sprintf("Commit %s", name))) if err != nil { return } _, err = w.Add(fmt.Sprintf("commit%s.txt", name)) if err != nil { return } hash, err = w.Commit(fmt.Sprintf("Commit %s", name), &git.CommitOptions{}) if err != nil { return } hashes[name] = hash return } // Creates a branch on the currently checked out commit b := func(r *git.Repository, name string) (err error) { w, err := r.Worktree() if err != nil { return } b := plumbing.ReferenceName(fmt.Sprintf("refs/heads/%s", name)) err = w.Checkout(&git.CheckoutOptions{Create: true, Force: false, Branch: b}) return } fs := memfs.New() // create new git repo r, err = git.Init(memory.NewStorage(), fs) if err != nil { return } config, err := r.Config() if err != nil { return } config.User.Name = "me" config.User.Email = "me@example.org" err = r.SetConfig(config) if err != nil { return } w, err := r.Worktree() if err != nil { return } // add a commit to the repo -- A -- hashA, err := c(r, fs, "A") if err != nil { return } _, err = r.CreateTag("initial", hashA, &git.CreateTagOptions{ Tagger: &object.Signature{ Name: config.User.Name, Email: config.User.Email, }, Message: "initial", }) if err != nil { return } // another commit -- B -- _, err = c(r, fs, "B") if err != nil { return } // checkout the first commit again err = w.Checkout(&git.CheckoutOptions{Hash: hashA}) if err != nil { return } // add another file as sucessor of the first commit -- C -- _, err = c(r, fs, "C") if err != nil { return } // and yet another commit -- D -- _, err = c(r, fs, "D") if err != nil { return } err = b(r, "branch1") return } r, hashes, err := prepareRepo() if err != nil { assert.FailNow(t, fmt.Sprintf("%v", err), err) } // Our repo contains these commits and branches: // // / C - D <-- HEAD <-- branch1 // A - B <-- master // // Tag 'initial' sits on commit A t.Parallel() t.Run("B against D", func(t *testing.T) { against(t, r, hashes["B"].String(), hashes["D"].String(), []plumbing.Hash{hashes["C"], hashes["D"]}) }) t.Run("A against B", func(t *testing.T) { against(t, r, hashes["A"].String(), hashes["B"].String(), []plumbing.Hash{hashes["B"]}) }) t.Run("B against HEAD", func(t *testing.T) { against(t, r, hashes["B"].String(), "HEAD", []plumbing.Hash{hashes["C"], hashes["D"]}) }) t.Run("B against HEAD~1", func(t *testing.T) { against(t, r, hashes["B"].String(), "HEAD~1", []plumbing.Hash{hashes["C"]}) }) t.Run("A against master", func(t *testing.T) { against(t, r, hashes["A"].String(), "master", []plumbing.Hash{hashes["B"]}) }) t.Run("master against a branch pointing to D", func(t *testing.T) { against(t, r, "master", "branch1", []plumbing.Hash{hashes["C"], hashes["D"]}) }) t.Run("A against master~1", func(t *testing.T) { against(t, r, hashes["A"].String(), "master~1", []plumbing.Hash{}) }) t.Run("Tag against C", func(t *testing.T) { against(t, r, "initial", hashes["C"].String(), []plumbing.Hash{hashes["C"]}) }) t.Run("Same ref results in empty result", func(t *testing.T) { against(t, r, hashes["A"].String(), hashes["A"].String(), []plumbing.Hash{}) }) t.Run("Invalid ref", func(t *testing.T) { // it is unlikely as hell, but at some time we might get a test failure here // when a commit with this hash has been created during preparation of the repo. // Maybe we should check first if a commit with that hash exists and if so try // another hash. paranoia :-) _, err := LogRange(r, "0123456789012345678901234567890123456789", "HEAD") assert.EqualError(t, err, "Cannot provide log range (from: '0123456789012345678901234567890123456789' not found): Trouble resolving '0123456789012345678901234567890123456789': reference not found") }) t.Run("Empty string as ref", func(t *testing.T) { _, err := LogRange(r, "", "HEAD") assert.EqualError(t, err, "Cannot provide log range (from: '' not found): Cannot get a commit for an empty ref") }) } type RepositoryMock struct { worktree *git.Worktree test *testing.T } func (r RepositoryMock) Worktree() (*git.Worktree, error) { if r.worktree != nil { return r.worktree, nil } return &git.Worktree{}, nil } func (r RepositoryMock) Push(o *git.PushOptions) error { assert.Equal(r.test, "http-basic-auth - user:*******", o.Auth.String()) return nil } type RepositoryMockError struct{} func (RepositoryMockError) Worktree() (*git.Worktree, error) { return nil, errors.New("error getting worktree") } func (RepositoryMockError) Push(*git.PushOptions) error { return errors.New("error on push commits") } type WorktreeMockFailing struct { failingAdd bool failingCommit bool failingCheckout bool } func (w WorktreeMockFailing) Add(string) (plumbing.Hash, error) { if w.failingAdd { return [20]byte{}, errors.New("failed to add file") } return [20]byte{}, nil } func (w WorktreeMockFailing) Commit(string, *git.CommitOptions) (plumbing.Hash, error) { if w.failingCommit { return [20]byte{}, errors.New("failed to commit file") } return [20]byte{}, nil } func (w WorktreeMockFailing) Checkout(*git.CheckoutOptions) error { if w.failingCheckout { return errors.New("failed to checkout branch") } return nil } type WorktreeMock struct { expectedBranchName string checkedOutBranch string create bool author string commitAll bool } func (WorktreeMock) Add(string) (plumbing.Hash, error) { return [20]byte{1, 2, 3}, nil } func (w *WorktreeMock) Commit(_ string, options *git.CommitOptions) (plumbing.Hash, error) { w.author = options.Author.Name w.commitAll = options.All return [20]byte{4, 5, 6}, nil } func (w *WorktreeMock) Checkout(opts *git.CheckoutOptions) error { w.checkedOutBranch = string(opts.Branch) w.create = opts.Create return nil } type WorktreeUtilsNewBranch struct{} func (WorktreeUtilsNewBranch) Add(string) (plumbing.Hash, error) { panic("implement me") } func (WorktreeUtilsNewBranch) Commit(string, *git.CommitOptions) (plumbing.Hash, error) { panic("implement me") } func (WorktreeUtilsNewBranch) Checkout(opts *git.CheckoutOptions) error { if opts.Create { return nil } return errors.New("branch already exists") } type UtilsGitMock struct { path string isBare bool authString string URL string } func (u *UtilsGitMock) plainClone(path string, isBare bool, o *git.CloneOptions) (*git.Repository, error) { u.path = path u.isBare = isBare u.authString = o.Auth.String() u.URL = o.URL return nil, nil } func (u *UtilsGitMock) plainOpen(path string) (*git.Repository, error) { u.path = path return nil, nil } type UtilsGitMockError struct{} func (UtilsGitMockError) plainClone(string, bool, *git.CloneOptions) (*git.Repository, error) { return nil, errors.New("error during clone") } func (UtilsGitMockError) plainOpen(path string) (*git.Repository, error) { return nil, errors.New("error during git plain open") }