Skip to content

Commit 1479ff4

Browse files
authored
Merge pull request cli#38 from github/pr-create
Add `pr create` command Closes cli#59
2 parents 55ab2f6 + 135efda commit 1479ff4

File tree

13 files changed

+590
-7
lines changed

13 files changed

+590
-7
lines changed

api/queries.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -401,6 +401,45 @@ func PullRequestsForBranch(client *Client, ghRepo Repo, branch string) ([]PullRe
401401
return prs, nil
402402
}
403403

404+
func CreatePullRequest(client *Client, ghRepo Repo, params map[string]interface{}) (*PullRequest, error) {
405+
repoID, err := GitHubRepoId(client, ghRepo)
406+
if err != nil {
407+
return nil, err
408+
}
409+
410+
query := `
411+
mutation CreatePullRequest($input: CreatePullRequestInput!) {
412+
createPullRequest(input: $input) {
413+
pullRequest {
414+
url
415+
}
416+
}
417+
}`
418+
419+
inputParams := map[string]interface{}{
420+
"repositoryId": repoID,
421+
}
422+
for key, val := range params {
423+
inputParams[key] = val
424+
}
425+
variables := map[string]interface{}{
426+
"input": inputParams,
427+
}
428+
429+
result := struct {
430+
CreatePullRequest struct {
431+
PullRequest PullRequest
432+
}
433+
}{}
434+
435+
err = client.GraphQL(query, variables, &result)
436+
if err != nil {
437+
return nil, err
438+
}
439+
440+
return &result.CreatePullRequest.PullRequest, nil
441+
}
442+
404443
func PullRequestList(client *Client, vars map[string]interface{}, limit int) ([]PullRequest, error) {
405444
type response struct {
406445
Repository struct {

command/pr.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313

1414
func init() {
1515
RootCmd.AddCommand(prCmd)
16+
prCmd.AddCommand(prCreateCmd)
1617
prCmd.AddCommand(prListCmd)
1718
prCmd.AddCommand(prStatusCmd)
1819
prCmd.AddCommand(prViewCmd)

command/pr_create.go

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
package command
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"runtime"
7+
8+
"github.com/AlecAivazis/survey/v2"
9+
"github.com/github/gh-cli/api"
10+
"github.com/github/gh-cli/context"
11+
"github.com/github/gh-cli/git"
12+
"github.com/pkg/errors"
13+
"github.com/spf13/cobra"
14+
)
15+
16+
func prCreate(cmd *cobra.Command, _ []string) error {
17+
ctx := contextForCommand(cmd)
18+
19+
ucc, err := git.UncommittedChangeCount()
20+
if err != nil {
21+
return err
22+
}
23+
if ucc > 0 {
24+
noun := "change"
25+
if ucc > 1 {
26+
// TODO: use pluralize helper
27+
noun = noun + "s"
28+
}
29+
30+
cmd.Printf("Warning: %d uncommitted %s\n", ucc, noun)
31+
}
32+
33+
head, err := ctx.Branch()
34+
if err != nil {
35+
return errors.Wrap(err, "could not determine current branch")
36+
}
37+
38+
remote, err := guessRemote(ctx)
39+
if err != nil {
40+
return err
41+
}
42+
43+
if err = git.Push(remote, fmt.Sprintf("HEAD:%s", head)); err != nil {
44+
return fmt.Errorf("was not able to push to remote '%s': %s", remote, err)
45+
}
46+
47+
title, err := cmd.Flags().GetString("title")
48+
if err != nil {
49+
return errors.Wrap(err, "could not parse title")
50+
}
51+
body, err := cmd.Flags().GetString("body")
52+
if err != nil {
53+
return errors.Wrap(err, "could not parse body")
54+
}
55+
56+
interactive := title == "" || body == ""
57+
58+
inProgress := struct {
59+
Body string
60+
Title string
61+
}{}
62+
63+
if interactive {
64+
confirmed := false
65+
editor := determineEditor()
66+
67+
for !confirmed {
68+
titleQuestion := &survey.Question{
69+
Name: "title",
70+
Prompt: &survey.Input{
71+
Message: "PR Title",
72+
Default: inProgress.Title,
73+
},
74+
}
75+
bodyQuestion := &survey.Question{
76+
Name: "body",
77+
Prompt: &survey.Editor{
78+
Message: fmt.Sprintf("PR Body (%s)", editor),
79+
FileName: "*.md",
80+
Default: inProgress.Body,
81+
AppendDefault: true,
82+
Editor: editor,
83+
},
84+
}
85+
86+
qs := []*survey.Question{}
87+
if title == "" {
88+
qs = append(qs, titleQuestion)
89+
}
90+
if body == "" {
91+
qs = append(qs, bodyQuestion)
92+
}
93+
94+
err := survey.Ask(qs, &inProgress)
95+
if err != nil {
96+
return errors.Wrap(err, "could not prompt")
97+
}
98+
confirmAnswers := struct {
99+
Confirmation string
100+
}{}
101+
confirmQs := []*survey.Question{
102+
{
103+
Name: "confirmation",
104+
Prompt: &survey.Select{
105+
Message: "Submit?",
106+
Options: []string{
107+
"Yes",
108+
"Edit",
109+
"Cancel",
110+
},
111+
},
112+
},
113+
}
114+
115+
err = survey.Ask(confirmQs, &confirmAnswers)
116+
if err != nil {
117+
return errors.Wrap(err, "could not prompt")
118+
}
119+
120+
switch confirmAnswers.Confirmation {
121+
case "Yes":
122+
confirmed = true
123+
case "Edit":
124+
continue
125+
case "Cancel":
126+
cmd.Println("Discarding pull request")
127+
return nil
128+
}
129+
}
130+
}
131+
132+
if title == "" {
133+
title = inProgress.Title
134+
}
135+
if body == "" {
136+
body = inProgress.Body
137+
}
138+
base, err := cmd.Flags().GetString("base")
139+
if err != nil {
140+
return errors.Wrap(err, "could not parse base")
141+
}
142+
if base == "" {
143+
// TODO: use default branch for the repo
144+
base = "master"
145+
}
146+
147+
client, err := apiClientForContext(ctx)
148+
if err != nil {
149+
return errors.Wrap(err, "could not initialize api client")
150+
}
151+
152+
repo, err := ctx.BaseRepo()
153+
if err != nil {
154+
return errors.Wrap(err, "could not determine GitHub repo")
155+
}
156+
157+
isDraft, err := cmd.Flags().GetBool("draft")
158+
if err != nil {
159+
return errors.Wrap(err, "could not parse draft")
160+
}
161+
162+
params := map[string]interface{}{
163+
"title": title,
164+
"body": body,
165+
"draft": isDraft,
166+
"baseRefName": base,
167+
"headRefName": head,
168+
}
169+
170+
pr, err := api.CreatePullRequest(client, repo, params)
171+
if err != nil {
172+
return errors.Wrap(err, "failed to create PR")
173+
}
174+
175+
fmt.Fprintln(cmd.OutOrStdout(), pr.URL)
176+
return nil
177+
}
178+
179+
func guessRemote(ctx context.Context) (string, error) {
180+
remotes, err := ctx.Remotes()
181+
if err != nil {
182+
return "", errors.Wrap(err, "could not read git remotes")
183+
}
184+
185+
// TODO: consolidate logic with fsContext.BaseRepo
186+
// TODO: check if the GH repo that the remote points to is writeable
187+
remote, err := remotes.FindByName("upstream", "github", "origin", "*")
188+
if err != nil {
189+
return "", errors.Wrap(err, "could not determine suitable remote")
190+
}
191+
192+
return remote.Name, nil
193+
}
194+
195+
func determineEditor() string {
196+
if runtime.GOOS == "windows" {
197+
return "notepad"
198+
}
199+
if v := os.Getenv("VISUAL"); v != "" {
200+
return v
201+
}
202+
if e := os.Getenv("EDITOR"); e != "" {
203+
return e
204+
}
205+
return "nano"
206+
}
207+
208+
var prCreateCmd = &cobra.Command{
209+
Use: "create",
210+
Short: "Create a pull request",
211+
RunE: prCreate,
212+
}
213+
214+
func init() {
215+
prCreateCmd.Flags().BoolP("draft", "d", false,
216+
"Mark PR as a draft")
217+
prCreateCmd.Flags().StringP("title", "t", "",
218+
"Supply a title. Will prompt for one otherwise.")
219+
prCreateCmd.Flags().StringP("body", "b", "",
220+
"Supply a body. Will prompt for one otherwise.")
221+
prCreateCmd.Flags().StringP("base", "T", "",
222+
"The branch into which you want your code merged")
223+
}

0 commit comments

Comments
 (0)