diff --git a/cmd/abort.go b/cmd/abort.go index 40f5bc3..a2b79bb 100644 --- a/cmd/abort.go +++ b/cmd/abort.go @@ -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") diff --git a/cmd/continue.go b/cmd/continue.go index 9dacf20..fc6b052 100644 --- a/cmd/continue.go +++ b/cmd/continue.go @@ -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 { @@ -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 { @@ -96,7 +96,7 @@ 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, @@ -104,18 +104,18 @@ func runContinue(cmd *cobra.Command, args []string) error { 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 } @@ -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 { diff --git a/cmd/cascade.go b/cmd/restack.go similarity index 91% rename from cmd/cascade.go rename to cmd/restack.go index 0a5699e..9804c43 100644 --- a/cmd/cascade.go +++ b/cmd/restack.go @@ -1,4 +1,4 @@ -// cmd/cascade.go +// cmd/restack.go package cmd import ( @@ -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() @@ -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'") } @@ -76,18 +76,18 @@ 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 # 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) } @@ -95,7 +95,7 @@ func runCascade(cmd *cobra.Command, args []string) error { // 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 { @@ -103,9 +103,9 @@ func runCascade(cmd *cobra.Command, args []string) error { } } - 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) @@ -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. @@ -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. @@ -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 @@ -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, @@ -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 @@ -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" diff --git a/cmd/submit.go b/cmd/submit.go index 55be8b7..12d8c78 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -204,7 +204,7 @@ 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, @@ -212,15 +212,15 @@ func runSubmit(cmd *cobra.Command, args []string) error { 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 @@ -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 diff --git a/cmd/sync.go b/cmd/sync.go index 049774b..0c375bd 100644 --- a/cmd/sync.go +++ b/cmd/sync.go @@ -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) @@ -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 } @@ -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)) @@ -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) @@ -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 { diff --git a/cmd/undo.go b/cmd/undo.go index 76ac651..c205812 100644 --- a/cmd/undo.go +++ b/cmd/undo.go @@ -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") } diff --git a/e2e/chaos_manual_git_test.go b/e2e/chaos_manual_git_test.go index f432aca..bdbf488 100644 --- a/e2e/chaos_manual_git_test.go +++ b/e2e/chaos_manual_git_test.go @@ -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") @@ -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) diff --git a/e2e/chaos_remote_test.go b/e2e/chaos_remote_test.go index 5c5d027..b436d27 100644 --- a/e2e/chaos_remote_test.go +++ b/e2e/chaos_remote_test.go @@ -15,22 +15,22 @@ func TestRemoteTrunkAhead(t *testing.T) { // Simulate remote main moving ahead (another dev merged something) env.SimulateSomeoneElsePushed("main") - // Local doesn't know yet - cascade uses local main + // Local doesn't know yet - restack uses local main env.Git("checkout", "feature-1") - result := env.Run("cascade") + result := env.Run("restack") // Should succeed with local state (feature-1 already up-to-date with local main) if result.Failed() { - t.Errorf("cascade should work with local state: %s", result.Stderr) + t.Errorf("restack should work with local state: %s", result.Stderr) } // After fetch, local sees remote is ahead env.FetchOrigin() - // Now cascade would pick up the new main commits - result = env.Run("cascade") + // Now restack would pick up the new main commits + result = env.Run("restack") if result.Failed() { - t.Errorf("cascade after fetch should work: %s", result.Stderr) + t.Errorf("restack after fetch should work: %s", result.Stderr) } } @@ -47,7 +47,7 @@ func TestLocalTrunkAhead(t *testing.T) { // Don't push! env.Git("checkout", "feature-1") - env.MustRun("cascade") + env.MustRun("restack") // Should work with local main (includes unpushed commit) env.AssertAncestor("main", "feature-1") @@ -96,14 +96,14 @@ func TestSomeoneElsePushedToMyBranch(t *testing.T) { // Submit should fail - remote has diverged (--force-with-lease protects us) result := env.Run("submit", "--dry-run") - // In dry-run, cascade/push phases shown but no actual push + // In dry-run, restack/push phases shown but no actual push // The actual failure would happen on real push with --force-with-lease if result.Failed() { t.Logf("submit dry-run result: %s", result.Stderr) } } -func TestSubmitAfterCascade(t *testing.T) { +func TestSubmitAfterRestack(t *testing.T) { env := NewTestEnvWithRemote(t) env.MustRun("init") @@ -121,13 +121,13 @@ func TestSubmitAfterCascade(t *testing.T) { env.Git("checkout", "feature-1") result := env.MustRun("submit", "--dry-run") - // Should show cascade needed + // Should show restack needed if !result.ContainsStdout("Would rebase") { t.Error("submit should show rebase would happen") } } -func TestFetchBeforeCascade(t *testing.T) { +func TestFetchBeforeRestack(t *testing.T) { env := NewTestEnvWithRemote(t) env.MustRun("init") @@ -137,10 +137,10 @@ func TestFetchBeforeCascade(t *testing.T) { // Remote main moves env.SimulateSomeoneElsePushed("main") - // Without fetch, cascade uses stale local main - result := env.Run("cascade") - // Document: does cascade auto-fetch? Or use local state? + // Without fetch, restack uses stale local main + result := env.Run("restack") + // Document: does restack auto-fetch? Or use local state? if result.Failed() { - t.Errorf("cascade should not fail: %s", result.Stderr) + t.Errorf("restack should not fail: %s", result.Stderr) } } diff --git a/e2e/error_cases_test.go b/e2e/error_cases_test.go index be8ea83..afc4585 100644 --- a/e2e/error_cases_test.go +++ b/e2e/error_cases_test.go @@ -38,7 +38,7 @@ func TestCreateWithModifiedTrackedFile(t *testing.T) { env.AssertBranch("feature-1") } -func TestCascadeWithDirtyTree(t *testing.T) { +func TestRestackWithDirtyTree(t *testing.T) { env := NewTestEnv(t) env.MustRun("init") @@ -52,11 +52,11 @@ func TestCascadeWithDirtyTree(t *testing.T) { // Make dirty with modified tracked file env.WriteFile("README.md", "modified content") - result := env.Run("cascade") + result := env.Run("restack") // Should auto-stash and succeed if result.Failed() { - t.Errorf("cascade should auto-stash and succeed, got: %s", result.Stderr) + t.Errorf("restack should auto-stash and succeed, got: %s", result.Stderr) } if !result.ContainsStdout("Auto-stashed") { t.Errorf("expected auto-stash message, got: %s", result.Stdout) diff --git a/e2e/cascade_test.go b/e2e/restack_test.go similarity index 86% rename from e2e/cascade_test.go rename to e2e/restack_test.go index 1667de8..95595d2 100644 --- a/e2e/cascade_test.go +++ b/e2e/restack_test.go @@ -1,9 +1,9 @@ -// e2e/cascade_test.go +// e2e/restack_test.go package e2e_test import "testing" -func TestCascadeSimple(t *testing.T) { +func TestRestackSimple(t *testing.T) { env := NewTestEnv(t) env.MustRun("init") @@ -18,9 +18,9 @@ func TestCascadeSimple(t *testing.T) { env.Git("checkout", "main") env.CreateCommit("main moved forward") - // Cascade from feature-a + // Restack from feature-a env.Git("checkout", "feature-a") - env.MustRun("cascade") + env.MustRun("restack") // Verify ancestry env.AssertAncestor("main", "feature-a") @@ -28,7 +28,7 @@ func TestCascadeSimple(t *testing.T) { env.AssertNoRebaseInProgress() } -func TestCascadeDeepStack(t *testing.T) { +func TestRestackDeepStack(t *testing.T) { env := NewTestEnv(t) env.MustRun("init") @@ -43,9 +43,9 @@ func TestCascadeDeepStack(t *testing.T) { env.Git("checkout", "main") env.CreateCommit("main update") - // Cascade from first branch + // Restack from first branch env.Git("checkout", "feat-1") - env.MustRun("cascade") + env.MustRun("restack") // Verify chain env.AssertAncestor("main", "feat-1") @@ -54,17 +54,17 @@ func TestCascadeDeepStack(t *testing.T) { } } -func TestCascadeWithConflict(t *testing.T) { +func TestRestackWithConflict(t *testing.T) { env := NewTestEnv(t) env.MustRun("init") _ = env.CreateStackWithConflict() - result := env.Run("cascade") + result := env.Run("restack") - // Cascade returns non-zero on conflict (consistent with git rebase) + // Restack returns non-zero on conflict (consistent with git rebase) if result.Success() { - t.Error("cascade should return non-zero exit on conflict") + t.Error("restack should return non-zero exit on conflict") } if !result.ContainsStdout("CONFLICT") { t.Errorf("expected CONFLICT in output, got: %s", result.Stdout) @@ -72,15 +72,15 @@ func TestCascadeWithConflict(t *testing.T) { env.AssertRebaseInProgress() } -func TestCascadeAbort(t *testing.T) { +func TestRestackAbort(t *testing.T) { env := NewTestEnv(t) env.MustRun("init") env.CreateStackWithConflict() - result := env.Run("cascade") + result := env.Run("restack") if result.Success() { - t.Fatal("expected cascade to fail on conflict") + t.Fatal("expected restack to fail on conflict") } env.AssertRebaseInProgress() @@ -89,15 +89,15 @@ func TestCascadeAbort(t *testing.T) { env.AssertNoRebaseInProgress() } -func TestCascadeContinue(t *testing.T) { +func TestRestackContinue(t *testing.T) { env := NewTestEnv(t) env.MustRun("init") conflictFile := env.CreateStackWithConflict() - result := env.Run("cascade") + result := env.Run("restack") if result.Success() { - t.Fatal("expected cascade to fail on conflict") + t.Fatal("expected restack to fail on conflict") } env.AssertRebaseInProgress() @@ -108,7 +108,7 @@ func TestCascadeContinue(t *testing.T) { env.AssertNoRebaseInProgress() } -func TestCascadeReturnsToOriginalBranch(t *testing.T) { +func TestRestackReturnsToOriginalBranch(t *testing.T) { env := NewTestEnv(t) env.MustRun("init") @@ -126,11 +126,11 @@ func TestCascadeReturnsToOriginalBranch(t *testing.T) { env.Git("checkout", "main") env.CreateCommit("main moved forward") - // Start cascade from feature-a (not the deepest branch) + // Start restack from feature-a (not the deepest branch) env.Git("checkout", "feature-a") env.AssertBranch("feature-a") - env.MustRun("cascade") + env.MustRun("restack") // Verify we returned to feature-a, not feature-c (the last restacked branch) env.AssertBranch("feature-a") @@ -141,7 +141,7 @@ func TestCascadeReturnsToOriginalBranch(t *testing.T) { env.AssertAncestor("feature-b", "feature-c") } -func TestCascadeStaleForkPointFromManualRebase(t *testing.T) { +func TestRestackStaleForkPointFromManualRebase(t *testing.T) { // Reproduces the bug where a manual rebase outside gh-stack leaves the // fork point stale. On the next restack after main advances, the stale // fork point would trigger an --onto rebase that replays too many commits. @@ -180,7 +180,7 @@ func TestCascadeStaleForkPointFromManualRebase(t *testing.T) { env.AssertNoRebaseInProgress() } -func TestCascadeStaleForkPointDetectedDuringRebase(t *testing.T) { +func TestRestackStaleForkPointDetectedDuringRebase(t *testing.T) { // Even if the "already up to date" refresh was missed, the ancestor check // in the useOnto logic should prevent --onto with a stale fork point. env := NewTestEnv(t) @@ -228,15 +228,15 @@ func TestCascadeStaleForkPointDetectedDuringRebase(t *testing.T) { env.AssertNoRebaseInProgress() } -func TestCascadeForkPointUpdatedAfterContinue(t *testing.T) { +func TestRestackForkPointUpdatedAfterContinue(t *testing.T) { env := NewTestEnv(t) env.MustRun("init") // Create a stack that will conflict conflictFile := env.CreateStackWithConflict() - // Cascade will hit a conflict on feature-b - result := env.Run("cascade") + // Restack will hit a conflict on feature-b + result := env.Run("restack") if result.Success() { t.Fatal("expected conflict") } @@ -261,7 +261,7 @@ func TestCascadeForkPointUpdatedAfterContinue(t *testing.T) { } } -func TestCascadeOntoUsedForRewrittenParent(t *testing.T) { +func TestRestackOntoUsedForRewrittenParent(t *testing.T) { // Verify that --onto IS used when the parent's history was actually // rewritten (not just a stale fork point). env := NewTestEnv(t) diff --git a/e2e/scenario_helpers_test.go b/e2e/scenario_helpers_test.go index c4bd2f5..e9edc40 100644 --- a/e2e/scenario_helpers_test.go +++ b/e2e/scenario_helpers_test.go @@ -31,7 +31,7 @@ func (e *TestEnv) CreateConflict(baseBranch, featureBranch string) string { } // CreateStackWithConflict creates a 3-branch stack where cascading will conflict. -// After setup, you should be on feature-a and cascade from there. +// After setup, you should be on feature-a and restack from there. // The conflict occurs when feature-b rebases onto feature-a (after feature-a picks up main's changes). func (e *TestEnv) CreateStackWithConflict() string { e.t.Helper() @@ -61,7 +61,7 @@ func (e *TestEnv) CreateStackWithConflict() string { e.Git("add", conflictFile) e.Git("commit", "-m", "main: modify shared.txt") - // Return to feature-a for cascade (cascade operates on current + descendants) + // Return to feature-a for restack (restack operates on current + descendants) e.Git("checkout", "feature-a") return conflictFile diff --git a/e2e/submit_test.go b/e2e/submit_test.go index 74c2dfb..cc45b32 100644 --- a/e2e/submit_test.go +++ b/e2e/submit_test.go @@ -76,7 +76,7 @@ func TestSubmitCurrentOnlyDryRun(t *testing.T) { env.Git("checkout", "feat-a") - // Submit --current-only should NOT cascade feat-b + // Submit --current-only should NOT restack feat-b result := env.MustRun("submit", "--dry-run", "--current-only") // Should mention feat-a but not feat-b in push phase @@ -89,7 +89,7 @@ func TestSubmitCurrentOnlyDryRun(t *testing.T) { } } -func TestSubmitWithCascadeNeeded(t *testing.T) { +func TestSubmitWithRestackNeeded(t *testing.T) { env := NewTestEnvWithRemote(t) env.MustRun("init") @@ -109,7 +109,7 @@ func TestSubmitWithCascadeNeeded(t *testing.T) { // Should show rebase would happen if !strings.Contains(result.Stdout, "Would rebase feat-b onto feat-a") { - t.Error("expected cascade of feat-b onto feat-a") + t.Error("expected restack of feat-b onto feat-a") } } @@ -125,7 +125,7 @@ func TestSubmitAlreadyUpToDate(t *testing.T) { // Should indicate already up to date if !strings.Contains(result.Stdout, "already up to date") { - t.Error("expected 'already up to date' for branch that doesn't need cascade") + t.Error("expected 'already up to date' for branch that doesn't need restack") } } diff --git a/e2e/sync_test.go b/e2e/sync_test.go index 42200c8..821bfc9 100644 --- a/e2e/sync_test.go +++ b/e2e/sync_test.go @@ -5,11 +5,11 @@ import "testing" // Note: Full E2E testing of sync's return-to-original-branch behavior is limited // because sync requires a real GitHub remote. The underlying return-to-branch logic -// is tested via TestCascadeReturnsToOriginalBranch in cascade_test.go, since sync -// delegates restacking to the same doCascadeWithState function. +// is tested via TestRestackReturnsToOriginalBranch in restack_test.go, since sync +// delegates restacking to the same doRestackWithState function. // // The fix for issue #58 (sync leaving user on wrong branch) is validated by: -// 1. TestCascadeReturnsToOriginalBranch - tests the shared restack infrastructure +// 1. TestRestackReturnsToOriginalBranch - tests the shared restack infrastructure // 2. cmd/sync_test.go unit tests - test sync-specific starting branch capture: // - TestSyncStartingBranchCapture - normal branch detection // - TestSyncStartingBranchDetachedHEAD - "HEAD" normalization for detached state diff --git a/e2e/undo_test.go b/e2e/undo_test.go index 9223b05..2f5b6e2 100644 --- a/e2e/undo_test.go +++ b/e2e/undo_test.go @@ -7,7 +7,7 @@ import ( "testing" ) -func TestUndoCascade(t *testing.T) { +func TestUndoRestack(t *testing.T) { env := NewTestEnv(t) env.MustRun("init") @@ -24,9 +24,9 @@ func TestUndoCascade(t *testing.T) { env.Git("checkout", "main") env.CreateCommit("main moved forward") - // Cascade from feature-a + // Restack from feature-a env.Git("checkout", "feature-a") - env.MustRun("cascade") + env.MustRun("restack") // Verify branches moved newFeatureASha := env.BranchTip("feature-a") @@ -38,7 +38,7 @@ func TestUndoCascade(t *testing.T) { t.Error("feature-b should have been rebased") } - // Undo the cascade + // Undo the restack env.MustRun("undo", "--force") // Verify branches restored @@ -56,7 +56,7 @@ func TestUndoDryRun(t *testing.T) { env := NewTestEnv(t) env.MustRun("init") - // Create stack and cascade + // Create stack and restack env.MustRun("create", "feature-a") env.CreateCommit("feature a work") featureASha := env.BranchTip("feature-a") @@ -65,12 +65,12 @@ func TestUndoDryRun(t *testing.T) { env.CreateCommit("main moved forward") env.Git("checkout", "feature-a") - env.MustRun("cascade") + env.MustRun("restack") // Verify branch moved - cascadedSha := env.BranchTip("feature-a") - if cascadedSha == featureASha { - t.Fatal("cascade didn't change SHA") + restackdSha := env.BranchTip("feature-a") + if restackdSha == featureASha { + t.Fatal("restack didn't change SHA") } // Dry run should not change anything @@ -79,9 +79,9 @@ func TestUndoDryRun(t *testing.T) { t.Error("expected dry-run message in output") } - // Branch should still be at cascaded SHA + // Branch should still be at restackd SHA afterDryRunSha := env.BranchTip("feature-a") - if afterDryRunSha != cascadedSha { + if afterDryRunSha != restackdSha { t.Error("dry-run should not have changed branch") } } @@ -109,9 +109,9 @@ func TestUndoRestoresConfig(t *testing.T) { env.CreateCommit("main moved forward") env.Git("checkout", "feature-a") - env.MustRun("cascade") + env.MustRun("restack") - // Cascade updates fork point + // Restack updates fork point newForkPoint := env.GetStackConfig("branch.feature-a.stackforkpoint") if newForkPoint == originalForkPoint { // Fork point should have changed (or been set) @@ -139,7 +139,7 @@ func TestUndoArchivesSnapshot(t *testing.T) { env.CreateCommit("main moved forward") env.Git("checkout", "feature-a") - env.MustRun("cascade") + env.MustRun("restack") // Verify snapshot exists undoDir := filepath.Join(env.WorkDir, ".git", "stack-undo") @@ -183,7 +183,7 @@ func TestUndoArchivesSnapshot(t *testing.T) { } } -func TestCascadeWithAutoStashRestoresAfterSuccess(t *testing.T) { +func TestRestackWithAutoStashRestoresAfterSuccess(t *testing.T) { env := NewTestEnv(t) env.MustRun("init") @@ -198,19 +198,19 @@ func TestCascadeWithAutoStashRestoresAfterSuccess(t *testing.T) { // Create uncommitted changes env.WriteFile("uncommitted.txt", "uncommitted content\n") - // Cascade should auto-stash and then restore after success - result := env.MustRun("cascade") + // Restack should auto-stash and then restore after success + result := env.MustRun("restack") if !result.ContainsStdout("Auto-stashed") { t.Error("expected auto-stash message") } if !result.ContainsStdout("Restoring auto-stashed") { - t.Error("expected restore message after successful cascade") + t.Error("expected restore message after successful restack") } - // Uncommitted file should be restored after successful cascade + // Uncommitted file should be restored after successful restack content, err := os.ReadFile(filepath.Join(env.WorkDir, "uncommitted.txt")) if err != nil { - t.Errorf("uncommitted file should be present after cascade: %v", err) + t.Errorf("uncommitted file should be present after restack: %v", err) } else if string(content) != "uncommitted content\n" { t.Errorf("uncommitted file has wrong content: %q", content) } @@ -229,9 +229,9 @@ func TestUndoRestoresOriginalBranch(t *testing.T) { env.Git("checkout", "main") env.CreateCommit("main moved forward") - // Cascade from feature-a + // Restack from feature-a env.Git("checkout", "feature-a") - env.MustRun("cascade") + env.MustRun("restack") // Should still be on feature-a env.AssertBranch("feature-a") @@ -239,39 +239,39 @@ func TestUndoRestoresOriginalBranch(t *testing.T) { // Now checkout something else env.Git("checkout", "feature-b") - // Undo should restore to feature-a (original head at time of cascade) + // Undo should restore to feature-a (original head at time of restack) env.MustRun("undo", "--force") env.AssertBranch("feature-a") } -func TestUndoBlockedDuringCascade(t *testing.T) { +func TestUndoBlockedDuringRestack(t *testing.T) { env := NewTestEnv(t) env.MustRun("init") conflictFile := env.CreateStackWithConflict() _ = conflictFile - // Start cascade (will conflict) - result := env.Run("cascade") + // Start restack (will conflict) + result := env.Run("restack") if result.Success() { - t.Fatal("expected cascade to fail on conflict") + t.Fatal("expected restack to fail on conflict") } env.AssertRebaseInProgress() // Undo should be blocked result = env.Run("undo", "--force") if result.Success() { - t.Error("undo should fail during cascade in progress") + t.Error("undo should fail during restack in progress") } if !result.ContainsStdout("operation is in progress") && !result.ContainsStderr("operation is in progress") { t.Errorf("expected message about operation in progress, got stdout: %s, stderr: %s", result.Stdout, result.Stderr) } - // Abort the cascade + // Abort the restack env.MustRun("abort") } -func TestMultipleCascadesUndoLatest(t *testing.T) { +func TestMultipleRestacksUndoLatest(t *testing.T) { env := NewTestEnv(t) env.MustRun("init") @@ -280,33 +280,33 @@ func TestMultipleCascadesUndoLatest(t *testing.T) { env.CreateCommit("feature a v1") featureAv1 := env.BranchTip("feature-a") - // First cascade trigger + // First restack trigger env.Git("checkout", "main") env.CreateCommit("main update 1") env.Git("checkout", "feature-a") - env.MustRun("cascade") + env.MustRun("restack") featureAAfterFirst := env.BranchTip("feature-a") if featureAAfterFirst == featureAv1 { - t.Fatal("first cascade didn't change SHA") + t.Fatal("first restack didn't change SHA") } - // Second cascade trigger + // Second restack trigger env.Git("checkout", "main") env.CreateCommit("main update 2") env.Git("checkout", "feature-a") - env.MustRun("cascade") + env.MustRun("restack") featureAAfterSecond := env.BranchTip("feature-a") if featureAAfterSecond == featureAAfterFirst { - t.Fatal("second cascade didn't change SHA") + t.Fatal("second restack didn't change SHA") } - // Undo should restore to state before SECOND cascade (not first) + // Undo should restore to state before SECOND restack (not first) env.MustRun("undo", "--force") featureAAfterUndo := env.BranchTip("feature-a") if featureAAfterUndo != featureAAfterFirst { - t.Errorf("undo should restore to state before second cascade: expected %s, got %s", + t.Errorf("undo should restore to state before second restack: expected %s, got %s", featureAAfterFirst, featureAAfterUndo) } } diff --git a/e2e/worktree_test.go b/e2e/worktree_test.go index 6b8933f..f0ee38f 100644 --- a/e2e/worktree_test.go +++ b/e2e/worktree_test.go @@ -151,7 +151,7 @@ func TestRestackWithoutWorktreeFlagErrors(t *testing.T) { func TestSyncWithWorktree(t *testing.T) { // sync requires a real GitHub remote which we can't simulate in E2E tests. // Instead, verify the --worktrees flag is accepted by the sync command - // and test the cascade-with-worktrees behavior (which sync delegates to) + // and test the restack-with-worktrees behavior (which sync delegates to) // via the restack tests above. env := NewTestEnv(t) env.MustRun("init") diff --git a/internal/state/state.go b/internal/state/state.go index 39b9a0e..2caad10 100644 --- a/internal/state/state.go +++ b/internal/state/state.go @@ -10,21 +10,21 @@ import ( const stateFile = "STACK_CASCADE_STATE" -// Operation types for cascade state. +// Operation types for restack state. const ( - OperationCascade = "cascade" + OperationRestack = "restack" OperationSubmit = "submit" ) -// ErrNoState is returned when no cascade state exists. -var ErrNoState = errors.New("no cascade in progress") +// ErrNoState is returned when no restack state exists. +var ErrNoState = errors.New("no restack in progress") -// CascadeState represents the state of an in-progress cascade or submit operation. -type CascadeState struct { +// RestackState represents the state of an in-progress restack or submit operation. +type RestackState struct { Current string `json:"current"` Pending []string `json:"pending"` OriginalHead string `json:"original_head"` - // Operation is "cascade" or "submit" - determines what happens after cascade completes + // Operation is "restack" or "submit" - determines what happens after restack completes Operation string `json:"operation,omitempty"` // UpdateOnly (submit only) - if true, don't create new PRs, only update existing UpdateOnly bool `json:"update_only,omitempty"` @@ -33,7 +33,7 @@ type CascadeState struct { // PushOnly (submit only) - if true, skip PR creation/update phase entirely PushOnly bool `json:"push_only,omitempty"` // Branches (submit only) - the complete list of branches being submitted. - // Used to rebuild the full set for push/PR phases after cascade completes. + // Used to rebuild the full set for push/PR phases after restack completes. Branches []string `json:"branches,omitempty"` // StashRef is the commit hash of auto-stashed changes (if any). // Used to restore working tree changes when operation completes or is aborted. @@ -44,8 +44,8 @@ type CascadeState struct { Worktrees map[string]string `json:"worktrees,omitempty"` } -// Save persists cascade state to .git/STACK_CASCADE_STATE. -func Save(gitDir string, s *CascadeState) error { +// Save persists restack state to .git/STACK_CASCADE_STATE. +func Save(gitDir string, s *RestackState) error { path := filepath.Join(gitDir, stateFile) data, err := json.MarshalIndent(s, "", " ") if err != nil { @@ -54,8 +54,8 @@ func Save(gitDir string, s *CascadeState) error { return os.WriteFile(path, data, 0644) } -// Load reads cascade state from .git/STACK_CASCADE_STATE. -func Load(gitDir string) (*CascadeState, error) { +// Load reads restack state from .git/STACK_CASCADE_STATE. +func Load(gitDir string) (*RestackState, error) { path := filepath.Join(gitDir, stateFile) data, err := os.ReadFile(path) if err != nil { @@ -65,14 +65,14 @@ func Load(gitDir string) (*CascadeState, error) { return nil, err } - var s CascadeState + var s RestackState if err := json.Unmarshal(data, &s); err != nil { return nil, err } return &s, nil } -// Remove deletes the cascade state file. +// Remove deletes the restack state file. func Remove(gitDir string) error { path := filepath.Join(gitDir, stateFile) err := os.Remove(path) @@ -82,7 +82,7 @@ func Remove(gitDir string) error { return err } -// Exists checks if a cascade is in progress. +// Exists checks if a restack is in progress. func Exists(gitDir string) bool { path := filepath.Join(gitDir, stateFile) _, err := os.Stat(path) diff --git a/internal/state/state_test.go b/internal/state/state_test.go index d60fb53..6be9cc8 100644 --- a/internal/state/state_test.go +++ b/internal/state/state_test.go @@ -9,12 +9,12 @@ import ( "github.com/boneskull/gh-stack/internal/state" ) -func TestCascadeState(t *testing.T) { +func TestRestackState(t *testing.T) { dir := t.TempDir() gitDir := filepath.Join(dir, ".git") os.Mkdir(gitDir, 0755) - s := &state.CascadeState{ + s := &state.RestackState{ Current: "feature-b", Pending: []string{"feature-c", "feature-d"}, OriginalHead: "abc123", @@ -38,7 +38,7 @@ func TestCascadeState(t *testing.T) { } } -func TestCascadeStateNotExists(t *testing.T) { +func TestRestackStateNotExists(t *testing.T) { dir := t.TempDir() gitDir := filepath.Join(dir, ".git") os.Mkdir(gitDir, 0755) @@ -56,7 +56,7 @@ func TestSubmitState(t *testing.T) { t.Fatal(err) } - s := &state.CascadeState{ + s := &state.RestackState{ Current: "feature-b", Pending: []string{"feature-c"}, OriginalHead: "abc123", @@ -95,7 +95,7 @@ func TestSubmitStatePushOnly(t *testing.T) { t.Fatal(err) } - s := &state.CascadeState{ + s := &state.RestackState{ Current: "feature-b", Pending: []string{"feature-c"}, OriginalHead: "abc123", diff --git a/internal/undo/undo_test.go b/internal/undo/undo_test.go index fcfbc76..ce27da0 100644 --- a/internal/undo/undo_test.go +++ b/internal/undo/undo_test.go @@ -18,7 +18,7 @@ func TestSaveAndLoadLatest(t *testing.T) { t.Fatal(err) } - snapshot := undo.NewSnapshot("cascade", "gh stack cascade", "feature-a") + snapshot := undo.NewSnapshot("restack", "gh stack restack", "feature-a") snapshot.Branches["feature-a"] = undo.BranchState{ SHA: "abc123def456", StackParent: "main", @@ -39,11 +39,11 @@ func TestSaveAndLoadLatest(t *testing.T) { t.Fatalf("LoadLatest failed: %v", err) } - if loaded.Operation != "cascade" { - t.Errorf("Operation mismatch: %q != %q", loaded.Operation, "cascade") + if loaded.Operation != "restack" { + t.Errorf("Operation mismatch: %q != %q", loaded.Operation, "restack") } - if loaded.Command != "gh stack cascade" { - t.Errorf("Command mismatch: %q != %q", loaded.Command, "gh stack cascade") + if loaded.Command != "gh stack restack" { + t.Errorf("Command mismatch: %q != %q", loaded.Command, "gh stack restack") } if loaded.OriginalHead != "feature-a" { t.Errorf("OriginalHead mismatch: %q != %q", loaded.OriginalHead, "feature-a") @@ -79,7 +79,7 @@ func TestLoadLatestReturnsNewest(t *testing.T) { } // Create first snapshot - snapshot1 := undo.NewSnapshot("cascade", "gh stack cascade", "feature-a") + snapshot1 := undo.NewSnapshot("restack", "gh stack restack", "feature-a") snapshot1.Timestamp = time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC) snapshot1.Branches["feature-a"] = undo.BranchState{SHA: "first"} if err := undo.Save(gitDir, snapshot1); err != nil { @@ -125,7 +125,7 @@ func TestArchive(t *testing.T) { t.Fatal(err) } - snapshot := undo.NewSnapshot("cascade", "gh stack cascade", "main") + snapshot := undo.NewSnapshot("restack", "gh stack restack", "main") snapshot.Branches["feature"] = undo.BranchState{SHA: "abc123"} if err := undo.Save(gitDir, snapshot); err != nil { t.Fatal(err) @@ -172,7 +172,7 @@ func TestList(t *testing.T) { } // Create multiple snapshots - for i, op := range []string{"cascade", "submit", "sync"} { + for i, op := range []string{"restack", "submit", "sync"} { snapshot := undo.NewSnapshot(op, "gh stack "+op, "main") snapshot.Timestamp = time.Date(2024, 1, i+1, 12, 0, 0, 0, time.UTC) snapshot.Branches["feature"] = undo.BranchState{SHA: "abc"} @@ -194,8 +194,8 @@ func TestList(t *testing.T) { if snapshots[0].Operation != "sync" { t.Errorf("Expected sync (newest) first, got %q", snapshots[0].Operation) } - if snapshots[2].Operation != "cascade" { - t.Errorf("Expected cascade (oldest) last, got %q", snapshots[2].Operation) + if snapshots[2].Operation != "restack" { + t.Errorf("Expected restack (oldest) last, got %q", snapshots[2].Operation) } } @@ -210,7 +210,7 @@ func TestExists(t *testing.T) { t.Error("Exists should return false when no snapshots") } - snapshot := undo.NewSnapshot("cascade", "gh stack cascade", "main") + snapshot := undo.NewSnapshot("restack", "gh stack restack", "main") snapshot.Branches["feature"] = undo.BranchState{SHA: "abc"} if err := undo.Save(gitDir, snapshot); err != nil { t.Fatal(err) @@ -272,7 +272,7 @@ func TestStashRef(t *testing.T) { t.Fatal(err) } - snapshot := undo.NewSnapshot("cascade", "gh stack cascade", "main") + snapshot := undo.NewSnapshot("restack", "gh stack restack", "main") snapshot.StashRef = "stash@{0}" snapshot.Branches["feature"] = undo.BranchState{SHA: "abc"} @@ -297,7 +297,7 @@ func TestRemove(t *testing.T) { t.Fatal(err) } - snapshot := undo.NewSnapshot("cascade", "gh stack cascade", "main") + snapshot := undo.NewSnapshot("restack", "gh stack restack", "main") snapshot.Branches["feature"] = undo.BranchState{SHA: "abc"} if err := undo.Save(gitDir, snapshot); err != nil { t.Fatal(err) @@ -331,7 +331,7 @@ func TestSavePrunesOldSnapshots(t *testing.T) { // Create 55 snapshots (exceeds the 50 limit) for i := range 55 { - snapshot := undo.NewSnapshot("cascade", "gh stack cascade", "main") + snapshot := undo.NewSnapshot("restack", "gh stack restack", "main") // Use distinct timestamps to ensure unique filenames snapshot.Timestamp = time.Date(2024, 1, 1, 0, 0, i, 0, time.UTC) snapshot.Branches["feature"] = undo.BranchState{SHA: "abc"} @@ -373,7 +373,7 @@ func TestArchivePrunesOldSnapshots(t *testing.T) { // Create and archive 55 snapshots (exceeds the 50 limit) for i := range 55 { - snapshot := undo.NewSnapshot("cascade", "gh stack cascade", "main") + snapshot := undo.NewSnapshot("restack", "gh stack restack", "main") snapshot.Timestamp = time.Date(2024, 1, 1, 0, 0, i, 0, time.UTC) snapshot.Branches["feature"] = undo.BranchState{SHA: "abc"} if err := undo.Save(gitDir, snapshot); err != nil {