Skip to content

Commit

Permalink
Return error instead of creating empty commits
Browse files Browse the repository at this point in the history
BuildTree now returns an ErrEmptyCommit error, when there are no
changes to be committed. This can be opted-out via
CommitOptions.AllowEmptyCommits.

This is a breaking change which enables applications to detect when
empty commits are to be created.

Some instances in which this can occur is when the fs (e.g. `billy/osfs`)
make changes to the underlying files, causing a conflict between what
the previous Git worktree state was, and the current state. Changes to
the fs implementations are orthogonal to this, and will be dealt with
separately.

The new behaviour aligns with the Git CLI, in which empty commits
returns the error message: 'nothing to commit, working tree clean'.

Signed-off-by: Paulo Gomes <pjbgf@linux.com>
  • Loading branch information
pjbgf committed Dec 3, 2022
1 parent 3e07c50 commit a513415
Show file tree
Hide file tree
Showing 4 changed files with 75 additions and 6 deletions.
4 changes: 4 additions & 0 deletions options.go
Expand Up @@ -458,6 +458,10 @@ type CommitOptions struct {
// All automatically stage files that have been modified and deleted, but
// new files you have not told Git about are not affected.
All bool
// AllowEmptyCommits enable empty commits to be created. An empty commit
// is when no changes to the tree were made, but a new commit message is
// provided. The default behavior is false, which results in ErrEmptyCommit.
AllowEmptyCommits bool
// Author is the author's signature of the commit. If Author is empty the
// Name and Email is read from the config, and time.Now it's used as When.
Author *object.Signature
Expand Down
15 changes: 13 additions & 2 deletions worktree_commit.go
Expand Up @@ -2,6 +2,7 @@ package git

import (
"bytes"
"errors"
"path"
"sort"
"strings"
Expand All @@ -16,6 +17,12 @@ import (
"github.com/go-git/go-billy/v5"
)

var (
// ErrEmptyCommit occurs when a commit is attempted using a clean
// working tree, with no changes to be committed.
ErrEmptyCommit = errors.New("cannot create empty commit: clean working tree")
)

// Commit stores the current contents of the index in a new commit along with
// a log message from the user describing the changes.
func (w *Worktree) Commit(msg string, opts *CommitOptions) (plumbing.Hash, error) {
Expand All @@ -39,7 +46,7 @@ func (w *Worktree) Commit(msg string, opts *CommitOptions) (plumbing.Hash, error
s: w.r.Storer,
}

tree, err := h.BuildTree(idx)
tree, err := h.BuildTree(idx, opts)
if err != nil {
return plumbing.ZeroHash, err
}
Expand Down Expand Up @@ -145,7 +152,11 @@ type buildTreeHelper struct {

// BuildTree builds the tree objects and push its to the storer, the hash
// of the root tree is returned.
func (h *buildTreeHelper) BuildTree(idx *index.Index) (plumbing.Hash, error) {
func (h *buildTreeHelper) BuildTree(idx *index.Index, opts *CommitOptions) (plumbing.Hash, error) {
if len(idx.Entries) == 0 && (opts == nil || !opts.AllowEmptyCommits) {
return plumbing.ZeroHash, ErrEmptyCommit
}

const rootNode = ""
h.trees = map[string]*object.Tree{rootNode: {}}
h.entries = map[string]*object.TreeEntry{}
Expand Down
26 changes: 25 additions & 1 deletion worktree_commit_test.go
Expand Up @@ -26,12 +26,18 @@ import (
)

func (s *WorktreeSuite) TestCommitEmptyOptions(c *C) {
r, err := Init(memory.NewStorage(), memfs.New())
fs := memfs.New()
r, err := Init(memory.NewStorage(), fs)
c.Assert(err, IsNil)

w, err := r.Worktree()
c.Assert(err, IsNil)

util.WriteFile(fs, "foo", []byte("foo"), 0644)

_, err = w.Add("foo")
c.Assert(err, IsNil)

hash, err := w.Commit("foo", &CommitOptions{})
c.Assert(err, IsNil)
c.Assert(hash.IsZero(), Equals, false)
Expand Down Expand Up @@ -65,6 +71,24 @@ func (s *WorktreeSuite) TestCommitInitial(c *C) {
assertStorageStatus(c, r, 1, 1, 1, expected)
}

func (s *WorktreeSuite) TestNothingToCommit(c *C) {
expected := plumbing.NewHash("838ea833ce893e8555907e5ef224aa076f5e274a")

r, err := Init(memory.NewStorage(), memfs.New())
c.Assert(err, IsNil)

w, err := r.Worktree()
c.Assert(err, IsNil)

hash, err := w.Commit("failed empty commit\n", &CommitOptions{Author: defaultSignature()})
c.Assert(hash, Equals, plumbing.ZeroHash)
c.Assert(err, Equals, ErrEmptyCommit)

hash, err = w.Commit("enable empty commits\n", &CommitOptions{Author: defaultSignature(), AllowEmptyCommits: true})
c.Assert(hash, Equals, expected)
c.Assert(err, IsNil)
}

func (s *WorktreeSuite) TestCommitParent(c *C) {
expected := plumbing.NewHash("ef3ca05477530b37f48564be33ddd48063fc7a22")

Expand Down
36 changes: 33 additions & 3 deletions worktree_test.go
Expand Up @@ -3,7 +3,6 @@ package git
import (
"bytes"
"context"
"errors"
"io"
"io/ioutil"
"os"
Expand Down Expand Up @@ -2167,6 +2166,8 @@ func (s *WorktreeSuite) TestGrep(c *C) {
}

func (s *WorktreeSuite) TestAddAndCommit(c *C) {
expectedFiles := 2

dir, clean := s.TemporalDir()
defer clean()

Expand All @@ -2176,29 +2177,58 @@ func (s *WorktreeSuite) TestAddAndCommit(c *C) {
w, err := repo.Worktree()
c.Assert(err, IsNil)

os.WriteFile(filepath.Join(dir, "foo"), []byte("bar"), 0o644)
os.WriteFile(filepath.Join(dir, "bar"), []byte("foo"), 0o644)

_, err = w.Add(".")
c.Assert(err, IsNil)

w.Commit("Test Add And Commit", &CommitOptions{Author: &object.Signature{
_, err = w.Commit("Test Add And Commit", &CommitOptions{Author: &object.Signature{
Name: "foo",
Email: "foo@foo.foo",
When: time.Now(),
}})
c.Assert(err, IsNil)

iter, err := w.r.Log(&LogOptions{})
c.Assert(err, IsNil)

filesFound := 0
err = iter.ForEach(func(c *object.Commit) error {
files, err := c.Files()
if err != nil {
return err
}

err = files.ForEach(func(f *object.File) error {
return errors.New("Expected no files, got at least 1")
filesFound++
return nil
})
return err
})
c.Assert(err, IsNil)
c.Assert(filesFound, Equals, expectedFiles)
}

func (s *WorktreeSuite) TestAddAndCommitEmpty(c *C) {
dir, clean := s.TemporalDir()
defer clean()

repo, err := PlainInit(dir, false)
c.Assert(err, IsNil)

w, err := repo.Worktree()
c.Assert(err, IsNil)

_, err = w.Add(".")
c.Assert(err, IsNil)

_, err = w.Commit("Test Add And Commit", &CommitOptions{Author: &object.Signature{
Name: "foo",
Email: "foo@foo.foo",
When: time.Now(),
}})
c.Assert(err, Equals, ErrEmptyCommit)
}

func (s *WorktreeSuite) TestLinkedWorktree(c *C) {
Expand Down

0 comments on commit a513415

Please sign in to comment.