Merge pull request #13082 from maxbeizer/discussion-cmd

Add `discussion` command group scaffolding
This commit is contained in:
Babak K. Shandiz 2026-04-02 18:14:07 +01:00 committed by GitHub
commit 65f5b21121
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 1221 additions and 0 deletions

View file

@ -0,0 +1,26 @@
// Package client provides an abstraction layer for interacting with the
// GitHub Discussions GraphQL API. The DiscussionClient interface defines all
// supported operations and can be replaced with a mock in tests.
package client
import "github.com/cli/cli/v2/internal/ghrepo"
//go:generate moq -rm -out client_mock.go . DiscussionClient
// DiscussionClient defines operations for interacting with the GitHub Discussions API.
type DiscussionClient interface {
List(repo ghrepo.Interface, filters ListFilters, limit int) ([]Discussion, int, error)
Search(repo ghrepo.Interface, filters SearchFilters, limit int) ([]Discussion, int, error)
GetByNumber(repo ghrepo.Interface, number int) (*Discussion, error)
GetWithComments(repo ghrepo.Interface, number int, commentLimit int, order string) (*Discussion, error)
ListCategories(repo ghrepo.Interface) ([]DiscussionCategory, error)
Create(repo ghrepo.Interface, input CreateDiscussionInput) (*Discussion, error)
Update(repo ghrepo.Interface, input UpdateDiscussionInput) (*Discussion, error)
Close(repo ghrepo.Interface, id string, reason CloseReason) (*Discussion, error)
Reopen(repo ghrepo.Interface, id string) (*Discussion, error)
AddComment(repo ghrepo.Interface, discussionID string, body string, replyToID string) (*DiscussionComment, error)
Lock(repo ghrepo.Interface, id string, reason string) error
Unlock(repo ghrepo.Interface, id string) error
MarkAnswer(repo ghrepo.Interface, commentID string) error
UnmarkAnswer(repo ghrepo.Interface, commentID string) error
}

View file

@ -0,0 +1,76 @@
package client
import (
"fmt"
"net/http"
"github.com/cli/cli/v2/api"
"github.com/cli/cli/v2/internal/ghrepo"
)
type discussionClient struct {
gql *api.Client
}
// NewDiscussionClient creates a DiscussionClient backed by the given HTTP client.
func NewDiscussionClient(httpClient *http.Client) DiscussionClient {
return &discussionClient{
gql: api.NewClientFromHTTP(httpClient),
}
}
func (c *discussionClient) List(_ ghrepo.Interface, _ ListFilters, _ int) ([]Discussion, int, error) {
return nil, 0, fmt.Errorf("not implemented")
}
func (c *discussionClient) Search(_ ghrepo.Interface, _ SearchFilters, _ int) ([]Discussion, int, error) {
return nil, 0, fmt.Errorf("not implemented")
}
func (c *discussionClient) GetByNumber(_ ghrepo.Interface, _ int) (*Discussion, error) {
return nil, fmt.Errorf("not implemented")
}
func (c *discussionClient) GetWithComments(_ ghrepo.Interface, _ int, _ int, _ string) (*Discussion, error) {
return nil, fmt.Errorf("not implemented")
}
func (c *discussionClient) ListCategories(_ ghrepo.Interface) ([]DiscussionCategory, error) {
return nil, fmt.Errorf("not implemented")
}
func (c *discussionClient) Create(_ ghrepo.Interface, _ CreateDiscussionInput) (*Discussion, error) {
return nil, fmt.Errorf("not implemented")
}
func (c *discussionClient) Update(_ ghrepo.Interface, _ UpdateDiscussionInput) (*Discussion, error) {
return nil, fmt.Errorf("not implemented")
}
func (c *discussionClient) Close(_ ghrepo.Interface, _ string, _ CloseReason) (*Discussion, error) {
return nil, fmt.Errorf("not implemented")
}
func (c *discussionClient) Reopen(_ ghrepo.Interface, _ string) (*Discussion, error) {
return nil, fmt.Errorf("not implemented")
}
func (c *discussionClient) AddComment(_ ghrepo.Interface, _ string, _ string, _ string) (*DiscussionComment, error) {
return nil, fmt.Errorf("not implemented")
}
func (c *discussionClient) Lock(_ ghrepo.Interface, _ string, _ string) error {
return fmt.Errorf("not implemented")
}
func (c *discussionClient) Unlock(_ ghrepo.Interface, _ string) error {
return fmt.Errorf("not implemented")
}
func (c *discussionClient) MarkAnswer(_ ghrepo.Interface, _ string) error {
return fmt.Errorf("not implemented")
}
func (c *discussionClient) UnmarkAnswer(_ ghrepo.Interface, _ string) error {
return fmt.Errorf("not implemented")
}

View file

