Skip to content

Commit

Permalink
Add issue create --editor
Browse files Browse the repository at this point in the history
  • Loading branch information
notomo committed Oct 4, 2023
1 parent 3443a75 commit f5918e9
Show file tree
Hide file tree
Showing 7 changed files with 260 additions and 16 deletions.
55 changes: 43 additions & 12 deletions pkg/cmd/issue/create/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,18 @@ import (
)

type CreateOptions struct {
HttpClient func() (*http.Client, error)
Config func() (config.Config, error)
IO *iostreams.IOStreams
BaseRepo func() (ghrepo.Interface, error)
Browser browser.Browser
Prompter prShared.Prompt
HttpClient func() (*http.Client, error)
Config func() (config.Config, error)
IO *iostreams.IOStreams
BaseRepo func() (ghrepo.Interface, error)
Browser browser.Browser
Prompter prShared.Prompt
TitledEditSurvey func(string, string) (string, string, error)

RootDirOverride string

HasRepoOverride bool
EditorMode bool
WebMode bool
RecoverFile string

Expand All @@ -44,11 +46,12 @@ type CreateOptions struct {

func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Command {
opts := &CreateOptions{
IO: f.IOStreams,
HttpClient: f.HttpClient,
Config: f.Config,
Browser: f.Browser,
Prompter: f.Prompter,
IO: f.IOStreams,
HttpClient: f.HttpClient,
Config: f.Config,
Browser: f.Browser,
Prompter: f.Prompter,
TitledEditSurvey: prShared.TitledEditSurvey(f.Config, f.IOStreams),
}

var bodyFile string
Expand Down Expand Up @@ -96,12 +99,20 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co
return errors.New("`--template` is not supported when using `--body` or `--body-file`")
}

opts.Interactive = !(titleProvided && bodyProvided)
opts.Interactive = !opts.EditorMode && !(titleProvided && bodyProvided)

if opts.Interactive && !opts.IO.CanPrompt() {
return cmdutil.FlagErrorf("must provide `--title` and `--body` when not running interactively")
}

if err := cmdutil.MutuallyExclusive(
"specify only one of `--editor` or `--web`",
opts.EditorMode,
opts.WebMode,
); err != nil {
return err
}

if runF != nil {
return runF(opts)
}
Expand All @@ -112,6 +123,7 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co
cmd.Flags().StringVarP(&opts.Title, "title", "t", "", "Supply a title. Will prompt for one otherwise.")
cmd.Flags().StringVarP(&opts.Body, "body", "b", "", "Supply a body. Will prompt for one otherwise.")
cmd.Flags().StringVarP(&bodyFile, "body-file", "F", "", "Read body text from `file` (use \"-\" to read from standard input)")
cmd.Flags().BoolVarP(&opts.EditorMode, "editor", "e", false, "Skip prompts and open the text editor to write the title and body in. The first line is the title and the rest text is the body.")
cmd.Flags().BoolVarP(&opts.WebMode, "web", "w", false, "Open the browser to create an issue")
cmd.Flags().StringSliceVarP(&opts.Assignees, "assignee", "a", nil, "Assign people by their `login`. Use \"@me\" to self-assign.")
cmd.Flags().StringSliceVarP(&opts.Labels, "label", "l", nil, "Add labels by `name`")
Expand Down Expand Up @@ -285,6 +297,25 @@ func createRun(opts *CreateOptions) (err error) {
return
}
} else {
if opts.EditorMode {
if opts.Template != "" {
var template prShared.Template
template, err = tpl.Select(opts.Template)
if err != nil {
return
}
if tb.Title == "" {
tb.Title = template.Title()
}
templateNameForSubmit = template.NameForSubmit()
tb.Body = string(template.Body())
}

tb.Title, tb.Body, err = opts.TitledEditSurvey(tb.Title, tb.Body)
if err != nil {
return
}
}
if tb.Title == "" {
err = fmt.Errorf("title can't be blank")
return
Expand Down
101 changes: 101 additions & 0 deletions pkg/cmd/issue/create/create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,41 @@ func TestNewCmdCreate(t *testing.T) {
cli: `-t mytitle --template "bug report" --body-file "body_file.md"`,
wantsErr: true,
},
{
name: "editor",
tty: false,
cli: "--editor",
wantsErr: false,
wantsOpts: CreateOptions{
Title: "",
Body: "",
RecoverFile: "",
WebMode: false,
EditorMode: true,
Interactive: false,
},
},
{
name: "editor and template",
tty: false,
cli: `--editor --template "bug report"`,
wantsErr: false,
wantsOpts: CreateOptions{
Title: "",
Body: "",
RecoverFile: "",
WebMode: false,
EditorMode: true,
Template: "bug report",
Interactive: false,
},
},
{
name: "editor and web",
tty: false,
cli: "--editor --web",
wantsErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand Down Expand Up @@ -309,6 +344,72 @@ func Test_createRun(t *testing.T) {
},
wantsErr: "cannot open in browser: maximum URL length exceeded",
},
{
name: "editor",
httpStubs: func(r *httpmock.Registry) {
r.Register(
httpmock.GraphQL(`query RepositoryInfo\b`),
httpmock.StringResponse(`
{ "data": { "repository": {
"id": "REPOID",
"hasIssuesEnabled": true
} } }`))
r.Register(
httpmock.GraphQL(`mutation IssueCreate\b`),
httpmock.GraphQLMutation(`
{ "data": { "createIssue": { "issue": {
"URL": "https://github.com/OWNER/REPO/issues/12"
} } } }
`, func(inputs map[string]interface{}) {
assert.Equal(t, "title", inputs["title"])
assert.Equal(t, "body", inputs["body"])
}))
},
opts: CreateOptions{
EditorMode: true,
TitledEditSurvey: func(string, string) (string, string, error) { return "title", "body", nil },
},
wantsStdout: "https://github.com/OWNER/REPO/issues/12\n",
wantsStderr: "\nCreating issue in OWNER/REPO\n\n",
},
{
name: "editor and template",
httpStubs: func(r *httpmock.Registry) {
r.Register(
httpmock.GraphQL(`query RepositoryInfo\b`),
httpmock.StringResponse(`
{ "data": { "repository": {
"id": "REPOID",
"hasIssuesEnabled": true
} } }`))
r.Register(
httpmock.GraphQL(`query IssueTemplates\b`),
httpmock.StringResponse(`
{ "data": { "repository": { "issueTemplates": [
{ "name": "Bug report",
"title": "bug: ",
"body": "Does not work :((" }
] } } }`),
)
r.Register(
httpmock.GraphQL(`mutation IssueCreate\b`),
httpmock.GraphQLMutation(`
{ "data": { "createIssue": { "issue": {
"URL": "https://github.com/OWNER/REPO/issues/12"
} } } }
`, func(inputs map[string]interface{}) {
assert.Equal(t, "bug: ", inputs["title"])
assert.Equal(t, "Does not work :((", inputs["body"])
}))
},
opts: CreateOptions{
EditorMode: true,
Template: "Bug report",
TitledEditSurvey: func(title string, body string) (string, string, error) { return title, body, nil },
},
wantsStdout: "https://github.com/OWNER/REPO/issues/12\n",
wantsStderr: "\nCreating issue in OWNER/REPO\n\n",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand Down
20 changes: 20 additions & 0 deletions pkg/cmd/pr/shared/survey.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@ import (
"strings"

"github.com/cli/cli/v2/api"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/cli/cli/v2/pkg/surveyext"
)

type Action int
Expand Down Expand Up @@ -317,3 +320,20 @@ func MetadataSurvey(p Prompt, io *iostreams.IOStreams, baseRepo ghrepo.Interface

return nil
}

func TitledEditSurvey(cf func() (config.Config, error), io *iostreams.IOStreams) func(string, string) (string, string, error) {
return func(initialTitle, initialBody string) (string, string, error) {
editorCommand, err := cmdutil.DetermineEditor(cf)
if err != nil {
return "", "", err
}
initialValue := initialTitle + "\n" + initialBody
titleAndBody, err := surveyext.Edit(editorCommand, "*.md", initialValue, io.In, io.Out, io.ErrOut)
if err != nil {
return "", "", err
}
titleAndBody = strings.ReplaceAll(titleAndBody, "\r\n", "\n")
title, body, _ := strings.Cut(titleAndBody, "\n")
return title, body, nil
}
}
18 changes: 16 additions & 2 deletions pkg/cmd/pr/shared/templates.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ import (
)

type issueTemplate struct {
Gname string `graphql:"name"`
Gbody string `graphql:"body"`
Gname string `graphql:"name"`
Gbody string `graphql:"body"`
Gtitle string `graphql:"title"`
}

type pullRequestTemplate struct {
Expand All @@ -37,6 +38,10 @@ func (t *issueTemplate) Body() []byte {
return []byte(t.Gbody)
}

func (t *issueTemplate) Title() string {
return t.Gtitle
}

func (t *pullRequestTemplate) Name() string {
return t.Gname
}
Expand All @@ -49,6 +54,10 @@ func (t *pullRequestTemplate) Body() []byte {
return []byte(t.Gbody)
}

func (t *pullRequestTemplate) Title() string {
return ""
}

func listIssueTemplates(httpClient *http.Client, repo ghrepo.Interface) ([]Template, error) {
var query struct {
Repository struct {
Expand Down Expand Up @@ -109,6 +118,7 @@ type Template interface {
Name() string
NameForSubmit() string
Body() []byte
Title() string
}

type iprompter interface {
Expand Down Expand Up @@ -294,3 +304,7 @@ func (t *filesystemTemplate) NameForSubmit() string {
func (t *filesystemTemplate) Body() []byte {
return githubtemplate.ExtractContents(t.path)
}

func (t *filesystemTemplate) Title() string {
return githubtemplate.ExtractTitle(t.path)
}
6 changes: 4 additions & 2 deletions pkg/cmd/pr/shared/templates_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ func TestTemplateManager_hasAPI(t *testing.T) {
httpmock.GraphQL(`query IssueTemplates\b`),
httpmock.StringResponse(`{"data":{"repository":{
"issueTemplates": [
{"name": "Bug report", "body": "I found a problem"},
{"name": "Feature request", "body": "I need a feature"}
{"name": "Bug report", "body": "I found a problem", "title": "bug: "},
{"name": "Feature request", "body": "I need a feature", "title": "request: "}
]
}}}`))

Expand Down Expand Up @@ -62,6 +62,7 @@ func TestTemplateManager_hasAPI(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, "Feature request", tpl.NameForSubmit())
assert.Equal(t, "I need a feature", string(tpl.Body()))
assert.Equal(t, "request: ", tpl.Title())
}

func TestTemplateManager_hasAPI_PullRequest(t *testing.T) {
Expand Down Expand Up @@ -112,6 +113,7 @@ func TestTemplateManager_hasAPI_PullRequest(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, "", tpl.NameForSubmit())
assert.Equal(t, "I fixed a problem", string(tpl.Body()))
assert.Equal(t, "", tpl.Title())
}

func TestTemplateManagerSelect(t *testing.T) {
Expand Down
15 changes: 15 additions & 0 deletions pkg/githubtemplate/github_template.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,21 @@ func ExtractName(filePath string) string {
return path.Base(filePath)
}

// ExtractTitle returns the title of the template from YAML front-matter
func ExtractTitle(filePath string) string {
contents, err := os.ReadFile(filePath)
frontmatterBoundaries := detectFrontmatter(contents)
if err == nil && frontmatterBoundaries[0] == 0 {
templateData := struct {
Title string
}{}
if err := yaml.Unmarshal(contents[0:frontmatterBoundaries[1]], &templateData); err == nil && templateData.Title != "" {
return templateData.Title
}
}
return ""
}

// ExtractContents returns the template contents without the YAML front-matter
func ExtractContents(filePath string) []byte {
contents, err := os.ReadFile(filePath)
Expand Down

0 comments on commit f5918e9

Please sign in to comment.