preserve and restore issue/pr input on failure
This commit is contained in:
parent
e92cd43259
commit
d300526318
10 changed files with 457 additions and 71 deletions
|
|
@ -26,6 +26,8 @@ type CreateOptions struct {
|
||||||
|
|
||||||
RepoOverride string
|
RepoOverride string
|
||||||
WebMode bool
|
WebMode bool
|
||||||
|
JSONFill bool
|
||||||
|
JSONInput string
|
||||||
|
|
||||||
Title string
|
Title string
|
||||||
Body string
|
Body string
|
||||||
|
|
@ -62,6 +64,7 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co
|
||||||
titleProvided := cmd.Flags().Changed("title")
|
titleProvided := cmd.Flags().Changed("title")
|
||||||
bodyProvided := cmd.Flags().Changed("body")
|
bodyProvided := cmd.Flags().Changed("body")
|
||||||
opts.RepoOverride, _ = cmd.Flags().GetString("repo")
|
opts.RepoOverride, _ = cmd.Flags().GetString("repo")
|
||||||
|
opts.JSONFill = cmd.Flags().Changed("json")
|
||||||
|
|
||||||
opts.Interactive = !(titleProvided && bodyProvided)
|
opts.Interactive = !(titleProvided && bodyProvided)
|
||||||
|
|
||||||
|
|
@ -69,6 +72,14 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co
|
||||||
return &cmdutil.FlagError{Err: errors.New("must provide --title and --body when not running interactively")}
|
return &cmdutil.FlagError{Err: errors.New("must provide --title and --body when not running interactively")}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if opts.JSONFill {
|
||||||
|
opts.Interactive = false
|
||||||
|
|
||||||
|
if opts.WebMode {
|
||||||
|
return errors.New("--web and --json are mutually exclusive")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if runF != nil {
|
if runF != nil {
|
||||||
return runF(opts)
|
return runF(opts)
|
||||||
}
|
}
|
||||||
|
|
@ -83,20 +94,21 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co
|
||||||
cmd.Flags().StringSliceVarP(&opts.Labels, "label", "l", nil, "Add labels by `name`")
|
cmd.Flags().StringSliceVarP(&opts.Labels, "label", "l", nil, "Add labels by `name`")
|
||||||
cmd.Flags().StringSliceVarP(&opts.Projects, "project", "p", nil, "Add the issue to projects by `name`")
|
cmd.Flags().StringSliceVarP(&opts.Projects, "project", "p", nil, "Add the issue to projects by `name`")
|
||||||
cmd.Flags().StringVarP(&opts.Milestone, "milestone", "m", "", "Add the issue to a milestone by `name`")
|
cmd.Flags().StringVarP(&opts.Milestone, "milestone", "m", "", "Add the issue to a milestone by `name`")
|
||||||
|
cmd.Flags().StringVarP(&opts.JSONInput, "json", "j", "", "Use JSON to populate and submit issue")
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func createRun(opts *CreateOptions) error {
|
func createRun(opts *CreateOptions) (err error) {
|
||||||
httpClient, err := opts.HttpClient()
|
httpClient, err := opts.HttpClient()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return
|
||||||
}
|
}
|
||||||
apiClient := api.NewClientFromHTTP(httpClient)
|
apiClient := api.NewClientFromHTTP(httpClient)
|
||||||
|
|
||||||
baseRepo, err := opts.BaseRepo()
|
baseRepo, err := opts.BaseRepo()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
templateFiles, legacyTemplate := prShared.FindTemplates(opts.RootDirOverride, "ISSUE_TEMPLATE")
|
templateFiles, legacyTemplate := prShared.FindTemplates(opts.RootDirOverride, "ISSUE_TEMPLATE")
|
||||||
|
|
@ -123,7 +135,7 @@ func createRun(opts *CreateOptions) error {
|
||||||
if opts.Title != "" || opts.Body != "" {
|
if opts.Title != "" || opts.Body != "" {
|
||||||
openURL, err = prShared.WithPrAndIssueQueryParams(openURL, tb)
|
openURL, err = prShared.WithPrAndIssueQueryParams(openURL, tb)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return
|
||||||
}
|
}
|
||||||
} else if len(templateFiles) > 1 {
|
} else if len(templateFiles) > 1 {
|
||||||
openURL += "/choose"
|
openURL += "/choose"
|
||||||
|
|
@ -140,24 +152,28 @@ func createRun(opts *CreateOptions) error {
|
||||||
|
|
||||||
repo, err := api.GitHubRepo(apiClient, baseRepo)
|
repo, err := api.GitHubRepo(apiClient, baseRepo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return
|
||||||
}
|
}
|
||||||
if !repo.HasIssuesEnabled {
|
if !repo.HasIssuesEnabled {
|
||||||
return fmt.Errorf("the '%s' repository has disabled issues", ghrepo.FullName(baseRepo))
|
err = fmt.Errorf("the '%s' repository has disabled issues", ghrepo.FullName(baseRepo))
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
action := prShared.SubmitAction
|
action := prShared.SubmitAction
|
||||||
|
|
||||||
if opts.Interactive {
|
if opts.Interactive {
|
||||||
editorCommand, err := cmdutil.DetermineEditor(opts.Config)
|
var editorCommand string
|
||||||
|
editorCommand, err = cmdutil.DetermineEditor(opts.Config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defer prShared.PreserveInput(opts.IO, &tb, &err)()
|
||||||
|
|
||||||
if tb.Title == "" {
|
if tb.Title == "" {
|
||||||
err = prShared.TitleSurvey(&tb)
|
err = prShared.TitleSurvey(&tb)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -166,12 +182,12 @@ func createRun(opts *CreateOptions) error {
|
||||||
|
|
||||||
templateContent, err = prShared.TemplateSurvey(templateFiles, legacyTemplate, tb)
|
templateContent, err = prShared.TemplateSurvey(templateFiles, legacyTemplate, tb)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = prShared.BodySurvey(&tb, templateContent, editorCommand)
|
err = prShared.BodySurvey(&tb, templateContent, editorCommand)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if tb.Body == "" {
|
if tb.Body == "" {
|
||||||
|
|
@ -179,31 +195,40 @@ func createRun(opts *CreateOptions) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
action, err := prShared.ConfirmSubmission(!tb.HasMetadata(), repo.ViewerCanTriage())
|
var action prShared.Action
|
||||||
|
action, err = prShared.ConfirmSubmission(!tb.HasMetadata(), repo.ViewerCanTriage())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to confirm: %w", err)
|
err = fmt.Errorf("unable to confirm: %w", err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if action == prShared.MetadataAction {
|
if action == prShared.MetadataAction {
|
||||||
err = prShared.MetadataSurvey(opts.IO, apiClient, baseRepo, &tb)
|
err = prShared.MetadataSurvey(opts.IO, apiClient, baseRepo, &tb)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
action, err = prShared.ConfirmSubmission(!tb.HasMetadata(), false)
|
action, err = prShared.ConfirmSubmission(!tb.HasMetadata(), false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if action == prShared.CancelAction {
|
if action == prShared.CancelAction {
|
||||||
fmt.Fprintln(opts.IO.ErrOut, "Discarding.")
|
fmt.Fprintln(opts.IO.ErrOut, "Discarding.")
|
||||||
|
return
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
} else if opts.JSONFill {
|
||||||
|
err = prShared.FillFromJSON(opts.IO, opts.JSONInput, &tb)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
action = prShared.SubmitAction
|
||||||
} else {
|
} else {
|
||||||
if tb.Title == "" {
|
if tb.Title == "" {
|
||||||
return fmt.Errorf("title can't be blank")
|
err = fmt.Errorf("title can't be blank")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -211,7 +236,7 @@ func createRun(opts *CreateOptions) error {
|
||||||
openURL := ghrepo.GenerateRepoURL(baseRepo, "issues/new")
|
openURL := ghrepo.GenerateRepoURL(baseRepo, "issues/new")
|
||||||
openURL, err = prShared.WithPrAndIssueQueryParams(openURL, tb)
|
openURL, err = prShared.WithPrAndIssueQueryParams(openURL, tb)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return
|
||||||
}
|
}
|
||||||
if isTerminal {
|
if isTerminal {
|
||||||
fmt.Fprintf(opts.IO.ErrOut, "Opening %s in your browser.\n", utils.DisplayURL(openURL))
|
fmt.Fprintf(opts.IO.ErrOut, "Opening %s in your browser.\n", utils.DisplayURL(openURL))
|
||||||
|
|
@ -225,12 +250,13 @@ func createRun(opts *CreateOptions) error {
|
||||||
|
|
||||||
err = prShared.AddMetadataToIssueParams(apiClient, baseRepo, params, &tb)
|
err = prShared.AddMetadataToIssueParams(apiClient, baseRepo, params, &tb)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
newIssue, err := api.IssueCreate(apiClient, repo, params)
|
var newIssue *api.Issue
|
||||||
|
newIssue, err = api.IssueCreate(apiClient, repo, params)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Fprintln(opts.IO.Out, newIssue.URL)
|
fmt.Fprintln(opts.IO.Out, newIssue.URL)
|
||||||
|
|
@ -238,5 +264,5 @@ func createRun(opts *CreateOptions) error {
|
||||||
panic("Unreachable state")
|
panic("Unreachable state")
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -126,6 +126,46 @@ func TestIssueCreate(t *testing.T) {
|
||||||
eq(t, output.String(), "https://github.com/OWNER/REPO/issues/12\n")
|
eq(t, output.String(), "https://github.com/OWNER/REPO/issues/12\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestIssueCreate_JSON(t *testing.T) {
|
||||||
|
http := &httpmock.Registry{}
|
||||||
|
defer http.Verify(t)
|
||||||
|
|
||||||
|
http.StubResponse(200, bytes.NewBufferString(`
|
||||||
|
{ "data": { "repository": {
|
||||||
|
"id": "REPOID",
|
||||||
|
"hasIssuesEnabled": true
|
||||||
|
} } }
|
||||||
|
`))
|
||||||
|
http.StubResponse(200, bytes.NewBufferString(`
|
||||||
|
{ "data": { "createIssue": { "issue": {
|
||||||
|
"URL": "https://github.com/OWNER/REPO/issues/12"
|
||||||
|
} } } }
|
||||||
|
`))
|
||||||
|
|
||||||
|
output, err := runCommand(http, true, `-j'{"title":"cool", "body":"issue"}'`)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("error running command `issue create`: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
bodyBytes, _ := ioutil.ReadAll(http.Requests[1].Body)
|
||||||
|
reqBody := struct {
|
||||||
|
Variables struct {
|
||||||
|
Input struct {
|
||||||
|
RepositoryID string
|
||||||
|
Title string
|
||||||
|
Body string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}{}
|
||||||
|
_ = json.Unmarshal(bodyBytes, &reqBody)
|
||||||
|
|
||||||
|
eq(t, reqBody.Variables.Input.RepositoryID, "REPOID")
|
||||||
|
eq(t, reqBody.Variables.Input.Title, "cool")
|
||||||
|
eq(t, reqBody.Variables.Input.Body, "issue")
|
||||||
|
|
||||||
|
eq(t, output.String(), "https://github.com/OWNER/REPO/issues/12\n")
|
||||||
|
}
|
||||||
|
|
||||||
func TestIssueCreate_nonLegacyTemplate(t *testing.T) {
|
func TestIssueCreate_nonLegacyTemplate(t *testing.T) {
|
||||||
http := &httpmock.Registry{}
|
http := &httpmock.Registry{}
|
||||||
defer http.Verify(t)
|
defer http.Verify(t)
|
||||||
|
|
|
||||||
|
|
@ -38,8 +38,10 @@ type CreateOptions struct {
|
||||||
RootDirOverride string
|
RootDirOverride string
|
||||||
RepoOverride string
|
RepoOverride string
|
||||||
|
|
||||||
Autofill bool
|
Autofill bool
|
||||||
WebMode bool
|
WebMode bool
|
||||||
|
JSONFill bool
|
||||||
|
JSONInput string
|
||||||
|
|
||||||
IsDraft bool
|
IsDraft bool
|
||||||
Title string
|
Title string
|
||||||
|
|
@ -99,11 +101,12 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co
|
||||||
`),
|
`),
|
||||||
Args: cmdutil.NoArgsQuoteReminder,
|
Args: cmdutil.NoArgsQuoteReminder,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
opts.JSONFill = cmd.Flags().Changed("json")
|
||||||
opts.TitleProvided = cmd.Flags().Changed("title")
|
opts.TitleProvided = cmd.Flags().Changed("title")
|
||||||
opts.BodyProvided = cmd.Flags().Changed("body")
|
opts.BodyProvided = cmd.Flags().Changed("body")
|
||||||
opts.RepoOverride, _ = cmd.Flags().GetString("repo")
|
opts.RepoOverride, _ = cmd.Flags().GetString("repo")
|
||||||
|
|
||||||
if !opts.IO.CanPrompt() && !opts.WebMode && !opts.TitleProvided && !opts.Autofill {
|
if !opts.IO.CanPrompt() && !opts.JSONFill && !opts.WebMode && !opts.TitleProvided && !opts.Autofill {
|
||||||
return &cmdutil.FlagError{Err: errors.New("--title or --fill required when not running interactively")}
|
return &cmdutil.FlagError{Err: errors.New("--title or --fill required when not running interactively")}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -114,6 +117,10 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co
|
||||||
return errors.New("the --reviewer flag is not supported with --web")
|
return errors.New("the --reviewer flag is not supported with --web")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if opts.JSONFill && opts.WebMode {
|
||||||
|
return errors.New("--web and --json are mutually exclusive")
|
||||||
|
}
|
||||||
|
|
||||||
if runF != nil {
|
if runF != nil {
|
||||||
return runF(opts)
|
return runF(opts)
|
||||||
}
|
}
|
||||||
|
|
@ -134,6 +141,7 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co
|
||||||
fl.StringSliceVarP(&opts.Labels, "label", "l", nil, "Add labels by `name`")
|
fl.StringSliceVarP(&opts.Labels, "label", "l", nil, "Add labels by `name`")
|
||||||
fl.StringSliceVarP(&opts.Projects, "project", "p", nil, "Add the pull request to projects by `name`")
|
fl.StringSliceVarP(&opts.Projects, "project", "p", nil, "Add the pull request to projects by `name`")
|
||||||
fl.StringVarP(&opts.Milestone, "milestone", "m", "", "Add the pull request to a milestone by `name`")
|
fl.StringVarP(&opts.Milestone, "milestone", "m", "", "Add the pull request to a milestone by `name`")
|
||||||
|
fl.StringVarP(&opts.JSONInput, "json", "j", "", "Use JSON to populate and submit PR")
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
@ -141,14 +149,14 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co
|
||||||
func createRun(opts *CreateOptions) (err error) {
|
func createRun(opts *CreateOptions) (err error) {
|
||||||
ctx, err := NewCreateContext(opts)
|
ctx, err := NewCreateContext(opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
client := ctx.Client
|
client := ctx.Client
|
||||||
|
|
||||||
state, err := NewIssueState(*ctx, *opts)
|
state, err := NewIssueState(*ctx, *opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.WebMode {
|
if opts.WebMode {
|
||||||
|
|
@ -156,9 +164,9 @@ func createRun(opts *CreateOptions) (err error) {
|
||||||
state.Title = opts.Title
|
state.Title = opts.Title
|
||||||
state.Body = opts.Body
|
state.Body = opts.Body
|
||||||
}
|
}
|
||||||
err := handlePush(*opts, *ctx)
|
err = handlePush(*opts, *ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return
|
||||||
}
|
}
|
||||||
return previewPR(*opts, *ctx, *state)
|
return previewPR(*opts, *ctx, *state)
|
||||||
}
|
}
|
||||||
|
|
@ -199,35 +207,51 @@ func createRun(opts *CreateOptions) (err error) {
|
||||||
if opts.Autofill || (opts.TitleProvided && opts.BodyProvided) {
|
if opts.Autofill || (opts.TitleProvided && opts.BodyProvided) {
|
||||||
err = handlePush(*opts, *ctx)
|
err = handlePush(*opts, *ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return
|
||||||
}
|
}
|
||||||
return submitPR(*opts, *ctx, *state)
|
return submitPR(*opts, *ctx, *state)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if opts.JSONFill {
|
||||||
|
err = shared.FillFromJSON(opts.IO, opts.JSONInput, state)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not use JSON input: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = handlePush(*opts, *ctx)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return submitPR(*opts, *ctx, *state)
|
||||||
|
}
|
||||||
|
|
||||||
if !opts.TitleProvided {
|
if !opts.TitleProvided {
|
||||||
err = shared.TitleSurvey(state)
|
err = shared.TitleSurvey(state)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
editorCommand, err := cmdutil.DetermineEditor(opts.Config)
|
editorCommand, err := cmdutil.DetermineEditor(opts.Config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defer shared.PreserveInput(opts.IO, state, &err)()
|
||||||
|
|
||||||
templateContent := ""
|
templateContent := ""
|
||||||
if !opts.BodyProvided {
|
if !opts.BodyProvided {
|
||||||
templateFiles, legacyTemplate := shared.FindTemplates(opts.RootDirOverride, "PULL_REQUEST_TEMPLATE")
|
templateFiles, legacyTemplate := shared.FindTemplates(opts.RootDirOverride, "PULL_REQUEST_TEMPLATE")
|
||||||
|
|
||||||
templateContent, err = shared.TemplateSurvey(templateFiles, legacyTemplate, *state)
|
templateContent, err = shared.TemplateSurvey(templateFiles, legacyTemplate, *state)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = shared.BodySurvey(state, templateContent, editorCommand)
|
err = shared.BodySurvey(state, templateContent, editorCommand)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if state.Body == "" {
|
if state.Body == "" {
|
||||||
|
|
@ -244,12 +268,12 @@ func createRun(opts *CreateOptions) (err error) {
|
||||||
if action == shared.MetadataAction {
|
if action == shared.MetadataAction {
|
||||||
err = shared.MetadataSurvey(opts.IO, client, ctx.BaseRepo, state)
|
err = shared.MetadataSurvey(opts.IO, client, ctx.BaseRepo, state)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
action, err = shared.ConfirmSubmission(!state.HasMetadata(), false)
|
action, err = shared.ConfirmSubmission(!state.HasMetadata(), false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -260,7 +284,7 @@ func createRun(opts *CreateOptions) (err error) {
|
||||||
|
|
||||||
err = handlePush(*opts, *ctx)
|
err = handlePush(*opts, *ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if action == shared.PreviewAction {
|
if action == shared.PreviewAction {
|
||||||
|
|
@ -271,7 +295,8 @@ func createRun(opts *CreateOptions) (err error) {
|
||||||
return submitPR(*opts, *ctx, *state)
|
return submitPR(*opts, *ctx, *state)
|
||||||
}
|
}
|
||||||
|
|
||||||
return errors.New("expected to cancel, preview, or submit")
|
err = errors.New("expected to cancel, preview, or submit")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func initDefaultTitleBody(ctx CreateContext, state *shared.IssueMetadataState) error {
|
func initDefaultTitleBody(ctx CreateContext, state *shared.IssueMetadataState) error {
|
||||||
|
|
|
||||||
|
|
@ -136,6 +136,54 @@ func TestPRCreate_nontty_insufficient_flags(t *testing.T) {
|
||||||
assert.Equal(t, "", output.String())
|
assert.Equal(t, "", output.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPRCreate_json(t *testing.T) {
|
||||||
|
http := initFakeHTTP()
|
||||||
|
defer http.Verify(t)
|
||||||
|
|
||||||
|
http.StubRepoInfoResponse("OWNER", "REPO", "master")
|
||||||
|
http.StubResponse(200, bytes.NewBufferString(`
|
||||||
|
{ "data": { "repository": { "pullRequests": { "nodes" : [
|
||||||
|
] } } } }
|
||||||
|
`))
|
||||||
|
http.StubResponse(200, bytes.NewBufferString(`
|
||||||
|
{ "data": { "createPullRequest": { "pullRequest": {
|
||||||
|
"URL": "https://github.com/OWNER/REPO/pull/12"
|
||||||
|
} } } }
|
||||||
|
`))
|
||||||
|
|
||||||
|
cs, cmdTeardown := test.InitCmdStubber()
|
||||||
|
defer cmdTeardown()
|
||||||
|
|
||||||
|
cs.Stub("") // git status
|
||||||
|
cs.Stub("1234567890,commit 0\n2345678901,commit 1") // git log
|
||||||
|
|
||||||
|
output, err := runCommand(http, nil, "feature", false, `-j'{"title":"cool", "body":"pr"}' -H feature`)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
bodyBytes, _ := ioutil.ReadAll(http.Requests[2].Body)
|
||||||
|
reqBody := struct {
|
||||||
|
Variables struct {
|
||||||
|
Input struct {
|
||||||
|
RepositoryID string
|
||||||
|
Title string
|
||||||
|
Body string
|
||||||
|
BaseRefName string
|
||||||
|
HeadRefName string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}{}
|
||||||
|
_ = json.Unmarshal(bodyBytes, &reqBody)
|
||||||
|
|
||||||
|
assert.Equal(t, "REPOID", reqBody.Variables.Input.RepositoryID)
|
||||||
|
assert.Equal(t, "cool", reqBody.Variables.Input.Title)
|
||||||
|
assert.Equal(t, "pr", reqBody.Variables.Input.Body)
|
||||||
|
assert.Equal(t, "master", reqBody.Variables.Input.BaseRefName)
|
||||||
|
assert.Equal(t, "feature", reqBody.Variables.Input.HeadRefName)
|
||||||
|
|
||||||
|
assert.Equal(t, "", output.Stderr())
|
||||||
|
assert.Equal(t, "https://github.com/OWNER/REPO/pull/12\n", output.String())
|
||||||
|
}
|
||||||
|
|
||||||
func TestPRCreate_nontty(t *testing.T) {
|
func TestPRCreate_nontty(t *testing.T) {
|
||||||
http := initFakeHTTP()
|
http := initFakeHTTP()
|
||||||
defer http.Verify(t)
|
defer http.Verify(t)
|
||||||
|
|
|
||||||
66
pkg/cmd/pr/shared/preserve.go
Normal file
66
pkg/cmd/pr/shared/preserve.go
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
package shared
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/cli/cli/pkg/iostreams"
|
||||||
|
)
|
||||||
|
|
||||||
|
func dumpPath(random int64) string {
|
||||||
|
r := fmt.Sprintf("%x", random)
|
||||||
|
r = r[len(r)-5:]
|
||||||
|
dumpFilename := fmt.Sprintf("gh%s.json", r)
|
||||||
|
return filepath.Join(os.TempDir(), dumpFilename)
|
||||||
|
}
|
||||||
|
|
||||||
|
func PreserveInput(io *iostreams.IOStreams, state *IssueMetadataState, createErr *error) func() {
|
||||||
|
return func() {
|
||||||
|
if !state.IsDirty() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if *createErr == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
out := io.ErrOut
|
||||||
|
|
||||||
|
// this extra newline guards against appending to the end of a survey line
|
||||||
|
fmt.Fprintln(out)
|
||||||
|
|
||||||
|
data, err := json.Marshal(state)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(out, "failed to save input to file: %s\n", err)
|
||||||
|
fmt.Fprintln(out, "would have saved:")
|
||||||
|
fmt.Fprintf(out, "%v\n", state)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dp := dumpPath(time.Now().UnixNano())
|
||||||
|
|
||||||
|
err = io.WriteFile(dp, data)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(out, "failed to save input to file: %s\n", err)
|
||||||
|
fmt.Fprintln(out, "would have saved:")
|
||||||
|
fmt.Fprintln(out, string(data))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cs := io.ColorScheme()
|
||||||
|
|
||||||
|
issueType := "pr"
|
||||||
|
if state.Type == IssueMetadata {
|
||||||
|
issueType = "issue"
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintf(out, "%s operation failed. input saved to: %s\n", cs.FailureIcon(), dp)
|
||||||
|
fmt.Fprintf(out, "resubmit with: gh %s create -j@%s\n", issueType, dp)
|
||||||
|
|
||||||
|
// some whitespace before the actual error
|
||||||
|
fmt.Fprintln(out)
|
||||||
|
}
|
||||||
|
}
|
||||||
114
pkg/cmd/pr/shared/preserve_test.go
Normal file
114
pkg/cmd/pr/shared/preserve_test.go
Normal file
|
|
@ -0,0 +1,114 @@
|
||||||
|
package shared
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/cli/cli/pkg/iostreams"
|
||||||
|
"github.com/cli/cli/test"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_dumpPath(t *testing.T) {
|
||||||
|
// mostly pointless test
|
||||||
|
var random int64 = 1234567890
|
||||||
|
tempDir := os.TempDir()
|
||||||
|
assert.Equal(t, dumpPath(random), tempDir+"/gh602d2.json")
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_PreserveInput(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
state *IssueMetadataState
|
||||||
|
err bool
|
||||||
|
wantErrLines []string
|
||||||
|
wantPreservation bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "err, no changes to state",
|
||||||
|
err: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no err, no changes to state",
|
||||||
|
err: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no err, changes to state",
|
||||||
|
state: &IssueMetadataState{
|
||||||
|
dirty: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "err, title/body input received",
|
||||||
|
state: &IssueMetadataState{
|
||||||
|
dirty: true,
|
||||||
|
Title: "almost a",
|
||||||
|
Body: "jill sandwich",
|
||||||
|
Reviewers: []string{"barry", "chris"},
|
||||||
|
Labels: []string{"sandwich"},
|
||||||
|
},
|
||||||
|
wantErrLines: []string{
|
||||||
|
`X operation failed. input saved to:.*\.json`,
|
||||||
|
`resubmit with: gh issue create -j@.*\.json`,
|
||||||
|
},
|
||||||
|
err: true,
|
||||||
|
wantPreservation: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "err, metadata received",
|
||||||
|
state: &IssueMetadataState{
|
||||||
|
Reviewers: []string{"barry", "chris"},
|
||||||
|
Labels: []string{"sandwich"},
|
||||||
|
},
|
||||||
|
wantErrLines: []string{
|
||||||
|
`X operation failed. input saved to:.*\.json`,
|
||||||
|
`resubmit with: gh issue create -j@.*\.json`,
|
||||||
|
},
|
||||||
|
err: true,
|
||||||
|
wantPreservation: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "err, dirty, pull request",
|
||||||
|
state: &IssueMetadataState{
|
||||||
|
dirty: true,
|
||||||
|
Title: "a pull request",
|
||||||
|
Type: PRMetadata,
|
||||||
|
},
|
||||||
|
wantErrLines: []string{
|
||||||
|
`X operation failed. input saved to:.*\.json`,
|
||||||
|
`resubmit with: gh pr create -j@.*\.json`,
|
||||||
|
},
|
||||||
|
err: true,
|
||||||
|
wantPreservation: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if tt.state == nil {
|
||||||
|
tt.state = &IssueMetadataState{}
|
||||||
|
}
|
||||||
|
io, _, _, errOut := iostreams.Test()
|
||||||
|
io.WriteOverride = []byte{}
|
||||||
|
var err error
|
||||||
|
if tt.err {
|
||||||
|
err = errors.New("error during creation")
|
||||||
|
}
|
||||||
|
|
||||||
|
PreserveInput(io, tt.state, &err)()
|
||||||
|
|
||||||
|
if tt.wantPreservation {
|
||||||
|
test.ExpectLines(t, errOut.String(), tt.wantErrLines...)
|
||||||
|
preserved := &IssueMetadataState{}
|
||||||
|
assert.NoError(t, json.Unmarshal(io.WriteOverride, preserved))
|
||||||
|
preserved.dirty = tt.state.dirty
|
||||||
|
assert.Equal(t, preserved, tt.state)
|
||||||
|
} else {
|
||||||
|
assert.Equal(t, errOut.String(), "")
|
||||||
|
assert.Equal(t, string(io.WriteOverride), "")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
73
pkg/cmd/pr/shared/state.go
Normal file
73
pkg/cmd/pr/shared/state.go
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
package shared
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/cli/cli/api"
|
||||||
|
"github.com/cli/cli/pkg/iostreams"
|
||||||
|
)
|
||||||
|
|
||||||
|
type metadataStateType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
IssueMetadata metadataStateType = iota
|
||||||
|
PRMetadata
|
||||||
|
)
|
||||||
|
|
||||||
|
type IssueMetadataState struct {
|
||||||
|
Type metadataStateType
|
||||||
|
|
||||||
|
Draft bool
|
||||||
|
|
||||||
|
Body string
|
||||||
|
Title string
|
||||||
|
|
||||||
|
Metadata []string
|
||||||
|
Reviewers []string
|
||||||
|
Assignees []string
|
||||||
|
Labels []string
|
||||||
|
Projects []string
|
||||||
|
Milestones []string
|
||||||
|
|
||||||
|
MetadataResult *api.RepoMetadataResult
|
||||||
|
|
||||||
|
dirty bool // whether user i/o has modified this
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tb *IssueMetadataState) MarkDirty() {
|
||||||
|
tb.dirty = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tb *IssueMetadataState) IsDirty() bool {
|
||||||
|
return tb.dirty || tb.HasMetadata()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tb *IssueMetadataState) HasMetadata() bool {
|
||||||
|
return len(tb.Reviewers) > 0 ||
|
||||||
|
len(tb.Assignees) > 0 ||
|
||||||
|
len(tb.Labels) > 0 ||
|
||||||
|
len(tb.Projects) > 0 ||
|
||||||
|
len(tb.Milestones) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func FillFromJSON(io *iostreams.IOStreams, JSONInput string, state *IssueMetadataState) error {
|
||||||
|
var data []byte
|
||||||
|
var err error
|
||||||
|
if strings.HasPrefix(JSONInput, "@") {
|
||||||
|
data, err = io.ReadUserFile(JSONInput[1:])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read file %s: %w", JSONInput[1:], err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
data = []byte(JSONInput)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.Unmarshal(data, state)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("JSON parsing failure: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
@ -16,38 +16,6 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type Action int
|
type Action int
|
||||||
type metadataStateType int
|
|
||||||
|
|
||||||
const (
|
|
||||||
IssueMetadata metadataStateType = iota
|
|
||||||
PRMetadata
|
|
||||||
)
|
|
||||||
|
|
||||||
type IssueMetadataState struct {
|
|
||||||
Type metadataStateType
|
|
||||||
|
|
||||||
Draft bool
|
|
||||||
|
|
||||||
Body string
|
|
||||||
Title string
|
|
||||||
|
|
||||||
Metadata []string
|
|
||||||
Reviewers []string
|
|
||||||
Assignees []string
|
|
||||||
Labels []string
|
|
||||||
Projects []string
|
|
||||||
Milestones []string
|
|
||||||
|
|
||||||
MetadataResult *api.RepoMetadataResult
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tb *IssueMetadataState) HasMetadata() bool {
|
|
||||||
return len(tb.Reviewers) > 0 ||
|
|
||||||
len(tb.Assignees) > 0 ||
|
|
||||||
len(tb.Labels) > 0 ||
|
|
||||||
len(tb.Projects) > 0 ||
|
|
||||||
len(tb.Milestones) > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
SubmitAction Action = iota
|
SubmitAction Action = iota
|
||||||
|
|
@ -170,6 +138,8 @@ func BodySurvey(state *IssueMetadataState, templateContent, editorCommand string
|
||||||
state.Body += templateContent
|
state.Body += templateContent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
preBody := state.Body
|
||||||
|
|
||||||
// TODO should just be an AskOne but ran into problems with the stubber
|
// TODO should just be an AskOne but ran into problems with the stubber
|
||||||
qs := []*survey.Question{
|
qs := []*survey.Question{
|
||||||
{
|
{
|
||||||
|
|
@ -193,10 +163,16 @@ func BodySurvey(state *IssueMetadataState, templateContent, editorCommand string
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if state.Body != "" && preBody != state.Body {
|
||||||
|
state.MarkDirty()
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func TitleSurvey(state *IssueMetadataState) error {
|
func TitleSurvey(state *IssueMetadataState) error {
|
||||||
|
preTitle := state.Title
|
||||||
|
|
||||||
// TODO should just be an AskOne but ran into problems with the stubber
|
// TODO should just be an AskOne but ran into problems with the stubber
|
||||||
qs := []*survey.Question{
|
qs := []*survey.Question{
|
||||||
{
|
{
|
||||||
|
|
@ -213,6 +189,10 @@ func TitleSurvey(state *IssueMetadataState) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if preTitle != state.Title {
|
||||||
|
state.MarkDirty()
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -122,6 +122,10 @@ func (c *ColorScheme) WarningIcon() string {
|
||||||
return c.Yellow("!")
|
return c.Yellow("!")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *ColorScheme) FailureIcon() string {
|
||||||
|
return c.Red("X")
|
||||||
|
}
|
||||||
|
|
||||||
func (c *ColorScheme) ColorFromString(s string) func(string) string {
|
func (c *ColorScheme) ColorFromString(s string) func(string) string {
|
||||||
s = strings.ToLower(s)
|
s = strings.ToLower(s)
|
||||||
var fn func(string) string
|
var fn func(string) string
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,8 @@ type IOStreams struct {
|
||||||
pagerProcess *os.Process
|
pagerProcess *os.Process
|
||||||
|
|
||||||
neverPrompt bool
|
neverPrompt bool
|
||||||
|
|
||||||
|
WriteOverride []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *IOStreams) ColorEnabled() bool {
|
func (s *IOStreams) ColorEnabled() bool {
|
||||||
|
|
@ -268,6 +270,14 @@ func (s *IOStreams) ReadUserFile(fn string) ([]byte, error) {
|
||||||
return ioutil.ReadAll(r)
|
return ioutil.ReadAll(r)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *IOStreams) WriteFile(fn string, data []byte) error {
|
||||||
|
if s.WriteOverride != nil {
|
||||||
|
s.WriteOverride = data
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return ioutil.WriteFile(fn, data, 0660)
|
||||||
|
}
|
||||||
|
|
||||||
func System() *IOStreams {
|
func System() *IOStreams {
|
||||||
stdoutIsTTY := isTerminal(os.Stdout)
|
stdoutIsTTY := isTerminal(os.Stdout)
|
||||||
stderrIsTTY := isTerminal(os.Stderr)
|
stderrIsTTY := isTerminal(os.Stderr)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue