Skip to content

Provisioning: commit only once staged changes #108590

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Jul 24, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Add unit tests
  • Loading branch information
MissingRoberto committed Jul 24, 2025
commit 5c30c3a81ae35480cadc3c8d0ebc3d374d244e15
34 changes: 26 additions & 8 deletions pkg/registry/apis/provisioning/repository/git/staged.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,10 @@ func (r *stagedGitRepository) Create(ctx context.Context, path, ref string, data
return err
}

if err := r.commit(ctx, r.writer, message); err != nil {
return err
if !r.opts.CommitOnlyOnce {
if err := r.commit(ctx, r.writer, message); err != nil {
return err
}
}

if r.opts.PushOnWrites {
Expand Down Expand Up @@ -116,8 +118,10 @@ func (r *stagedGitRepository) Write(ctx context.Context, path, ref string, data
}
}

if err := r.commit(ctx, r.writer, message); err != nil {
return err
if !r.opts.CommitOnlyOnce {
if err := r.commit(ctx, r.writer, message); err != nil {
return err
}
}

if r.opts.PushOnWrites {
Expand All @@ -140,8 +144,10 @@ func (r *stagedGitRepository) Update(ctx context.Context, path, ref string, data
return err
}

if err := r.commit(ctx, r.writer, message); err != nil {
return err
if !r.opts.CommitOnlyOnce {
if err := r.commit(ctx, r.writer, message); err != nil {
return err
}
}

if r.opts.PushOnWrites {
Expand All @@ -160,8 +166,10 @@ func (r *stagedGitRepository) Delete(ctx context.Context, path, ref, message str
return err
}

if err := r.commit(ctx, r.writer, message); err != nil {
return err
if !r.opts.CommitOnlyOnce {
if err := r.commit(ctx, r.writer, message); err != nil {
return err
}
}

if r.opts.PushOnWrites {
Expand All @@ -178,6 +186,16 @@ func (r *stagedGitRepository) Push(ctx context.Context) error {
defer cancel()
}

if r.opts.CommitOnlyOnce {
message := r.opts.CommitOnlyOnceMessage
if message == "" {
message = "Staged changes"
}
if err := r.commit(ctx, r.writer, message); err != nil {
return err
}
}

return r.writer.Push(ctx)
}

Expand Down
144 changes: 127 additions & 17 deletions pkg/registry/apis/provisioning/repository/git/staged_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package git
import (
"context"
"errors"
"strings"
"testing"
"time"

Expand Down Expand Up @@ -67,6 +68,23 @@ func TestNewStagedGitRepository(t *testing.T) {
},
wantError: nil,
},
{
name: "succeeds with CommitOnlyOnce option",
setupMock: func(mockClient *mocks.FakeClient) {
mockClient.GetRefReturns(nanogit.Ref{
Name: "refs/heads/main",
Hash: hash.Hash{1, 2, 3},
}, nil)
mockWriter := &mocks.FakeStagedWriter{}
mockClient.NewStagedWriterReturns(mockWriter, nil)
},
opts: repository.StageOptions{
PushOnWrites: false,
CommitOnlyOnce: true,
CommitOnlyOnceMessage: "Custom commit message",
},
wantError: nil,
},
{
name: "fails with GetRef error",
setupMock: func(mockClient *mocks.FakeClient) {
Expand Down Expand Up @@ -123,6 +141,8 @@ func TestNewStagedGitRepository(t *testing.T) {
actualOpts := stagedRepo.(*stagedGitRepository).opts
require.Equal(t, tt.opts.PushOnWrites, actualOpts.PushOnWrites)
require.Equal(t, tt.opts.Timeout, actualOpts.Timeout)
require.Equal(t, tt.opts.CommitOnlyOnce, actualOpts.CommitOnlyOnce)
require.Equal(t, tt.opts.CommitOnlyOnceMessage, actualOpts.CommitOnlyOnceMessage)
}
})
}
Expand Down Expand Up @@ -363,6 +383,22 @@ func TestStagedGitRepository_Create(t *testing.T) {
wantError: errors.New("push failed"),
expectPush: true, // Push is still called even though it fails
},
{
name: "succeeds with CommitOnlyOnce - no immediate commit",
setupMock: func(mockWriter *mocks.FakeStagedWriter) {
mockWriter.CreateBlobReturns(hash.Hash{1, 2, 3}, nil)
},
opts: repository.StageOptions{
PushOnWrites: false,
CommitOnlyOnce: true,
},
path: "test.yaml",
ref: "",
data: []byte("content"),
message: "Create test file",
wantError: nil,
expectPush: false,
},
}

for _, tt := range tests {
Expand All @@ -386,6 +422,15 @@ func TestStagedGitRepository_Create(t *testing.T) {
} else if tt.wantError == nil {
require.Equal(t, 0, mockWriter.PushCallCount())
}

// Verify commit behavior based on CommitOnlyOnce option
if tt.opts.CommitOnlyOnce {
require.Equal(t, 0, mockWriter.CommitCallCount(), "No commits should be made when CommitOnlyOnce is true")
} else if tt.wantError == nil {
require.Equal(t, 1, mockWriter.CommitCallCount(), "One commit should be made when CommitOnlyOnce is false")
} else if tt.wantError != nil && !strings.Contains(tt.wantError.Error(), "commit") {
require.Equal(t, 0, mockWriter.CommitCallCount(), "No commits should be made when error occurs before commit")
}
})
}
}
Expand Down Expand Up @@ -781,42 +826,96 @@ func TestStagedGitRepository_Delete(t *testing.T) {

func TestStagedGitRepository_Push(t *testing.T) {
tests := []struct {
name string
setupMock func(*mocks.FakeStagedWriter)
wantError error
expectCalls int
name string
opts repository.StageOptions
setupMock func(*mocks.FakeStagedWriter)
wantError error
expectPushCalls int
expectCommitCalls int
}{
{
name: "succeeds with empty ref",
name: "succeeds with normal push",
opts: repository.StageOptions{},
setupMock: func(mockWriter *mocks.FakeStagedWriter) {
mockWriter.PushReturns(nil)
},
wantError: nil,
expectCalls: 1,
wantError: nil,
expectPushCalls: 1,
expectCommitCalls: 0,
},
{
name: "succeeds with matching ref",
name: "succeeds with timeout",
opts: repository.StageOptions{
Timeout: time.Second * 5,
},
setupMock: func(mockWriter *mocks.FakeStagedWriter) {
mockWriter.PushReturns(nil)
},
wantError: nil,
expectCalls: 1,
wantError: nil,
expectPushCalls: 1,
expectCommitCalls: 0,
},
{
name: "succeeds with timeout",
name: "succeeds with CommitOnlyOnce and default message",
opts: repository.StageOptions{
CommitOnlyOnce: true,
},
setupMock: func(mockWriter *mocks.FakeStagedWriter) {
mockWriter.CommitReturns(&nanogit.Commit{}, nil)
mockWriter.PushReturns(nil)
},
wantError: nil,
expectPushCalls: 1,
expectCommitCalls: 1,
},
{
name: "succeeds with CommitOnlyOnce and custom message",
opts: repository.StageOptions{
CommitOnlyOnce: true,
CommitOnlyOnceMessage: "Custom commit message",
},
setupMock: func(mockWriter *mocks.FakeStagedWriter) {
mockWriter.CommitReturns(&nanogit.Commit{}, nil)
mockWriter.PushReturns(nil)
},
wantError: nil,
expectCalls: 1,
wantError: nil,
expectPushCalls: 1,
expectCommitCalls: 1,
},
{
name: "fails with commit error when CommitOnlyOnce",
opts: repository.StageOptions{
CommitOnlyOnce: true,
},
setupMock: func(mockWriter *mocks.FakeStagedWriter) {
mockWriter.CommitReturns(&nanogit.Commit{}, errors.New("commit failed"))
},
wantError: errors.New("commit changes: commit failed"),
expectPushCalls: 0,
expectCommitCalls: 1,
},
{
name: "fails with push error",
opts: repository.StageOptions{},
setupMock: func(mockWriter *mocks.FakeStagedWriter) {
mockWriter.PushReturns(errors.New("push failed"))
},
wantError: errors.New("push failed"),
expectCalls: 1,
wantError: errors.New("push failed"),
expectPushCalls: 1,
expectCommitCalls: 0,
},
{
name: "fails with push error after successful commit",
opts: repository.StageOptions{
CommitOnlyOnce: true,
},
setupMock: func(mockWriter *mocks.FakeStagedWriter) {
mockWriter.CommitReturns(&nanogit.Commit{}, nil)
mockWriter.PushReturns(errors.New("push failed"))
},
wantError: errors.New("push failed"),
expectPushCalls: 1,
expectCommitCalls: 1,
},
}

Expand All @@ -825,7 +924,7 @@ func TestStagedGitRepository_Push(t *testing.T) {
mockWriter := &mocks.FakeStagedWriter{}
tt.setupMock(mockWriter)

stagedRepo := createTestStagedRepositoryWithWriter(mockWriter, repository.StageOptions{})
stagedRepo := createTestStagedRepositoryWithWriter(mockWriter, tt.opts)

err := stagedRepo.Push(context.Background())

Expand All @@ -835,7 +934,18 @@ func TestStagedGitRepository_Push(t *testing.T) {
require.NoError(t, err)
}

require.Equal(t, tt.expectCalls, mockWriter.PushCallCount())
require.Equal(t, tt.expectPushCalls, mockWriter.PushCallCount())
require.Equal(t, tt.expectCommitCalls, mockWriter.CommitCallCount())

// Verify commit message when CommitOnlyOnce is used
if tt.opts.CommitOnlyOnce && tt.expectCommitCalls > 0 && tt.wantError == nil {
_, actualMessage, _, _ := mockWriter.CommitArgsForCall(0)
expectedMessage := tt.opts.CommitOnlyOnceMessage
if expectedMessage == "" {
expectedMessage = "Staged changes"
}
require.Equal(t, expectedMessage, actualMessage)
}
})
}
}
Expand Down
4 changes: 4 additions & 0 deletions pkg/registry/apis/provisioning/repository/staged.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ type StageOptions struct {
PushOnWrites bool
// Maximum time allowed for clone operation in seconds (0 means no limit)
Timeout time.Duration
// Commit only once on push instead of intermediate commits
CommitOnlyOnce bool
// Commit message to use when CommitOnlyOnce is true
CommitOnlyOnceMessage string
}

//go:generate mockery --name StageableRepository --structname MockStageableRepository --inpackage --filename stageable_repository_mock.go --with-expecter
Expand Down