@ -0,0 +1,773 @@
// Code generated by moq; DO NOT EDIT.
// github.com/matryer/moq
package client
import (
"github.com/cli/cli/v2/internal/ghrepo"
"sync"
)
// Ensure, that DiscussionClientMock does implement DiscussionClient.
// If this is not the case, regenerate this file with moq.
var _ DiscussionClient = &DiscussionClientMock{}
// DiscussionClientMock is a mock implementation of DiscussionClient.
//
// func TestSomethingThatUsesDiscussionClient(t *testing.T) {
//
// // make and configure a mocked DiscussionClient
// mockedDiscussionClient := &DiscussionClientMock{
// AddCommentFunc: func(repo ghrepo.Interface, discussionID string, body string, replyToID string) (*DiscussionComment, error) {
// panic("mock out the AddComment method")
// },
// CloseFunc: func(repo ghrepo.Interface, id string, reason CloseReason) (*Discussion, error) {
// panic("mock out the Close method")
// },
// CreateFunc: func(repo ghrepo.Interface, input CreateDiscussionInput) (*Discussion, error) {
// panic("mock out the Create method")
// },
// GetByNumberFunc: func(repo ghrepo.Interface, number int) (*Discussion, error) {
// panic("mock out the GetByNumber method")
// },
// GetWithCommentsFunc: func(repo ghrepo.Interface, number int, commentLimit int, order string) (*Discussion, error) {
// panic("mock out the GetWithComments method")
// },
// ListFunc: func(repo ghrepo.Interface, filters ListFilters, limit int) ([]Discussion, int, error) {
// panic("mock out the List method")
// },
// ListCategoriesFunc: func(repo ghrepo.Interface) ([]DiscussionCategory, error) {
// panic("mock out the ListCategories method")
// },
// LockFunc: func(repo ghrepo.Interface, id string, reason string) error {
// panic("mock out the Lock method")
// },
// MarkAnswerFunc: func(repo ghrepo.Interface, commentID string) error {
// panic("mock out the MarkAnswer method")
// },
// ReopenFunc: func(repo ghrepo.Interface, id string) (*Discussion, error) {
// panic("mock out the Reopen method")
// },
// SearchFunc: func(repo ghrepo.Interface, filters SearchFilters, limit int) ([]Discussion, int, error) {
// panic("mock out the Search method")
// },
// UnlockFunc: func(repo ghrepo.Interface, id string) error {
// panic("mock out the Unlock method")
// },
// UnmarkAnswerFunc: func(repo ghrepo.Interface, commentID string) error {
// panic("mock out the UnmarkAnswer method")
// },
// UpdateFunc: func(repo ghrepo.Interface, input UpdateDiscussionInput) (*Discussion, error) {
// panic("mock out the Update method")
// },
// }
//
// // use mockedDiscussionClient in code that requires DiscussionClient
// // and then make assertions.
//
// }
type DiscussionClientMock struct {
// AddCommentFunc mocks the AddComment method.
AddCommentFunc func(repo ghrepo.Interface, discussionID string, body string, replyToID string) (*DiscussionComment, error)
// CloseFunc mocks the Close method.
CloseFunc func(repo ghrepo.Interface, id string, reason CloseReason) (*Discussion, error)
// CreateFunc mocks the Create method.
CreateFunc func(repo ghrepo.Interface, input CreateDiscussionInput) (*Discussion, error)
// GetByNumberFunc mocks the GetByNumber method.
GetByNumberFunc func(repo ghrepo.Interface, number int) (*Discussion, error)
// GetWithCommentsFunc mocks the GetWithComments method.
GetWithCommentsFunc func(repo ghrepo.Interface, number int, commentLimit int, order string) (*Discussion, error)
// ListFunc mocks the List method.
ListFunc func(repo ghrepo.Interface, filters ListFilters, limit int) ([]Discussion, int, error)
// ListCategoriesFunc mocks the ListCategories method.
ListCategoriesFunc func(repo ghrepo.Interface) ([]DiscussionCategory, error)
// LockFunc mocks the Lock method.
LockFunc func(repo ghrepo.Interface, id string, reason string) error
// MarkAnswerFunc mocks the MarkAnswer method.
MarkAnswerFunc func(repo ghrepo.Interface, commentID string) error
// ReopenFunc mocks the Reopen method.
ReopenFunc func(repo ghrepo.Interface, id string) (*Discussion, error)
// SearchFunc mocks the Search method.
SearchFunc func(repo ghrepo.Interface, filters SearchFilters, limit int) ([]Discussion, int, error)
// UnlockFunc mocks the Unlock method.
UnlockFunc func(repo ghrepo.Interface, id string) error
// UnmarkAnswerFunc mocks the UnmarkAnswer method.
UnmarkAnswerFunc func(repo ghrepo.Interface, commentID string) error
// UpdateFunc mocks the Update method.
UpdateFunc func(repo ghrepo.Interface, input UpdateDiscussionInput) (*Discussion, error)
// calls tracks calls to the methods.
calls struct {
// AddComment holds details about calls to the AddComment method.
AddComment []struct {
// Repo is the repo argument value.
Repo ghrepo.Interface
// DiscussionID is the discussionID argument value.
DiscussionID string
// Body is the body argument value.
Body string
// ReplyToID is the replyToID argument value.
ReplyToID string
}
// Close holds details about calls to the Close method.
Close []struct {
// Repo is the repo argument value.
Repo ghrepo.Interface
// ID is the id argument value.
ID string
// Reason is the reason argument value.
Reason CloseReason
}
// Create holds details about calls to the Create method.
Create []struct {
// Repo is the repo argument value.
Repo ghrepo.Interface
// Input is the input argument value.
Input CreateDiscussionInput
}
// GetByNumber holds details about calls to the GetByNumber method.
GetByNumber []struct {
// Repo is the repo argument value.
Repo ghrepo.Interface
// Number is the number argument value.
Number int
}
// GetWithComments holds details about calls to the GetWithComments method.
GetWithComments []struct {
// Repo is the repo argument value.
Repo ghrepo.Interface
// Number is the number argument value.
Number int
// CommentLimit is the commentLimit argument value.
CommentLimit int
// Order is the order argument value.
Order string
}
// List holds details about calls to the List method.
List []struct {
// Repo is the repo argument value.
Repo ghrepo.Interface
// Filters is the filters argument value.
Filters ListFilters
// Limit is the limit argument value.
Limit int
}
// ListCategories holds details about calls to the ListCategories method.
ListCategories []struct {
// Repo is the repo argument value.
Repo ghrepo.Interface
}
// Lock holds details about calls to the Lock method.
Lock []struct {
// Repo is the repo argument value.
Repo ghrepo.Interface
// ID is the id argument value.
ID string
// Reason is the reason argument value.
Reason string
}
// MarkAnswer holds details about calls to the MarkAnswer method.
MarkAnswer []struct {
// Repo is the repo argument value.
Repo ghrepo.Interface
// CommentID is the commentID argument value.
CommentID string
}
// Reopen holds details about calls to the Reopen method.
Reopen []struct {
// Repo is the repo argument value.
Repo ghrepo.Interface
// ID is the id argument value.
ID string
}
// Search holds details about calls to the Search method.
Search []struct {
// Repo is the repo argument value.
Repo ghrepo.Interface
// Filters is the filters argument value.
Filters SearchFilters
// Limit is the limit argument value.
Limit int
}
// Unlock holds details about calls to the Unlock method.
Unlock []struct {
// Repo is the repo argument value.
Repo ghrepo.Interface
// ID is the id argument value.
ID string
}
// UnmarkAnswer holds details about calls to the UnmarkAnswer method.
UnmarkAnswer []struct {
// Repo is the repo argument value.
Repo ghrepo.Interface
// CommentID is the commentID argument value.
CommentID string
}
// Update holds details about calls to the Update method.
Update []struct {
// Repo is the repo argument value.
Repo ghrepo.Interface
// Input is the input argument value.
Input UpdateDiscussionInput
}
}
lockAddComment sync.RWMutex
lockClose sync.RWMutex
lockCreate sync.RWMutex
lockGetByNumber sync.RWMutex
lockGetWithComments sync.RWMutex
lockList sync.RWMutex
lockListCategories sync.RWMutex
lockLock sync.RWMutex
lockMarkAnswer sync.RWMutex
lockReopen sync.RWMutex
lockSearch sync.RWMutex
lockUnlock sync.RWMutex
lockUnmarkAnswer sync.RWMutex
lockUpdate sync.RWMutex
}
// AddComment calls AddCommentFunc.
func (mock *DiscussionClientMock) AddComment(repo ghrepo.Interface, discussionID string, body string, replyToID string) (*DiscussionComment, error) {
if mock.AddCommentFunc == nil {
panic("DiscussionClientMock.AddCommentFunc: method is nil but DiscussionClient.AddComment was just called")
}
callInfo := struct {
Repo ghrepo.Interface
DiscussionID string
Body string
ReplyToID string
}{
Repo: repo,
DiscussionID: discussionID,
Body: body,
ReplyToID: replyToID,
}
mock.lockAddComment.Lock()
mock.calls.AddComment = append(mock.calls.AddComment, callInfo)
mock.lockAddComment.Unlock()
return mock.AddCommentFunc(repo, discussionID, body, replyToID)
}
// AddCommentCalls gets all the calls that were made to AddComment.
// Check the length with:
//
// len(mockedDiscussionClient.AddCommentCalls())
func (mock *DiscussionClientMock) AddCommentCalls() []struct {
Repo ghrepo.Interface
DiscussionID string
Body string
ReplyToID string
} {
var calls []struct {
Repo ghrepo.Interface
DiscussionID string
Body string
ReplyToID string
}
mock.lockAddComment.RLock()
calls = mock.calls.AddComment
mock.lockAddComment.RUnlock()
return calls
}
// Close calls CloseFunc.
func (mock *DiscussionClientMock) Close(repo ghrepo.Interface, id string, reason CloseReason) (*Discussion, error) {
if mock.CloseFunc == nil {
panic("DiscussionClientMock.CloseFunc: method is nil but DiscussionClient.Close was just called")
}
callInfo := struct {
Repo ghrepo.Interface
ID string
Reason CloseReason
}{
Repo: repo,
ID: id,
Reason: reason,
}
mock.lockClose.Lock()
mock.calls.Close = append(mock.calls.Close, callInfo)
mock.lockClose.Unlock()
return mock.CloseFunc(repo, id, reason)
}
// CloseCalls gets all the calls that were made to Close.
// Check the length with:
//
// len(mockedDiscussionClient.CloseCalls())
func (mock *DiscussionClientMock) CloseCalls() []struct {
Repo ghrepo.Interface
ID string
Reason CloseReason
} {
var calls []struct {
Repo ghrepo.Interface
ID string
Reason CloseReason
}
mock.lockClose.RLock()
calls = mock.calls.Close
mock.lockClose.RUnlock()
return calls
}
// Create calls CreateFunc.
func (mock *DiscussionClientMock) Create(repo ghrepo.Interface, input CreateDiscussionInput) (*Discussion, error) {
if mock.CreateFunc == nil {
panic("DiscussionClientMock.CreateFunc: method is nil but DiscussionClient.Create was just called")
}
callInfo := struct {
Repo ghrepo.Interface
Input CreateDiscussionInput
}{
Repo: repo,
Input: input,
}
mock.lockCreate.Lock()
mock.calls.Create = append(mock.calls.Create, callInfo)
mock.lockCreate.Unlock()
return mock.CreateFunc(repo, input)
}
// CreateCalls gets all the calls that were made to Create.
// Check the length with:
//
// len(mockedDiscussionClient.CreateCalls())
func (mock *DiscussionClientMock) CreateCalls() []struct {
Repo ghrepo.Interface
Input CreateDiscussionInput
} {
var calls []struct {
Repo ghrepo.Interface
Input CreateDiscussionInput
}
mock.lockCreate.RLock()
calls = mock.calls.Create
mock.lockCreate.RUnlock()
return calls
}
// GetByNumber calls GetByNumberFunc.
func (mock *DiscussionClientMock) GetByNumber(repo ghrepo.Interface, number int) (*Discussion, error) {
if mock.GetByNumberFunc == nil {
panic("DiscussionClientMock.GetByNumberFunc: method is nil but DiscussionClient.GetByNumber was just called")
}
callInfo := struct {
Repo ghrepo.Interface
Number int
}{
Repo: repo,
Number: number,
}
mock.lockGetByNumber.Lock()
mock.calls.GetByNumber = append(mock.calls.GetByNumber, callInfo)
mock.lockGetByNumber.Unlock()
return mock.GetByNumberFunc(repo, number)
}
// GetByNumberCalls gets all the calls that were made to GetByNumber.
// Check the length with:
//
// len(mockedDiscussionClient.GetByNumberCalls())
func (mock *DiscussionClientMock) GetByNumberCalls() []struct {
Repo ghrepo.Interface
Number int
} {
var calls []struct {
Repo ghrepo.Interface
Number int
}
mock.lockGetByNumber.RLock()
calls = mock.calls.GetByNumber
mock.lockGetByNumber.RUnlock()
return calls
}
// GetWithComments calls GetWithCommentsFunc.
func (mock *DiscussionClientMock) GetWithComments(repo ghrepo.Interface, number int, commentLimit int, order string) (*Discussion, error) {
if mock.GetWithCommentsFunc == nil {
panic("DiscussionClientMock.GetWithCommentsFunc: method is nil but DiscussionClient.GetWithComments was just called")
}
callInfo := struct {
Repo ghrepo.Interface
Number int
CommentLimit int
Order string
}{
Repo: repo,
Number: number,
CommentLimit: commentLimit,
Order: order,
}
mock.lockGetWithComments.Lock()
mock.calls.GetWithComments = append(mock.calls.GetWithComments, callInfo)
mock.lockGetWithComments.Unlock()
return mock.GetWithCommentsFunc(repo, number, commentLimit, order)
}
// GetWithCommentsCalls gets all the calls that were made to GetWithComments.
// Check the length with:
//
// len(mockedDiscussionClient.GetWithCommentsCalls())
func (mock *DiscussionClientMock) GetWithCommentsCalls() []struct {
Repo ghrepo.Interface
Number int
CommentLimit int
Order string
} {
var calls []struct {
Repo ghrepo.Interface
Number int
CommentLimit int
Order string
}
mock.lockGetWithComments.RLock()
calls = mock.calls.GetWithComments
mock.lockGetWithComments.RUnlock()
return calls
}
// List calls ListFunc.
func (mock *DiscussionClientMock) List(repo ghrepo.Interface, filters ListFilters, limit int) ([]Discussion, int, error) {
if mock.ListFunc == nil {
panic("DiscussionClientMock.ListFunc: method is nil but DiscussionClient.List was just called")
}
callInfo := struct {
Repo ghrepo.Interface
Filters ListFilters
Limit int
}{
Repo: repo,
Filters: filters,
Limit: limit,
}
mock.lockList.Lock()
mock.calls.List = append(mock.calls.List, callInfo)
mock.lockList.Unlock()
return mock.ListFunc(repo, filters, limit)
}
// ListCalls gets all the calls that were made to List.
// Check the length with:
//
// len(mockedDiscussionClient.ListCalls())
func (mock *DiscussionClientMock) ListCalls() []struct {
Repo ghrepo.Interface
Filters ListFilters
Limit int
} {
var calls []struct {
Repo ghrepo.Interface
Filters ListFilters
Limit int
}
mock.lockList.RLock()
calls = mock.calls.List
mock.lockList.RUnlock()
return calls
}
// ListCategories calls ListCategoriesFunc.
func (mock *DiscussionClientMock) ListCategories(repo ghrepo.Interface) ([]DiscussionCategory, error) {
if mock.ListCategoriesFunc == nil {
panic("DiscussionClientMock.ListCategoriesFunc: method is nil but DiscussionClient.ListCategories was just called")
}
callInfo := struct {
Repo ghrepo.Interface
}{
Repo: repo,
}
mock.lockListCategories.Lock()
mock.calls.ListCategories = append(mock.calls.ListCategories, callInfo)
mock.lockListCategories.Unlock()
return mock.ListCategoriesFunc(repo)
}
// ListCategoriesCalls gets all the calls that were made to ListCategories.
// Check the length with:
//
// len(mockedDiscussionClient.ListCategoriesCalls())
func (mock *DiscussionClientMock) ListCategoriesCalls() []struct {
Repo ghrepo.Interface
} {
var calls []struct {
Repo ghrepo.Interface
}
mock.lockListCategories.RLock()
calls = mock.calls.ListCategories
mock.lockListCategories.RUnlock()
return calls
}
// Lock calls LockFunc.
func (mock *DiscussionClientMock) Lock(repo ghrepo.Interface, id string, reason string) error {
if mock.LockFunc == nil {
panic("DiscussionClientMock.LockFunc: method is nil but DiscussionClient.Lock was just called")
}
callInfo := struct {
Repo ghrepo.Interface
ID string
Reason string
}{
Repo: repo,
ID: id,
Reason: reason,
}
mock.lockLock.Lock()
mock.calls.Lock = append(mock.calls.Lock, callInfo)
mock.lockLock.Unlock()
return mock.LockFunc(repo, id, reason)
}
// LockCalls gets all the calls that were made to Lock.
// Check the length with:
//
// len(mockedDiscussionClient.LockCalls())
func (mock *DiscussionClientMock) LockCalls() []struct {
Repo ghrepo.Interface
ID string
Reason string
} {
var calls []struct {
Repo ghrepo.Interface
ID string
Reason string
}
mock.lockLock.RLock()
calls = mock.calls.Lock
mock.lockLock.RUnlock()
return calls
}
// MarkAnswer calls MarkAnswerFunc.
func (mock *DiscussionClientMock) MarkAnswer(repo ghrepo.Interface, commentID string) error {
if mock.MarkAnswerFunc == nil {
panic("DiscussionClientMock.MarkAnswerFunc: method is nil but DiscussionClient.MarkAnswer was just called")
}
callInfo := struct {
Repo ghrepo.Interface
CommentID string
}{
Repo: repo,
CommentID: commentID,
}
mock.lockMarkAnswer.Lock()
mock.calls.MarkAnswer = append(mock.calls.MarkAnswer, callInfo)
mock.lockMarkAnswer.Unlock()
return mock.MarkAnswerFunc(repo, commentID)
}
// MarkAnswerCalls gets all the calls that were made to MarkAnswer.
// Check the length with:
//
// len(mockedDiscussionClient.MarkAnswerCalls())
func (mock *DiscussionClientMock) MarkAnswerCalls() []struct {
Repo ghrepo.Interface
CommentID string
} {
var calls []struct {
Repo ghrepo.Interface
CommentID string
}
mock.lockMarkAnswer.RLock()
calls = mock.calls.MarkAnswer
mock.lockMarkAnswer.RUnlock()
return calls
}
// Reopen calls ReopenFunc.
func (mock *DiscussionClientMock) Reopen(repo ghrepo.Interface, id string) (*Discussion, error) {
if mock.ReopenFunc == nil {
panic("DiscussionClientMock.ReopenFunc: method is nil but DiscussionClient.Reopen was just called")
}
callInfo := struct {
Repo ghrepo.Interface
ID string
}{
Repo: repo,
ID: id,
}
mock.lockReopen.Lock()
mock.calls.Reopen = append(mock.calls.Reopen, callInfo)
mock.lockReopen.Unlock()
return mock.ReopenFunc(repo, id)
}
// ReopenCalls gets all the calls that were made to Reopen.
// Check the length with:
//
// len(mockedDiscussionClient.ReopenCalls())
func (mock *DiscussionClientMock) ReopenCalls() []struct {
Repo ghrepo.Interface
ID string
} {
var calls []struct {
Repo ghrepo.Interface
ID string
}
mock.lockReopen.RLock()
calls = mock.calls.Reopen
mock.lockReopen.RUnlock()
return calls
}
// Search calls SearchFunc.
func (mock *DiscussionClientMock) Search(repo ghrepo.Interface, filters SearchFilters, limit int) ([]Discussion, int, error) {
if mock.SearchFunc == nil {
panic("DiscussionClientMock.SearchFunc: method is nil but DiscussionClient.Search was just called")
}
callInfo := struct {
Repo ghrepo.Interface
Filters SearchFilters
Limit int
}{
Repo: repo,
Filters: filters,
Limit: limit,
}
mock.lockSearch.Lock()
mock.calls.Search = append(mock.calls.Search, callInfo)
mock.lockSearch.Unlock()
return mock.SearchFunc(repo, filters, limit)
}
// SearchCalls gets all the calls that were made to Search.
// Check the length with:
//
// len(mockedDiscussionClient.SearchCalls())
func (mock *DiscussionClientMock) SearchCalls() []struct {
Repo ghrepo.Interface
Filters SearchFilters
Limit int
} {
var calls []struct {
Repo ghrepo.Interface
Filters SearchFilters
Limit int
}
mock.lockSearch.RLock()
calls = mock.calls.Search
mock.lockSearch.RUnlock()
return calls
}
// Unlock calls UnlockFunc.
func (mock *DiscussionClientMock) Unlock(repo ghrepo.Interface, id string) error {
if mock.UnlockFunc == nil {
panic("DiscussionClientMock.UnlockFunc: method is nil but DiscussionClient.Unlock was just called")
}
callInfo := struct {
Repo ghrepo.Interface
ID string
}{
Repo: repo,
ID: id,
}
mock.lockUnlock.Lock()
mock.calls.Unlock = append(mock.calls.Unlock, callInfo)
mock.lockUnlock.Unlock()
return mock.UnlockFunc(repo, id)
}
// UnlockCalls gets all the calls that were made to Unlock.
// Check the length with:
//
// len(mockedDiscussionClient.UnlockCalls())
func (mock *DiscussionClientMock) UnlockCalls() []struct {
Repo ghrepo.Interface
ID string
} {
var calls []struct {
Repo ghrepo.Interface
ID string
}
mock.lockUnlock.RLock()
calls = mock.calls.Unlock
mock.lockUnlock.RUnlock()
return calls
}
// UnmarkAnswer calls UnmarkAnswerFunc.
func (mock *DiscussionClientMock) UnmarkAnswer(repo ghrepo.Interface, commentID string) error {
if mock.UnmarkAnswerFunc == nil {
panic("DiscussionClientMock.UnmarkAnswerFunc: method is nil but DiscussionClient.UnmarkAnswer was just called")
}
callInfo := struct {
Repo ghrepo.Interface
CommentID string
}{
Repo: repo,
CommentID: commentID,
}
mock.lockUnmarkAnswer.Lock()
mock.calls.UnmarkAnswer = append(mock.calls.UnmarkAnswer, callInfo)
mock.lockUnmarkAnswer.Unlock()
return mock.UnmarkAnswerFunc(repo, commentID)
}
// UnmarkAnswerCalls gets all the calls that were made to UnmarkAnswer.
// Check the length with:
//
// len(mockedDiscussionClient.UnmarkAnswerCalls())
func (mock *DiscussionClientMock) UnmarkAnswerCalls() []struct {
Repo ghrepo.Interface
CommentID string
} {
var calls []struct {
Repo ghrepo.Interface
CommentID string
}
mock.lockUnmarkAnswer.RLock()
calls = mock.calls.UnmarkAnswer
mock.lockUnmarkAnswer.RUnlock()
return calls
}
// Update calls UpdateFunc.
func (mock *DiscussionClientMock) Update(repo ghrepo.Interface, input UpdateDiscussionInput) (*Discussion, error) {
if mock.UpdateFunc == nil {
panic("DiscussionClientMock.UpdateFunc: method is nil but DiscussionClient.Update was just called")
}
callInfo := struct {
Repo ghrepo.Interface
Input UpdateDiscussionInput
}{
Repo: repo,
Input: input,
}
mock.lockUpdate.Lock()
mock.calls.Update = append(mock.calls.Update, callInfo)
mock.lockUpdate.Unlock()
return mock.UpdateFunc(repo, input)
}
// UpdateCalls gets all the calls that were made to Update.
// Check the length with:
//
// len(mockedDiscussionClient.UpdateCalls())
func (mock *DiscussionClientMock) UpdateCalls() []struct {
Repo ghrepo.Interface
Input UpdateDiscussionInput
} {
var calls []struct {
Repo ghrepo.Interface
Input UpdateDiscussionInput
}
mock.lockUpdate.RLock()
calls = mock.calls.Update
mock.lockUpdate.RUnlock()
return calls
}

