77 "strings"
88 "time"
99
10+ "github.com/AlecAivazis/survey/v2"
1011 "github.com/MakeNowJust/heredoc"
1112 "github.com/cli/cli/api"
1213 "github.com/cli/cli/context"
@@ -17,6 +18,7 @@ import (
1718 "github.com/cli/cli/pkg/cmdutil"
1819 "github.com/cli/cli/pkg/githubtemplate"
1920 "github.com/cli/cli/pkg/iostreams"
21+ "github.com/cli/cli/pkg/prompt"
2022 "github.com/cli/cli/utils"
2123 "github.com/spf13/cobra"
2224)
@@ -40,6 +42,7 @@ type CreateOptions struct {
4042 Title string
4143 Body string
4244 BaseBranch string
45+ HeadBranch string
4346
4447 Reviewers []string
4548 Assignees []string
@@ -60,13 +63,23 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co
6063 cmd := & cobra.Command {
6164 Use : "create" ,
6265 Short : "Create a pull request" ,
66+ Long : heredoc .Doc (`
67+ Create a pull request on GitHub.
68+
69+ When the current branch isn't fully pushed to a git remote, a prompt will ask where
70+ to push the branch and offer an option to fork the base repository. Use '--head' to
71+ explicitly skip any forking or pushing behavior.
72+
73+ A prompt will also ask for the title and the body of the pull request. Use '--title'
74+ and '--body' to skip this, or use '--fill' to autofill these values from git commits.
75+ ` ),
6376 Example : heredoc .Doc (`
6477 $ gh pr create --title "The bug is fixed" --body "Everything works again"
6578 $ gh issue create --label "bug,help wanted"
6679 $ gh issue create --label bug --label "help wanted"
6780 $ gh pr create --reviewer monalisa,hubot
6881 $ gh pr create --project "Roadmap"
69- $ gh pr create --base develop
82+ $ gh pr create --base develop --head monalisa:feature
7083 ` ),
7184 Args : cmdutil .NoArgsQuoteReminder ,
7285 RunE : func (cmd * cobra.Command , args []string ) error {
@@ -96,9 +109,10 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co
96109
97110 fl := cmd .Flags ()
98111 fl .BoolVarP (& opts .IsDraft , "draft" , "d" , false , "Mark pull request as a draft" )
99- fl .StringVarP (& opts .Title , "title" , "t" , "" , "Supply a title. Will prompt for one otherwise." )
100- fl .StringVarP (& opts .Body , "body" , "b" , "" , "Supply a body. Will prompt for one otherwise." )
101- fl .StringVarP (& opts .BaseBranch , "base" , "B" , "" , "The branch into which you want your code merged" )
112+ fl .StringVarP (& opts .Title , "title" , "t" , "" , "Title for the pull request" )
113+ fl .StringVarP (& opts .Body , "body" , "b" , "" , "Body for the pull request" )
114+ fl .StringVarP (& opts .BaseBranch , "base" , "B" , "" , "The `branch` into which you want your code merged" )
115+ fl .StringVarP (& opts .HeadBranch , "head" , "H" , "" , "The `branch` that contains commits for your pull request (default: current branch)" )
102116 fl .BoolVarP (& opts .WebMode , "web" , "w" , false , "Open the web browser to create a pull request" )
103117 fl .BoolVarP (& opts .Autofill , "fill" , "f" , false , "Do not prompt for title/body and just use commit info" )
104118 fl .StringSliceVarP (& opts .Reviewers , "reviewer" , "r" , nil , "Request reviews from people by their `login`" )
@@ -132,6 +146,8 @@ func createRun(opts *CreateOptions) error {
132146 if r , ok := br .(* api.Repository ); ok {
133147 baseRepo = r
134148 } else {
149+ // TODO: if RepoNetwork is going to be requested anyway in `repoContext.HeadRepos()`,
150+ // consider piggybacking on that result instead of performing a separate lookup
135151 var err error
136152 baseRepo , err = api .GitHubRepo (client , br )
137153 if err != nil {
@@ -142,34 +158,108 @@ func createRun(opts *CreateOptions) error {
142158 return fmt .Errorf ("could not determine base repository: %w" , err )
143159 }
144160
145- headBranch , err := opts .Branch ()
146- if err != nil {
147- return fmt .Errorf ("could not determine the current branch: %w" , err )
161+ isPushEnabled := false
162+ headBranch := opts .HeadBranch
163+ headBranchLabel := opts .HeadBranch
164+ if headBranch == "" {
165+ headBranch , err = opts .Branch ()
166+ if err != nil {
167+ return fmt .Errorf ("could not determine the current branch: %w" , err )
168+ }
169+ headBranchLabel = headBranch
170+ isPushEnabled = true
171+ } else if idx := strings .IndexRune (headBranch , ':' ); idx >= 0 {
172+ headBranch = headBranch [idx + 1 :]
173+ }
174+
175+ if ucc , err := git .UncommittedChangeCount (); err == nil && ucc > 0 {
176+ fmt .Fprintf (opts .IO .ErrOut , "Warning: %s\n " , utils .Pluralize (ucc , "uncommitted change" ))
148177 }
149178
150179 var headRepo ghrepo.Interface
151180 var headRemote * context.Remote
152181
153- // determine whether the head branch is already pushed to a remote
154- headBranchPushedTo := determineTrackingBranch (remotes , headBranch )
155- if headBranchPushedTo != nil {
156- for _ , r := range remotes {
157- if r .Name != headBranchPushedTo .RemoteName {
158- continue
182+ if isPushEnabled {
183+ // determine whether the head branch is already pushed to a remote
184+ if pushedTo := determineTrackingBranch (remotes , headBranch ); pushedTo != nil {
185+ isPushEnabled = false
186+ for _ , r := range remotes {
187+ if r .Name != pushedTo .RemoteName {
188+ continue
189+ }
190+ headRepo = r
191+ headRemote = r
192+ break
159193 }
160- headRepo = r
161- headRemote = r
162- break
163194 }
164195 }
165196
166- // otherwise, determine the head repository with info obtained from the API
167- if headRepo == nil {
168- if r , err := repoContext .HeadRepo (baseRepo ); err == nil {
169- headRepo = r
197+ // otherwise, ask the user for the head repository using info obtained from the API
198+ if headRepo == nil && isPushEnabled && opts .IO .CanPrompt () {
199+ pushableRepos , err := repoContext .HeadRepos ()
200+ if err != nil {
201+ return err
202+ }
203+
204+ if len (pushableRepos ) == 0 {
205+ pushableRepos , err = api .RepoFindForks (client , baseRepo , 3 )
206+ if err != nil {
207+ return err
208+ }
209+ }
210+
211+ currentLogin , err := api .CurrentLoginName (client , baseRepo .RepoHost ())
212+ if err != nil {
213+ return err
214+ }
215+
216+ hasOwnFork := false
217+ var pushOptions []string
218+ for _ , r := range pushableRepos {
219+ pushOptions = append (pushOptions , ghrepo .FullName (r ))
220+ if r .RepoOwner () == currentLogin {
221+ hasOwnFork = true
222+ }
223+ }
224+
225+ if ! hasOwnFork {
226+ pushOptions = append (pushOptions , "Create a fork of " + ghrepo .FullName (baseRepo ))
227+ }
228+ pushOptions = append (pushOptions , "Skip pushing the branch" )
229+ pushOptions = append (pushOptions , "Cancel" )
230+
231+ var selectedOption int
232+ err = prompt .SurveyAskOne (& survey.Select {
233+ Message : fmt .Sprintf ("Where should we push the '%s' branch?" , headBranch ),
234+ Options : pushOptions ,
235+ }, & selectedOption )
236+ if err != nil {
237+ return err
238+ }
239+
240+ if selectedOption < len (pushableRepos ) {
241+ headRepo = pushableRepos [selectedOption ]
242+ if ! ghrepo .IsSame (baseRepo , headRepo ) {
243+ headBranchLabel = fmt .Sprintf ("%s:%s" , headRepo .RepoOwner (), headBranch )
244+ }
245+ } else if pushOptions [selectedOption ] == "Skip pushing the branch" {
246+ isPushEnabled = false
247+ } else if pushOptions [selectedOption ] == "Cancel" {
248+ return cmdutil .SilentError
249+ } else {
250+ // "Create a fork of ..."
251+ if baseRepo .IsPrivate {
252+ return fmt .Errorf ("cannot fork private repository %s" , ghrepo .FullName (baseRepo ))
253+ }
254+ headBranchLabel = fmt .Sprintf ("%s:%s" , currentLogin , headBranch )
170255 }
171256 }
172257
258+ if headRepo == nil && isPushEnabled && ! opts .IO .CanPrompt () {
259+ fmt .Fprintf (opts .IO .ErrOut , "aborted: you must first push the current branch to a remote, or use the --head flag" )
260+ return cmdutil .SilentError
261+ }
262+
173263 baseBranch := opts .BaseBranch
174264 if baseBranch == "" {
175265 baseBranch = baseRepo .DefaultBranchRef .Name
@@ -178,10 +268,6 @@ func createRun(opts *CreateOptions) error {
178268 return fmt .Errorf ("must be on a branch named differently than %q" , baseBranch )
179269 }
180270
181- if ucc , err := git .UncommittedChangeCount (); err == nil && ucc > 0 {
182- fmt .Fprintf (opts .IO .ErrOut , "Warning: %s\n " , utils .Pluralize (ucc , "uncommitted change" ))
183- }
184-
185271 var milestoneTitles []string
186272 if opts .Milestone != "" {
187273 milestoneTitles = []string {opts .Milestone }
@@ -211,10 +297,6 @@ func createRun(opts *CreateOptions) error {
211297 }
212298
213299 if ! opts .WebMode {
214- headBranchLabel := headBranch
215- if headRepo != nil && ! ghrepo .IsSame (baseRepo , headRepo ) {
216- headBranchLabel = fmt .Sprintf ("%s:%s" , headRepo .RepoOwner (), headBranch )
217- }
218300 existingPR , err := api .PullRequestForBranch (client , baseRepo , baseBranch , headBranchLabel )
219301 var notFound * api.NotFoundError
220302 if err != nil && ! errors .As (err , & notFound ) {
@@ -297,23 +379,15 @@ func createRun(opts *CreateOptions) error {
297379 didForkRepo := false
298380 // if a head repository could not be determined so far, automatically create
299381 // one by forking the base repository
300- if headRepo == nil {
301- if baseRepo .IsPrivate {
302- return fmt .Errorf ("cannot fork private repository '%s'" , ghrepo .FullName (baseRepo ))
303- }
382+ if headRepo == nil && isPushEnabled {
304383 headRepo , err = api .ForkRepo (client , baseRepo )
305384 if err != nil {
306385 return fmt .Errorf ("error forking repo: %w" , err )
307386 }
308387 didForkRepo = true
309388 }
310389
311- headBranchLabel := headBranch
312- if ! ghrepo .IsSame (baseRepo , headRepo ) {
313- headBranchLabel = fmt .Sprintf ("%s:%s" , headRepo .RepoOwner (), headBranch )
314- }
315-
316- if headRemote == nil {
390+ if headRemote == nil && headRepo != nil {
317391 headRemote , _ = repoContext .RemoteForRepo (headRepo )
318392 }
319393
@@ -324,7 +398,7 @@ func createRun(opts *CreateOptions) error {
324398 //
325399 // In either case, we want to add the head repo as a new git remote so we
326400 // can push to it.
327- if headRemote == nil {
401+ if headRemote == nil && isPushEnabled {
328402 cfg , err := opts .Config ()
329403 if err != nil {
330404 return err
@@ -345,7 +419,7 @@ func createRun(opts *CreateOptions) error {
345419 }
346420
347421 // automatically push the branch if it hasn't been pushed anywhere yet
348- if headBranchPushedTo == nil {
422+ if isPushEnabled {
349423 pushTries := 0
350424 maxPushTries := 3
351425 for {
0 commit comments