Skip to content

Commit c8466c7

Browse files
committed
gitsync: fix remote tracking
The bug was that `reference: refs/heads/main` would only check out once, and not update the local repo and checkout after commits were pushed to the remote. Signed-off-by: Stephan Renatus <stephan.renatus@gmail.com>
1 parent 73351b7 commit c8466c7

File tree

2 files changed

+304
-3
lines changed

2 files changed

+304
-3
lines changed

internal/gitsync/gitsync.go

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -244,8 +244,8 @@ func (s *Synchronizer) execute(ctx context.Context) (bool, string, error) {
244244
Auth: authMethod,
245245
Force: true,
246246
RefSpecs: []gitconfig.RefSpec{
247-
gitconfig.RefSpec(fmt.Sprintf("+refs/heads/*:refs/remotes/%s/refs/heads/*", remote)),
248-
gitconfig.RefSpec(fmt.Sprintf("+refs/tags/*:refs/remotes/%s/refs/tags/*", remote)),
247+
gitconfig.RefSpec(fmt.Sprintf("+refs/heads/*:refs/remotes/%s/*", remote)),
248+
gitconfig.RefSpec("+refs/tags/*:refs/tags/*"),
249249
},
250250
}); err != nil && err != git.NoErrAlreadyUpToDate {
251251
return false, "", err
@@ -258,7 +258,15 @@ func (s *Synchronizer) execute(ctx context.Context) (bool, string, error) {
258258
case s.config.Commit != nil:
259259
opts.Hash = plumbing.NewHash(*s.config.Commit)
260260
case s.config.Reference != nil:
261-
ref := fmt.Sprintf("refs/remotes/%s/%s", remote, *s.config.Reference)
261+
ref := *s.config.Reference
262+
if after, ok := strings.CutPrefix(ref, "refs/heads/"); ok {
263+
ref = fmt.Sprintf("refs/remotes/%s/%s", remote, after)
264+
} else if strings.HasPrefix(ref, "refs/tags/") {
265+
// Tags are stored locally as refs/tags/*, so keep the full path
266+
} else {
267+
// Short name like "main" -> refs/remotes/origin/main
268+
ref = fmt.Sprintf("refs/remotes/%s/%s", remote, ref)
269+
}
262270
opts.Branch = plumbing.ReferenceName(ref)
263271
}
264272

internal/gitsync/gitsync_test.go

Lines changed: 293 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"testing"
1616

1717
"github.com/go-git/go-git/v5"
18+
"github.com/go-git/go-git/v5/plumbing"
1819
"github.com/go-git/go-git/v5/plumbing/object"
1920
"github.com/open-policy-agent/opa-control-plane/internal/config"
2021
"github.com/open-policy-agent/opa-control-plane/internal/gitsync"
@@ -114,6 +115,298 @@ func TestGitsyncLocal(t *testing.T) {
114115
}
115116
}
116117