View file

@ -0,0 +1,265 @@
package client
import "time"
// Discussion represents a GitHub Discussion as a domain object.
// Fields carry no JSON tags; serialization is handled by ExportData.
type Discussion struct {
ID string
Number int
Title string
Body string
URL string
State string
StateReason string
Author DiscussionAuthor
Category DiscussionCategory
Labels []DiscussionLabel
Answered bool
AnswerChosenAt time.Time
AnswerChosenBy *DiscussionAuthor
Comments DiscussionCommentList
ReactionGroups []ReactionGroup
CreatedAt time.Time
UpdatedAt time.Time
ClosedAt time.Time
Locked bool
}
// ExportData returns a map of the requested fields for JSON output.
// Because domain types carry no JSON struct tags, each field is mapped
// explicitly rather than using reflection.
func (d Discussion) ExportData(fields []string) map[string]interface{} {
data := map[string]interface{}{}
for _, f := range fields {
switch f {
case "id":
data[f] = d.ID
case "number":
data[f] = d.Number
case "title":
data[f] = d.Title
case "body":
data[f] = d.Body
case "url":
data[f] = d.URL
case "state":
data[f] = d.State
case "stateReason":
data[f] = d.StateReason
case "author":
data[f] = d.Author.Export()
case "category":
data[f] = d.Category.Export()
case "labels":
labels := make([]interface{}, len(d.Labels))
for i, l := range d.Labels {
labels[i] = l.Export()
}
data[f] = labels
case "answered":
data[f] = d.Answered
case "answerChosenAt":
if d.AnswerChosenAt.IsZero() {
data[f] = nil
} else {
data[f] = d.AnswerChosenAt
}
case "answerChosenBy":
if d.AnswerChosenBy == nil {
data[f] = nil
} else {
data[f] = d.AnswerChosenBy.Export()
}
case "comments":
comments := make([]interface{}, len(d.Comments.Comments))
for i, c := range d.Comments.Comments {
comments[i] = c.Export()
}
data[f] = map[string]interface{}{
"totalCount": d.Comments.TotalCount,
"nodes": comments,
}
case "reactionGroups":
groups := make([]interface{}, len(d.ReactionGroups))
for i, rg := range d.ReactionGroups {
groups[i] = rg.Export()
}
data[f] = groups
case "createdAt":
data[f] = d.CreatedAt
case "updatedAt":
data[f] = d.UpdatedAt
case "closedAt":
if d.ClosedAt.IsZero() {
data[f] = nil
} else {
data[f] = d.ClosedAt
}
case "locked":
data[f] = d.Locked
}
}
return data
}
// DiscussionAuthor represents the author of a discussion or comment.
type DiscussionAuthor struct {
ID string
Login string
Name string
}
// Export returns the author as a map for JSON output.
func (a DiscussionAuthor) Export() map[string]interface{} {
return map[string]interface{}{
"id": a.ID,
"login": a.Login,
"name": a.Name,
}
}
// DiscussionCategory represents a discussion category within a repository.
type DiscussionCategory struct {
ID string
Name string
Slug string
Emoji string
IsAnswerable bool
}
// Export returns the category as a map for JSON output.
func (c DiscussionCategory) Export() map[string]interface{} {
return map[string]interface{}{
"id": c.ID,
"name": c.Name,
"slug": c.Slug,
"emoji": c.Emoji,
"isAnswerable": c.IsAnswerable,
}
}
// DiscussionLabel represents a label applied to a discussion.
type DiscussionLabel struct {
ID string
Name string
Color string
}
// Export returns the label as a map for JSON output.
func (l DiscussionLabel) Export() map[string]interface{} {
return map[string]interface{}{
"id": l.ID,
"name": l.Name,
"color": l.Color,
}
}
// DiscussionComment represents a comment or reply on a discussion.
type DiscussionComment struct {
ID string
URL string
Author DiscussionAuthor
Body string
CreatedAt time.Time
IsAnswer bool
UpvoteCount int
ReactionGroups []ReactionGroup
Replies []DiscussionComment
TotalReplies int
}
// Export returns the comment as a map for JSON output.
func (c DiscussionComment) Export() map[string]interface{} {
replies := make([]interface{}, len(c.Replies))
for i, r := range c.Replies {
replies[i] = r.Export()
}
reactions := make([]interface{}, len(c.ReactionGroups))
for i, rg := range c.ReactionGroups {
reactions[i] = rg.Export()
}
return map[string]interface{}{
"id": c.ID,
"url": c.URL,
"author": c.Author.Export(),
"body": c.Body,
"createdAt": c.CreatedAt,
"isAnswer": c.IsAnswer,
"upvoteCount": c.UpvoteCount,
"reactionGroups": reactions,
"replies": replies,
"totalReplies": c.TotalReplies,
}
}
// DiscussionCommentList represents a paginated list of comments on a discussion.
type DiscussionCommentList struct {
Comments []DiscussionComment
TotalCount int
}
// ReactionGroup represents a set of reactions of the same type.
type ReactionGroup struct {
Content string
TotalCount int
}
// Export returns the reaction group as a map for JSON output.
func (rg ReactionGroup) Export() map[string]interface{} {
return map[string]interface{}{
"content": rg.Content,
"totalCount": rg.TotalCount,
}
}
// CloseReason represents the reason for closing a discussion.
type CloseReason string
const (
// CloseReasonResolved indicates the discussion topic has been resolved.
CloseReasonResolved CloseReason = "RESOLVED"
// CloseReasonOutdated indicates the discussion is no longer relevant.
CloseReasonOutdated CloseReason = "OUTDATED"
// CloseReasonDuplicate indicates the discussion is a duplicate of another.
CloseReasonDuplicate CloseReason = "DUPLICATE"
)
// ListFilters holds parameters for the repository.discussions query.
// CategoryID must be resolved by the caller before passing to List.
type ListFilters struct {
State string
CategoryID string
Answered *bool
OrderBy string
Direction string
}
// SearchFilters holds parameters for the search query used when
// author or label filtering is required.
type SearchFilters struct {
Author string
Labels []string
State string
Category string
Answered *bool
OrderBy string
Direction string
}
// CreateDiscussionInput holds the parameters for creating a discussion.
type CreateDiscussionInput struct {
RepositoryID string
CategoryID string
Title string
Body string
}
// UpdateDiscussionInput holds optional parameters for updating a discussion.
// Nil pointer fields are left unchanged.
type UpdateDiscussionInput struct {
DiscussionID string
Title *string
Body *string
CategoryID *string
}

