From f9fec823ad2f18771b9003259d5ed649e87f33a4 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Wed, 4 Mar 2026 16:01:52 -0700 Subject: [PATCH] Add godoc comments to exported symbols in internal/featuredetection and internal/prompter Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- internal/featuredetection/detector_mock.go | 23 ++++++++++++++++++ .../featuredetection/feature_detection.go | 19 ++++++++++++++- internal/prompter/prompter.go | 24 +++++++++++++++++++ internal/prompter/test.go | 17 +++++++++++++ 4 files changed, 82 insertions(+), 1 deletion(-) diff --git a/internal/featuredetection/detector_mock.go b/internal/featuredetection/detector_mock.go index 552f197d9..75589c79c 100644 --- a/internal/featuredetection/detector_mock.go +++ b/internal/featuredetection/detector_mock.go @@ -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, diff --git a/internal/featuredetection/feature_detection.go b/internal/featuredetection/feature_detection.go index e40c134bc..d2bf30b40 100644 --- a/internal/featuredetection/feature_detection.go +++ b/internal/featuredetection/feature_detection.go @@ -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). diff --git a/internal/prompter/prompter.go b/internal/prompter/prompter.go index 2bf49eb58..ae98d4058 100644 --- a/internal/prompter/prompter.go +++ b/internal/prompter/prompter.go @@ -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{ diff --git a/internal/prompter/test.go b/internal/prompter/test.go index 599fd3893..09928c09c 100644 --- a/internal/prompter/test.go +++ b/internal/prompter/test.go @@ -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, ", ")) }