diff --git a/compose/builder.go b/compose/builder.go index 6b69370..a000bfa 100644 --- a/compose/builder.go +++ b/compose/builder.go @@ -156,7 +156,7 @@ func createBuilder(c *Composer, targetDir, sourceDir string, packages []*Package func getVersionedMap(gitDir string) (map[string]bool, error) { versionedFiles := make(map[string]bool) - repo, err := git.PlainOpen(gitDir) + repo, err := git.PlainOpenWithOptions(gitDir, &git.PlainOpenOptions{EnableDotGitCommonDir: true}) if err != nil { return versionedFiles, err } diff --git a/compose/builder_test.go b/compose/builder_test.go new file mode 100644 index 0000000..b09cc0a --- /dev/null +++ b/compose/builder_test.go @@ -0,0 +1,139 @@ +package compose + +import ( + "os" + "path/filepath" + "testing" + "time" + + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/plumbing/object" +) + +func TestGetVersionedMap(t *testing.T) { + // Create a temporary git repo with some files. + repoDir := t.TempDir() + + repo, err := git.PlainInit(repoDir, false) + if err != nil { + t.Fatalf("failed to init repo: %v", err) + } + + // Create test files. + files := []string{"file1.txt", "dir/file2.txt"} + for _, f := range files { + p := filepath.Join(repoDir, f) + if err := os.MkdirAll(filepath.Dir(p), 0750); err != nil { + t.Fatalf("failed to create dir: %v", err) + } + if err := os.WriteFile(p, []byte("content"), 0600); err != nil { + t.Fatalf("failed to write file: %v", err) + } + } + + // Stage and commit. + wt, err := repo.Worktree() + if err != nil { + t.Fatalf("failed to get worktree: %v", err) + } + for _, f := range files { + if _, err := wt.Add(f); err != nil { + t.Fatalf("failed to add file: %v", err) + } + } + _, err = wt.Commit("initial commit", &git.CommitOptions{ + Author: &object.Signature{ + Name: "test", + Email: "test@test.com", + When: time.Now(), + }, + }) + if err != nil { + t.Fatalf("failed to commit: %v", err) + } + + versionedMap, err := getVersionedMap(repoDir) + if err != nil { + t.Fatalf("getVersionedMap failed: %v", err) + } + + // Verify files are in the map. + for _, f := range files { + if !versionedMap[f] { + t.Errorf("expected %q in versioned map", f) + } + } + + // Verify parent dir is in the map. + if !versionedMap["dir"] { + t.Error("expected 'dir' in versioned map") + } +} + +func TestGetVersionedMapWorktree(t *testing.T) { + // Create a temporary git repo. + repoDir := t.TempDir() + + repo, err := git.PlainInit(repoDir, false) + if err != nil { + t.Fatalf("failed to init repo: %v", err) + } + + // Create a file and commit on main. + testFile := "file.txt" + if err := os.WriteFile(filepath.Join(repoDir, testFile), []byte("main content"), 0600); err != nil { + t.Fatalf("failed to write file: %v", err) + } + + wt, err := repo.Worktree() + if err != nil { + t.Fatalf("failed to get worktree: %v", err) + } + if _, err := wt.Add(testFile); err != nil { + t.Fatalf("failed to add file: %v", err) + } + _, err = wt.Commit("initial commit", &git.CommitOptions{ + Author: &object.Signature{ + Name: "test", + Email: "test@test.com", + When: time.Now(), + }, + }) + if err != nil { + t.Fatalf("failed to commit: %v", err) + } + + // Create a branch for the worktree. + headRef, err := repo.Head() + if err != nil { + t.Fatalf("failed to get HEAD: %v", err) + } + + // Use git command to create the worktree, as go-git doesn't have + // a built-in worktree add command. + worktreeDir := t.TempDir() + cmd := testGitCommand(t, repoDir, "worktree", "add", worktreeDir, "-b", "test-branch", headRef.Hash().String()) + if out, err := cmd.CombinedOutput(); err != nil { + t.Fatalf("failed to create worktree: %v\n%s", err, out) + } + + // Verify the worktree has a .git file (not directory). + gitPath := filepath.Join(worktreeDir, ".git") + fi, err := os.Lstat(gitPath) + if err != nil { + t.Fatalf("failed to stat .git: %v", err) + } + if fi.IsDir() { + t.Fatal("expected .git to be a file in worktree, got directory") + } + + // This is the key test: getVersionedMap must work on a worktree. + versionedMap, err := getVersionedMap(worktreeDir) + if err != nil { + t.Fatalf("getVersionedMap failed on worktree: %v", err) + } + + if !versionedMap[testFile] { + t.Errorf("expected %q in versioned map from worktree", testFile) + } +} diff --git a/compose/git.go b/compose/git.go index 8284630..d28878d 100644 --- a/compose/git.go +++ b/compose/git.go @@ -156,7 +156,7 @@ func (g *gitDownloader) EnsureLatest(pkg *Package, downloadPath string) (bool, e return false, nil } - r, err := git.PlainOpen(downloadPath) + r, err := git.PlainOpenWithOptions(downloadPath, &git.PlainOpenOptions{EnableDotGitCommonDir: true}) if err != nil { g.k.Log().Debug("git init error", "err", err) return false, nil diff --git a/compose/git_test.go b/compose/git_test.go new file mode 100644 index 0000000..d74221e --- /dev/null +++ b/compose/git_test.go @@ -0,0 +1,106 @@ +package compose + +import ( + "os" + "os/exec" + "path/filepath" + "testing" + "time" + + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/plumbing/object" +) + +func testGitCommand(t *testing.T, dir string, args ...string) *exec.Cmd { + t.Helper() + cmd := exec.Command("git", args...) + cmd.Dir = dir + return cmd +} + +func TestPlainOpenWorktree(t *testing.T) { + // Create a temporary git repo. + repoDir := t.TempDir() + + repo, err := git.PlainInit(repoDir, false) + if err != nil { + t.Fatalf("failed to init repo: %v", err) + } + + // Create a file and commit. + testFile := "hello.txt" + if err := os.WriteFile(filepath.Join(repoDir, testFile), []byte("hello"), 0600); err != nil { + t.Fatalf("failed to write file: %v", err) + } + + wt, err := repo.Worktree() + if err != nil { + t.Fatalf("failed to get worktree: %v", err) + } + if _, err := wt.Add(testFile); err != nil { + t.Fatalf("failed to add file: %v", err) + } + commitHash, err := wt.Commit("initial commit", &git.CommitOptions{ + Author: &object.Signature{ + Name: "test", + Email: "test@test.com", + When: time.Now(), + }, + }) + if err != nil { + t.Fatalf("failed to commit: %v", err) + } + + // Create a worktree using git CLI. + worktreeDir := t.TempDir() + cmd := testGitCommand(t, repoDir, "worktree", "add", worktreeDir, "-b", "wt-branch", commitHash.String()) + if out, err := cmd.CombinedOutput(); err != nil { + t.Fatalf("failed to create worktree: %v\n%s", err, out) + } + + // Verify .git is a file. + fi, err := os.Lstat(filepath.Join(worktreeDir, ".git")) + if err != nil { + t.Fatalf("failed to stat .git: %v", err) + } + if fi.IsDir() { + t.Fatal("expected .git to be a file in worktree, got directory") + } + + // PlainOpenWithOptions with EnableDotGitCommonDir should work. + r, err := git.PlainOpenWithOptions(worktreeDir, &git.PlainOpenOptions{EnableDotGitCommonDir: true}) + if err != nil { + t.Fatalf("PlainOpenWithOptions failed on worktree: %v", err) + } + + // Should be able to resolve HEAD. + head, err := r.Head() + if err != nil { + t.Fatalf("failed to get HEAD from worktree: %v", err) + } + + // Should be able to access objects (this is what fails without EnableDotGitCommonDir). + commit, err := r.CommitObject(head.Hash()) + if err != nil { + t.Fatalf("failed to get commit object from worktree: %v", err) + } + + tree, err := commit.Tree() + if err != nil { + t.Fatalf("failed to get tree from worktree: %v", err) + } + + // Verify we can iterate files. + var files []string + err = tree.Files().ForEach(func(f *object.File) error { + files = append(files, f.Name) + return nil + }) + if err != nil { + t.Fatalf("failed to iterate files: %v", err) + } + + if len(files) != 1 || files[0] != testFile { + t.Errorf("expected [%s], got %v", testFile, files) + } +}