View file

@ -0,0 +1,33 @@
package discussion
import (
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/spf13/cobra"
)
// NewCmdDiscussion returns the top-level "discussion" command.
func NewCmdDiscussion(f *cmdutil.Factory) *cobra.Command {
cmd := &cobra.Command{
Use: "discussion <command>",
Short: "Manage discussions",
Long: "Work with GitHub Discussions.",
Example: heredoc.Doc(`
$ gh discussion list
$ gh discussion create --category "General" --title "Hello"
$ gh discussion view 123
`),
Annotations: map[string]string{
"help:arguments": heredoc.Doc(`
A discussion can be supplied as argument in any of the following formats:
- by number, e.g. "123"; or
- by URL, e.g. "https://github.com/OWNER/REPO/discussions/123".
`),
},
GroupID: "core",
}
cmdutil.EnableRepoOverride(cmd, f)
return cmd
}

View file

@ -0,0 +1,21 @@
// Package shared provides factory functions, field definitions, and display
// helpers used across discussion subcommands.
package shared
import (
"github.com/cli/cli/v2/pkg/cmd/discussion/client"
"github.com/cli/cli/v2/pkg/cmdutil"
)
// DiscussionClientFunc returns a factory function that creates a DiscussionClient
// from the given Factory. The returned function is intended to be stored in
// command Options structs and called lazily inside RunE.
func DiscussionClientFunc(f *cmdutil.Factory) func() (client.DiscussionClient, error) {
return func() (client.DiscussionClient, error) {
httpClient, err := f.HttpClient()
if err != nil {
return nil, err
}
return client.NewDiscussionClient(httpClient), nil
}
}

