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
2 changes: 1 addition & 1 deletion cmd/abort.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ func runAbort(cmd *cobra.Command, args []string) error {

g := git.New(cwd)

// Check if cascade in progress
// Check if restack in progress
st, err := state.Load(g.GetGitDir())
if err != nil {
return errors.New("no operation in progress")
Expand Down
18 changes: 9 additions & 9 deletions cmd/continue.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ func runContinue(cmd *cobra.Command, args []string) error {
}

// Update fork point for the branch that just finished rebasing.
// The cascade loop's fork point update only fires after a non-conflicting
// The restack loop's fork point update only fires after a non-conflicting
// rebase, so branches that hit a conflict never get their fork point
// refreshed -- leaving it stale for future operations.
if parent, parentErr := cfg.GetParent(st.Current); parentErr == nil {
Expand All @@ -84,7 +84,7 @@ func runContinue(cmd *cobra.Command, args []string) error {
return err
}

// If there are more branches to cascade, continue cascading
// If there are more branches to restack, continue cascading
if len(st.Pending) > 0 {
var branches []*tree.Node
for _, name := range st.Pending {
Expand All @@ -96,26 +96,26 @@ func runContinue(cmd *cobra.Command, args []string) error {
// Remove state file before continuing (will be recreated if conflict)
_ = state.Remove(g.GetGitDir()) //nolint:errcheck // cleanup

if cascadeErr := doCascadeWithState(g, cfg, branches, CascadeOptions{
if restackErr := doRestackWithState(g, cfg, branches, RestackOptions{
Operation: st.Operation,
UpdateOnly: st.UpdateOnly,
OpenWeb: st.Web,
PushOnly: st.PushOnly,
Branches: st.Branches,
StashRef: st.StashRef,
Worktrees: st.Worktrees,
}, s); cascadeErr != nil {
// Stash handling is done by doCascadeWithState (conflict saves in state, errors restore)
if !errors.Is(cascadeErr, ErrConflict) && st.StashRef != "" {
}, s); restackErr != nil {
// Stash handling is done by doRestackWithState (conflict saves in state, errors restore)
if !errors.Is(restackErr, ErrConflict) && st.StashRef != "" {
fmt.Println("Restoring auto-stashed changes...")
if popErr := g.StashPop(st.StashRef); popErr != nil {
fmt.Printf("%s could not restore stashed changes (commit %s): %v\n", s.WarningIcon(), git.AbbrevSHA(st.StashRef), popErr)
}
}
return cascadeErr // Another conflict - state saved
return restackErr // Another conflict - state saved
}
} else {
// No more branches to cascade - cleanup state
// No more branches to restack - cleanup state
_ = state.Remove(g.GetGitDir()) //nolint:errcheck // cleanup
}

Expand Down Expand Up @@ -160,7 +160,7 @@ func runContinue(cmd *cobra.Command, args []string) error {
return err
}

// Restore stash after cascade completes
// Restore stash after restack completes
if st.StashRef != "" {
fmt.Println("Restoring auto-stashed changes...")
if popErr := g.StashPop(st.StashRef); popErr != nil {
Expand Down
64 changes: 32 additions & 32 deletions cmd/cascade.go → cmd/restack.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// cmd/cascade.go
// cmd/restack.go
package cmd

import (
Expand All @@ -15,31 +15,31 @@ import (
"github.com/spf13/cobra"
)

// ErrConflict indicates a rebase conflict occurred during cascade.
// ErrConflict indicates a rebase conflict occurred during restack.
var ErrConflict = errors.New("rebase conflict: resolve and run 'gh stack continue', or 'gh stack abort'")

var cascadeCmd = &cobra.Command{
var restackCmd = &cobra.Command{
Use: "restack",
Aliases: []string{"cascade"},
Short: "Rebase current branch and descendants onto their parents",
Long: `Rebase the current branch onto its parent, then recursively restack descendants.`,
RunE: runCascade,
RunE: runRestack,
}

var (
cascadeOnlyFlag bool
cascadeDryRunFlag bool
cascadeWorktreesFlag bool
restackOnlyFlag bool
restackDryRunFlag bool
restackWorktreesFlag bool
)

func init() {
cascadeCmd.Flags().BoolVar(&cascadeOnlyFlag, "only", false, "only restack current branch, not descendants")
cascadeCmd.Flags().BoolVar(&cascadeDryRunFlag, "dry-run", false, "show what would be done")
cascadeCmd.Flags().BoolVar(&cascadeWorktreesFlag, "worktrees", false, "rebase branches checked out in linked worktrees in-place")
rootCmd.AddCommand(cascadeCmd)
restackCmd.Flags().BoolVar(&restackOnlyFlag, "only", false, "only restack current branch, not descendants")
restackCmd.Flags().BoolVar(&restackDryRunFlag, "dry-run", false, "show what would be done")
restackCmd.Flags().BoolVar(&restackWorktreesFlag, "worktrees", false, "rebase branches checked out in linked worktrees in-place")
rootCmd.AddCommand(restackCmd)
}

func runCascade(cmd *cobra.Command, args []string) error {
func runRestack(cmd *cobra.Command, args []string) error {
s := style.New()

cwd, err := os.Getwd()
Expand All @@ -54,7 +54,7 @@ func runCascade(cmd *cobra.Command, args []string) error {

g := git.New(cwd)

// Check if cascade already in progress
// Check if restack already in progress
if state.Exists(g.GetGitDir()) {
return errors.New("operation already in progress; use 'gh stack continue' or 'gh stack abort'")
}
Expand All @@ -76,36 +76,36 @@ func runCascade(cmd *cobra.Command, args []string) error {
return fmt.Errorf("branch %q is not tracked in the stack\n\nTo add it, run:\n gh stack adopt %s # to stack on %s\n gh stack adopt -p <parent> # to stack on a different branch", currentBranch, trunk, trunk)
}

// Collect branches to cascade
// Collect branches to restack
var branches []*tree.Node
branches = append(branches, node)
if !cascadeOnlyFlag {
if !restackOnlyFlag {
branches = append(branches, tree.GetDescendants(node)...)
}

// Save undo snapshot (unless dry-run)
var stashRef string
if !cascadeDryRunFlag {
if !restackDryRunFlag {
var saveErr error
stashRef, saveErr = saveUndoSnapshot(g, cfg, branches, nil, "cascade", "gh stack restack", s)
stashRef, saveErr = saveUndoSnapshot(g, cfg, branches, nil, "restack", "gh stack restack", s)
if saveErr != nil {
fmt.Printf("%s could not save undo state: %v\n", s.WarningIcon(), saveErr)
}
}

// Build worktree map if --worktrees flag is set
var worktrees map[string]string
if cascadeWorktreesFlag {
if restackWorktreesFlag {
var wtErr error
worktrees, wtErr = g.ListWorktrees()
if wtErr != nil {
return fmt.Errorf("failed to list worktrees: %w", wtErr)
}
}

err = doCascadeWithState(g, cfg, branches, CascadeOptions{
DryRun: cascadeDryRunFlag,
Operation: state.OperationCascade,
err = doRestackWithState(g, cfg, branches, RestackOptions{
DryRun: restackDryRunFlag,
Operation: state.OperationRestack,
StashRef: stashRef,
Worktrees: worktrees,
}, s)
Expand All @@ -121,15 +121,15 @@ func runCascade(cmd *cobra.Command, args []string) error {
return err
}

// CascadeOptions configures the behaviour of doCascadeWithState.
// RestackOptions configures the behaviour of doRestackWithState.
//
// The submit-specific fields (UpdateOnly, OpenWeb, PushOnly, Branches) are
// only meaningful when Operation is state.OperationSubmit; they are persisted
// to cascade state so that the push/PR phases can be resumed after a conflict.
type CascadeOptions struct {
// to restack state so that the push/PR phases can be resumed after a conflict.
type RestackOptions struct {
// DryRun prints what would be done without actually rebasing.
DryRun bool
// Operation is the type of operation being performed (state.OperationCascade
// Operation is the type of operation being performed (state.OperationRestack
// or state.OperationSubmit).
Operation string
// UpdateOnly skips creating new PRs; only existing PRs are updated.
Expand All @@ -140,8 +140,8 @@ type CascadeOptions struct {
// PushOnly skips the PR creation/update phase entirely. Submit-only.
PushOnly bool
// Branches is the complete list of branch names being submitted, used
// to rebuild the full set for push/PR phases after cascade completes.
// Submit-only. Mirrors state.CascadeState.Branches.
// to rebuild the full set for push/PR phases after restack completes.
// Submit-only. Mirrors state.RestackState.Branches.
Branches []string
// StashRef is the commit hash of auto-stashed changes (if any), persisted
// to state so they can be restored when the operation completes or is aborted.
Expand All @@ -152,8 +152,8 @@ type CascadeOptions struct {
Worktrees map[string]string
}

// doCascadeWithState performs cascade and saves state with the given operation type.
func doCascadeWithState(g *git.Git, cfg *config.Config, branches []*tree.Node, opts CascadeOptions, s *style.Style) error {
// doRestackWithState performs restack and saves state with the given operation type.
func doRestackWithState(g *git.Git, cfg *config.Config, branches []*tree.Node, opts RestackOptions, s *style.Style) error {
originalBranch, err := g.CurrentBranch()
if err != nil {
return err
Expand Down Expand Up @@ -287,7 +287,7 @@ func doCascadeWithState(g *git.Git, cfg *config.Config, branches []*tree.Node, o
remaining = append(remaining, r.Name)
}

st := &state.CascadeState{
st := &state.RestackState{
Current: b.Name,
Pending: remaining,
OriginalHead: originalHead,
Expand Down Expand Up @@ -323,7 +323,7 @@ func doCascadeWithState(g *git.Git, cfg *config.Config, branches []*tree.Node, o

// Return to original branch
if !opts.DryRun {
_ = g.Checkout(originalBranch) //nolint:errcheck // best effort - cascade succeeded
_ = g.Checkout(originalBranch) //nolint:errcheck // best effort - restack succeeded
}

return nil
Expand All @@ -332,7 +332,7 @@ func doCascadeWithState(g *git.Git, cfg *config.Config, branches []*tree.Node, o
// displayOperationName maps internal operation constants to user-facing names.
func displayOperationName(op string) string {
switch op {
case state.OperationCascade:
case state.OperationRestack:
return "Restack"
case state.OperationSubmit:
return "Submit"
Expand Down
10 changes: 5 additions & 5 deletions cmd/submit.go
Original file line number Diff line number Diff line change
Expand Up @@ -204,23 +204,23 @@ func runSubmit(cmd *cobra.Command, args []string) error {

// Phase 1: Restack
fmt.Println(s.Bold("=== Phase 1: Restack ==="))
if cascadeErr := doCascadeWithState(g, cfg, branches, CascadeOptions{
if restackErr := doRestackWithState(g, cfg, branches, RestackOptions{
DryRun: submitDryRunFlag,
Operation: state.OperationSubmit,
UpdateOnly: submitUpdateOnlyFlag,
OpenWeb: submitWebFlag,
PushOnly: submitPushOnlyFlag,
Branches: branchNames,
StashRef: stashRef,
}, s); cascadeErr != nil {
}, s); restackErr != nil {
// Stash is saved in state for conflicts; restore on other errors
if !errors.Is(cascadeErr, ErrConflict) && stashRef != "" {
if !errors.Is(restackErr, ErrConflict) && stashRef != "" {
fmt.Println("Restoring auto-stashed changes...")
if popErr := g.StashPop(stashRef); popErr != nil {
fmt.Printf("%s could not restore stashed changes (commit %s): %v\n", s.WarningIcon(), git.AbbrevSHA(stashRef), popErr)
}
}
return cascadeErr
return restackErr
}

// Phases 2 & 3
Expand Down Expand Up @@ -255,7 +255,7 @@ type SubmitOptions struct {
}

// doSubmitPushAndPR handles push and PR creation/update phases.
// This is called after cascade succeeds (or from continue after conflict resolution).
// This is called after restack succeeds (or from continue after conflict resolution).
func doSubmitPushAndPR(g *git.Git, cfg *config.Config, root *tree.Node, branches []*tree.Node, opts SubmitOptions, s *style.Style) error {
var decisions []*prDecision
var ghClient *github.Client
Expand Down
18 changes: 9 additions & 9 deletions cmd/sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,13 @@ var syncCmd = &cobra.Command{
}

var (
syncNoCascadeFlag bool
syncNoRestackFlag bool
syncDryRunFlag bool
syncWorktreesFlag bool
)

func init() {
syncCmd.Flags().BoolVar(&syncNoCascadeFlag, "no-restack", false, "skip restacking branches")
syncCmd.Flags().BoolVar(&syncNoRestackFlag, "no-restack", false, "skip restacking branches")
syncCmd.Flags().BoolVar(&syncDryRunFlag, "dry-run", false, "show what would be done")
syncCmd.Flags().BoolVar(&syncWorktreesFlag, "worktrees", false, "rebase branches checked out in linked worktrees in-place")
rootCmd.AddCommand(syncCmd)
Expand Down Expand Up @@ -202,7 +202,7 @@ func runSync(cmd *cobra.Command, args []string) error {
// Check if branch content is identical to trunk (squash merge detection)
isContentMerged, diffErr := g.IsContentMerged(branch, trunk)
if diffErr != nil {
// Can't determine, let cascade try
// Can't determine, let restack try
continue
}

Expand Down Expand Up @@ -330,7 +330,7 @@ func runSync(cmd *cobra.Command, args []string) error {
fmt.Printf("Rebasing %s onto %s (from fork point %s)...\n", s.Branch(rt.childName), s.Branch(trunk), displayForkPoint)
if rebaseErr := g.RebaseOnto(trunk, rt.forkPoint, rt.childName); rebaseErr != nil {
fmt.Printf("%s --onto rebase failed, will try normal restack: %v\n", s.WarningIcon(), rebaseErr)
// Don't return error - let cascade try
// Don't return error - let restack try
} else {
fmt.Printf("%s Rebased %s successfully\n", s.SuccessIcon(), s.Branch(rt.childName))

Expand All @@ -348,8 +348,8 @@ func runSync(cmd *cobra.Command, args []string) error {
_ = g.Checkout(currentBranch) //nolint:errcheck // best effort
}

// Cascade all (if not disabled)
if !syncNoCascadeFlag {
// Restack all (if not disabled)
if !syncNoRestackFlag {
fmt.Println(s.Bold("\nRestacking all branches..."))
// Rebuild tree after modifications
root, err = tree.Build(cfg)
Expand All @@ -367,13 +367,13 @@ func runSync(cmd *cobra.Command, args []string) error {
}
}

// Cascade from trunk's children
// Restack from trunk's children
for _, child := range root.Children {
allBranches := []*tree.Node{child}
allBranches = append(allBranches, tree.GetDescendants(child)...)
if err := doCascadeWithState(g, cfg, allBranches, CascadeOptions{
if err := doRestackWithState(g, cfg, allBranches, RestackOptions{
DryRun: syncDryRunFlag,
Operation: state.OperationCascade,
Operation: state.OperationRestack,
StashRef: stashRef,
Worktrees: worktrees,
}, s); err != nil {
Expand Down
2 changes: 1 addition & 1 deletion cmd/undo.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ func runUndo(cmd *cobra.Command, args []string) error {
g := git.New(cwd)
gitDir := g.GetGitDir()

// Check if a cascade/submit is in progress
// Check if a restack/submit is in progress
if state.Exists(gitDir) {
return errors.New("cannot undo while an operation is in progress; run 'gh stack continue' or 'gh stack abort' first")
}
Expand Down
8 changes: 4 additions & 4 deletions e2e/chaos_manual_git_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ func TestManualRebase(t *testing.T) {
env.Git("checkout", "main")
env.CreateCommit("main moved")

// User rebases manually with git instead of cascade
// User rebases manually with git instead of restack
env.Git("checkout", "feature-1")
env.Git("rebase", "main")

Expand Down Expand Up @@ -105,20 +105,20 @@ func TestManualBranchDelete(t *testing.T) {
}
}

func TestManualRebaseAbortDuringCascade(t *testing.T) {
func TestManualRebaseAbortDuringRestack(t *testing.T) {
env := NewTestEnv(t)
env.MustRun("init")

// Set up conflict scenario
env.CreateStackWithConflict()
env.Run("cascade") // Will conflict and leave rebase in progress
env.Run("restack") // Will conflict and leave rebase in progress

env.AssertRebaseInProgress()

// User manually aborts with git instead of gh-stack abort
env.Git("rebase", "--abort")

// gh-stack abort should still work (cleans up cascade state)
// gh-stack abort should still work (cleans up restack state)
result := env.Run("abort")
if result.Failed() {
t.Errorf("abort should succeed after manual rebase --abort, got: %s", result.Stderr)
Expand Down
Loading
Loading