Files
Sameen Karim 756002b4c9 ensure local trunk branch for required operations (#127)
* Ensure trunk branch exists locally before commands that need it

When a user starts a stack after renaming their initial branch
(e.g. `git branch -m newbranch`), the trunk branch (e.g. main) may
not exist as a local branch. Commands that pass the trunk name to git
operations like merge-base, rebase, or rev-parse then fail with:

  fatal: Not a valid object name main

Add an `ensureLocalTrunk` helper that checks whether the trunk branch
exists locally and, if not, fetches it from the remote and creates a
local tracking branch. This mirrors the pattern already used in the
checkout command for importing stacks.

Commands updated:
- modify: call ensureLocalTrunk before the linearity check in
  CheckStackLinearity, which uses IsAncestor(trunk, branch). This was
  the originally reported failure.
- rebase: call ensureLocalTrunk after fetch and before fastForwardTrunk
  and the cascade rebase. git rebase requires a locally resolvable ref;
  the remote tracking ref alone is not sufficient.
- trunk: call ensureLocalTrunk before CheckoutBranch so that
  `gh stack trunk` works even when trunk was never created locally.
- checkout: refactor the existing inline BranchExists + CreateBranch
  block to use the shared helper.

Also fix an incorrect comment in fastForwardTrunk that claimed "the
remote tracking ref is sufficient for rebasing" — verified empirically
that `git rebase main` fails when main has no local branch, even after
fetching origin/main.

Commands that were already safe and required no changes:
- sync: fetches trunk explicitly and fastForwardTrunk guards with
  BranchExists
- push, switch, navigate, unstack: do not reference trunk
- add, submit: do not require trunk as a local git ref
- view: handles IsAncestor errors gracefully (false positive is
  acceptable since rebase will fix it)

* add check to avoid unnecessary remote selection prompt
2026-06-15 13:54:20 -04:00

67 lines
1.5 KiB
Go

package cmd
import (
"errors"
"github.com/github/gh-stack/internal/config"
"github.com/github/gh-stack/internal/git"
"github.com/spf13/cobra"
)
func TrunkCmd(cfg *config.Config) *cobra.Command {
return &cobra.Command{
Use: "trunk",
Short: "Check out the trunk branch of the stack",
Long: `Check out the trunk branch of the current stack.
The trunk is the base branch that the stack is built on (e.g., main or develop).
You must be on a branch that is part of a stack.`,
Example: ` # Jump to the trunk branch
$ gh stack trunk`,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
return runTrunk(cfg)
},
}
}
func runTrunk(cfg *config.Config) error {
result, err := loadStack(cfg, "")
if err != nil {
if errors.Is(err, errInterrupt) {
return ErrSilent
}
return ErrNotInStack
}
s := result.Stack
currentBranch := result.CurrentBranch
trunk := s.Trunk.Branch
if currentBranch == trunk {
cfg.Printf("Already on trunk branch %s", trunk)
return nil
}
// Ensure trunk exists locally before checkout.
if !git.BranchExists(trunk) {
remote, err := pickRemote(cfg, currentBranch, "")
if err != nil {
if !errors.Is(err, errInterrupt) {
cfg.Errorf("failed to resolve remote: %s", err)
}
return ErrSilent
}
if err := ensureLocalTrunk(cfg, trunk, remote); err != nil {
cfg.Errorf("%s", err)
return ErrSilent
}
}
if err := git.CheckoutBranch(trunk); err != nil {
return err
}
cfg.Successf("Switched to %s", trunk)
return nil
}