View file

@ -0,0 +1,25 @@
package shared
// DiscussionFields lists the field names available for --json output on
// discussion commands.
var DiscussionFields = []string{
"id",
"number",
"title",
"body",
"url",
"state",
"stateReason",
"author",
"category",
"labels",
"answered",
"answerChosenAt",
"answerChosenBy",
"comments",
"reactionGroups",
"createdAt",
"updatedAt",
"closedAt",
"locked",
}

View file

@ -20,6 +20,7 @@ import (
completionCmd "github.com/cli/cli/v2/pkg/cmd/completion"
configCmd "github.com/cli/cli/v2/pkg/cmd/config"
copilotCmd "github.com/cli/cli/v2/pkg/cmd/copilot"
discussionCmd "github.com/cli/cli/v2/pkg/cmd/discussion"
extensionCmd "github.com/cli/cli/v2/pkg/cmd/extension"
"github.com/cli/cli/v2/pkg/cmd/factory"
gistCmd "github.com/cli/cli/v2/pkg/cmd/gist"
@ -157,6 +158,7 @@ func NewCmdRoot(f *cmdutil.Factory, version, buildDate string) (*cobra.Command,
cmd.AddCommand(agentTaskCmd.NewCmdAgentTask(&repoResolvingCmdFactory))
cmd.AddCommand(browseCmd.NewCmdBrowse(&repoResolvingCmdFactory, nil))
cmd.AddCommand(discussionCmd.NewCmdDiscussion(&repoResolvingCmdFactory))
cmd.AddCommand(prCmd.NewCmdPR(&repoResolvingCmdFactory))
cmd.AddCommand(orgCmd.NewCmdOrg(&repoResolvingCmdFactory))
cmd.AddCommand(issueCmd.NewCmdIssue(&repoResolvingCmdFactory))