rebase without trunk by skarim · Pull Request #129 · github/gh-stack · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
74 changes: 50 additions & 24 deletions cmd/rebase.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ type rebaseOptions struct {
upstack bool
cont bool
abort bool
noTrunk bool
remote string
committerDateIsAuthorDate bool
}
Expand All @@ -34,6 +35,7 @@ type rebaseState struct {
UseOnto bool `json:"useOnto,omitempty"`
OntoOldBase string `json:"ontoOldBase,omitempty"`
CommitterDateIsAuthorDate bool `json:"committerDateIsAuthorDate,omitempty"`
NoTrunk bool `json:"noTrunk,omitempty"`
}

const rebaseStateFile = "gh-stack-rebase-state"
Expand All @@ -47,7 +49,11 @@ func RebaseCmd(cfg *config.Config) *cobra.Command {
Long: `Pull from remote and do a cascading rebase across the stack.

Ensures that each branch in the stack has the tip of the previous
layer in its commit history, rebasing if necessary.`,
layer in its commit history, rebasing if necessary.

Use --no-trunk to skip fetching and rebasing with the trunk branch.
Only the inter-branch rebases are performed (branch 2 onto branch 1,
branch 3 onto branch 2, etc.).`,
Example: ` # Rebase the entire stack
$ gh stack rebase

Expand All @@ -57,6 +63,9 @@ layer in its commit history, rebasing if necessary.`,
# Only rebase from current branch to the top
$ gh stack rebase --upstack

# Rebase stack branches without pulling from or rebasing with trunk
$ gh stack rebase --no-trunk

# Continue after resolving conflicts
$ gh stack rebase --continue

Expand All @@ -73,6 +82,7 @@ layer in its commit history, rebasing if necessary.`,

cmd.Flags().BoolVar(&opts.downstack, "downstack", false, "Only rebase branches from trunk to current branch")
cmd.Flags().BoolVar(&opts.upstack, "upstack", false, "Only rebase branches from current branch to top")
cmd.Flags().BoolVar(&opts.noTrunk, "no-trunk", false, "Skip trunk — only rebase stack branches onto each other")
cmd.Flags().BoolVar(&opts.cont, "continue", false, "Continue rebase after resolving conflicts")
cmd.Flags().BoolVar(&opts.abort, "abort", false, "Abort rebase and restore all branches")
cmd.Flags().StringVar(&opts.remote, "remote", "", "Remote to fetch from (defaults to auto-detected remote)")
Expand Down Expand Up @@ -115,32 +125,34 @@ func runRebase(cfg *config.Config, opts *rebaseOptions) error {
return ErrSilent
}

// Resolve remote for fetch and trunk comparison
remote, err := pickRemote(cfg, currentBranch, opts.remote)
if err != nil {
if !errors.Is(err, errInterrupt) {
cfg.Errorf("%s", err)
if !opts.noTrunk {
// Resolve remote for fetch and trunk comparison
remote, err := pickRemote(cfg, currentBranch, opts.remote)
if err != nil {
if !errors.Is(err, errInterrupt) {
cfg.Errorf("%s", err)
}
return ErrSilent
}
return ErrSilent
}

if err := git.Fetch(remote); err != nil {
cfg.Warningf("Failed to fetch %s: %v", remote, err)
} else {
cfg.Successf("Fetched %s", remote)
}
if err := git.Fetch(remote); err != nil {
cfg.Warningf("Failed to fetch %s: %v", remote, err)
} else {
cfg.Successf("Fetched %s", remote)
}

// Ensure trunk exists locally before fast-forward or cascade rebase.
if err := ensureLocalTrunk(cfg, s.Trunk.Branch, remote); err != nil {
cfg.Errorf("%s", err)
return ErrSilent
}
// Ensure trunk exists locally before fast-forward or cascade rebase.
if err := ensureLocalTrunk(cfg, s.Trunk.Branch, remote); err != nil {
cfg.Errorf("%s", err)
return ErrSilent
}

// Fast-forward trunk so the cascade rebase targets the latest upstream.
fastForwardTrunk(cfg, s.Trunk.Branch, remote, currentBranch)
// Fast-forward trunk so the cascade rebase targets the latest upstream.
fastForwardTrunk(cfg, s.Trunk.Branch, remote, currentBranch)

// Fast-forward stack branches that are behind their remote tracking branch.
fastForwardBranches(cfg, s, remote, currentBranch)
// Fast-forward stack branches that are behind their remote tracking branch.
fastForwardBranches(cfg, s, remote, currentBranch)
}

cfg.Printf("Stack detected: %s", s.DisplayChain())

Expand All @@ -163,6 +175,11 @@ func runRebase(cfg *config.Config, opts *rebaseOptions) error {
startIdx = currentIdx
}

// With --no-trunk, skip the first branch (which would rebase onto trunk).
if opts.noTrunk && startIdx < 1 {
startIdx = 1
}

branchesToRebase := s.Branches[startIdx:endIdx]

if len(branchesToRebase) == 0 {
Expand Down Expand Up @@ -224,6 +241,7 @@ func runRebase(cfg *config.Config, opts *rebaseOptions) error {
UseOnto: rebaseResult.NeedsOnto,
OntoOldBase: rebaseResult.OntoOldBase,
CommitterDateIsAuthorDate: opts.committerDateIsAuthorDate,
NoTrunk: opts.noTrunk,
}
if err := saveRebaseState(gitDir, state); err != nil {
cfg.Warningf("failed to save rebase state: %s", err)
Expand Down Expand Up @@ -263,7 +281,11 @@ func runRebase(cfg *config.Config, opts *rebaseOptions) error {
rangeDesc = fmt.Sprintf("All upstack branches from %s", currentBranch)
}

cfg.Printf("%s rebased locally with %s", rangeDesc, s.Trunk.Branch)
if opts.noTrunk {
cfg.Printf("%s rebased locally (without trunk)", rangeDesc)
} else {
cfg.Printf("%s rebased locally with %s", rangeDesc, s.Trunk.Branch)
}
cfg.Printf("To push up your changes, run `%s`",
cfg.ColorCyan("gh stack push"))

Expand Down Expand Up @@ -393,7 +415,11 @@ func continueRebase(cfg *config.Config, gitDir string) error {

stack.SaveNonBlocking(gitDir, sf)

cfg.Printf("All branches in stack rebased locally with %s", s.Trunk.Branch)
if state.NoTrunk {
cfg.Printf("All branches in stack rebased locally (without trunk)")
} else {
cfg.Printf("All branches in stack rebased locally with %s", s.Trunk.Branch)
}
cfg.Printf("To push up your changes and open/update the stack of PRs, run `%s`",
cfg.ColorCyan("gh stack submit"))

Expand Down
226 changes: 226 additions & 0 deletions cmd/rebase_test.go
Loading