Add discussion view command
Implement `gh discussion view` for viewing a single discussion with: - Number or URL argument via shared.ParseDiscussionArg - TTY output: title, metadata (state, category, author, age, comment count), labels, markdown-rendered body, reactions - Context-aware author attribution: "Asked by" for answerable categories (Q&A), "Started by" for others - Non-TTY output: key-value pairs matching `gh issue view` format - JSON output via Exporter (Discussion.ExportData) - --web flag to open in browser - Pager support for TTY output Also adds: - GetByNumber client method with not-found detection - shared.ParseDiscussionArg for number/URL/#number parsing - shared.ReactionGroupList for emoji reaction display Comment threading (--comments) is deferred to the next PR. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
d7276c7ef9
commit
d9e9751823
6 changed files with 712 additions and 2 deletions
|
|
@ -351,8 +351,53 @@ func (c *discussionClient) Search(repo ghrepo.Interface, filters SearchFilters,
|
|||
return &result, nil
|
||||
}
|
||||
|
||||
func (c *discussionClient) GetByNumber(_ ghrepo.Interface, _ int) (*Discussion, error) {
|
||||
return nil, fmt.Errorf("not implemented")
|
||||
func (c *discussionClient) GetByNumber(repo ghrepo.Interface, number int) (*Discussion, error) {
|
||||
query := fmt.Sprintf(`query DiscussionByNumber($owner: String!, $name: String!, $number: Int!) {
|
||||
repository(owner: $owner, name: $name) {
|
||||
hasDiscussionsEnabled
|
||||
discussion(number: $number) {
|
||||
%s
|
||||
body
|
||||
comments { totalCount }
|
||||
}
|
||||
}
|
||||
}`, discussionFields)
|
||||
|
||||
variables := map[string]interface{}{
|
||||
"owner": repo.RepoOwner(),
|
||||
"name": repo.RepoName(),
|
||||
"number": number,
|
||||
}
|
||||
|
||||
type response struct {
|
||||
Repository struct {
|
||||
HasDiscussionsEnabled bool `json:"hasDiscussionsEnabled"`
|
||||
Discussion *struct {
|
||||
discussionNode
|
||||
Body string `json:"body"`
|
||||
Comments struct {
|
||||
TotalCount int `json:"totalCount"`
|
||||
} `json:"comments"`
|
||||
} `json:"discussion"`
|
||||
} `json:"repository"`
|
||||
}
|
||||
|
||||
var data response
|
||||
err := c.gql.GraphQL(repo.RepoHost(), query, variables, &data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !data.Repository.HasDiscussionsEnabled {
|
||||
return nil, fmt.Errorf("the '%s/%s' repository has discussions disabled", repo.RepoOwner(), repo.RepoName())
|
||||
}
|
||||
if data.Repository.Discussion == nil {
|
||||
return nil, fmt.Errorf("discussion #%d not found in '%s/%s'", number, repo.RepoOwner(), repo.RepoName())
|
||||
}
|
||||
|
||||
d := mapDiscussion(data.Repository.Discussion.discussionNode)
|
||||
d.Body = data.Repository.Discussion.Body
|
||||
d.Comments = DiscussionCommentList{TotalCount: data.Repository.Discussion.Comments.TotalCount}
|
||||
return &d, nil
|
||||
}
|
||||
|
||||
func (c *discussionClient) GetWithComments(_ ghrepo.Interface, _ int, _ int, _ string) (*Discussion, error) {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package discussion
|
|||
import (
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
cmdList "github.com/cli/cli/v2/pkg/cmd/discussion/list"
|
||||
cmdView "github.com/cli/cli/v2/pkg/cmd/discussion/view"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
|
@ -36,5 +37,9 @@ func NewCmdDiscussion(f *cmdutil.Factory) *cobra.Command {
|
|||
cmdList.NewCmdList(f, nil),
|
||||
)
|
||||
|
||||
cmdutil.AddGroup(cmd, "Targeted commands",
|
||||
cmdView.NewCmdView(f, nil),
|
||||
)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
|
|
|||
35
pkg/cmd/discussion/shared/display.go
Normal file
35
pkg/cmd/discussion/shared/display.go
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
package shared
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/cli/cli/v2/pkg/cmd/discussion/client"
|
||||
)
|
||||
|
||||
var reactionEmoji = map[string]string{
|
||||
"THUMBS_UP": "\U0001f44d",
|
||||
"THUMBS_DOWN": "\U0001f44e",
|
||||
"LAUGH": "\U0001f604",
|
||||
"HOORAY": "\U0001f389",
|
||||
"CONFUSED": "\U0001f615",
|
||||
"HEART": "\u2764\ufe0f",
|
||||
"ROCKET": "\U0001f680",
|
||||
"EYES": "\U0001f440",
|
||||
}
|
||||
|
||||
// ReactionGroupList formats reaction groups for display.
|
||||
func ReactionGroupList(groups []client.ReactionGroup) string {
|
||||
var parts []string
|
||||
for _, g := range groups {
|
||||
if g.TotalCount == 0 {
|
||||
continue
|
||||
}
|
||||
emoji := reactionEmoji[g.Content]
|
||||
if emoji == "" {
|
||||
emoji = g.Content
|
||||
}
|
||||
parts = append(parts, fmt.Sprintf("%s %d", emoji, g.TotalCount))
|
||||
}
|
||||
return strings.Join(parts, " • ")
|
||||
}
|
||||
40
pkg/cmd/discussion/shared/lookup.go
Normal file
40
pkg/cmd/discussion/shared/lookup.go
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
package shared
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
)
|
||||
|
||||
var discussionURLRE = regexp.MustCompile(`^/([^/]+)/([^/]+)/discussions/(\d+)`)
|
||||
|
||||
// ParseDiscussionArg parses a discussion number or URL from a command argument.
|
||||
// It returns the discussion number and, if the argument was a URL, a repo override.
|
||||
func ParseDiscussionArg(arg string) (int, ghrepo.Interface, error) {
|
||||
if num, err := strconv.Atoi(arg); err == nil {
|
||||
return num, nil, nil
|
||||
}
|
||||
|
||||
if len(arg) > 1 && arg[0] == '#' {
|
||||
if num, err := strconv.Atoi(arg[1:]); err == nil {
|
||||
return num, nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
u, err := url.Parse(arg)
|
||||
if err != nil || (u.Scheme != "http" && u.Scheme != "https") {
|
||||
return 0, nil, fmt.Errorf("invalid discussion argument: %s", arg)
|
||||
}
|
||||
|
||||
m := discussionURLRE.FindStringSubmatch(u.Path)
|
||||
if m == nil {
|
||||
return 0, nil, fmt.Errorf("invalid discussion URL: %s", arg)
|
||||
}
|
||||
|
||||
num, _ := strconv.Atoi(m[3])
|
||||
repo := ghrepo.NewWithHost(m[1], m[2], u.Hostname())
|
||||
return num, repo, nil
|
||||
}
|
||||
232
pkg/cmd/discussion/view/view.go
Normal file
232
pkg/cmd/discussion/view/view.go
Normal file
|
|
@ -0,0 +1,232 @@
|
|||
package view
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/v2/internal/browser"
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/cli/cli/v2/internal/text"
|
||||
"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/cli/cli/v2/pkg/markdown"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// ViewOptions holds the configuration for the view command.
|
||||
type ViewOptions struct {
|
||||
IO *iostreams.IOStreams
|
||||
BaseRepo func() (ghrepo.Interface, error)
|
||||
Browser browser.Browser
|
||||
Client func() (client.DiscussionClient, error)
|
||||
|
||||
DiscussionNumber int
|
||||
WebMode bool
|
||||
Exporter cmdutil.Exporter
|
||||
Now func() time.Time
|
||||
}
|
||||
|
||||
// NewCmdView creates the "discussion view" command.
|
||||
func NewCmdView(f *cmdutil.Factory, runF func(*ViewOptions) error) *cobra.Command {
|
||||
opts := &ViewOptions{
|
||||
IO: f.IOStreams,
|
||||
Browser: f.Browser,
|
||||
Now: time.Now,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "view {<number> | <url>}",
|
||||
Short: "View a discussion",
|
||||
Long: heredoc.Docf(`
|
||||
Display the title, body, and other information about a discussion.
|
||||
|
||||
With %[1]s--web%[1]s flag, open the discussion in a web browser instead.
|
||||
`, "`"),
|
||||
Example: heredoc.Doc(`
|
||||
# View a discussion by number
|
||||
$ gh discussion view 123
|
||||
|
||||
# View a discussion by URL
|
||||
$ gh discussion view https://github.com/OWNER/REPO/discussions/123
|
||||
|
||||
# Open in browser
|
||||
$ gh discussion view 123 --web
|
||||
`),
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
number, repo, err := shared.ParseDiscussionArg(args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if repo != nil {
|
||||
opts.BaseRepo = func() (ghrepo.Interface, error) {
|
||||
return repo, nil
|
||||
}
|
||||
} else {
|
||||
opts.BaseRepo = f.BaseRepo
|
||||
}
|
||||
|
||||
opts.DiscussionNumber = number
|
||||
opts.Client = shared.DiscussionClientFunc(f)
|
||||
|
||||
if runF != nil {
|
||||
return runF(opts)
|
||||
}
|
||||
return viewRun(opts)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().BoolVarP(&opts.WebMode, "web", "w", false, "Open a discussion in the browser")
|
||||
cmdutil.AddJSONFlags(cmd, &opts.Exporter, shared.DiscussionFields)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func viewRun(opts *ViewOptions) error {
|
||||
repo, err := opts.BaseRepo()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if opts.WebMode {
|
||||
openURL := ghrepo.GenerateRepoURL(repo, "discussions/%d", opts.DiscussionNumber)
|
||||
if opts.IO.IsStdoutTTY() {
|
||||
fmt.Fprintf(opts.IO.ErrOut, "Opening %s in your browser.\n", text.DisplayURL(openURL))
|
||||
}
|
||||
return opts.Browser.Browse(openURL)
|
||||
}
|
||||
|
||||
c, err := opts.Client()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
opts.IO.DetectTerminalTheme()
|
||||
opts.IO.StartProgressIndicator()
|
||||
|
||||
discussion, err := c.GetByNumber(repo, opts.DiscussionNumber)
|
||||
|
||||
opts.IO.StopProgressIndicator()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if opts.Exporter != nil {
|
||||
return opts.Exporter.Write(opts.IO, discussion)
|
||||
}
|
||||
|
||||
if err := opts.IO.StartPager(); err != nil {
|
||||
fmt.Fprintf(opts.IO.ErrOut, "error starting pager: %v\n", err)
|
||||
}
|
||||
defer opts.IO.StopPager()
|
||||
|
||||
if opts.IO.IsStdoutTTY() {
|
||||
return printHumanView(opts, discussion)
|
||||
}
|
||||
|
||||
return printRawView(opts.IO.Out, discussion)
|
||||
}
|
||||
|
||||
func printHumanView(opts *ViewOptions, d *client.Discussion) error {
|
||||
out := opts.IO.Out
|
||||
cs := opts.IO.ColorScheme()
|
||||
|
||||
numberStr := fmt.Sprintf("#%d", d.Number)
|
||||
if d.State == "OPEN" {
|
||||
numberStr = cs.Green(numberStr)
|
||||
} else {
|
||||
numberStr = cs.Muted(numberStr)
|
||||
}
|
||||
fmt.Fprintf(out, "%s %s\n", cs.Bold(d.Title), numberStr)
|
||||
|
||||
state := "Open"
|
||||
stateColor := cs.Green
|
||||
if d.State != "OPEN" {
|
||||
state = "Closed"
|
||||
stateColor = cs.Muted
|
||||
}
|
||||
|
||||
verb := "Started by"
|
||||
if d.Category.IsAnswerable {
|
||||
verb = "Asked by"
|
||||
}
|
||||
|
||||
fmt.Fprintf(out, "%s · %s · %s %s · %s · %s\n",
|
||||
stateColor(state),
|
||||
d.Category.Name,
|
||||
verb,
|
||||
d.Author.Login,
|
||||
text.FuzzyAgo(opts.Now(), d.CreatedAt),
|
||||
text.Pluralize(d.Comments.TotalCount, "comment"),
|
||||
)
|
||||
|
||||
if labels := labelList(d.Labels, cs); labels != "" {
|
||||
fmt.Fprint(out, cs.Bold("Labels: "))
|
||||
fmt.Fprintln(out, labels)
|
||||
}
|
||||
|
||||
var md string
|
||||
if d.Body == "" {
|
||||
md = fmt.Sprintf("\n %s\n\n", cs.Muted("No description provided"))
|
||||
} else {
|
||||
var err error
|
||||
md, err = markdown.Render(d.Body,
|
||||
markdown.WithTheme(opts.IO.TerminalTheme()),
|
||||
markdown.WithWrap(opts.IO.TerminalWidth()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
fmt.Fprintf(out, "\n%s\n", md)
|
||||
|
||||
if reactions := shared.ReactionGroupList(d.ReactionGroups); reactions != "" {
|
||||
fmt.Fprintln(out, reactions)
|
||||
fmt.Fprintln(out)
|
||||
}
|
||||
|
||||
fmt.Fprintf(out, cs.Muted("View this discussion on GitHub: %s\n"), d.URL)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func printRawView(out io.Writer, d *client.Discussion) error {
|
||||
fmt.Fprintf(out, "title:\t%s\n", d.Title)
|
||||
fmt.Fprintf(out, "state:\t%s\n", d.State)
|
||||
fmt.Fprintf(out, "category:\t%s\n", d.Category.Name)
|
||||
fmt.Fprintf(out, "author:\t%s\n", d.Author.Login)
|
||||
fmt.Fprintf(out, "labels:\t%s\n", labelList(d.Labels, nil))
|
||||
fmt.Fprintf(out, "comments:\t%d\n", d.Comments.TotalCount)
|
||||
fmt.Fprintf(out, "number:\t%d\n", d.Number)
|
||||
fmt.Fprintf(out, "url:\t%s\n", d.URL)
|
||||
fmt.Fprintln(out, "--")
|
||||
fmt.Fprintln(out, d.Body)
|
||||
return nil
|
||||
}
|
||||
|
||||
func labelList(labels []client.DiscussionLabel, cs *iostreams.ColorScheme) string {
|
||||
if len(labels) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
sort.SliceStable(labels, func(i, j int) bool {
|
||||
return strings.ToLower(labels[i].Name) < strings.ToLower(labels[j].Name)
|
||||
})
|
||||
|
||||
names := make([]string, len(labels))
|
||||
for i, l := range labels {
|
||||
if cs == nil {
|
||||
names[i] = l.Name
|
||||
} else {
|
||||
names[i] = cs.Label(l.Color, l.Name)
|
||||
}
|
||||
}
|
||||
return strings.Join(names, ", ")
|
||||
}
|
||||
353
pkg/cmd/discussion/view/view_test.go
Normal file
353
pkg/cmd/discussion/view/view_test.go
Normal file
|
|
@ -0,0 +1,353 @@
|
|||
package view
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cli/cli/v2/internal/browser"
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"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/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func testDiscussion() *client.Discussion {
|
||||
return &client.Discussion{
|
||||
ID: "D_123",
|
||||
Number: 123,
|
||||
Title: "How to authenticate with SSO?",
|
||||
Body: "I need help with SSO authentication.",
|
||||
URL: "https://github.com/OWNER/REPO/discussions/123",
|
||||
State: "OPEN",
|
||||
Author: client.DiscussionAuthor{Login: "monalisa"},
|
||||
Category: client.DiscussionCategory{
|
||||
Name: "Q&A", Slug: "q-a", IsAnswerable: true,
|
||||
},
|
||||
Labels: []client.DiscussionLabel{{Name: "help-wanted", Color: "0075ca"}},
|
||||
Answered: false,
|
||||
Comments: client.DiscussionCommentList{TotalCount: 3},
|
||||
ReactionGroups: []client.ReactionGroup{
|
||||
{Content: "THUMBS_UP", TotalCount: 5},
|
||||
{Content: "ROCKET", TotalCount: 2},
|
||||
},
|
||||
CreatedAt: time.Date(2025, 3, 1, 0, 0, 0, 0, time.UTC),
|
||||
UpdatedAt: time.Date(2025, 3, 1, 0, 0, 0, 0, time.UTC),
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewCmdView(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
args []string
|
||||
wantNum int
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "number argument",
|
||||
args: []string{"123"},
|
||||
wantNum: 123,
|
||||
},
|
||||
{
|
||||
name: "hash number argument",
|
||||
args: []string{"#456"},
|
||||
wantNum: 456,
|
||||
},
|
||||
{
|
||||
name: "URL argument",
|
||||
args: []string{"https://github.com/OWNER/REPO/discussions/789"},
|
||||
wantNum: 789,
|
||||
},
|
||||
{
|
||||
name: "invalid argument",
|
||||
args: []string{"not-a-number"},
|
||||
wantErr: "invalid discussion argument",
|
||||
},
|
||||
{
|
||||
name: "no arguments",
|
||||
args: []string{},
|
||||
wantErr: "accepts 1 arg(s), received 0",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
f := &cmdutil.Factory{}
|
||||
ios, _, _, _ := iostreams.Test()
|
||||
f.IOStreams = ios
|
||||
f.BaseRepo = func() (ghrepo.Interface, error) {
|
||||
return ghrepo.New("OWNER", "REPO"), nil
|
||||
}
|
||||
f.Browser = &browser.Stub{}
|
||||
|
||||
var gotOpts *ViewOptions
|
||||
cmd := NewCmdView(f, func(opts *ViewOptions) error {
|
||||
gotOpts = opts
|
||||
return nil
|
||||
})
|
||||
|
||||
cmd.SetArgs(tt.args)
|
||||
cmd.SetOut(&bytes.Buffer{})
|
||||
cmd.SetErr(&bytes.Buffer{})
|
||||
|
||||
err := cmd.Execute()
|
||||
if tt.wantErr != "" {
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), tt.wantErr)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.wantNum, gotOpts.DiscussionNumber)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestViewRun_tty(t *testing.T) {
|
||||
ios, _, stdout, _ := iostreams.Test()
|
||||
ios.SetStdoutTTY(true)
|
||||
ios.SetStderrTTY(true)
|
||||
|
||||
d := testDiscussion()
|
||||
mock := &client.DiscussionClientMock{
|
||||
GetByNumberFunc: func(repo ghrepo.Interface, number int) (*client.Discussion, error) {
|
||||
return d, nil
|
||||
},
|
||||
}
|
||||
|
||||
opts := &ViewOptions{
|
||||
IO: ios,
|
||||
BaseRepo: func() (ghrepo.Interface, error) {
|
||||
return ghrepo.New("OWNER", "REPO"), nil
|
||||
},
|
||||
Client: func() (client.DiscussionClient, error) {
|
||||
return mock, nil
|
||||
},
|
||||
DiscussionNumber: 123,
|
||||
Now: func() time.Time { return time.Date(2025, 3, 1, 1, 0, 0, 0, time.UTC) },
|
||||
}
|
||||
|
||||
err := viewRun(opts)
|
||||
require.NoError(t, err)
|
||||
|
||||
out := stdout.String()
|
||||
assert.Contains(t, out, "How to authenticate with SSO?")
|
||||
assert.Contains(t, out, "#123")
|
||||
assert.Contains(t, out, "Q&A")
|
||||
assert.Contains(t, out, "Asked by")
|
||||
assert.Contains(t, out, "monalisa")
|
||||
assert.Contains(t, out, "3 comments")
|
||||
assert.Contains(t, out, "help-wanted")
|
||||
assert.Contains(t, out, "View this discussion on GitHub")
|
||||
}
|
||||
|
||||
func TestViewRun_nontty(t *testing.T) {
|
||||
ios, _, stdout, _ := iostreams.Test()
|
||||
ios.SetStdoutTTY(false)
|
||||
|
||||
d := testDiscussion()
|
||||
mock := &client.DiscussionClientMock{
|
||||
GetByNumberFunc: func(repo ghrepo.Interface, number int) (*client.Discussion, error) {
|
||||
return d, nil
|
||||
},
|
||||
}
|
||||
|
||||
opts := &ViewOptions{
|
||||
IO: ios,
|
||||
BaseRepo: func() (ghrepo.Interface, error) {
|
||||
return ghrepo.New("OWNER", "REPO"), nil
|
||||
},
|
||||
Client: func() (client.DiscussionClient, error) {
|
||||
return mock, nil
|
||||
},
|
||||
DiscussionNumber: 123,
|
||||
Now: time.Now,
|
||||
}
|
||||
|
||||
err := viewRun(opts)
|
||||
require.NoError(t, err)
|
||||
|
||||
out := stdout.String()
|
||||
assert.Contains(t, out, "title:\tHow to authenticate with SSO?")
|
||||
assert.Contains(t, out, "state:\tOPEN")
|
||||
assert.Contains(t, out, "category:\tQ&A")
|
||||
assert.Contains(t, out, "author:\tmonalisa")
|
||||
assert.Contains(t, out, "labels:\thelp-wanted")
|
||||
assert.Contains(t, out, "number:\t123")
|
||||
assert.Contains(t, out, "--")
|
||||
assert.Contains(t, out, "I need help with SSO authentication.")
|
||||
}
|
||||
|
||||
func TestViewRun_json(t *testing.T) {
|
||||
ios, _, stdout, _ := iostreams.Test()
|
||||
ios.SetStdoutTTY(false)
|
||||
|
||||
d := testDiscussion()
|
||||
mock := &client.DiscussionClientMock{
|
||||
GetByNumberFunc: func(repo ghrepo.Interface, number int) (*client.Discussion, error) {
|
||||
return d, nil
|
||||
},
|
||||
}
|
||||
|
||||
exporter := cmdutil.NewJSONExporter()
|
||||
exporter.SetFields(shared.DiscussionFields)
|
||||
|
||||
opts := &ViewOptions{
|
||||
IO: ios,
|
||||
BaseRepo: func() (ghrepo.Interface, error) {
|
||||
return ghrepo.New("OWNER", "REPO"), nil
|
||||
},
|
||||
Client: func() (client.DiscussionClient, error) {
|
||||
return mock, nil
|
||||
},
|
||||
DiscussionNumber: 123,
|
||||
Exporter: exporter,
|
||||
Now: time.Now,
|
||||
}
|
||||
|
||||
err := viewRun(opts)
|
||||
require.NoError(t, err)
|
||||
|
||||
out := stdout.String()
|
||||
assert.Contains(t, out, `"title"`)
|
||||
assert.Contains(t, out, `"number"`)
|
||||
assert.Contains(t, out, "How to authenticate with SSO?")
|
||||
}
|
||||
|
||||
func TestViewRun_web(t *testing.T) {
|
||||
ios, _, _, stderr := iostreams.Test()
|
||||
ios.SetStdoutTTY(true)
|
||||
ios.SetStderrTTY(true)
|
||||
|
||||
b := &browser.Stub{}
|
||||
|
||||
opts := &ViewOptions{
|
||||
IO: ios,
|
||||
BaseRepo: func() (ghrepo.Interface, error) {
|
||||
return ghrepo.New("OWNER", "REPO"), nil
|
||||
},
|
||||
Browser: b,
|
||||
DiscussionNumber: 123,
|
||||
WebMode: true,
|
||||
Now: time.Now,
|
||||
}
|
||||
|
||||
err := viewRun(opts)
|
||||
require.NoError(t, err)
|
||||
|
||||
b.Verify(t, "https://github.com/OWNER/REPO/discussions/123")
|
||||
assert.Contains(t, stderr.String(), "Opening")
|
||||
}
|
||||
|
||||
func TestViewRun_urlArg(t *testing.T) {
|
||||
ios, _, stdout, _ := iostreams.Test()
|
||||
ios.SetStdoutTTY(false)
|
||||
|
||||
d := testDiscussion()
|
||||
d.URL = "https://github.com/OTHER/REPO/discussions/42"
|
||||
d.Number = 42
|
||||
|
||||
mock := &client.DiscussionClientMock{
|
||||
GetByNumberFunc: func(repo ghrepo.Interface, number int) (*client.Discussion, error) {
|
||||
assert.Equal(t, "OTHER", repo.RepoOwner())
|
||||
assert.Equal(t, "REPO", repo.RepoName())
|
||||
assert.Equal(t, 42, number)
|
||||
return d, nil
|
||||
},
|
||||
}
|
||||
|
||||
f := &cmdutil.Factory{}
|
||||
f.IOStreams = ios
|
||||
f.BaseRepo = func() (ghrepo.Interface, error) {
|
||||
return ghrepo.New("OWNER", "REPO"), nil
|
||||
}
|
||||
f.Browser = &browser.Stub{}
|
||||
|
||||
var gotOpts *ViewOptions
|
||||
cmd := NewCmdView(f, func(opts *ViewOptions) error {
|
||||
gotOpts = opts
|
||||
opts.Client = func() (client.DiscussionClient, error) {
|
||||
return mock, nil
|
||||
}
|
||||
return viewRun(opts)
|
||||
})
|
||||
|
||||
cmd.SetArgs([]string{"https://github.com/OTHER/REPO/discussions/42"})
|
||||
cmd.SetOut(&bytes.Buffer{})
|
||||
cmd.SetErr(&bytes.Buffer{})
|
||||
|
||||
err := cmd.Execute()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 42, gotOpts.DiscussionNumber)
|
||||
|
||||
out := stdout.String()
|
||||
assert.Contains(t, out, "number:\t42")
|
||||
}
|
||||
|
||||
func TestViewRun_answerable(t *testing.T) {
|
||||
ios, _, stdout, _ := iostreams.Test()
|
||||
ios.SetStdoutTTY(true)
|
||||
ios.SetStderrTTY(true)
|
||||
|
||||
d := testDiscussion()
|
||||
d.Category.IsAnswerable = true
|
||||
|
||||
mock := &client.DiscussionClientMock{
|
||||
GetByNumberFunc: func(repo ghrepo.Interface, number int) (*client.Discussion, error) {
|
||||
return d, nil
|
||||
},
|
||||
}
|
||||
|
||||
opts := &ViewOptions{
|
||||
IO: ios,
|
||||
BaseRepo: func() (ghrepo.Interface, error) {
|
||||
return ghrepo.New("OWNER", "REPO"), nil
|
||||
},
|
||||
Client: func() (client.DiscussionClient, error) {
|
||||
return mock, nil
|
||||
},
|
||||
DiscussionNumber: 123,
|
||||
Now: func() time.Time { return time.Date(2025, 3, 1, 1, 0, 0, 0, time.UTC) },
|
||||
}
|
||||
|
||||
err := viewRun(opts)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, stdout.String(), "Asked by")
|
||||
}
|
||||
|
||||
func TestViewRun_notAnswerable(t *testing.T) {
|
||||
ios, _, stdout, _ := iostreams.Test()
|
||||
ios.SetStdoutTTY(true)
|
||||
ios.SetStderrTTY(true)
|
||||
|
||||
d := testDiscussion()
|
||||
d.Category.Name = "General"
|
||||
d.Category.IsAnswerable = false
|
||||
|
||||
mock := &client.DiscussionClientMock{
|
||||
GetByNumberFunc: func(repo ghrepo.Interface, number int) (*client.Discussion, error) {
|
||||
return d, nil
|
||||
},
|
||||
}
|
||||
|
||||
opts := &ViewOptions{
|
||||
IO: ios,
|
||||
BaseRepo: func() (ghrepo.Interface, error) {
|
||||
return ghrepo.New("OWNER", "REPO"), nil
|
||||
},
|
||||
Client: func() (client.DiscussionClient, error) {
|
||||
return mock, nil
|
||||
},
|
||||
DiscussionNumber: 123,
|
||||
Now: func() time.Time { return time.Date(2025, 3, 1, 1, 0, 0, 0, time.UTC) },
|
||||
}
|
||||
|
||||
err := viewRun(opts)
|
||||
require.NoError(t, err)
|
||||
|
||||
out := stdout.String()
|
||||
assert.Contains(t, out, "Started by")
|
||||
assert.NotContains(t, out, "Asked by")
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue