Repository navigation
feat: add discussion command set
#13541
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+13,075
−0
Merged
Changes from 1 commit
Commits
Show all changes
47 commits
Select commit
Hold shift + click to select a range
51b7653
feat(discussion/client): add discussion client package
babakks d3a1538
feat(discussion/shared): add shared utilities for discussion commands
babakks 8b73951
feat(discussion list): add discussion list command
babakks 57008e7
feat(discussion view): add discussion view command
babakks 7bd67a8
feat(discussion create): add discussion create command
babakks 797effe
feat(discussion edit): add discussion edit command
babakks dc5d7c1
fix(discussion): various polish and small fixes
babakks 0d93148
refactor(discussion): extract command-level consts for enums
babakks 97b2296
refactor(discussion list): make pager call/error consistent with view…
babakks dcd3507
docs(discussion/client): add godoc to exported consts
babakks d6a089d
chore(discussion/client): fix formatting
babakks b102900
fix(discussion): handle partial failure on create/update label mutations
babakks 6dcb0b0
docs(discussion view): improve long help text for clarity
babakks fbd733e
refactor(discussion): add Cursor field and ExportData to DiscussionLi…
babakks e104885
refactor(discussion/client): precheck discussions enabled via getRepo…
babakks ca8e126
fix(discussion list): print "answered" instead of checkmark in non-tt…
babakks ada8583
test(discussion): add acceptance tests for discussion commands
babakks 8ec0830
docs(acceptance): add new jq2env and jq-assert functions
babakks f147d02
test(discussion list): consolidate tests into table-driven format
babakks e61df07
Merge branch 'trunk' into feature/discussion
babakks c1f3c1a
fix(discussion): add missing repo flag override
babakks 9d413e7
test(discussion list): rename TestNewCmdList2 to TestNewCmdList
babakks e2d150d
fix(discussion): remove redundant error wrapping on ListCategories
babakks 95fc89c
chore(discussion): remove unused HttpClient field from create and edit
babakks 4166ecf
chore: apply formatting
babakks 2618999
feat(discussion/client): add comment manipulation methods
babakks 6f5e114
feat(discussion): add discussion comment command
babakks 82ac0d7
refactor(acceptance): use discussion comment command instead of raw A…
babakks 2629753
test(acceptance): add discussion comment acceptance test
babakks 61a4476
test(discussion comment): add non-tty delete flag validation test case
babakks d026f8f
feat(discussion): support comment URLs in --replies and comment command
babakks 6394ca8
test(acceptance): cover discussion comment URLs in comment and view t…
babakks bc7ed48
chore: fix formatting
babakks 55928c9
refactor(discussion view): replace --replies flag with positional com…
babakks 8747c69
refactor(discussion/client): take host instead of repo in GetComment
babakks 869c044
test(acceptance): use positional comment argument in discussion view …
babakks 8d2b059
fix(discussion view): error when --comments is used with a comment ar…
babakks 42db02d
Merge pull request #13620 from cli/babakks/add-discussion-comment
babakks 27aabfa
fix(discussion view): use color scheme method for success icon
babakks 951d76e
refactor(discussion list): simplify no-results message and use succes…
babakks 9f2da11
fix(discussion view): show comments and replies in chronological order
babakks 06d2e34
docs(discussion list): clarify answered examples refer to Q&A discuss…
babakks 5d77247
fix(discussion view): print only requested items in non-tty output
babakks d63ab8d
fix(discussion/shared): error on out-of-range discussion number in URL
babakks 5e6a58b
test(acceptance): fix discussion comment acceptance tests
babakks 616d929
chore(discussion/client): rename client files
babakks 69855b7
fix(discussion comment): fix bug in requiring body/body-file in add/e…
babakks File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
feat(discussion edit): add discussion edit command
Add `gh discussion edit` with support for: - Non-interactive editing with --title, --body, --body-file, --category flags - Label management with --add-label and --remove-label flags - Interactive mode with multi-select prompts for fields to edit - Flag presence detection (allows setting body to empty string) - Label name to ID resolution via shared.ResolveLabels - Body input from stdin via --body-file - Co-authored-by: Max Beizer <max.beizer@gmail.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Loading branch information
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,272 @@ | ||
| package edit | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "net/http" | ||
| "strings" | ||
|
|
||
| "github.com/MakeNowJust/heredoc" | ||
| "github.com/cli/cli/v2/internal/ghrepo" | ||
| "github.com/cli/cli/v2/internal/prompter" | ||
| "github.com/cli/cli/v2/pkg/cmd/discussion/client" | ||
| "github.com/cli/cli/v2/pkg/cmd/discussion/shared" | ||
| "github.com/cli/cli/v2/pkg/cmdutil" | ||
| "github.com/cli/cli/v2/pkg/iostreams" | ||
| "github.com/spf13/cobra" | ||
| ) | ||
|
|
||
| // EditOptions holds the configuration for the discussion edit command. | ||
| type EditOptions struct { | ||
| IO *iostreams.IOStreams | ||
| HttpClient func() (*http.Client, error) | ||
| BaseRepo func() (ghrepo.Interface, error) | ||
| Client func() (client.DiscussionClient, error) | ||
| Prompter prompter.Prompter | ||
|
|
||
| Interactive bool | ||
| TitleProvided bool | ||
| BodyProvided bool | ||
| CategoryProvided bool | ||
| LabelsProvided bool | ||
|
|
||
| DiscussionNumber int | ||
| Title string | ||
| Body string | ||
| BodyFile string | ||
| Category string | ||
| AddLabels []string | ||
| RemoveLabels []string | ||
| } | ||
|
|
||
| // NewCmdEdit returns a cobra command for editing a GitHub Discussion. | ||
| func NewCmdEdit(f *cmdutil.Factory, runF func(*EditOptions) error) *cobra.Command { | ||
| opts := &EditOptions{ | ||
| IO: f.IOStreams, | ||
| HttpClient: f.HttpClient, | ||
| Prompter: f.Prompter, | ||
| Client: shared.DiscussionClientFunc(f), | ||
| } | ||
|
|
||
| cmd := &cobra.Command{ | ||
| Use: "edit {<number> | <url>}", | ||
| Short: "Edit a discussion (preview)", | ||
| Long: heredoc.Doc(` | ||
| Edit a GitHub Discussion. | ||
|
|
||
| Without flags, the command runs interactively when connected to a terminal. | ||
| Use flags to update specific fields non-interactively. | ||
| `), | ||
| Example: heredoc.Doc(` | ||
| # Edit interactively | ||
| $ gh discussion edit 123 | ||
|
|
||
| # Update title, body, and category | ||
| $ gh discussion edit 123 --title "Updated title" --body "Updated body" --category "Ideas" | ||
|
|
||
| # Update body from a file | ||
| $ gh discussion edit 123 --body-file body.md | ||
|
|
||
| # Add and remove labels | ||
| $ gh discussion edit 123 --add-label "bug,help wanted" --remove-label "stale" | ||
| `), | ||
| Args: cobra.ExactArgs(1), | ||
| RunE: func(cmd *cobra.Command, args []string) error { | ||
| number, repo, err := shared.ParseDiscussionArg(args[0]) | ||
| if err != nil { | ||
| return cmdutil.FlagErrorWrap(err) | ||
| } | ||
|
|
||
| if repo != nil { | ||
| opts.BaseRepo = func() (ghrepo.Interface, error) { | ||
| return repo, nil | ||
| } | ||
| } else { | ||
| opts.BaseRepo = f.BaseRepo | ||
| } | ||
|
|
||
| opts.DiscussionNumber = number | ||
|
|
||
| if err := cmdutil.MutuallyExclusive("specify only one of --body or --body-file", | ||
| cmd.Flags().Changed("body"), cmd.Flags().Changed("body-file")); err != nil { | ||
| return err | ||
| } | ||
|
|
||
| opts.TitleProvided = cmd.Flags().Changed("title") | ||
| opts.BodyProvided = cmd.Flags().Changed("body") || cmd.Flags().Changed("body-file") | ||
| opts.CategoryProvided = cmd.Flags().Changed("category") | ||
| opts.LabelsProvided = len(opts.AddLabels) > 0 || len(opts.RemoveLabels) > 0 | ||
|
|
||
| noFlagsSet := !opts.TitleProvided && !opts.BodyProvided && !opts.CategoryProvided && !opts.LabelsProvided | ||
| if noFlagsSet && !opts.IO.CanPrompt() { | ||
| return cmdutil.FlagErrorf("specify at least one flag to update the discussion non-interactively") | ||
| } | ||
|
|
||
| opts.Interactive = noFlagsSet | ||
|
|
||
| if runF != nil { | ||
| return runF(opts) | ||
| } | ||
| return editRun(opts) | ||
| }, | ||
| } | ||
|
|
||
| cmdutil.EnableRepoOverride(cmd, f) | ||
|
|
||
| cmd.Flags().StringVarP(&opts.Title, "title", "t", "", "New title for the discussion") | ||
| cmd.Flags().StringVarP(&opts.Body, "body", "b", "", "New body for the discussion") | ||
| cmd.Flags().StringVarP(&opts.BodyFile, "body-file", "F", "", "Read body text from file (use \"-\" to read from standard input)") | ||
| cmd.Flags().StringVarP(&opts.Category, "category", "c", "", "New category name or slug for the discussion") | ||
| cmd.Flags().StringSliceVar(&opts.AddLabels, "add-label", nil, "Add labels by `name`") | ||
| cmd.Flags().StringSliceVar(&opts.RemoveLabels, "remove-label", nil, "Remove labels by `name`") | ||
|
|
||
| return cmd | ||
| } | ||
|
|
||
| func editRun(opts *EditOptions) error { | ||
| repo, err := opts.BaseRepo() | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| c, err := opts.Client() | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| opts.IO.StartProgressIndicator() | ||
| discussion, err := c.GetByNumber(repo, opts.DiscussionNumber) | ||
| opts.IO.StopProgressIndicator() | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| input := client.UpdateDiscussionInput{ | ||
| DiscussionID: discussion.ID, | ||
| } | ||
|
|
||
| if opts.Interactive { | ||
| changed, err := promptEdit(opts, discussion, c, repo, &input) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| if !changed { | ||
| return fmt.Errorf("no changes made") | ||
| } | ||
| } else { | ||
| if opts.TitleProvided { | ||
| if strings.TrimSpace(opts.Title) == "" { | ||
| return cmdutil.FlagErrorf("title cannot be blank") | ||
| } | ||
| input.Title = &opts.Title | ||
| } | ||
| if opts.BodyProvided { | ||
| if opts.BodyFile != "" { | ||
| bodyBytes, err := cmdutil.ReadFile(opts.BodyFile, opts.IO.In) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| opts.Body = string(bodyBytes) | ||
| } | ||
| input.Body = &opts.Body | ||
| } | ||
| if opts.CategoryProvided { | ||
| opts.IO.StartProgressIndicator() | ||
| categories, err := c.ListCategories(repo) | ||
| opts.IO.StopProgressIndicator() | ||
| if err != nil { | ||
| return fmt.Errorf("fetching categories: %w", err) | ||
| } | ||
| cat, err := shared.MatchCategory(opts.Category, categories) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| input.CategoryID = &cat.ID | ||
| } | ||
|
|
||
| if opts.LabelsProvided { | ||
| opts.IO.StartProgressIndicator() | ||
| allLabels, err := c.ListLabels(repo) | ||
| opts.IO.StopProgressIndicator() | ||
| if err != nil { | ||
| return fmt.Errorf("fetching labels: %w", err) | ||
| } | ||
| if len(opts.AddLabels) > 0 { | ||
| input.AddLabelIDs, err = shared.ResolveLabels(allLabels, opts.AddLabels) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| } | ||
| if len(opts.RemoveLabels) > 0 { | ||
| input.RemoveLabelIDs, err = shared.ResolveLabels(allLabels, opts.RemoveLabels) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| opts.IO.StartProgressIndicator() | ||
| updated, err := c.Update(repo, input) | ||
| opts.IO.StopProgressIndicator() | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| fmt.Fprintln(opts.IO.Out, updated.URL) | ||
| return nil | ||
| } | ||
|
|
||
| // promptEdit runs the interactive flow, populating input with user choices. It returns a boolean indicating whether any | ||
| // changes were made, and an error if the process failed. | ||
| func promptEdit(opts *EditOptions, discussion *client.Discussion, c client.DiscussionClient, repo ghrepo.Interface, input *client.UpdateDiscussionInput) (bool, error) { | ||
| choices := []string{"Title", "Body", "Category"} | ||
|
babakks marked this conversation as resolved.
|
||
| selected, err := opts.Prompter.MultiSelect("What would you like to edit?", nil, choices) | ||
| if err != nil { | ||
| return false, err | ||
| } | ||
| if len(selected) == 0 { | ||
| return false, nil | ||
| } | ||
|
|
||
| for _, idx := range selected { | ||
| switch choices[idx] { | ||
| case "Title": | ||
| title, err := opts.Prompter.Input("Title", discussion.Title) | ||
| if err != nil { | ||
| return false, err | ||
| } | ||
| if strings.TrimSpace(title) == "" { | ||
| return false, fmt.Errorf("title cannot be blank") | ||
| } | ||
| input.Title = &title | ||
|
|
||
| case "Body": | ||
| body, err := opts.Prompter.MarkdownEditor("Body", discussion.Body, false) | ||
| if err != nil { | ||
| return false, err | ||
| } | ||
| input.Body = &body | ||
|
|
||
| case "Category": | ||
| opts.IO.StartProgressIndicator() | ||
| categories, err := c.ListCategories(repo) | ||
| opts.IO.StopProgressIndicator() | ||
| if err != nil { | ||
| return false, err | ||
| } | ||
| names := make([]string, len(categories)) | ||
| for i, cat := range categories { | ||
| names[i] = cat.Name | ||
| } | ||
| currentName := discussion.Category.Name | ||
| idx, err := opts.Prompter.Select("Category", currentName, names) | ||
| if err != nil { | ||
| return false, err | ||
| } | ||
| input.CategoryID = &categories[idx].ID | ||
| } | ||
| } | ||
|
|
||
| return true, nil | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.