Skip to content

Commit 7bd67a8

Browse files
babakksmaxbeizerCopilot
committed
feat(discussion create): add discussion create command
Add `gh discussion create` with support for: - Non-interactive creation with --title, --body, --category, --label flags - Interactive mode with prompts for title, body, and category - Body input from file via --body-file (including stdin) - Label resolution (name to ID) before creation - Category validation by name or slug Co-authored-by: Max Beizer <max.beizer@gmail.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 57008e7 commit 7bd67a8

2 files changed

Lines changed: 616 additions & 0 deletions

File tree

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
package create
2+
3+
import (
4+
"fmt"
5+
"net/http"
6+
"strings"
7+
8+
"github.com/MakeNowJust/heredoc"
9+
"github.com/cli/cli/v2/internal/ghrepo"
10+
"github.com/cli/cli/v2/internal/prompter"
11+
"github.com/cli/cli/v2/pkg/cmd/discussion/client"
12+
"github.com/cli/cli/v2/pkg/cmd/discussion/shared"
13+
"github.com/cli/cli/v2/pkg/cmdutil"
14+
"github.com/cli/cli/v2/pkg/iostreams"
15+
"github.com/spf13/cobra"
16+
)
17+
18+
// CreateOptions holds the configuration for the discussion create command.
19+
type CreateOptions struct {
20+
IO *iostreams.IOStreams
21+
HttpClient func() (*http.Client, error)
22+
BaseRepo func() (ghrepo.Interface, error)
23+
Client func() (client.DiscussionClient, error)
24+
Prompter prompter.Prompter
25+
26+
Title string
27+
Body string
28+
Category string
29+
Labels []string
30+
}
31+
32+
// NewCmdCreate returns a cobra command for creating a GitHub Discussion.
33+
func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Command {
34+
opts := &CreateOptions{
35+
IO: f.IOStreams,
36+
HttpClient: f.HttpClient,
37+
Prompter: f.Prompter,
38+
Client: shared.DiscussionClientFunc(f),
39+
}
40+
41+
cmd := &cobra.Command{
42+
Use: "create",
43+
Short: "Create a new discussion (preview)",
44+
Long: heredoc.Doc(`
45+
Create a new GitHub Discussion in a repository.
46+
47+
With '--title', '--body', and '--category', a discussion is created non-interactively.
48+
Omitting any of these flags triggers interactive prompts when connected to a terminal.
49+
`),
50+
Example: heredoc.Doc(`
51+
# Create interactively
52+
$ gh discussion create
53+
54+
# Create non-interactively
55+
$ gh discussion create --title "My question" --category "Q&A" --body "Details here"
56+
`),
57+
Args: cmdutil.NoArgsQuoteReminder,
58+
RunE: func(cmd *cobra.Command, args []string) error {
59+
opts.BaseRepo = f.BaseRepo
60+
61+
if opts.Title != "" && strings.TrimSpace(opts.Title) == "" {
62+
return cmdutil.FlagErrorf("title cannot be blank")
63+
}
64+
if opts.Body != "" && strings.TrimSpace(opts.Body) == "" {
65+
return cmdutil.FlagErrorf("body cannot be blank")
66+
}
67+
if opts.Category != "" && strings.TrimSpace(opts.Category) == "" {
68+
return cmdutil.FlagErrorf("category cannot be blank")
69+
}
70+
71+
needsInput := opts.Title == "" || opts.Category == "" || opts.Body == ""
72+
if needsInput && !opts.IO.CanPrompt() {
73+
return cmdutil.FlagErrorf("--title, --body, and --category are required when not running interactively")
74+
}
75+
76+
if runF != nil {
77+
return runF(opts)
78+
}
79+
return createRun(opts)
80+
},
81+
}
82+
83+
cmdutil.EnableRepoOverride(cmd, f)
84+
85+
cmd.Flags().StringVarP(&opts.Title, "title", "t", "", "Title for the discussion")
86+
cmd.Flags().StringVarP(&opts.Body, "body", "b", "", "Body for the discussion")
87+
cmd.Flags().StringVarP(&opts.Category, "category", "c", "", "Category name or slug for the discussion")
88+
cmd.Flags().StringSliceVarP(&opts.Labels, "label", "l", nil, "Labels to apply to the discussion")
89+
90+
return cmd
91+
}
92+
93+
func createRun(opts *CreateOptions) error {
94+
repo, err := opts.BaseRepo()
95+
if err != nil {
96+
return err
97+
}
98+
99+
c, err := opts.Client()
100+
if err != nil {
101+
return err
102+
}
103+
104+
opts.IO.StartProgressIndicator()
105+
categories, err := c.ListCategories(repo)
106+
opts.IO.StopProgressIndicator()
107+
if err != nil {
108+
return fmt.Errorf("fetching categories: %w", err)
109+
}
110+
111+
if opts.Title == "" {
112+
opts.Title, err = opts.Prompter.Input("Discussion title", "")
113+
if err != nil {
114+
return err
115+
}
116+
if strings.TrimSpace(opts.Title) == "" {
117+
return fmt.Errorf("title cannot be blank")
118+
}
119+
}
120+
121+
var category *client.DiscussionCategory
122+
if opts.Category != "" {
123+
category, err = shared.MatchCategory(opts.Category, categories)
124+
if err != nil {
125+
return err
126+
}
127+
} else {
128+
names := make([]string, len(categories))
129+
for i, cat := range categories {
130+
names[i] = cat.Name
131+
}
132+
idx, err := opts.Prompter.Select("Discussion category", "", names)
133+
if err != nil {
134+
return err
135+
}
136+
category = &categories[idx]
137+
}
138+
139+
if opts.Body == "" {
140+
opts.Body, err = opts.Prompter.MarkdownEditor("Discussion body", "", false)
141+
if err != nil {
142+
return err
143+
}
144+
if strings.TrimSpace(opts.Body) == "" {
145+
return fmt.Errorf("body cannot be blank")
146+
}
147+
}
148+
149+
var labelIDs []string
150+
if len(opts.Labels) > 0 {
151+
opts.IO.StartProgressIndicator()
152+
allLabels, err := c.ListLabels(repo)
153+
opts.IO.StopProgressIndicator()
154+
if err != nil {
155+
return err
156+
}
157+
158+
labelIDs, err = shared.ResolveLabels(allLabels, opts.Labels)
159+
if err != nil {
160+
return err
161+
}
162+
}
163+
164+
input := client.CreateDiscussionInput{
165+
CategoryID: category.ID,
166+
Title: opts.Title,
167+
Body: opts.Body,
168+
LabelIDs: labelIDs,
169+
}
170+
171+
opts.IO.StartProgressIndicator()
172+
discussion, err := c.Create(repo, input)
173+
opts.IO.StopProgressIndicator()
174+
if err != nil {
175+
return fmt.Errorf("failed to create discussion: %w", err)
176+
}
177+
178+
fmt.Fprintln(opts.IO.Out, discussion.URL)
179+
180+
return nil
181+
}

0 commit comments

Comments
 (0)