Skip to content

Commit 2d5c88a

Browse files
committed
fix: auto-initialize empty GitOps repo instead of erroring
go-git cannot clone a remote with no commits, so the first GitOps publish to a freshly-created repo failed with "clone: remote repository is empty". Detect transport.ErrEmptyRemoteRepository and fall back to an in-memory init on the target branch wired to the remote; the first WriteFile commit then creates the branch on push (explicit branch refspec).
1 parent 461feb1 commit 2d5c88a

2 files changed

Lines changed: 73 additions & 0 deletions

File tree

aif-operator/internal/git/client.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@ import (
1111
"github.com/go-git/go-billy/v5"
1212
"github.com/go-git/go-billy/v5/memfs"
1313
gogit "github.com/go-git/go-git/v5"
14+
"github.com/go-git/go-git/v5/config"
1415
"github.com/go-git/go-git/v5/plumbing"
1516
"github.com/go-git/go-git/v5/plumbing/object"
17+
"github.com/go-git/go-git/v5/plumbing/transport"
1618
gogithttp "github.com/go-git/go-git/v5/plumbing/transport/http"
1719
"github.com/go-git/go-git/v5/storage/memory"
1820

@@ -124,6 +126,12 @@ func (c *Client) clone(ctx context.Context) (*gogit.Repository, *gogit.Worktree,
124126
Depth: 1,
125127
Auth: c.auth,
126128
})
129+
if errors.Is(err, transport.ErrEmptyRemoteRepository) {
130+
// A freshly created GitOps repo has no commits yet, so go-git cannot
131+
// clone it. Initialize an in-memory repo on the target branch wired to
132+
// the remote; the first WriteFile commit then creates the branch on push.
133+
return c.initEmpty()
134+
}
127135
if err != nil {
128136
return nil, nil, fmt.Errorf("clone: %w", err)
129137
}
@@ -134,6 +142,32 @@ func (c *Client) clone(ctx context.Context) (*gogit.Repository, *gogit.Worktree,
134142
return repo, wt, nil
135143
}
136144

145+
// initEmpty builds an in-memory repository positioned on c.branch with the
146+
// remote configured, for when the remote exists but has no commits.
147+
func (c *Client) initEmpty() (*gogit.Repository, *gogit.Worktree, error) {
148+
repo, err := gogit.Init(memory.NewStorage(), memfs.New())
149+
if err != nil {
150+
return nil, nil, fmt.Errorf("init empty repo: %w", err)
151+
}
152+
if _, err := repo.CreateRemote(&config.RemoteConfig{
153+
Name: gogit.DefaultRemoteName,
154+
URLs: []string{c.repoURL},
155+
}); err != nil {
156+
return nil, nil, fmt.Errorf("create remote: %w", err)
157+
}
158+
// Point HEAD at the target branch so the initial commit lands there even
159+
// though it does not exist yet (orphan/initial commit).
160+
headRef := plumbing.NewSymbolicReference(plumbing.HEAD, plumbing.NewBranchReferenceName(c.branch))
161+
if err := repo.Storer.SetReference(headRef); err != nil {
162+
return nil, nil, fmt.Errorf("set HEAD to %s: %w", c.branch, err)
163+
}
164+
wt, err := repo.Worktree()
165+
if err != nil {
166+
return nil, nil, fmt.Errorf("worktree: %w", err)
167+
}
168+
return repo, wt, nil
169+
}
170+
137171
func (c *Client) commitAndPush(ctx context.Context, repo *gogit.Repository, wt *gogit.Worktree, msg string) (string, error) {
138172
hash, err := wt.Commit(msg, &gogit.CommitOptions{
139173
Author: &object.Signature{
@@ -145,8 +179,12 @@ func (c *Client) commitAndPush(ctx context.Context, repo *gogit.Repository, wt *
145179
if err != nil {
146180
return "", fmt.Errorf("commit: %w", err)
147181
}
182+
// Push the branch explicitly so this also creates the branch on a
183+
// previously-empty remote (where there is no tracking config to default to).
184+
branchRef := plumbing.NewBranchReferenceName(c.branch)
148185
if err := repo.PushContext(ctx, &gogit.PushOptions{
149186
RemoteName: gogit.DefaultRemoteName,
187+
RefSpecs: []config.RefSpec{config.RefSpec(fmt.Sprintf("%s:%s", branchRef, branchRef))},
150188
Auth: c.auth,
151189
}); err != nil && !errors.Is(err, gogit.NoErrAlreadyUpToDate) {
152190
return "", fmt.Errorf("push: %w", err)

aif-operator/internal/git/client_test.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,16 @@ func newTestRemote(t *testing.T) string {
6969
return "file://" + remoteDir
7070
}
7171

72+
// newEmptyTestRemote creates a bare git repo with NO commits (no branches),
73+
// mimicking a freshly created, never-initialized GitOps repository.
74+
func newEmptyTestRemote(t *testing.T) string {
75+
t.Helper()
76+
remoteDir := t.TempDir()
77+
_, err := gogit.PlainInit(remoteDir, true)
78+
require.NoError(t, err)
79+
return "file://" + remoteDir
80+
}
81+
7282
// readFileFromRemote clones the remote and reads filePath. Returns error if file not found.
7383
func readFileFromRemote(t *testing.T, remoteURL, filePath string) (string, error) {
7484
t.Helper()
@@ -120,6 +130,31 @@ func TestWriteFile(t *testing.T) {
120130
require.Equal(t, "content: true\n", got)
121131
}
122132

133+
func TestWriteFile_EmptyRemoteInitializes(t *testing.T) {
134+
remote := newEmptyTestRemote(t)
135+
c := newClient(t, remote)
136+
137+
hash, err := c.WriteFile(context.Background(),
138+
filepath.Join("workloads", "test.yaml"),
139+
"content: true\n",
140+
"chore: initialize gitops repo")
141+
require.NoError(t, err)
142+
require.NotEmpty(t, hash)
143+
144+
got, err := readFileFromRemote(t, remote, filepath.Join("workloads", "test.yaml"))
145+
require.NoError(t, err)
146+
require.Equal(t, "content: true\n", got)
147+
}
148+
149+
func TestDeleteFile_EmptyRemoteIsNoOp(t *testing.T) {
150+
remote := newEmptyTestRemote(t)
151+
c := newClient(t, remote)
152+
153+
hash, err := c.DeleteFile(context.Background(), "workloads/missing.yaml", "remove")
154+
require.NoError(t, err)
155+
require.Empty(t, hash) // nothing to delete on an empty repo
156+
}
157+
123158
func TestWriteFile_UnchangedContentIsNoOp(t *testing.T) {
124159
remote := newTestRemote(t)
125160
c := newClient(t, remote)

0 commit comments

Comments
 (0)