Add godoc comments to exported symbols in internal/featuredetection and internal/prompter

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Kynan Ware 2026-03-04 16:01:52 -07:00
parent 7367596c83
commit f9fec823ad
4 changed files with 82 additions and 1 deletions

View file

@ -2,99 +2,122 @@ package featuredetection
import "github.com/cli/cli/v2/internal/gh"
// DisabledDetectorMock is a mock Detector that returns zero-value features for all queries.
type DisabledDetectorMock struct{}
// IssueFeatures returns empty issue features.
func (md *DisabledDetectorMock) IssueFeatures() (IssueFeatures, error) {
return IssueFeatures{}, nil
}
// PullRequestFeatures returns empty pull request features.
func (md *DisabledDetectorMock) PullRequestFeatures() (PullRequestFeatures, error) {
return PullRequestFeatures{}, nil
}
// RepositoryFeatures returns empty repository features.
func (md *DisabledDetectorMock) RepositoryFeatures() (RepositoryFeatures, error) {
return RepositoryFeatures{}, nil
}
// ProjectsV1 returns unsupported for ProjectsV1.
func (md *DisabledDetectorMock) ProjectsV1() gh.ProjectsV1Support {
return gh.ProjectsV1Unsupported
}
// ProjectFeatures returns empty project features.
func (md *DisabledDetectorMock) ProjectFeatures() (ProjectFeatures, error) {
return ProjectFeatures{}, nil
}
// SearchFeatures returns search features with advanced issue search disabled.
func (md *DisabledDetectorMock) SearchFeatures() (SearchFeatures, error) {
return advancedIssueSearchNotSupported, nil
}
// ReleaseFeatures returns empty release features.
func (md *DisabledDetectorMock) ReleaseFeatures() (ReleaseFeatures, error) {
return ReleaseFeatures{}, nil
}
// ActionsFeatures returns empty actions features.
func (md *DisabledDetectorMock) ActionsFeatures() (ActionsFeatures, error) {
return ActionsFeatures{}, nil
}
// EnabledDetectorMock is a mock Detector that returns all features as enabled.
type EnabledDetectorMock struct{}
// IssueFeatures returns all issue features enabled.
func (md *EnabledDetectorMock) IssueFeatures() (IssueFeatures, error) {
return allIssueFeatures, nil
}
// PullRequestFeatures returns all pull request features enabled.
func (md *EnabledDetectorMock) PullRequestFeatures() (PullRequestFeatures, error) {
return allPullRequestFeatures, nil
}
// RepositoryFeatures returns all repository features enabled.
func (md *EnabledDetectorMock) RepositoryFeatures() (RepositoryFeatures, error) {
return allRepositoryFeatures, nil
}
// ProjectsV1 returns supported for ProjectsV1.
func (md *EnabledDetectorMock) ProjectsV1() gh.ProjectsV1Support {
return gh.ProjectsV1Supported
}
// ProjectFeatures returns all project features enabled.
func (md *EnabledDetectorMock) ProjectFeatures() (ProjectFeatures, error) {
return allProjectFeatures, nil
}
// SearchFeatures returns search features with advanced issue search disabled.
func (md *EnabledDetectorMock) SearchFeatures() (SearchFeatures, error) {
return advancedIssueSearchNotSupported, nil
}
// ReleaseFeatures returns release features with immutable releases enabled.
func (md *EnabledDetectorMock) ReleaseFeatures() (ReleaseFeatures, error) {
return ReleaseFeatures{
ImmutableReleases: true,
}, nil
}
// ActionsFeatures returns actions features with dispatch run details enabled.
func (md *EnabledDetectorMock) ActionsFeatures() (ActionsFeatures, error) {
return ActionsFeatures{
DispatchRunDetails: true,
}, nil
}
// AdvancedIssueSearchDetectorMock is a mock Detector with configurable search features.
type AdvancedIssueSearchDetectorMock struct {
EnabledDetectorMock
searchFeatures SearchFeatures
}
// SearchFeatures returns the configured search features.
func (md *AdvancedIssueSearchDetectorMock) SearchFeatures() (SearchFeatures, error) {
return md.searchFeatures, nil
}
// AdvancedIssueSearchUnsupported returns a mock detector where advanced issue search is not supported.
func AdvancedIssueSearchUnsupported() *AdvancedIssueSearchDetectorMock {
return &AdvancedIssueSearchDetectorMock{
searchFeatures: advancedIssueSearchNotSupported,
}
}
// AdvancedIssueSearchSupportedAsOptIn returns a mock detector where advanced issue search is opt-in.
func AdvancedIssueSearchSupportedAsOptIn() *AdvancedIssueSearchDetectorMock {
return &AdvancedIssueSearchDetectorMock{
searchFeatures: advancedIssueSearchSupportedAsOptIn,
}
}
// AdvancedIssueSearchSupportedAsOnlyBackend returns a mock detector where advanced issue search is the only backend.
func AdvancedIssueSearchSupportedAsOnlyBackend() *AdvancedIssueSearchDetectorMock {
return &AdvancedIssueSearchDetectorMock{
searchFeatures: advancedIssueSearchSupportedAsOnlyBackend,

View file

@ -1,4 +1,4 @@
package featuredetection
package featuredetection
import (
"net/http"
@ -11,6 +11,7 @@ import (
ghauth "github.com/cli/go-gh/v2/pkg/auth"
)
// Detector queries a GitHub host to determine which API features are available.
type Detector interface {
IssueFeatures() (IssueFeatures, error)
PullRequestFeatures() (PullRequestFeatures, error)
@ -22,6 +23,7 @@ type Detector interface {
ActionsFeatures() (ActionsFeatures, error)
}
// IssueFeatures describes the issue-related capabilities of a GitHub host.
type IssueFeatures struct {
StateReason bool
StateReasonDuplicate bool
@ -34,6 +36,7 @@ var allIssueFeatures = IssueFeatures{
ActorIsAssignable: true,
}
// PullRequestFeatures describes the pull-request-related capabilities of a GitHub host.
type PullRequestFeatures struct {
MergeQueue bool
// CheckRunAndStatusContextCounts indicates whether the API supports
@ -49,6 +52,7 @@ var allPullRequestFeatures = PullRequestFeatures{
CheckRunEvent: true,
}
// RepositoryFeatures describes the repository-related capabilities of a GitHub host.
type RepositoryFeatures struct {
PullRequestTemplateQuery bool
VisibilityField bool
@ -61,6 +65,7 @@ var allRepositoryFeatures = RepositoryFeatures{
AutoMerge: true,
}
// ProjectFeatures describes the project-related capabilities of a GitHub host.
type ProjectFeatures struct {
// ProjectItemQuery indicates support for the `query` argument on
// ProjectV2.items (supported on github.com and GHES 3.20+).
@ -71,6 +76,7 @@ var allProjectFeatures = ProjectFeatures{
ProjectItemQuery: true,
}
// SearchFeatures describes the search-related capabilities of a GitHub host.
type SearchFeatures struct {
// AdvancedIssueSearch indicates whether the host supports advanced issue
// search via API calls.
@ -108,10 +114,12 @@ var advancedIssueSearchSupportedAsOnlyBackend = SearchFeatures{
AdvancedIssueSearchAPIOptIn: false,
}
// ReleaseFeatures describes the release-related capabilities of a GitHub host.
type ReleaseFeatures struct {
ImmutableReleases bool
}
// ActionsFeatures describes the GitHub Actions capabilities of a GitHub host.
type ActionsFeatures struct {
// DispatchRunDetails indicates whether the API supports the `return_run_details`
// field in workflow dispatches that, when set to true, will return the details
@ -127,6 +135,7 @@ type detector struct {
httpClient *http.Client
}
// NewDetector creates a Detector that queries the given host using the provided HTTP client.
func NewDetector(httpClient *http.Client, host string) Detector {
return &detector{
httpClient: httpClient,
@ -134,6 +143,7 @@ func NewDetector(httpClient *http.Client, host string) Detector {
}
}
// IssueFeatures detects issue-related capabilities of the configured host.
func (d *detector) IssueFeatures() (IssueFeatures, error) {
if !ghauth.IsEnterprise(d.host) {
return allIssueFeatures, nil
@ -182,6 +192,7 @@ func (d *detector) IssueFeatures() (IssueFeatures, error) {
return features, nil
}
// PullRequestFeatures detects pull-request-related capabilities of the configured host.
func (d *detector) PullRequestFeatures() (PullRequestFeatures, error) {
// TODO: reinstate the short-circuit once the APIs are fully available on github.com
// https://github.com/cli/cli/issues/5778
@ -251,6 +262,7 @@ func (d *detector) PullRequestFeatures() (PullRequestFeatures, error) {
return features, nil
}
// RepositoryFeatures detects repository-related capabilities of the configured host.
func (d *detector) RepositoryFeatures() (RepositoryFeatures, error) {
if !ghauth.IsEnterprise(d.host) {
return allRepositoryFeatures, nil
@ -292,6 +304,7 @@ const (
enterpriseProjectsV1Removed = "3.17.0"
)
// ProjectsV1 determines whether the configured host supports classic projects (v1).
func (d *detector) ProjectsV1() gh.ProjectsV1Support {
if !ghauth.IsEnterprise(d.host) {
return gh.ProjectsV1Unsupported
@ -307,6 +320,7 @@ func (d *detector) ProjectsV1() gh.ProjectsV1Support {
return gh.ProjectsV1Unsupported
}
// ProjectFeatures detects project-related capabilities of the configured host.
func (d *detector) ProjectFeatures() (ProjectFeatures, error) {
if !ghauth.IsEnterprise(d.host) {
return allProjectFeatures, nil
@ -356,6 +370,7 @@ const (
enterpriseAdvancedIssueSearchSupport = "3.18.0"
)
// SearchFeatures detects search-related capabilities of the configured host.
func (d *detector) SearchFeatures() (SearchFeatures, error) {
// TODO advancedIssueSearchCleanup
// Once GHES 3.17 support ends, we don't need this and, probably, the entire search feature detection.
@ -441,6 +456,7 @@ func (d *detector) SearchFeatures() (SearchFeatures, error) {
return feature, nil
}
// ReleaseFeatures detects release-related capabilities of the configured host.
func (d *detector) ReleaseFeatures() (ReleaseFeatures, error) {
// TODO: immutableReleaseFullSupport
// Once all supported GHES versions fully support immutable releases, we can
@ -475,6 +491,7 @@ const (
enterpriseWorkflowDispatchRunDetailsSupport = "3.21.0"
)
// ActionsFeatures detects GitHub Actions capabilities of the configured host.
func (d *detector) ActionsFeatures() (ActionsFeatures, error) {
// TODO workflowDispatchRunDetailsCleanup
// Once GHES 3.20 support ends, we don't need feature detection for workflow dispatch (i.e. run details support).

View file

@ -14,6 +14,8 @@ import (
)
//go:generate moq -rm -out prompter_mock.go . Prompter
// Prompter defines an interface for interactive user prompts.
type Prompter interface {
// generic prompts from go-gh
@ -52,6 +54,7 @@ type Prompter interface {
MarkdownEditor(prompt string, defaultValue string, blankAllowed bool) (string, error)
}
// New creates a Prompter backed by the given editor command and IO streams.
func New(editorCmd string, io *iostreams.IOStreams) Prompter {
if io.AccessiblePrompterEnabled() {
return &accessiblePrompter{
@ -104,6 +107,7 @@ func (p *accessiblePrompter) addDefaultsToPrompt(prompt string, defaultValues []
return prompt
}
// Select prompts the user to select an option from a list using accessible forms.
func (p *accessiblePrompter) Select(prompt, defaultValue string, options []string) (int, error) {
var result int
@ -136,6 +140,7 @@ func (p *accessiblePrompter) Select(prompt, defaultValue string, options []strin
return result, err
}
// MultiSelect prompts the user to select multiple options using accessible forms.
func (p *accessiblePrompter) MultiSelect(prompt string, defaults []string, options []string) ([]int, error) {
var result []int
@ -174,6 +179,7 @@ func (p *accessiblePrompter) MultiSelect(prompt string, defaults []string, optio
return result, nil
}
// Input prompts the user to enter a string value using accessible forms.
func (p *accessiblePrompter) Input(prompt, defaultValue string) (string, error) {
result := defaultValue
prompt = p.addDefaultsToPrompt(prompt, []string{defaultValue})
@ -189,6 +195,7 @@ func (p *accessiblePrompter) Input(prompt, defaultValue string) (string, error)
return result, err
}
// Password prompts the user to enter a password using accessible forms.
func (p *accessiblePrompter) Password(prompt string) (string, error) {
var result string
// EchoModePassword is not used as password masking is unsupported in huh.
@ -210,6 +217,7 @@ func (p *accessiblePrompter) Password(prompt string) (string, error) {
return result, nil
}
// Confirm prompts the user to confirm an action using accessible forms.
func (p *accessiblePrompter) Confirm(prompt string, defaultValue bool) (bool, error) {
result := defaultValue
@ -233,6 +241,7 @@ func (p *accessiblePrompter) Confirm(prompt string, defaultValue bool) (bool, er
return result, nil
}
// AuthToken prompts the user to paste an authentication token using accessible forms.
func (p *accessiblePrompter) AuthToken() (string, error) {
var result string
// EchoModeNone and EchoModePassword both result in disabling echo mode
@ -257,6 +266,7 @@ func (p *accessiblePrompter) AuthToken() (string, error) {
return result, err
}
// ConfirmDeletion prompts the user to type a value to confirm deletion using accessible forms.
func (p *accessiblePrompter) ConfirmDeletion(requiredValue string) error {
form := p.newForm(
huh.NewGroup(
@ -274,6 +284,7 @@ func (p *accessiblePrompter) ConfirmDeletion(requiredValue string) error {
return form.Run()
}
// InputHostname prompts the user to enter a hostname using accessible forms.
func (p *accessiblePrompter) InputHostname() (string, error) {
var result string
form := p.newForm(
@ -292,6 +303,7 @@ func (p *accessiblePrompter) InputHostname() (string, error) {
return result, nil
}
// MarkdownEditor prompts the user to edit markdown in an external editor using accessible forms.
func (p *accessiblePrompter) MarkdownEditor(prompt, defaultValue string, blankAllowed bool) (string, error) {
var result string
skipOption := "skip"
@ -329,6 +341,7 @@ func (p *accessiblePrompter) MarkdownEditor(prompt, defaultValue string, blankAl
return text, nil
}
// MultiSelectWithSearch prompts the user to select multiple options with search using accessible forms.
func (p *accessiblePrompter) MultiSelectWithSearch(prompt, searchPrompt string, defaultValues, persistentValues []string, searchFunc func(string) MultiSelectSearchResult) ([]string, error) {
return multiSelectWithSearch(p, prompt, searchPrompt, defaultValues, persistentValues, searchFunc)
}
@ -341,18 +354,22 @@ type surveyPrompter struct {
editorCmd string
}
// Select prompts the user to select an option from a list using survey.
func (p *surveyPrompter) Select(prompt, defaultValue string, options []string) (int, error) {
return p.prompter.Select(prompt, defaultValue, options)
}
// MultiSelect prompts the user to select multiple options using survey.
func (p *surveyPrompter) MultiSelect(prompt string, defaultValues, options []string) ([]int, error) {
return p.prompter.MultiSelect(prompt, defaultValues, options)
}
// MultiSelectWithSearch prompts the user to select multiple options with search using survey.
func (p *surveyPrompter) MultiSelectWithSearch(prompt string, searchPrompt string, defaultValues, persistentValues []string, searchFunc func(string) MultiSelectSearchResult) ([]string, error) {
return multiSelectWithSearch(p, prompt, searchPrompt, defaultValues, persistentValues, searchFunc)
}
// MultiSelectSearchResult holds the results of a search within MultiSelectWithSearch.
type MultiSelectSearchResult struct {
Keys []string
Labels []string
@ -503,18 +520,22 @@ func multiSelectWithSearch(p Prompter, prompt, searchPrompt string, defaultValue
}
}
// Input prompts the user to enter a string value using survey.
func (p *surveyPrompter) Input(prompt, defaultValue string) (string, error) {
return p.prompter.Input(prompt, defaultValue)
}
// Password prompts the user to enter a password using survey.
func (p *surveyPrompter) Password(prompt string) (string, error) {
return p.prompter.Password(prompt)
}
// Confirm prompts the user to confirm an action using survey.
func (p *surveyPrompter) Confirm(prompt string, defaultValue bool) (bool, error) {
return p.prompter.Confirm(prompt, defaultValue)
}
// AuthToken prompts the user to paste an authentication token using survey.
func (p *surveyPrompter) AuthToken() (string, error) {
var result string
err := p.ask(&survey.Password{
@ -523,6 +544,7 @@ func (p *surveyPrompter) AuthToken() (string, error) {
return result, err
}
// ConfirmDeletion prompts the user to type a value to confirm deletion using survey.
func (p *surveyPrompter) ConfirmDeletion(requiredValue string) error {
var result string
return p.ask(
@ -539,6 +561,7 @@ func (p *surveyPrompter) ConfirmDeletion(requiredValue string) error {
}))
}
// InputHostname prompts the user to enter a hostname using survey.
func (p *surveyPrompter) InputHostname() (string, error) {
var result string
err := p.ask(
@ -550,6 +573,7 @@ func (p *surveyPrompter) InputHostname() (string, error) {
return result, err
}
// MarkdownEditor prompts the user to edit markdown in an external editor using survey.
func (p *surveyPrompter) MarkdownEditor(prompt, defaultValue string, blankAllowed bool) (string, error) {
var result string
err := p.ask(&surveyext.GhEditor{

View file

@ -9,6 +9,7 @@ import (
"github.com/stretchr/testify/assert"
)
// NewMockPrompter creates a MockPrompter for use in tests.
func NewMockPrompter(t *testing.T) *MockPrompter {
m := &MockPrompter{
t: t,
@ -22,6 +23,7 @@ func NewMockPrompter(t *testing.T) *MockPrompter {
return m
}
// MockPrompter is a test double for Prompter that uses registered stubs for responses.
type MockPrompter struct {
t *testing.T
ghPrompter.PrompterMock
@ -54,6 +56,7 @@ type multiSelectWithSearchStub struct {
fn func(string, string, []string, []string, func(string) MultiSelectSearchResult) ([]string, error)
}
// AuthToken returns a stubbed authentication token response.
func (m *MockPrompter) AuthToken() (string, error) {
var s authTokenStub
if len(m.authTokenStubs) == 0 {
@ -64,6 +67,7 @@ func (m *MockPrompter) AuthToken() (string, error) {
return s.fn()
}
// ConfirmDeletion returns a stubbed confirm-deletion response.
func (m *MockPrompter) ConfirmDeletion(prompt string) error {
var s confirmDeletionStub
if len(m.confirmDeletionStubs) == 0 {
@ -74,6 +78,7 @@ func (m *MockPrompter) ConfirmDeletion(prompt string) error {
return s.fn(prompt)
}
// InputHostname returns a stubbed hostname input response.
func (m *MockPrompter) InputHostname() (string, error) {
var s inputHostnameStub
if len(m.inputHostnameStubs) == 0 {
@ -84,6 +89,7 @@ func (m *MockPrompter) InputHostname() (string, error) {
return s.fn()
}
// MarkdownEditor returns a stubbed markdown editor response.
func (m *MockPrompter) MarkdownEditor(prompt, defaultValue string, blankAllowed bool) (string, error) {
var s markdownEditorStub
if len(m.markdownEditorStubs) == 0 {
@ -97,6 +103,7 @@ func (m *MockPrompter) MarkdownEditor(prompt, defaultValue string, blankAllowed
return s.fn(prompt, defaultValue, blankAllowed)
}
// MultiSelectWithSearch returns a stubbed multi-select with search response.
func (m *MockPrompter) MultiSelectWithSearch(prompt, searchPrompt string, defaults []string, persistentOptions []string, searchFunc func(string) MultiSelectSearchResult) ([]string, error) {
var s multiSelectWithSearchStub
if len(m.multiSelectWithSearchStubs) == 0 {
@ -107,22 +114,27 @@ func (m *MockPrompter) MultiSelectWithSearch(prompt, searchPrompt string, defaul
return s.fn(prompt, searchPrompt, defaults, persistentOptions, searchFunc)
}
// RegisterAuthToken registers a stub function for the AuthToken prompt.
func (m *MockPrompter) RegisterAuthToken(stub func() (string, error)) {
m.authTokenStubs = append(m.authTokenStubs, authTokenStub{fn: stub})
}
// RegisterConfirmDeletion registers a stub function for the ConfirmDeletion prompt.
func (m *MockPrompter) RegisterConfirmDeletion(prompt string, stub func(string) error) {
m.confirmDeletionStubs = append(m.confirmDeletionStubs, confirmDeletionStub{prompt: prompt, fn: stub})
}
// RegisterInputHostname registers a stub function for the InputHostname prompt.
func (m *MockPrompter) RegisterInputHostname(stub func() (string, error)) {
m.inputHostnameStubs = append(m.inputHostnameStubs, inputHostnameStub{fn: stub})
}
// RegisterMarkdownEditor registers a stub function for the MarkdownEditor prompt.
func (m *MockPrompter) RegisterMarkdownEditor(prompt string, stub func(string, string, bool) (string, error)) {
m.markdownEditorStubs = append(m.markdownEditorStubs, markdownEditorStub{prompt: prompt, fn: stub})
}
// Verify asserts that all registered stubs have been consumed.
func (m *MockPrompter) Verify() {
errs := []string{}
if len(m.authTokenStubs) > 0 {
@ -143,10 +155,12 @@ func (m *MockPrompter) Verify() {
}
}
// AssertOptions asserts that the expected and actual option slices are equal.
func AssertOptions(t *testing.T, expected, actual []string) {
assert.Equal(t, expected, actual)
}
// IndexFor returns the index of the given answer in the options slice.
func IndexFor(options []string, answer string) (int, error) {
for ix, a := range options {
if a == answer {
@ -156,6 +170,7 @@ func IndexFor(options []string, answer string) (int, error) {
return -1, NoSuchAnswerErr(answer, options)
}
// IndexesFor returns the indices of the given answers in the options slice.
func IndexesFor(options []string, answers ...string) ([]int, error) {
indexes := make([]int, len(answers))
for i, answer := range answers {
@ -168,10 +183,12 @@ func IndexesFor(options []string, answers ...string) ([]int, error) {
return indexes, nil
}
// NoSuchPromptErr returns an error indicating that no stub was registered for the given prompt.
func NoSuchPromptErr(prompt string) error {
return fmt.Errorf("no such prompt '%s'", prompt)
}
// NoSuchAnswerErr returns an error indicating that the answer was not found in the options.
func NoSuchAnswerErr(answer string, options []string) error {
return fmt.Errorf("no such answer '%s' in [%s]", answer, strings.Join(options, ", "))
}