22package cmd
33
44import (
5+ "context"
56 "fmt"
67 "os"
8+ "strings"
9+
10+ gh "github.com/cli/go-gh/v2"
11+ "github.com/spf13/cobra"
712
813 "github.com/boneskull/gh-stack/internal/config"
914 "github.com/boneskull/gh-stack/internal/git"
1015 "github.com/boneskull/gh-stack/internal/github"
1116 "github.com/boneskull/gh-stack/internal/tree"
12- "github.com/spf13/cobra"
1317)
1418
1519var prCmd = & cobra.Command {
16- Use : "pr" ,
20+ Use : "pr [-- <gh-pr-create-flags>...] " ,
1721 Short : "Create or update a PR for the current branch" ,
18- Long : `Create a new PR targeting the parent branch, or update an existing PR's base.` ,
19- RunE : runPR ,
22+ Long : `Create a new PR targeting the parent branch, or update an existing PR's base.
23+
24+ This command wraps 'gh pr create', automatically setting the base branch to the
25+ stack parent. Any additional flags after '--' are passed through to 'gh pr create'.
26+
27+ Examples:
28+ gh stack pr # Interactive PR creation
29+ gh stack pr -- --title "My PR" # With title
30+ gh stack pr -- --fill --web # Fill from commits, open in browser
31+ gh stack pr --base main # Override base branch` ,
32+ RunE : runPR ,
33+ DisableFlagParsing : false ,
2034}
2135
2236var prBaseFlag string
2337
2438func init () {
25- prCmd .Flags ().StringVar (& prBaseFlag , "base" , "" , "override base branch" )
39+ prCmd .Flags ().StringVar (& prBaseFlag , "base" , "" , "override base branch (default: stack parent) " )
2640 rootCmd .AddCommand (prCmd )
2741}
2842
@@ -37,8 +51,7 @@ func runPR(cmd *cobra.Command, args []string) error {
3751 return err
3852 }
3953
40- // Create GitHub client
41- gh , err := github .NewClient ()
54+ ghClient , err := github .NewClient ()
4255 if err != nil {
4356 return err
4457 }
@@ -52,80 +65,166 @@ func runPR(cmd *cobra.Command, args []string) error {
5265 // Get parent (base branch)
5366 parent , err := cfg .GetParent (branch )
5467 if err != nil {
55- return fmt .Errorf ("branch %q is not tracked" , branch )
56- }
57-
58- base := prBaseFlag
59- if base == "" {
60- base = parent
68+ return fmt .Errorf ("branch %q is not tracked; use 'gh stack create' or 'gh stack track' first" , branch )
6169 }
6270
63- // Get trunk for draft decision and comment generation
6471 trunk , err := cfg .GetTrunk ()
6572 if err != nil {
6673 return err
6774 }
6875
76+ base := prBaseFlag
77+ if base == "" {
78+ base = parent
79+ }
80+
6981 // Check if PR already exists
7082 existingPR , _ := cfg .GetPR (branch ) //nolint:errcheck // 0 is fine if no PR
7183 if existingPR > 0 {
72- // Update existing PR's base if needed
73- fmt .Printf ("PR #%d already exists, updating base to %q\n " , existingPR , base )
74- if updateErr := gh .UpdatePRBase (existingPR , base ); updateErr != nil {
75- return fmt .Errorf ("failed to update PR base: %w" , updateErr )
76- }
84+ return updateExistingPR (ghClient , cfg , existingPR , branch , base , trunk )
85+ }
7786
78- // Update stack comment
79- root , buildErr := tree .Build (cfg )
80- if buildErr != nil {
81- return fmt .Errorf ("build tree: %w" , buildErr )
82- }
83- comment := github .GenerateStackComment (root , branch , trunk )
84- if comment != "" {
85- if commentErr := gh .CreateOrUpdateStackComment (existingPR , comment ); commentErr != nil {
86- fmt .Printf ("Warning: failed to update stack comment: %v\n " , commentErr )
87- }
87+ // Build args for gh pr create
88+ ghArgs := []string {"pr" , "create" , "--base" , base }
89+
90+ // Auto-draft if not targeting trunk (middle of stack)
91+ if base != trunk {
92+ ghArgs = append (ghArgs , "--draft" )
93+ fmt .Printf ("Creating draft PR (base %q is not trunk %q)\n " , base , trunk )
94+ }
95+
96+ // Generate PR body from commits if user hasn't specified --body or --fill
97+ if ! hasBodyFlag (args ) {
98+ body , bodyErr := generatePRBody (g , base , branch )
99+ if bodyErr != nil {
100+ // Non-fatal: just skip auto-body and let user fill it in
101+ fmt .Printf ("Warning: could not generate PR body: %v\n " , bodyErr )
102+ } else if body != "" {
103+ ghArgs = append (ghArgs , "--body" , body )
88104 }
105+ }
106+
107+ // Pass through any additional args from user
108+ ghArgs = append (ghArgs , args ... )
109+
110+ // Let user interact with gh pr create
111+ ctx := context .Background ()
112+ if execErr := gh .ExecInteractive (ctx , ghArgs ... ); execErr != nil {
113+ return fmt .Errorf ("gh pr create failed: %w" , execErr )
114+ }
115+
116+ // Find the PR we just created
117+ pr , err := ghClient .FindPRByHead (branch )
118+ if err != nil {
119+ return fmt .Errorf ("failed to find created PR: %w" , err )
120+ }
121+ if pr == nil {
122+ // User might have cancelled
123+ fmt .Println ("No PR was created." )
89124 return nil
90125 }
91126
92- // Create new PR
93- fmt .Printf ("Creating PR for %q targeting %q...\n " , branch , base )
127+ // Store PR number
128+ if setErr := cfg .SetPR (branch , pr .Number ); setErr != nil {
129+ return setErr
130+ }
94131
95- var prNumber int
96- if base != trunk {
97- // Create as draft since it's part of a stack
98- prNumber , err = gh .CreateDraftPR (branch , base , branch , "" )
99- if err != nil {
100- return err
132+ // Post stack navigation comment
133+ root , err := tree .Build (cfg )
134+ if err != nil {
135+ return fmt .Errorf ("build tree: %w" , err )
136+ }
137+
138+ if err := ghClient .GenerateAndPostStackComment (root , branch , trunk , pr .Number ); err != nil {
139+ fmt .Printf ("Warning: failed to add stack comment: %v\n " , err )
140+ }
141+
142+ fmt .Printf ("Stored PR #%d for branch %q\n " , pr .Number , branch )
143+ return nil
144+ }
145+
146+ // updateExistingPR updates the base branch and stack comment for an existing PR.
147+ func updateExistingPR (ghClient * github.Client , cfg * config.Config , prNumber int , branch , base , trunk string ) error {
148+ fmt .Printf ("PR #%d already exists, updating base to %q\n " , prNumber , base )
149+
150+ if err := ghClient .UpdatePRBase (prNumber , base ); err != nil {
151+ return fmt .Errorf ("failed to update PR base: %w" , err )
152+ }
153+
154+ // Update stack comment
155+ root , err := tree .Build (cfg )
156+ if err != nil {
157+ return fmt .Errorf ("build tree: %w" , err )
158+ }
159+
160+ if err := ghClient .GenerateAndPostStackComment (root , branch , trunk , prNumber ); err != nil {
161+ fmt .Printf ("Warning: failed to update stack comment: %v\n " , err )
162+ }
163+
164+ fmt .Println (ghClient .PRURL (prNumber ))
165+ return nil
166+ }
167+
168+ // hasBodyFlag checks if the user has provided --body, -b, or --fill flags.
169+ func hasBodyFlag (args []string ) bool {
170+ for _ , arg := range args {
171+ // Check for --body or -b (with or without = syntax)
172+ if arg == "--body" || arg == "-b" || strings .HasPrefix (arg , "--body=" ) {
173+ return true
101174 }
102- fmt .Printf ("Created draft PR #%d for %s -> %s\n " , prNumber , branch , base )
103- } else {
104- prNumber , err = gh .CreatePR (branch , base , branch , "" )
105- if err != nil {
106- return err
175+ // Check for --fill or -f
176+ if arg == "--fill" || arg == "-f" {
177+ return true
178+ }
179+ // Check for --fill-first or --fill-verbose
180+ if arg == "--fill-first" || arg == "--fill-verbose" {
181+ return true
182+ }
183+ // Check for combined short flags like -bf
184+ if len (arg ) > 1 && arg [0 ] == '-' && arg [1 ] != '-' {
185+ for _ , c := range arg [1 :] {
186+ if c == 'b' || c == 'f' {
187+ return true
188+ }
189+ }
107190 }
108- fmt .Printf ("Created PR #%d for %s -> %s\n " , prNumber , branch , base )
109191 }
192+ return false
193+ }
110194
111- // Store PR number
112- if setPRErr := cfg .SetPR (branch , prNumber ); setPRErr != nil {
113- return setPRErr
195+ // generatePRBody creates a PR description from the commits between base and head.
196+ // For a single commit: returns the commit body.
197+ // For multiple commits: returns each commit as a markdown section.
198+ func generatePRBody (g * git.Git , base , head string ) (string , error ) {
199+ commits , err := g .GetCommits (base , head )
200+ if err != nil {
201+ return "" , err
114202 }
115203
116- // Post stack navigation comment
117- root , buildErr := tree .Build (cfg )
118- if buildErr != nil {
119- return fmt .Errorf ("build tree: %w" , buildErr )
204+ if len (commits ) == 0 {
205+ return "" , nil
120206 }
121207
122- comment := github .GenerateStackComment (root , branch , trunk )
123- if comment != "" {
124- if err := gh .CreateOrUpdateStackComment (prNumber , comment ); err != nil {
125- fmt .Printf ("Warning: failed to add stack comment: %v\n " , err )
126- // Don't fail the command for comment issues
208+ if len (commits ) == 1 {
209+ // Single commit: just use the body
210+ return commits [0 ].Body , nil
211+ }
212+
213+ // Multiple commits: format as markdown sections
214+ var sb strings.Builder
215+ for i , commit := range commits {
216+ if i > 0 {
217+ sb .WriteString ("\n " )
218+ }
219+ sb .WriteString ("### " )
220+ sb .WriteString (commit .Subject )
221+ sb .WriteString ("\n " )
222+ if commit .Body != "" {
223+ sb .WriteString ("\n " )
224+ sb .WriteString (commit .Body )
225+ sb .WriteString ("\n " )
127226 }
128227 }
129228
130- return nil
229+ return sb . String (), nil
131230}
0 commit comments