118+
// TestGitsyncLocalMainBranch tests the specific bug where using refs/heads/main
119+
// causes subsequent updates to not fetch new commits due to refspec mismatch.
120+
func TestGitsyncLocalMainBranch(t *testing.T) {
121+
// Create a git repository to use for testing.
122+
testRepositoryPath := t.TempDir() + "/testing"
123+
repository, err := git.PlainInit(testRepositoryPath, false)
124+
if err != nil {
125+
t.Fatalf("expected no error while initializing test repository: %v", err)
126+
}
127+
128+
// Get worktree
129+
w, err := repository.Worktree()
130+
if err != nil {
131+
t.Fatalf("expected no error while getting worktree: %v", err)
132+
}
133+
134+
// Create initial commit on master/main
135+
err = os.WriteFile(testRepositoryPath+"/README", []byte("first commit"), 0644)
136+
if err != nil {
137+
t.Fatalf("expected no error while creating new file: %v", err)
138+
}
139+
140+
_, err = w.Add("README")
141+
if err != nil {
142+
t.Fatalf("expected no error while adding file to worktree: %v", err)
143+
}
144+
145+
firstCommit, err := w.Commit("first", &git.CommitOptions{Author: &object.Signature{}})
146+
if err != nil {
147+
t.Fatalf("expected no error while committing changes: %v", err)
148+
}
149+
150+
// Create main branch pointing to the commit (go-git defaults to master)
151+
headRef, err := repository.Head()
152+
if err != nil {
153+
t.Fatalf("expected no error getting HEAD: %v", err)
154+
}
155+
156+
if err := w.Checkout(&git.CheckoutOptions{
157+
Branch: "refs/heads/main",
158+
Hash: headRef.Hash(),
159+
Create: true,
160+
}); err != nil {
161+
t.Fatalf("expected no error creating main branch: %v", err)
162+
}
163+
164+
// Create a new synchronizer with an empty directory to clone a repository into.
165+
clonedRepositoryPath := t.TempDir() + "/test-repo"
166+
167+
ref := "refs/heads/main"
168+
s := gitsync.New(clonedRepositoryPath, config.Git{
169+
Repo: testRepositoryPath,
170+
Reference: &ref,
171+
Commit: nil,
172+
}, "test-source")
173+
174+
ctx := t.Context()
175+
meta1, err := s.Execute(ctx)
176+
if err != nil {
177+
t.Fatalf("expected no error on first execute, got %v", err)
178+
}
179+
180+
// Verify the first commit hash
181+
if meta1["commit"] != firstCommit.String() {
182+
t.Fatalf("expected first commit hash %s, got %s", firstCommit.String(), meta1["commit"])
183+
}
184+
185+
data, err := os.ReadFile(clonedRepositoryPath + "/README")
186+
if err != nil {
187+
t.Fatalf("expected no error while reading file, got: %v", err)
188+
}
189+
190+
if string(data) != "first commit" {
191+
t.Fatalf("expected file content to be 'first commit', got: %s", string(data))
192+
}
193+
194+
// Inspect what refs were created after the clone
195+
clonedRepo, err := git.PlainOpen(clonedRepositoryPath)
196+
if err != nil {
197+
t.Fatalf("expected no error opening cloned repo: %v", err)
198+
}
199+
200+
refs, err := clonedRepo.References()
201+
if err != nil {
202+
t.Fatalf("expected no error getting references: %v", err)
203+
}
204+
205+
t.Log("Refs after initial clone:")
206+
err = refs.ForEach(func(ref *plumbing.Reference) error {
207+
t.Logf(" %s -> %s", ref.Name(), ref.Hash())
208+
return nil
209+
})
210+
if err != nil {
211+
t.Fatalf("expected no error iterating references: %v", err)
212+
}
213+
214+
// Now push a second commit to the main branch
215+
err = os.WriteFile(testRepositoryPath+"/README", []byte("second commit"), 0644)
216+
if err != nil {
217+
t.Fatalf("expected no error while creating new file: %v", err)
218+
}
219+
220+
_, err = w.Add("README")
221+
if err != nil {
222+
t.Fatalf("expected no error while adding file to worktree: %v", err)
223+
}
224+
225+
secondCommit, err := w.Commit("second", &git.CommitOptions{Author: &object.Signature{}})
226+
if err != nil {
227+
t.Fatalf("expected no error while committing changes: %v", err)
228+
}
229+
230+
t.Logf("Source repo now has second commit: %s", secondCommit.String())
231+
232+
// Execute again - this should fetch and checkout the new commit
233+
meta2, err := s.Execute(ctx)
234+
if err != nil {
235+
t.Fatalf("expected no error on second execute, got %v", err)
236+
}
237+
238+
// Inspect what refs exist after the second sync
239+
refs, err = clonedRepo.References()
240+
if err != nil {
241+
t.Fatalf("expected no error getting references: %v", err)
242+
}
243+
244+
t.Log("Refs after second sync:")
245+
err = refs.ForEach(func(ref *plumbing.Reference) error {
246+
t.Logf(" %s -> %s", ref.Name(), ref.Hash())
247+
return nil
248+
})
249+
if err != nil {
250+
t.Fatalf("expected no error iterating references: %v", err)
251+
}
252+
253+
// Verify the second commit hash
254+
if meta2["commit"] != secondCommit.String() {
255+
t.Fatalf("expected second commit hash %s, got %s", secondCommit.String(), meta2["commit"])
256+
}
257+
258+
// Verify file contents were updated
259+
data, err = os.ReadFile(clonedRepositoryPath + "/README")
260+
if err != nil {
261+
t.Fatalf("expected no error while reading file, got: %v", err)
262+
}
263+
264+
if string(data) != "second commit" {
265+
t.Fatalf("expected file content to be 'second commit', got: %s", string(data))
266+
}
267+
268+
head, err := clonedRepo.Head()
269+
if err != nil {
270+
t.Fatalf("expected no error getting HEAD: %v", err)
271+
}
272+
273+
if head.Hash().String() != secondCommit.String() {
274+
t.Fatalf("expected HEAD to be at second commit %s, got %s", secondCommit.String(), head.Hash().String())
275+
}
276+
}
277+
278+
// TestGitsyncLocalTag tests syncing to a git tag reference.
279+
func TestGitsyncLocalTag(t *testing.T) {
280+
testRepositoryPath := t.TempDir() + "/testing"
281+
repository, err := git.PlainInit(testRepositoryPath, false)
282+
if err != nil {
283+
t.Fatalf("expected no error while initializing test repository: %v", err)
284+
}
285+
286+
w, err := repository.Worktree()
287+
if err != nil {
288+
t.Fatalf("expected no error while getting worktree: %v", err)
289+
}
290+
291+
// Create first commit
292+
err = os.WriteFile(testRepositoryPath+"/README", []byte("v1.0 content"), 0644)
293+
if err != nil {
294+
t.Fatalf("expected no error while creating new file: %v", err)
295+
}
296+
297+
_, err = w.Add("README")
298+
if err != nil {
299+
t.Fatalf("expected no error while adding file to worktree: %v", err)
300+
}
301+
302+
firstCommit, err := w.Commit("first", &git.CommitOptions{Author: &object.Signature{}})
303+
if err != nil {
304+
t.Fatalf("expected no error while committing changes: %v", err)
305+
}
306+
307+
// Create tag v1.0
308+
_, err = repository.CreateTag("v1.0", firstCommit, nil)
309+
if err != nil {
310+
t.Fatalf("expected no error creating tag: %v", err)
311+
}
312+
313+
// Create second commit
314+
err = os.WriteFile(testRepositoryPath+"/README", []byte("v2.0 content"), 0644)
315+
if err != nil {
316+
t.Fatalf("expected no error while creating new file: %v", err)
317+
}
318+
319+
_, err = w.Add("README")
320+
if err != nil {
321+
t.Fatalf("expected no error while adding file to worktree: %v", err)
322+
}
323+
324+
secondCommit, err := w.Commit("second", &git.CommitOptions{Author: &object.Signature{}})
325+
if err != nil {
326+
t.Fatalf("expected no error while committing changes: %v", err)
327+
}
328+
329+
// Create tag v2.0
330+
_, err = repository.CreateTag("v2.0", secondCommit, nil)
331+
if err != nil {
332+
t.Fatalf("expected no error creating tag: %v", err)
333+
}
334+
335+
// Sync to tag v1.0
336+
clonedRepositoryPath := t.TempDir() + "/test-repo"
337+
ref := "refs/tags/v1.0"
338+
s := gitsync.New(clonedRepositoryPath, config.Git{
339+
Repo: testRepositoryPath,
340+
Reference: &ref,
341+
Commit: nil,
342+
}, "test-source")
343+
344+
ctx := t.Context()
345+
meta1, err := s.Execute(ctx)
346+
if err != nil {
347+
t.Fatalf("expected no error on first execute, got %v", err)
348+
}
349+
350+
if meta1["commit"] != firstCommit.String() {
351+
t.Fatalf("expected first commit hash %s, got %s", firstCommit.String(), meta1["commit"])
352+
}
353+
354+
data, err := os.ReadFile(clonedRepositoryPath + "/README")
355+
if err != nil {
356+
t.Fatalf("expected no error while reading file, got: %v", err)
357+
}
358+
359+
if string(data) != "v1.0 content" {
360+
t.Fatalf("expected file content to be 'v1.0 content', got: %s", string(data))
361+
}
362+
363+
// Now sync to tag v2.0 using a new synchronizer
364+
ref2 := "refs/tags/v2.0"
365+
s2 := gitsync.New(clonedRepositoryPath, config.Git{
366+
Repo: testRepositoryPath,
367+
Reference: &ref2,
368+
Commit: nil,
369+
}, "test-source")
370+
371+
meta2, err := s2.Execute(ctx)
372+
if err != nil {
373+
t.Fatalf("expected no error on second execute, got %v", err)
374+
}
375+
376+
if meta2["commit"] != secondCommit.String() {
377+
t.Fatalf("expected second commit hash %s, got %s", secondCommit.String(), meta2["commit"])
378+
}
379+
380+
data, err = os.ReadFile(clonedRepositoryPath + "/README")
381+
if err != nil {
382+
t.Fatalf("expected no error while reading file, got: %v", err)
383+
}
384+
385+
if string(data) != "v2.0 content" {
386+
t.Fatalf("expected file content to be 'v2.0 content', got: %s", string(data))
387+
}
388+
389+
// Verify the tag refs were created correctly in the cloned repo
390+
clonedRepo, err := git.PlainOpen(clonedRepositoryPath)
391+
if err != nil {
392+
t.Fatalf("expected no error opening cloned repo: %v", err)
393+
}
394+
395+
refs, err := clonedRepo.References()
396+
if err != nil {
397+
t.Fatalf("expected no error getting references: %v", err)
398+
}
399+
400+
t.Log("Refs after tag sync:")
401+
err = refs.ForEach(func(ref *plumbing.Reference) error {
402+
t.Logf(" %s -> %s", ref.Name(), ref.Hash())
403+
return nil
404+
})
405+
if err != nil {
406+
t.Fatalf("expected no error iterating references: %v", err)
407+
}
408+
}
409+
117410
// TestGitsyncSSH tests the functionality of the gitsync package with an SSH server.
118411
// It creates a temporary git repository, commits a file, and then uses the gitsync package to clone the repository over SSH.
119412
// It verifies that the cloned repository contains the expected content.

0 commit comments

Comments
 (0)