From 4077d402eef6045574a162c9196eaed311f9d598 Mon Sep 17 00:00:00 2001 From: Andrew A Date: Fri, 17 Apr 2026 21:08:34 +0200 Subject: [PATCH] fix: branch resolution in worktrees Fix an error that would occur in worktrees due to the structure of git files being different than expected. --- cmd/post_checkout.go | 37 ++++++++++-- cmd/post_checkout_test.go | 124 ++++++++++++++++++++++++++++++++++++++ cmd/run.go | 2 +- 3 files changed, 158 insertions(+), 5 deletions(-) create mode 100644 cmd/post_checkout_test.go diff --git a/cmd/post_checkout.go b/cmd/post_checkout.go index 10cc09d..0e47b53 100644 --- a/cmd/post_checkout.go +++ b/cmd/post_checkout.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "os" + "path/filepath" "strings" "github.com/spf13/cobra" @@ -20,7 +21,7 @@ func postCheckoutCmd() *cobra.Command { return nil } - branch, err := resolveBranch() + branch, err := resolveBranch(".") if err != nil { return err } @@ -38,10 +39,14 @@ func postCheckoutCmd() *cobra.Command { } } -func resolveBranch() (string, error) { - data, err := os.ReadFile(".git/HEAD") +func resolveBranch(dir string) (string, error) { + gitDir, err := resolveGitDir(dir) if err != nil { - return "", fmt.Errorf("reading .git/HEAD: %w", err) + return "", err + } + data, err := os.ReadFile(filepath.Join(gitDir, "HEAD")) + if err != nil { + return "", fmt.Errorf("reading HEAD: %w", err) } head := strings.TrimSpace(string(data)) const prefix = "ref: refs/heads/" @@ -51,3 +56,27 @@ func resolveBranch() (string, error) { // detached HEAD — return commit hash as-is return head, nil } + +// resolveGitDir returns the path to the actual git directory. +// In a worktree, .git is a file containing "gitdir: " rather than a directory. +func resolveGitDir(dir string) (string, error) { + dotGit := filepath.Join(dir, ".git") + info, err := os.Stat(dotGit) + if err != nil { + return "", fmt.Errorf("stat .git: %w", err) + } + if info.IsDir() { + return dotGit, nil + } + // worktree case: .git is a file pointing to the real git dir + data, err := os.ReadFile(dotGit) + if err != nil { + return "", fmt.Errorf("reading .git: %w", err) + } + line := strings.TrimSpace(string(data)) + const prefix = "gitdir: " + if !strings.HasPrefix(line, prefix) { + return "", fmt.Errorf("unexpected .git file content: %q", line) + } + return line[len(prefix):], nil +} diff --git a/cmd/post_checkout_test.go b/cmd/post_checkout_test.go new file mode 100644 index 0000000..a904852 --- /dev/null +++ b/cmd/post_checkout_test.go @@ -0,0 +1,124 @@ +package cmd + +import ( + "os" + "path/filepath" + "testing" +) + +func TestResolveGitDir_Directory(t *testing.T) { + dir := t.TempDir() + if err := os.Mkdir(filepath.Join(dir, ".git"), 0755); err != nil { + t.Fatal(err) + } + + got, err := resolveGitDir(dir) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got != filepath.Join(dir, ".git") { + t.Errorf("got %q, want %q", got, filepath.Join(dir, ".git")) + } +} + +func TestResolveGitDir_Worktree(t *testing.T) { + dir := t.TempDir() + realGitDir := "/some/repo/.git/worktrees/my-worktree" + content := "gitdir: " + realGitDir + "\n" + if err := os.WriteFile(filepath.Join(dir, ".git"), []byte(content), 0644); err != nil { + t.Fatal(err) + } + + got, err := resolveGitDir(dir) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got != realGitDir { + t.Errorf("got %q, want %q", got, realGitDir) + } +} + +func TestResolveGitDir_UnexpectedFileContent(t *testing.T) { + dir := t.TempDir() + if err := os.WriteFile(filepath.Join(dir, ".git"), []byte("not a gitdir pointer\n"), 0644); err != nil { + t.Fatal(err) + } + + _, err := resolveGitDir(dir) + if err == nil { + t.Fatal("expected error for unexpected .git file content, got nil") + } +} + +func TestResolveBranch_Normal(t *testing.T) { + dir := t.TempDir() + if err := os.Mkdir(filepath.Join(dir, ".git"), 0755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(dir, ".git", "HEAD"), []byte("ref: refs/heads/my-feature\n"), 0644); err != nil { + t.Fatal(err) + } + + got, err := resolveBranch(dir) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got != "my-feature" { + t.Errorf("got %q, want %q", got, "my-feature") + } +} + +func TestResolveBranch_DetachedHead(t *testing.T) { + dir := t.TempDir() + hash := "abc1234def5678900000000000000000000000000" + if err := os.Mkdir(filepath.Join(dir, ".git"), 0755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(dir, ".git", "HEAD"), []byte(hash+"\n"), 0644); err != nil { + t.Fatal(err) + } + + got, err := resolveBranch(dir) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got != hash { + t.Errorf("got %q, want %q", got, hash) + } +} + +func TestResolveBranch_Worktree(t *testing.T) { + // simulate the worktree structure: + // /.git (file) → /HEAD + realGitDir := t.TempDir() + if err := os.WriteFile(filepath.Join(realGitDir, "HEAD"), []byte("ref: refs/heads/worktree-branch\n"), 0644); err != nil { + t.Fatal(err) + } + + worktreeDir := t.TempDir() + gitFile := "gitdir: " + realGitDir + "\n" + if err := os.WriteFile(filepath.Join(worktreeDir, ".git"), []byte(gitFile), 0644); err != nil { + t.Fatal(err) + } + + got, err := resolveBranch(worktreeDir) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got != "worktree-branch" { + t.Errorf("got %q, want %q", got, "worktree-branch") + } +} + +func TestResolveBranch_MissingHead(t *testing.T) { + dir := t.TempDir() + if err := os.Mkdir(filepath.Join(dir, ".git"), 0755); err != nil { + t.Fatal(err) + } + // no HEAD file written + + _, err := resolveBranch(dir) + if err == nil { + t.Fatal("expected error for missing HEAD, got nil") + } +} diff --git a/cmd/run.go b/cmd/run.go index c4e773a..a6b525f 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -17,7 +17,7 @@ func runCmd() *cobra.Command { Short: "Manually apply env patching for the current branch", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { - branch, err := resolveBranch() + branch, err := resolveBranch(".") if err != nil { return err }