diff --git a/applier.go b/applier.go index 1091749..45cb798 100644 --- a/applier.go +++ b/applier.go @@ -32,7 +32,8 @@ type Applier struct { entries map[string]*github.TreeEntry uncommitted bool - applyOptions []gitdiff.ApplyOption + applyOptions []gitdiff.ApplyOption + allowEmptyCommits bool } // NewApplier creates a new Applier for a repository. The Applier applies @@ -53,6 +54,13 @@ func (a *Applier) SetApplyOptions(opts ...gitdiff.ApplyOption) { a.applyOptions = opts } +// SetAllowEmptyCommits controls whether Commit may create a commit without +// pending changes. Empty commits reuse the current tree. The default is false. +// This setting is preserved across calls to Commit and Reset. +func (a *Applier) SetAllowEmptyCommits(allow bool) { + a.allowEmptyCommits = allow +} + // Apply applies the changes in a file, adds the result to the list of pending // tree entries, and returns the entry. If the application succeeds, Apply // creates a blob in the repository with the modified content. @@ -231,8 +239,8 @@ func (a *Applier) CreateTree(ctx context.Context) (*github.Tree, error) { // Commit commits the latest tree, optionally using the details in tmpl and // header. If there are pending tree entries, it calls CreateTree before -// creating the commit. It returns an error if there are no pending trees or -// tree entries. +// creating the commit. If there are no pending trees or tree entries, it returns +// an error unless empty commits are enabled with [Applier.SetAllowEmptyCommits]. // // If tmpl is not nil, Commit uses it as a template for the new commit, // overwriting fields as needed. If header is not nil, Commit uses it to set @@ -243,7 +251,7 @@ func (a *Applier) CreateTree(ctx context.Context) (*github.Tree, error) { // message, the current time, and the authenticated user as needed for the // commit details. func (a *Applier) Commit(ctx context.Context, tmpl *github.Commit, header *gitdiff.PatchHeader) (*github.Commit, error) { - if !a.uncommitted && len(a.entries) == 0 { + if !a.allowEmptyCommits && !a.uncommitted && len(a.entries) == 0 { return nil, errors.New("no pending tree or tree entries") } if len(a.entries) > 0 { diff --git a/applier_test.go b/applier_test.go index 3f50ed8..47746c1 100644 --- a/applier_test.go +++ b/applier_test.go @@ -53,6 +53,35 @@ func TestApplier(t *testing.T) { } } +func TestApplierSupportsEmptyCommits(t *testing.T) { + tctx := prepareTestContext(t) + + t.Logf("Test ID: %s", tctx.ID) + t.Logf("Test Repository: %s", tctx.Repo.String()) + + createBranch(t, tctx) + defer cleanupBranches(t, tctx) + + applier := NewApplier(tctx.Client, tctx.Repo, tctx.BaseCommit) + applier.SetAllowEmptyCommits(true) + + commit, err := applier.Commit(tctx, nil, &gitdiff.PatchHeader{Title: "empty"}) + if err != nil { + t.Fatalf("error committing changes form empty patch: %v", err) + } + // calling "Commit" on the applier without making any calls to "Apply" to apply patches should create a new empty + // commit. The returned commit should be different from the base commit, its tree should be the same. + if commit.GetSHA() == tctx.BaseCommit.GetSHA() { + t.Errorf("empty commit has same SHA as base commit: %s", commit.GetSHA()) + } + if len(commit.Parents) != 1 || commit.Parents[0].GetSHA() != tctx.BaseCommit.GetSHA() { + t.Errorf("empty commit does not have base commit as its parent: %v", commit.Parents) + } + if commit.GetTree().GetSHA() != tctx.BaseTree.GetSHA() { + t.Errorf("empty commit changed tree: expected %s, actual %s", tctx.BaseTree.GetSHA(), commit.GetTree().GetSHA()) + } +} + type TestContext struct { context.Context