Skip to content

Commit c8d5ac9

Browse files
committed
feat(cmd): add cascade command
Implements cascade rebase workflow: - Rebases current branch onto parent, then recursively cascades to descendants - Supports --only flag to limit to current branch - Supports --dry-run flag to preview operations - Saves state on conflict for continue/abort recovery
1 parent 12f20cb commit c8d5ac9

File tree

1 file changed

+147
-0
lines changed

1 file changed

+147
-0
lines changed

cmd/cascade.go

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
// cmd/cascade.go
2+
package cmd
3+
4+
import (
5+
"fmt"
6+
"os"
7+
8+
"github.com/boneskull/gh-stack/internal/config"
9+
"github.com/boneskull/gh-stack/internal/git"
10+
"github.com/boneskull/gh-stack/internal/state"
11+
"github.com/boneskull/gh-stack/internal/tree"
12+
"github.com/spf13/cobra"
13+
)
14+
15+
var cascadeCmd = &cobra.Command{
16+
Use: "cascade",
17+
Short: "Rebase current branch and descendants onto their parents",
18+
Long: `Rebase the current branch onto its parent, then recursively cascade to descendants.`,
19+
RunE: runCascade,
20+
}
21+
22+
var (
23+
cascadeOnlyFlag bool
24+
cascadeDryRunFlag bool
25+
)
26+
27+
func init() {
28+
cascadeCmd.Flags().BoolVar(&cascadeOnlyFlag, "only", false, "only cascade current branch, not descendants")
29+
cascadeCmd.Flags().BoolVar(&cascadeDryRunFlag, "dry-run", false, "show what would be done")
30+
rootCmd.AddCommand(cascadeCmd)
31+
}
32+
33+
func runCascade(cmd *cobra.Command, args []string) error {
34+
cwd, err := os.Getwd()
35+
if err != nil {
36+
return err
37+
}
38+
39+
cfg, err := config.Load(cwd)
40+
if err != nil {
41+
return err
42+
}
43+
44+
g := git.New(cwd)
45+
46+
// Check for dirty working tree
47+
dirty, err := g.IsDirty()
48+
if err != nil {
49+
return err
50+
}
51+
if dirty {
52+
return fmt.Errorf("working tree has uncommitted changes; commit or stash first")
53+
}
54+
55+
// Check if cascade already in progress
56+
if state.Exists(g.GetGitDir()) {
57+
return fmt.Errorf("cascade already in progress; use 'gh stack continue' or 'gh stack abort'")
58+
}
59+
60+
currentBranch, err := g.CurrentBranch()
61+
if err != nil {
62+
return err
63+
}
64+
65+
// Build tree
66+
root, err := tree.Build(cfg)
67+
if err != nil {
68+
return err
69+
}
70+
71+
node := tree.FindNode(root, currentBranch)
72+
if node == nil {
73+
return fmt.Errorf("branch %q is not tracked", currentBranch)
74+
}
75+
76+
// Collect branches to cascade
77+
var branches []*tree.Node
78+
branches = append(branches, node)
79+
if !cascadeOnlyFlag {
80+
branches = append(branches, tree.GetDescendants(node)...)
81+
}
82+
83+
return doCascade(g, cfg, branches, cascadeDryRunFlag)
84+
}
85+
86+
func doCascade(g *git.Git, cfg *config.Config, branches []*tree.Node, dryRun bool) error {
87+
originalBranch, _ := g.CurrentBranch()
88+
originalHead, _ := g.GetTip(originalBranch)
89+
90+
for i, b := range branches {
91+
parent, err := cfg.GetParent(b.Name)
92+
if err != nil {
93+
continue // trunk or untracked
94+
}
95+
96+
// Check if rebase needed
97+
needsRebase, err := g.NeedsRebase(b.Name, parent)
98+
if err != nil {
99+
return err
100+
}
101+
102+
if !needsRebase {
103+
fmt.Printf("Cascading %s... already up to date\n", b.Name)
104+
continue
105+
}
106+
107+
if dryRun {
108+
fmt.Printf("Would rebase %s onto %s\n", b.Name, parent)
109+
continue
110+
}
111+
112+
fmt.Printf("Cascading %s onto %s...\n", b.Name, parent)
113+
114+
// Checkout and rebase
115+
if err := g.Checkout(b.Name); err != nil {
116+
return err
117+
}
118+
119+
if err := g.Rebase(parent); err != nil {
120+
// Rebase conflict - save state
121+
remaining := make([]string, 0, len(branches)-i-1)
122+
for _, r := range branches[i+1:] {
123+
remaining = append(remaining, r.Name)
124+
}
125+
126+
st := &state.CascadeState{
127+
Current: b.Name,
128+
Pending: remaining,
129+
OriginalHead: originalHead,
130+
}
131+
state.Save(g.GetGitDir(), st)
132+
133+
fmt.Printf("\nCONFLICT: Resolve conflicts and run 'gh stack continue', or 'gh stack abort' to cancel.\n")
134+
fmt.Printf("Remaining branches: %v\n", remaining)
135+
return nil
136+
}
137+
138+
fmt.Printf("Cascading %s... ok\n", b.Name)
139+
}
140+
141+
// Return to original branch
142+
if !dryRun {
143+
g.Checkout(originalBranch)
144+
}
145+
146+
return nil
147+
}

0 commit comments

Comments
 (0)