From a24680765d56ba42010245d34becc79b1153aef9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=BCttler?= Date: Mon, 8 Jun 2026 12:36:11 +0200 Subject: [PATCH 1/3] Add subcommand `branch show-base` Introduces a local caching layer for the resolved base (target) branch of local feature branches, avoiding redundant Forge API network calls. --- git/git.go | 78 ++++++++++++++++++++++ git/git_test.go | 127 ++++++++++++++++++++++++++++++++++++ internal/cli/branch.go | 41 ++++++++++++ internal/cli/branch_test.go | 7 +- internal/cli/pr.go | 26 +++++++- 5 files changed, 274 insertions(+), 5 deletions(-) create mode 100644 git/git.go create mode 100644 git/git_test.go diff --git a/git/git.go b/git/git.go new file mode 100644 index 0000000..61b201e --- /dev/null +++ b/git/git.go @@ -0,0 +1,78 @@ +package git + +import ( + "context" + "fmt" + "os/exec" + "strings" + + "github.com/git-pkgs/forge" +) + +// GetOrFetchBaseBranch returns the base branch of the given branch. +// It first checks the local git configuration for a cached value. +// If not found, it queries the forge API for an open pull request for the branch, +// caches the base branch name in the local git configuration, and returns it. +// If branch is empty, it uses the current branch. +func GetOrFetchBaseBranch(ctx context.Context, f forges.Forge, owner, repo, branch string, forceRefresh bool) (string, error) { + if branch == "" { + curr, err := runGit(ctx, "branch", "--show-current") + if err != nil { + return "", fmt.Errorf("failed to get current branch: %w", err) + } + branch = curr + } + if branch == "" { + return "", fmt.Errorf("empty branch name") + } + + // 1. Check local git config + configKey := fmt.Sprintf("branch.%s.forge-merge-base", branch) + if !forceRefresh { + if cached, err := runGit(ctx, "config", "--get", configKey); err == nil && cached != "" { + return cached, nil + } + } + + // 2. Fetch base branch via forge API + prs, err := f.PullRequests().List(ctx, owner, repo, forges.ListPROpts{ + State: "open", + Head: branch, + }) + if err != nil { + return "", fmt.Errorf("failed to list pull requests: %w", err) + } + + var baseBranch string + for _, pr := range prs { + if pr.Head.Ref == branch || strings.HasSuffix(pr.Head.Ref, ":"+branch) { + baseBranch = pr.Base.Ref + break + } + } + + if baseBranch == "" { + return "", fmt.Errorf("no open pull request found for branch %q", branch) + } + + // 3. Cache the resolved base branch in local git config + if _, err := runGit(ctx, "config", "--local", configKey, baseBranch); err != nil { + // Even if caching fails, we still return the resolved base branch. + } + + return baseBranch, nil +} + +func runGit(ctx context.Context, args ...string) (string, error) { + cmd := exec.CommandContext(ctx, "git", args...) + var stderr strings.Builder + cmd.Stderr = &stderr + out, err := cmd.Output() + if err != nil { + if stderr.Len() > 0 { + return "", fmt.Errorf("git %s: %w: %s", strings.Join(args, " "), err, strings.TrimSpace(stderr.String())) + } + return "", fmt.Errorf("git %s: %w", strings.Join(args, " "), err) + } + return strings.TrimSpace(string(out)), nil +} diff --git a/git/git_test.go b/git/git_test.go new file mode 100644 index 0000000..f1eaef9 --- /dev/null +++ b/git/git_test.go @@ -0,0 +1,127 @@ +package git + +import ( + "context" + "os" + "os/exec" + "testing" + + "github.com/git-pkgs/forge" +) + +type mockPRService struct { + forges.PullRequestService + prs []forges.PullRequest +} + +func (m *mockPRService) List(ctx context.Context, owner, repo string, opts forges.ListPROpts) ([]forges.PullRequest, error) { + return m.prs, nil +} + +type mockForge struct { + forges.Forge + prService *mockPRService +} + +func (m *mockForge) PullRequests() forges.PullRequestService { + return m.prService +} + +func TestGetOrFetchBaseBranch(t *testing.T) { + // Create temporary directory and run git init + tmpDir := t.TempDir() + origWd, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + defer func() { + _ = os.Chdir(origWd) + }() + + if err := os.Chdir(tmpDir); err != nil { + t.Fatal(err) + } + + // Initialize git repo + cmd := exec.Command("git", "init") + if err := cmd.Run(); err != nil { + t.Skip("git not available, skipping test") + } + + // We also need to configure a dummy user so git commands don't fail + _ = exec.Command("git", "config", "user.name", "test").Run() + _ = exec.Command("git", "config", "user.email", "test@example.com").Run() + + // 1. Test cached config + // Set config for branch "feature-xyz" + branch := "feature-xyz" + wantBase := "main" + cmdSet := exec.Command("git", "config", "branch.feature-xyz.forge-merge-base", wantBase) + if err := cmdSet.Run(); err != nil { + t.Fatal(err) + } + + ctx := context.Background() + // Pass nil Forge client since it should not be called when cached + gotBase, err := GetOrFetchBaseBranch(ctx, nil, "owner", "repo", branch, false) + if err != nil { + t.Fatalf("expected no error, got: %v", err) + } + if gotBase != wantBase { + t.Errorf("expected base branch %q, got %q", wantBase, gotBase) + } + + // 2. Test fetching from forge + // Delete the cached config first + _ = exec.Command("git", "config", "--unset", "branch.feature-xyz.forge-merge-base").Run() + + mock := &mockForge{ + prService: &mockPRService{ + prs: []forges.PullRequest{ + { + Number: 1, + State: "open", + Head: forges.PRBranch{ + Ref: branch, + }, + Base: forges.PRBranch{ + Ref: "develop", + }, + }, + }, + }, + } + + gotBase, err = GetOrFetchBaseBranch(ctx, mock, "owner", "repo", branch, false) + if err != nil { + t.Fatalf("expected no error, got: %v", err) + } + if gotBase != "develop" { + t.Errorf("expected base branch 'develop', got %q", gotBase) + } + + // Verify it was cached in git config + cachedVal, err := runGit(ctx, "config", "--get", "branch.feature-xyz.forge-merge-base") + if err != nil { + t.Fatalf("expected to read config, got: %v", err) + } + if cachedVal != "develop" { + t.Errorf("expected cached value to be 'develop', got %q", cachedVal) + } + + // 3. Test forceRefresh bypassing config cache + // Reset config to 'main' + cmdSet = exec.Command("git", "config", "branch.feature-xyz.forge-merge-base", "main") + if err := cmdSet.Run(); err != nil { + t.Fatal(err) + } + + // Calling with forceRefresh=true should bypass the "main" cache and get "develop" from mock + gotBase, err = GetOrFetchBaseBranch(ctx, mock, "owner", "repo", branch, true) + if err != nil { + t.Fatalf("expected no error, got: %v", err) + } + if gotBase != "develop" { + t.Errorf("expected base branch 'develop', got %q", gotBase) + } +} diff --git a/internal/cli/branch.go b/internal/cli/branch.go index 3f3ec77..a922a10 100644 --- a/internal/cli/branch.go +++ b/internal/cli/branch.go @@ -5,6 +5,7 @@ import ( "os" "github.com/git-pkgs/forge" + "github.com/git-pkgs/forge/git" "github.com/git-pkgs/forge/internal/output" "github.com/git-pkgs/forge/internal/resolve" "github.com/spf13/cobra" @@ -22,6 +23,7 @@ func init() { branchCmd.AddCommand(branchListCmd()) branchCmd.AddCommand(branchCreateCmd()) branchCmd.AddCommand(branchDeleteCmd()) + branchCmd.AddCommand(branchShowBaseCmd()) } func branchListCmd() *cobra.Command { @@ -153,3 +155,42 @@ func branchDeleteCmd() *cobra.Command { cmd.Flags().BoolVarP(&flagYes, "yes", "y", false, "Skip confirmation") return cmd } + +func branchShowBaseCmd() *cobra.Command { + var flagRefresh bool + + cmd := &cobra.Command{ + Use: "show-base [branch]", + Short: "Show the base branch for a branch", + Long: `Show the base branch for the specified branch (defaults to the current branch). + +It first checks for a cached base branch name under the local git config key +'branch..forge-merge-base' in .git/config. If not found, it queries the +forge API for an open pull request, caches the resolved target branch name back in +the local git configuration, and returns it.`, + Args: cobra.MaximumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + var branch string + if len(args) > 0 { + branch = args[0] + } + + forge, owner, repoName, _, err := resolve.Repo(flagRepo, flagForgeType) + if err != nil { + return err + } + + base, err := git.GetOrFetchBaseBranch(cmd.Context(), forge, owner, repoName, branch, flagRefresh) + if err != nil { + return err + } + + fmt.Println(base) + return nil + }, + } + + cmd.Flags().BoolVarP(&flagRefresh, "refresh", "r", false, "Force query the forge API and update cached base branch") + return cmd +} + diff --git a/internal/cli/branch_test.go b/internal/cli/branch_test.go index 134e7fb..09293ae 100644 --- a/internal/cli/branch_test.go +++ b/internal/cli/branch_test.go @@ -5,9 +5,10 @@ import "testing" func TestBranchSubcommands(t *testing.T) { subs := branchCmd.Commands() want := map[string]bool{ - "list": false, - "create": false, - "delete": false, + "list": false, + "create": false, + "delete": false, + "show-base": false, } for _, cmd := range subs { diff --git a/internal/cli/pr.go b/internal/cli/pr.go index 9007f07..0335566 100644 --- a/internal/cli/pr.go +++ b/internal/cli/pr.go @@ -80,6 +80,13 @@ func prViewCmd() *cobra.Command { return fmt.Errorf("getting PR #%d: %w", number, err) } + if pr.State == "open" && pr.Head.Ref != "" && pr.Base.Ref != "" { + headBranch := pr.Head.Ref + if exec.CommandContext(cmd.Context(), "git", "show-ref", "--verify", "--quiet", "refs/heads/"+headBranch).Run() == nil { + _ = exec.CommandContext(cmd.Context(), "git", "config", "--local", fmt.Sprintf("branch.%s.forge-merge-base", headBranch), pr.Base.Ref).Run() + } + } + if flagWeb { return openBrowser(pr.HTMLURL) } @@ -297,6 +304,10 @@ func prCreateCmd() *cobra.Command { return fmt.Errorf("creating pull request: %w", err) } + if flagHead != "" && pr.Base.Ref != "" { + _ = exec.CommandContext(cmd.Context(), "git", "config", "--local", fmt.Sprintf("branch.%s.forge-merge-base", flagHead), pr.Base.Ref).Run() + } + p := printer() if p.Format == output.JSON { return p.PrintJSON(pr) @@ -630,13 +641,24 @@ The argument can be a PR number or a full URL: localBranch = defaultLocalBranch(pr) } + var errCheckout error // A pull ref isn't present on the fork remote, only on origin, so // route it through the same-repo path even for fork PRs. if pr.Head.Fork != nil && !isFullRef(remoteRef) { - return checkoutForkPR(ctx, domain, pr, remoteRef, localBranch, flagRemoteName, flagDetach, flagForce) + errCheckout = checkoutForkPR(ctx, domain, pr, remoteRef, localBranch, flagRemoteName, flagDetach, flagForce) + } else { + errCheckout = checkoutSameRepoPR(ctx, remoteRef, localBranch, flagDetach, flagForce) } - return checkoutSameRepoPR(ctx, remoteRef, localBranch, flagDetach, flagForce) + if errCheckout != nil { + return errCheckout + } + + if !flagDetach && localBranch != "" && pr.Base.Ref != "" { + _ = exec.CommandContext(ctx, "git", "config", "--local", fmt.Sprintf("branch.%s.forge-merge-base", localBranch), pr.Base.Ref).Run() + } + + return nil }, } From fbb3b98822cbac3accf30bf2bbc973d2a4fa55f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=BCttler?= Date: Mon, 8 Jun 2026 12:47:48 +0200 Subject: [PATCH 2/3] fix golangci-lint formatting and staticcheck issues --- git/git.go | 5 ++--- internal/cli/branch.go | 3 +-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/git/git.go b/git/git.go index 61b201e..056fab1 100644 --- a/git/git.go +++ b/git/git.go @@ -56,9 +56,8 @@ func GetOrFetchBaseBranch(ctx context.Context, f forges.Forge, owner, repo, bran } // 3. Cache the resolved base branch in local git config - if _, err := runGit(ctx, "config", "--local", configKey, baseBranch); err != nil { - // Even if caching fails, we still return the resolved base branch. - } + // Even if caching fails, we still return the resolved base branch. + _, _ = runGit(ctx, "config", "--local", configKey, baseBranch) return baseBranch, nil } diff --git a/internal/cli/branch.go b/internal/cli/branch.go index a922a10..3ce52b8 100644 --- a/internal/cli/branch.go +++ b/internal/cli/branch.go @@ -168,7 +168,7 @@ It first checks for a cached base branch name under the local git config key 'branch..forge-merge-base' in .git/config. If not found, it queries the forge API for an open pull request, caches the resolved target branch name back in the local git configuration, and returns it.`, - Args: cobra.MaximumNArgs(1), + Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { var branch string if len(args) > 0 { @@ -193,4 +193,3 @@ the local git configuration, and returns it.`, cmd.Flags().BoolVarP(&flagRefresh, "refresh", "r", false, "Force query the forge API and update cached base branch") return cmd } - From 7b31fd560f4c96716c13f3bfefae0815f1c27c64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=BCttler?= Date: Tue, 9 Jun 2026 10:03:24 +0200 Subject: [PATCH 3/3] Address PR review feedback - Move git package to internal/git - Deduplicate cache-write sites using git.SetBaseBranch helper - Skip cache-write in pr view if PR is from a fork - Refactor git_test to avoid os.Chdir by passing/setting directory context - Clean up client-side Head filtering in GetOrFetchBaseBranch with comment --- internal/cli/branch.go | 6 ++-- internal/cli/pr.go | 9 +++--- {git => internal/git}/git.go | 29 ++++++++++++++---- {git => internal/git}/git_test.go | 49 +++++++++++++++++-------------- 4 files changed, 58 insertions(+), 35 deletions(-) rename {git => internal/git}/git.go (60%) rename {git => internal/git}/git_test.go (68%) diff --git a/internal/cli/branch.go b/internal/cli/branch.go index 3ce52b8..71aa5c4 100644 --- a/internal/cli/branch.go +++ b/internal/cli/branch.go @@ -5,7 +5,7 @@ import ( "os" "github.com/git-pkgs/forge" - "github.com/git-pkgs/forge/git" + "github.com/git-pkgs/forge/internal/git" "github.com/git-pkgs/forge/internal/output" "github.com/git-pkgs/forge/internal/resolve" "github.com/spf13/cobra" @@ -180,7 +180,7 @@ the local git configuration, and returns it.`, return err } - base, err := git.GetOrFetchBaseBranch(cmd.Context(), forge, owner, repoName, branch, flagRefresh) + base, err := git.GetOrFetchBaseBranch(cmd.Context(), forge, "", owner, repoName, branch, flagRefresh) if err != nil { return err } @@ -190,6 +190,6 @@ the local git configuration, and returns it.`, }, } - cmd.Flags().BoolVarP(&flagRefresh, "refresh", "r", false, "Force query the forge API and update cached base branch") + cmd.Flags().BoolVar(&flagRefresh, "refresh", false, "Force query the forge API and update cached base branch") return cmd } diff --git a/internal/cli/pr.go b/internal/cli/pr.go index 0335566..13886b8 100644 --- a/internal/cli/pr.go +++ b/internal/cli/pr.go @@ -9,6 +9,7 @@ import ( "strings" "github.com/git-pkgs/forge" + "github.com/git-pkgs/forge/internal/git" "github.com/git-pkgs/forge/internal/output" "github.com/git-pkgs/forge/internal/resolve" "github.com/spf13/cobra" @@ -80,10 +81,10 @@ func prViewCmd() *cobra.Command { return fmt.Errorf("getting PR #%d: %w", number, err) } - if pr.State == "open" && pr.Head.Ref != "" && pr.Base.Ref != "" { + if pr.State == "open" && pr.Head.Ref != "" && pr.Base.Ref != "" && pr.Head.Fork == nil { headBranch := pr.Head.Ref if exec.CommandContext(cmd.Context(), "git", "show-ref", "--verify", "--quiet", "refs/heads/"+headBranch).Run() == nil { - _ = exec.CommandContext(cmd.Context(), "git", "config", "--local", fmt.Sprintf("branch.%s.forge-merge-base", headBranch), pr.Base.Ref).Run() + _ = git.SetBaseBranch(cmd.Context(), "", headBranch, pr.Base.Ref) } } @@ -305,7 +306,7 @@ func prCreateCmd() *cobra.Command { } if flagHead != "" && pr.Base.Ref != "" { - _ = exec.CommandContext(cmd.Context(), "git", "config", "--local", fmt.Sprintf("branch.%s.forge-merge-base", flagHead), pr.Base.Ref).Run() + _ = git.SetBaseBranch(cmd.Context(), "", flagHead, pr.Base.Ref) } p := printer() @@ -655,7 +656,7 @@ The argument can be a PR number or a full URL: } if !flagDetach && localBranch != "" && pr.Base.Ref != "" { - _ = exec.CommandContext(ctx, "git", "config", "--local", fmt.Sprintf("branch.%s.forge-merge-base", localBranch), pr.Base.Ref).Run() + _ = git.SetBaseBranch(ctx, "", localBranch, pr.Base.Ref) } return nil diff --git a/git/git.go b/internal/git/git.go similarity index 60% rename from git/git.go rename to internal/git/git.go index 056fab1..a4112db 100644 --- a/git/git.go +++ b/internal/git/git.go @@ -1,3 +1,4 @@ +// Package git provides helpers for interacting with local git repositories and configurations. package git import ( @@ -14,9 +15,9 @@ import ( // If not found, it queries the forge API for an open pull request for the branch, // caches the base branch name in the local git configuration, and returns it. // If branch is empty, it uses the current branch. -func GetOrFetchBaseBranch(ctx context.Context, f forges.Forge, owner, repo, branch string, forceRefresh bool) (string, error) { +func GetOrFetchBaseBranch(ctx context.Context, f forges.Forge, dir, owner, repo, branch string, forceRefresh bool) (string, error) { if branch == "" { - curr, err := runGit(ctx, "branch", "--show-current") + curr, err := runGit(ctx, dir, "branch", "--show-current") if err != nil { return "", fmt.Errorf("failed to get current branch: %w", err) } @@ -29,7 +30,7 @@ func GetOrFetchBaseBranch(ctx context.Context, f forges.Forge, owner, repo, bran // 1. Check local git config configKey := fmt.Sprintf("branch.%s.forge-merge-base", branch) if !forceRefresh { - if cached, err := runGit(ctx, "config", "--get", configKey); err == nil && cached != "" { + if cached, err := runGit(ctx, dir, "config", "--get", configKey); err == nil && cached != "" { return cached, nil } } @@ -45,7 +46,9 @@ func GetOrFetchBaseBranch(ctx context.Context, f forges.Forge, owner, repo, bran var baseBranch string for _, pr := range prs { - if pr.Head.Ref == branch || strings.HasSuffix(pr.Head.Ref, ":"+branch) { + // GitLab, Gitea, and Bitbucket do not support filtering by Head on the server side, + // so we must filter client-side. We check if the head branch ref matches the requested branch. + if pr.Head.Ref == branch { baseBranch = pr.Base.Ref break } @@ -57,13 +60,27 @@ func GetOrFetchBaseBranch(ctx context.Context, f forges.Forge, owner, repo, bran // 3. Cache the resolved base branch in local git config // Even if caching fails, we still return the resolved base branch. - _, _ = runGit(ctx, "config", "--local", configKey, baseBranch) + _ = SetBaseBranch(ctx, dir, branch, baseBranch) return baseBranch, nil } -func runGit(ctx context.Context, args ...string) (string, error) { +// SetBaseBranch caches the base branch for a branch in the local git configuration. +func SetBaseBranch(ctx context.Context, dir, branch, base string) error { + if branch == "" { + return fmt.Errorf("empty branch name") + } + if base == "" { + return fmt.Errorf("empty base branch name") + } + configKey := fmt.Sprintf("branch.%s.forge-merge-base", branch) + _, err := runGit(ctx, dir, "config", "--local", configKey, base) + return err +} + +func runGit(ctx context.Context, dir string, args ...string) (string, error) { cmd := exec.CommandContext(ctx, "git", args...) + cmd.Dir = dir var stderr strings.Builder cmd.Stderr = &stderr out, err := cmd.Output() diff --git a/git/git_test.go b/internal/git/git_test.go similarity index 68% rename from git/git_test.go rename to internal/git/git_test.go index f1eaef9..9636ca5 100644 --- a/git/git_test.go +++ b/internal/git/git_test.go @@ -2,7 +2,6 @@ package git import ( "context" - "os" "os/exec" "testing" @@ -11,10 +10,15 @@ import ( type mockPRService struct { forges.PullRequestService - prs []forges.PullRequest + prs []forges.PullRequest + t *testing.T + expectedHead string } func (m *mockPRService) List(ctx context.Context, owner, repo string, opts forges.ListPROpts) ([]forges.PullRequest, error) { + if m.expectedHead != "" && opts.Head != m.expectedHead { + m.t.Errorf("expected opts.Head to be %q, got %q", m.expectedHead, opts.Head) + } return m.prs, nil } @@ -28,42 +32,38 @@ func (m *mockForge) PullRequests() forges.PullRequestService { } func TestGetOrFetchBaseBranch(t *testing.T) { - // Create temporary directory and run git init + // Create temporary directory tmpDir := t.TempDir() - origWd, err := os.Getwd() - if err != nil { - t.Fatal(err) - } - defer func() { - _ = os.Chdir(origWd) - }() - if err := os.Chdir(tmpDir); err != nil { - t.Fatal(err) - } - - // Initialize git repo + // Initialize git repo in tmpDir cmd := exec.Command("git", "init") + cmd.Dir = tmpDir if err := cmd.Run(); err != nil { t.Skip("git not available, skipping test") } // We also need to configure a dummy user so git commands don't fail - _ = exec.Command("git", "config", "user.name", "test").Run() - _ = exec.Command("git", "config", "user.email", "test@example.com").Run() + cmdName := exec.Command("git", "config", "user.name", "test") + cmdName.Dir = tmpDir + _ = cmdName.Run() + + cmdEmail := exec.Command("git", "config", "user.email", "test@example.com") + cmdEmail.Dir = tmpDir + _ = cmdEmail.Run() // 1. Test cached config // Set config for branch "feature-xyz" branch := "feature-xyz" wantBase := "main" cmdSet := exec.Command("git", "config", "branch.feature-xyz.forge-merge-base", wantBase) + cmdSet.Dir = tmpDir if err := cmdSet.Run(); err != nil { t.Fatal(err) } ctx := context.Background() // Pass nil Forge client since it should not be called when cached - gotBase, err := GetOrFetchBaseBranch(ctx, nil, "owner", "repo", branch, false) + gotBase, err := GetOrFetchBaseBranch(ctx, nil, tmpDir, "owner", "repo", branch, false) if err != nil { t.Fatalf("expected no error, got: %v", err) } @@ -73,10 +73,14 @@ func TestGetOrFetchBaseBranch(t *testing.T) { // 2. Test fetching from forge // Delete the cached config first - _ = exec.Command("git", "config", "--unset", "branch.feature-xyz.forge-merge-base").Run() + cmdUnset := exec.Command("git", "config", "--unset", "branch.feature-xyz.forge-merge-base") + cmdUnset.Dir = tmpDir + _ = cmdUnset.Run() mock := &mockForge{ prService: &mockPRService{ + t: t, + expectedHead: branch, prs: []forges.PullRequest{ { Number: 1, @@ -92,7 +96,7 @@ func TestGetOrFetchBaseBranch(t *testing.T) { }, } - gotBase, err = GetOrFetchBaseBranch(ctx, mock, "owner", "repo", branch, false) + gotBase, err = GetOrFetchBaseBranch(ctx, mock, tmpDir, "owner", "repo", branch, false) if err != nil { t.Fatalf("expected no error, got: %v", err) } @@ -101,7 +105,7 @@ func TestGetOrFetchBaseBranch(t *testing.T) { } // Verify it was cached in git config - cachedVal, err := runGit(ctx, "config", "--get", "branch.feature-xyz.forge-merge-base") + cachedVal, err := runGit(ctx, tmpDir, "config", "--get", "branch.feature-xyz.forge-merge-base") if err != nil { t.Fatalf("expected to read config, got: %v", err) } @@ -112,12 +116,13 @@ func TestGetOrFetchBaseBranch(t *testing.T) { // 3. Test forceRefresh bypassing config cache // Reset config to 'main' cmdSet = exec.Command("git", "config", "branch.feature-xyz.forge-merge-base", "main") + cmdSet.Dir = tmpDir if err := cmdSet.Run(); err != nil { t.Fatal(err) } // Calling with forceRefresh=true should bypass the "main" cache and get "develop" from mock - gotBase, err = GetOrFetchBaseBranch(ctx, mock, "owner", "repo", branch, true) + gotBase, err = GetOrFetchBaseBranch(ctx, mock, tmpDir, "owner", "repo", branch, true) if err != nil { t.Fatalf("expected no error, got: %v", err) }