diff --git a/pkg/cmd/pr/checkout/checkout.go b/pkg/cmd/pr/checkout/checkout.go index a137f92f5..c3e0dc1f6 100644 --- a/pkg/cmd/pr/checkout/checkout.go +++ b/pkg/cmd/pr/checkout/checkout.go @@ -19,6 +19,7 @@ import ( "github.com/spf13/cobra" ) +// CheckoutOptions holds the configuration for the pr checkout command. type CheckoutOptions struct { HttpClient func() (*http.Client, error) GitClient *git.Client @@ -35,6 +36,7 @@ type CheckoutOptions struct { BranchName string } +// NewCmdCheckout creates the cobra command for checking out a pull request locally. func NewCmdCheckout(f *cmdutil.Factory, runF func(*CheckoutOptions) error) *cobra.Command { opts := &CheckoutOptions{ IO: f.IOStreams, @@ -290,6 +292,7 @@ func executeCmds(client *git.Client, credentialPattern git.CredentialPattern, cm return nil } +// PRResolver resolves a pull request and its base repository for checkout. type PRResolver interface { Resolve() (*api.PullRequest, ghrepo.Interface, error) } @@ -299,6 +302,7 @@ type specificPRResolver struct { selector string } +// Resolve finds a pull request by its selector string and returns it along with its base repository. func (r *specificPRResolver) Resolve() (*api.PullRequest, ghrepo.Interface, error) { pr, baseRepo, err := r.prFinder.Find(shared.FindOptions{ Selector: r.selector, @@ -326,6 +330,7 @@ type promptingPRResolver struct { baseRepo ghrepo.Interface } +// Resolve interactively prompts the user to select a pull request from a list of open PRs. func (r *promptingPRResolver) Resolve() (*api.PullRequest, ghrepo.Interface, error) { r.io.StartProgressIndicator() listResult, err := r.prLister.List(shared.ListOptions{ diff --git a/pkg/cmd/pr/checks/aggregate.go b/pkg/cmd/pr/checks/aggregate.go index 91cec4335..5dcca17ea 100644 --- a/pkg/cmd/pr/checks/aggregate.go +++ b/pkg/cmd/pr/checks/aggregate.go @@ -29,6 +29,7 @@ type checkCounts struct { Canceled int } +// ExportData returns the check's fields as a map for structured data export. func (ch *check) ExportData(fields []string) map[string]interface{} { return cmdutil.StructExportData(ch, fields) } diff --git a/pkg/cmd/pr/checks/checks.go b/pkg/cmd/pr/checks/checks.go index 9256958aa..0d77625a2 100644 --- a/pkg/cmd/pr/checks/checks.go +++ b/pkg/cmd/pr/checks/checks.go @@ -32,6 +32,7 @@ var prCheckFields = []string{ "description", } +// ChecksOptions holds the configuration for the pr checks command. type ChecksOptions struct { HttpClient func() (*http.Client, error) IO *iostreams.IOStreams @@ -49,6 +50,7 @@ type ChecksOptions struct { Required bool } +// NewCmdChecks creates the cobra command for viewing CI status checks on a pull request. func NewCmdChecks(f *cmdutil.Factory, runF func(*ChecksOptions) error) *cobra.Command { var interval int opts := &ChecksOptions{ diff --git a/pkg/cmd/pr/close/close.go b/pkg/cmd/pr/close/close.go index 063501ef7..7adbf62d9 100644 --- a/pkg/cmd/pr/close/close.go +++ b/pkg/cmd/pr/close/close.go @@ -14,6 +14,7 @@ import ( "github.com/spf13/cobra" ) +// CloseOptions holds the configuration for the pr close command. type CloseOptions struct { HttpClient func() (*http.Client, error) GitClient *git.Client @@ -28,6 +29,7 @@ type CloseOptions struct { DeleteLocalBranch bool } +// NewCmdClose creates the cobra command for closing a pull request. func NewCmdClose(f *cmdutil.Factory, runF func(*CloseOptions) error) *cobra.Command { opts := &CloseOptions{ IO: f.IOStreams, diff --git a/pkg/cmd/pr/comment/comment.go b/pkg/cmd/pr/comment/comment.go index 2eed7d353..d03922340 100644 --- a/pkg/cmd/pr/comment/comment.go +++ b/pkg/cmd/pr/comment/comment.go @@ -8,6 +8,7 @@ import ( "github.com/spf13/cobra" ) +// NewCmdComment creates the cobra command for adding a comment to a pull request. func NewCmdComment(f *cmdutil.Factory, runF func(*shared.CommentableOptions) error) *cobra.Command { opts := &shared.CommentableOptions{ IO: f.IOStreams, diff --git a/pkg/cmd/pr/create/create.go b/pkg/cmd/pr/create/create.go index 3949ecbb6..b16c7ef09 100644 --- a/pkg/cmd/pr/create/create.go +++ b/pkg/cmd/pr/create/create.go @@ -30,6 +30,7 @@ import ( "github.com/spf13/cobra" ) +// CreateOptions holds the configuration and inputs for creating a pull request. type CreateOptions struct { // This struct stores user input and factory functions Detector fd.Detector @@ -99,10 +100,12 @@ type baseRefs struct { baseBranchName string } +// BaseRef returns the name of the base branch. func (r baseRefs) BaseRef() string { return r.baseBranchName } +// BaseRepo returns the base repository for the pull request. func (r baseRefs) BaseRepo() *api.Repository { return r.baseRepo } @@ -114,10 +117,12 @@ type skipPushRefs struct { qualifiedHeadRef shared.QualifiedHeadRef } +// QualifiedHeadRef returns the fully qualified head ref string for cross-repo or same-repo PRs. func (r skipPushRefs) QualifiedHeadRef() string { return r.qualifiedHeadRef.String() } +// UnqualifiedHeadRef returns the head branch name without any owner prefix. func (r skipPushRefs) UnqualifiedHeadRef() string { return r.qualifiedHeadRef.BranchName() } @@ -132,6 +137,7 @@ type pushableRefs struct { headBranchName string } +// QualifiedHeadRef returns the head ref, prefixed with the owner if the head and base repos differ. func (r pushableRefs) QualifiedHeadRef() string { if ghrepo.IsSame(r.headRepo, r.baseRepo) { return r.headBranchName @@ -139,10 +145,12 @@ func (r pushableRefs) QualifiedHeadRef() string { return fmt.Sprintf("%s:%s", r.headRepo.RepoOwner(), r.headBranchName) } +// UnqualifiedHeadRef returns the head branch name without any owner prefix. func (r pushableRefs) UnqualifiedHeadRef() string { return r.headBranchName } +// HeadRepo returns the repository where the head branch resides. func (r pushableRefs) HeadRepo() ghrepo.Interface { return r.headRepo } @@ -158,10 +166,12 @@ type forkableRefs struct { qualifiedHeadRef shared.QualifiedHeadRef } +// QualifiedHeadRef returns the fully qualified head ref string for a fork-based PR. func (r forkableRefs) QualifiedHeadRef() string { return r.qualifiedHeadRef.String() } +// UnqualifiedHeadRef returns the head branch name without any owner prefix. func (r forkableRefs) UnqualifiedHeadRef() string { return r.qualifiedHeadRef.BranchName() } @@ -190,6 +200,7 @@ type CreateContext struct { GitClient *git.Client } +// NewCmdCreate creates a new cobra.Command for opening a pull request. func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Command { opts := &CreateOptions{ IO: f.IOStreams, @@ -641,6 +652,7 @@ func initDefaultTitleBody(ctx CreateContext, state *shared.IssueMetadataState, u return nil } +// NewIssueState creates an IssueMetadataState populated from the given CreateContext and CreateOptions. func NewIssueState(ctx CreateContext, opts CreateOptions) (*shared.IssueMetadataState, error) { var milestoneTitles []string if opts.Milestone != "" { @@ -673,6 +685,7 @@ func NewIssueState(ctx CreateContext, opts CreateOptions) (*shared.IssueMetadata return state, nil } +// NewCreateContext initializes a CreateContext by resolving remotes, repos, and branch information. func NewCreateContext(opts *CreateOptions) (*CreateContext, error) { httpClient, err := opts.HttpClient() if err != nil { diff --git a/pkg/cmd/pr/create/regexp_writer.go b/pkg/cmd/pr/create/regexp_writer.go index 500637d7c..fc4117d19 100644 --- a/pkg/cmd/pr/create/regexp_writer.go +++ b/pkg/cmd/pr/create/regexp_writer.go @@ -6,10 +6,12 @@ import ( "regexp" ) +// NewRegexpWriter creates a RegexpWriter that replaces matches of re with repl in the output. func NewRegexpWriter(out io.Writer, re *regexp.Regexp, repl string) *RegexpWriter { return &RegexpWriter{out: out, re: *re, repl: repl} } +// RegexpWriter is an io.Writer that filters output by replacing lines matching a regexp. type RegexpWriter struct { out io.Writer re regexp.Regexp @@ -17,6 +19,7 @@ type RegexpWriter struct { buf []byte } +// Write processes data by applying regexp replacements line by line and writing the result. func (s *RegexpWriter) Write(data []byte) (int, error) { if len(data) == 0 { return 0, nil @@ -51,6 +54,7 @@ func (s *RegexpWriter) Write(data []byte) (int, error) { return len(data), nil } +// Flush writes any remaining buffered data after applying regexp replacements. func (s *RegexpWriter) Flush() (int, error) { if len(s.buf) > 0 { repl := []byte(s.repl) diff --git a/pkg/cmd/pr/diff/diff.go b/pkg/cmd/pr/diff/diff.go index 6d37bf1e5..a067e35d2 100644 --- a/pkg/cmd/pr/diff/diff.go +++ b/pkg/cmd/pr/diff/diff.go @@ -24,6 +24,7 @@ import ( "golang.org/x/text/transform" ) +// DiffOptions holds the configuration for the pr diff command. type DiffOptions struct { HttpClient func() (*http.Client, error) IO *iostreams.IOStreams @@ -38,6 +39,7 @@ type DiffOptions struct { BrowserMode bool } +// NewCmdDiff creates the cobra command for viewing the diff of a pull request. func NewCmdDiff(f *cmdutil.Factory, runF func(*DiffOptions) error) *cobra.Command { opts := &DiffOptions{ IO: f.IOStreams, diff --git a/pkg/cmd/pr/edit/edit.go b/pkg/cmd/pr/edit/edit.go index 157fc8f8a..9dd2ce446 100644 --- a/pkg/cmd/pr/edit/edit.go +++ b/pkg/cmd/pr/edit/edit.go @@ -21,6 +21,7 @@ import ( "golang.org/x/sync/errgroup" ) +// EditOptions holds the configuration for the pr edit command. type EditOptions struct { HttpClient func() (*http.Client, error) IO *iostreams.IOStreams @@ -39,6 +40,7 @@ type EditOptions struct { shared.Editable } +// NewCmdEdit creates the cobra command for editing a pull request's properties. func NewCmdEdit(f *cmdutil.Factory, runF func(*EditOptions) error) *cobra.Command { opts := &EditOptions{ IO: f.IOStreams, @@ -529,6 +531,7 @@ func updatePullRequestReviewsREST(client *api.Client, repo ghrepo.Interface, num return wg.Wait() } +// Surveyor provides interactive prompts for selecting and editing pull request fields. type Surveyor interface { FieldsToEdit(*shared.Editable) error EditFields(*shared.Editable, string) error @@ -538,24 +541,29 @@ type surveyor struct { P shared.EditPrompter } +// FieldsToEdit prompts the user to select which pull request fields to edit. func (s surveyor) FieldsToEdit(editable *shared.Editable) error { return shared.FieldsToEditSurvey(s.P, editable) } +// EditFields prompts the user to provide new values for the selected pull request fields. func (s surveyor) EditFields(editable *shared.Editable, editorCmd string) error { return shared.EditFieldsSurvey(s.P, editable, editorCmd) } +// EditableOptionsFetcher fetches the available options for editable pull request fields. type EditableOptionsFetcher interface { EditableOptionsFetch(*api.Client, ghrepo.Interface, *shared.Editable, gh.ProjectsV1Support) error } type fetcher struct{} +// EditableOptionsFetch retrieves the available options such as labels, assignees, and milestones for editing. func (f fetcher) EditableOptionsFetch(client *api.Client, repo ghrepo.Interface, opts *shared.Editable, projectsV1Support gh.ProjectsV1Support) error { return shared.FetchOptions(client, repo, opts, projectsV1Support) } +// EditorRetriever retrieves the user's preferred text editor command. type EditorRetriever interface { Retrieve() (string, error) } @@ -564,6 +572,7 @@ type editorRetriever struct { config func() (gh.Config, error) } +// Retrieve returns the user's configured editor command. func (e editorRetriever) Retrieve() (string, error) { return cmdutil.DetermineEditor(e.config) } diff --git a/pkg/cmd/pr/list/list.go b/pkg/cmd/pr/list/list.go index a7e40384a..bb6a9179a 100644 --- a/pkg/cmd/pr/list/list.go +++ b/pkg/cmd/pr/list/list.go @@ -20,6 +20,7 @@ import ( "github.com/spf13/cobra" ) +// ListOptions holds the configuration for the pr list command. type ListOptions struct { HttpClient func() (*http.Client, error) IO *iostreams.IOStreams @@ -43,6 +44,7 @@ type ListOptions struct { Now func() time.Time } +// NewCmdList creates the cobra command for listing pull requests in a repository. func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Command { opts := &ListOptions{ IO: f.IOStreams, diff --git a/pkg/cmd/pr/merge/http.go b/pkg/cmd/pr/merge/http.go index 6b705e6ef..d3454e9a0 100644 --- a/pkg/cmd/pr/merge/http.go +++ b/pkg/cmd/pr/merge/http.go @@ -9,21 +9,33 @@ import ( "github.com/shurcooL/githubv4" ) +// PullRequestMergeMethod represents the strategy used to merge a pull request. type PullRequestMergeMethod int const ( + // PullRequestMergeMethodMerge creates a merge commit. PullRequestMergeMethodMerge PullRequestMergeMethod = iota + // PullRequestMergeMethodRebase rebases the commits onto the base branch. PullRequestMergeMethodRebase + // PullRequestMergeMethodSquash squashes all commits into a single commit. PullRequestMergeMethodSquash ) +// Merge state status constants represent the GitHub GraphQL MergeStateStatus values. const ( - MergeStateStatusBehind = "BEHIND" - MergeStateStatusBlocked = "BLOCKED" - MergeStateStatusClean = "CLEAN" - MergeStateStatusDirty = "DIRTY" + // MergeStateStatusBehind indicates the head branch is behind the base branch. + MergeStateStatusBehind = "BEHIND" + // MergeStateStatusBlocked indicates the merge is blocked by branch protection or other rules. + MergeStateStatusBlocked = "BLOCKED" + // MergeStateStatusClean indicates the branch is clean and ready to merge. + MergeStateStatusClean = "CLEAN" + // MergeStateStatusDirty indicates the branch has merge conflicts. + MergeStateStatusDirty = "DIRTY" + // MergeStateStatusHasHooks indicates the branch has pre-receive hooks that must pass. MergeStateStatusHasHooks = "HAS_HOOKS" - MergeStateStatusMerged = "MERGED" + // MergeStateStatusMerged indicates the pull request has already been merged. + MergeStateStatusMerged = "MERGED" + // MergeStateStatusUnstable indicates the branch has failing required status checks. MergeStateStatusUnstable = "UNSTABLE" ) diff --git a/pkg/cmd/pr/merge/merge.go b/pkg/cmd/pr/merge/merge.go index 2049b85ae..d3558fd94 100644 --- a/pkg/cmd/pr/merge/merge.go +++ b/pkg/cmd/pr/merge/merge.go @@ -23,6 +23,7 @@ type editor interface { Edit(string, string) (string, error) } +// MergeOptions holds the configuration for the pr merge command. type MergeOptions struct { HttpClient func() (*http.Client, error) GitClient *git.Client @@ -57,6 +58,7 @@ type MergeOptions struct { // ErrAlreadyInMergeQueue indicates that the pull request is already in a merge queue var ErrAlreadyInMergeQueue = errors.New("already in merge queue") +// NewCmdMerge creates the cobra command for merging a pull request. func NewCmdMerge(f *cmdutil.Factory, runF func(*MergeOptions) error) *cobra.Command { opts := &MergeOptions{ IO: f.IOStreams, @@ -703,6 +705,7 @@ type userEditor struct { config func() (gh.Config, error) } +// Edit opens the user's preferred text editor to edit the given text and returns the result. func (e *userEditor) Edit(filename, startingText string) (string, error) { editorCommand, err := cmdutil.DetermineEditor(e.config) if err != nil { diff --git a/pkg/cmd/pr/pr.go b/pkg/cmd/pr/pr.go index e73193084..8dbba0b0b 100644 --- a/pkg/cmd/pr/pr.go +++ b/pkg/cmd/pr/pr.go @@ -23,6 +23,7 @@ import ( "github.com/spf13/cobra" ) +// NewCmdPR creates the top-level cobra command for managing pull requests. func NewCmdPR(f *cmdutil.Factory) *cobra.Command { cmd := &cobra.Command{ Use: "pr ", diff --git a/pkg/cmd/pr/ready/ready.go b/pkg/cmd/pr/ready/ready.go index 7b985a3ce..4f83d1602 100644 --- a/pkg/cmd/pr/ready/ready.go +++ b/pkg/cmd/pr/ready/ready.go @@ -13,6 +13,7 @@ import ( "github.com/spf13/cobra" ) +// ReadyOptions holds the configuration for the pr ready command. type ReadyOptions struct { HttpClient func() (*http.Client, error) IO *iostreams.IOStreams @@ -23,6 +24,7 @@ type ReadyOptions struct { Undo bool } +// NewCmdReady creates the cobra command for marking a pull request as ready for review. func NewCmdReady(f *cmdutil.Factory, runF func(*ReadyOptions) error) *cobra.Command { opts := &ReadyOptions{ IO: f.IOStreams, diff --git a/pkg/cmd/pr/reopen/reopen.go b/pkg/cmd/pr/reopen/reopen.go index 2aae2b8f5..702934de3 100644 --- a/pkg/cmd/pr/reopen/reopen.go +++ b/pkg/cmd/pr/reopen/reopen.go @@ -12,6 +12,7 @@ import ( "github.com/spf13/cobra" ) +// ReopenOptions holds the configuration for the pr reopen command. type ReopenOptions struct { HttpClient func() (*http.Client, error) IO *iostreams.IOStreams @@ -22,6 +23,7 @@ type ReopenOptions struct { Comment string } +// NewCmdReopen creates the cobra command for reopening a closed pull request. func NewCmdReopen(f *cmdutil.Factory, runF func(*ReopenOptions) error) *cobra.Command { opts := &ReopenOptions{ IO: f.IOStreams, diff --git a/pkg/cmd/pr/revert/revert.go b/pkg/cmd/pr/revert/revert.go index 544550d41..f3cebf094 100644 --- a/pkg/cmd/pr/revert/revert.go +++ b/pkg/cmd/pr/revert/revert.go @@ -13,6 +13,7 @@ import ( "github.com/spf13/cobra" ) +// RevertOptions holds the configuration for the pr revert command. type RevertOptions struct { HttpClient func() (*http.Client, error) IO *iostreams.IOStreams @@ -27,6 +28,7 @@ type RevertOptions struct { IsDraft bool } +// NewCmdRevert creates the cobra command for reverting a merged pull request. func NewCmdRevert(f *cmdutil.Factory, runF func(*RevertOptions) error) *cobra.Command { opts := &RevertOptions{ IO: f.IOStreams, diff --git a/pkg/cmd/pr/review/review.go b/pkg/cmd/pr/review/review.go index cafa6ce8f..c16b764cf 100644 --- a/pkg/cmd/pr/review/review.go +++ b/pkg/cmd/pr/review/review.go @@ -17,6 +17,7 @@ import ( "github.com/spf13/cobra" ) +// ReviewOptions holds the configuration for the pr review command. type ReviewOptions struct { HttpClient func() (*http.Client, error) Config func() (gh.Config, error) @@ -31,6 +32,7 @@ type ReviewOptions struct { Body string } +// NewCmdReview creates the cobra command for submitting a review on a pull request. func NewCmdReview(f *cmdutil.Factory, runF func(*ReviewOptions) error) *cobra.Command { opts := &ReviewOptions{ IO: f.IOStreams, diff --git a/pkg/cmd/pr/shared/commentable.go b/pkg/cmd/pr/shared/commentable.go index 015d84a4b..afef2abb8 100644 --- a/pkg/cmd/pr/shared/commentable.go +++ b/pkg/cmd/pr/shared/commentable.go @@ -20,20 +20,26 @@ import ( var errNoUserComments = errors.New("no comments found for current user") var errDeleteNotConfirmed = errors.New("deletion not confirmed") +// InputType represents the method used for providing comment input. type InputType int const ( + // InputTypeEditor indicates input will be provided via an external editor. InputTypeEditor InputType = iota + // InputTypeInline indicates input is provided directly as a flag value. InputTypeInline + // InputTypeWeb indicates the comment will be created via the browser. InputTypeWeb ) +// Commentable represents an entity that can be commented on, such as an issue or pull request. type Commentable interface { Link() string Identifier() string CurrentUserComments() []api.Comment } +// CommentableOptions holds the configuration for creating, updating, or deleting comments. type CommentableOptions struct { IO *iostreams.IOStreams HttpClient func() (*http.Client, error) @@ -55,6 +61,7 @@ type CommentableOptions struct { Host string } +// CommentablePreRun validates flags and configures input type and interactivity before the command runs. func CommentablePreRun(cmd *cobra.Command, opts *CommentableOptions) error { inputFlags := 0 if cmd.Flags().Changed("body") { @@ -105,6 +112,7 @@ func CommentablePreRun(cmd *cobra.Command, opts *CommentableOptions) error { return nil } +// CommentableRun executes the comment action (create, update, or delete) based on the configured options. func CommentableRun(opts *CommentableOptions) error { commentable, repo, err := opts.RetrieveCommentable() if err != nil { @@ -305,12 +313,14 @@ func deleteComment(commentable Commentable, opts *CommentableOptions) error { return nil } +// CommentableConfirmSubmitSurvey returns a function that prompts the user to confirm comment submission. func CommentableConfirmSubmitSurvey(p Prompt) func() (bool, error) { return func() (bool, error) { return p.Confirm("Submit?", true) } } +// CommentableInteractiveEditSurvey returns a function that opens an editor for composing a comment interactively. func CommentableInteractiveEditSurvey(cf func() (gh.Config, error), io *iostreams.IOStreams) func(string) (string, error) { return func(initialValue string) (string, error) { editorCommand, err := cmdutil.DetermineEditor(cf) @@ -324,12 +334,14 @@ func CommentableInteractiveEditSurvey(cf func() (gh.Config, error), io *iostream } } +// CommentableInteractiveCreateIfNoneSurvey returns a function that prompts the user to create a comment when none exist. func CommentableInteractiveCreateIfNoneSurvey(p Prompt) func() (bool, error) { return func() (bool, error) { return p.Confirm("No comments found. Create one?", true) } } +// CommentableEditSurvey returns a function that opens an editor for composing a comment non-interactively. func CommentableEditSurvey(cf func() (gh.Config, error), io *iostreams.IOStreams) func(string) (string, error) { return func(initialValue string) (string, error) { editorCommand, err := cmdutil.DetermineEditor(cf) @@ -340,6 +352,7 @@ func CommentableEditSurvey(cf func() (gh.Config, error), io *iostreams.IOStreams } } +// CommentableConfirmDeleteLastComment returns a function that prompts the user to confirm deletion of their last comment. func CommentableConfirmDeleteLastComment(p Prompt) func(string) (bool, error) { return func(body string) (bool, error) { return p.Confirm(fmt.Sprintf("Delete the comment: %q?", body), true) diff --git a/pkg/cmd/pr/shared/comments.go b/pkg/cmd/pr/shared/comments.go index 7c6e9154c..ac80778a4 100644 --- a/pkg/cmd/pr/shared/comments.go +++ b/pkg/cmd/pr/shared/comments.go @@ -12,6 +12,7 @@ import ( "github.com/cli/cli/v2/pkg/markdown" ) +// Comment represents a comment on an issue or pull request with metadata accessors. type Comment interface { Identifier() string AuthorLogin() string @@ -26,6 +27,7 @@ type Comment interface { Status() string } +// RawCommentList returns a plaintext representation of comments and reviews sorted by creation time. func RawCommentList(comments api.Comments, reviews api.PullRequestReviews) string { sortedComments := sortComments(comments, reviews) var b strings.Builder @@ -50,6 +52,7 @@ func formatRawComment(comment Comment) string { return b.String() } +// CommentList returns a formatted string of comments and reviews, optionally showing only the newest. func CommentList(io *iostreams.IOStreams, comments api.Comments, reviews api.PullRequestReviews, preview bool) (string, error) { sortedComments := sortComments(comments, reviews) if preview && len(sortedComments) > 0 { diff --git a/pkg/cmd/pr/shared/completion.go b/pkg/cmd/pr/shared/completion.go index 9f68c0bda..4bd1d7c0f 100644 --- a/pkg/cmd/pr/shared/completion.go +++ b/pkg/cmd/pr/shared/completion.go @@ -11,6 +11,7 @@ import ( "github.com/cli/cli/v2/internal/ghrepo" ) +// RequestableReviewersForCompletion returns a sorted list of requestable reviewers for shell completion. func RequestableReviewersForCompletion(httpClient *http.Client, repo ghrepo.Interface) ([]string, error) { client := api.NewClientFromHTTP(api.NewCachedHTTPClient(httpClient, time.Minute*2)) diff --git a/pkg/cmd/pr/shared/display.go b/pkg/cmd/pr/shared/display.go index b4d83c719..a0cf7a6ad 100644 --- a/pkg/cmd/pr/shared/display.go +++ b/pkg/cmd/pr/shared/display.go @@ -9,6 +9,7 @@ import ( "github.com/cli/cli/v2/pkg/iostreams" ) +// StateTitleWithColor returns the title-cased PR state colorized appropriately, showing "Draft" for draft PRs. func StateTitleWithColor(cs *iostreams.ColorScheme, pr api.PullRequest) string { prStateColorFunc := cs.ColorFromString(ColorForPRState(pr)) if pr.State == "OPEN" && pr.IsDraft { @@ -17,6 +18,7 @@ func StateTitleWithColor(cs *iostreams.ColorScheme, pr api.PullRequest) string { return prStateColorFunc(text.Title(pr.State)) } +// PrStateWithDraft returns the PR state string, substituting "DRAFT" for open draft PRs. func PrStateWithDraft(pr *api.PullRequest) string { if pr.IsDraft && pr.State == "OPEN" { return "DRAFT" @@ -25,6 +27,7 @@ func PrStateWithDraft(pr *api.PullRequest) string { return pr.State } +// ColorForPRState returns a color name suitable for the given pull request's state and draft status. func ColorForPRState(pr api.PullRequest) string { switch pr.State { case "OPEN": @@ -41,6 +44,7 @@ func ColorForPRState(pr api.PullRequest) string { } } +// ColorForIssueState returns a color name suitable for the given issue's state and state reason. func ColorForIssueState(issue api.Issue) string { switch issue.State { case "OPEN": @@ -55,14 +59,17 @@ func ColorForIssueState(issue api.Issue) string { } } +// PrintHeader prints a bold header line to the IOStreams output. func PrintHeader(io *iostreams.IOStreams, s string) { fmt.Fprintln(io.Out, io.ColorScheme().Bold(s)) } +// PrintMessage prints a muted message line to the IOStreams output. func PrintMessage(io *iostreams.IOStreams, s string) { fmt.Fprintln(io.Out, io.ColorScheme().Muted(s)) } +// ListNoResults returns a NoResultsError with a message appropriate for whether filters were applied. func ListNoResults(repoName string, itemName string, hasFilters bool) error { if hasFilters { return cmdutil.NewNoResultsError(fmt.Sprintf("no %ss match your search in %s", itemName, repoName)) @@ -70,6 +77,7 @@ func ListNoResults(repoName string, itemName string, hasFilters bool) error { return cmdutil.NewNoResultsError(fmt.Sprintf("no open %ss in %s", itemName, repoName)) } +// ListHeader returns a summary string describing how many items are shown out of the total, with filter context. func ListHeader(repoName string, itemName string, matchCount int, totalMatchCount int, hasFilters bool) string { if hasFilters { matchVerb := "match" @@ -82,6 +90,7 @@ func ListHeader(repoName string, itemName string, matchCount int, totalMatchCoun return fmt.Sprintf("Showing %d of %s in %s", matchCount, text.Pluralize(totalMatchCount, fmt.Sprintf("open %s", itemName)), repoName) } +// PrCheckStatusSummaryWithColor returns a colorized one-line summary of a pull request's CI check status. func PrCheckStatusSummaryWithColor(cs *iostreams.ColorScheme, checks api.PullRequestChecksStatus) string { var summary = cs.Muted("No checks") if checks.Total > 0 { diff --git a/pkg/cmd/pr/shared/editable.go b/pkg/cmd/pr/shared/editable.go index 31b253d9b..b8492c581 100644 --- a/pkg/cmd/pr/shared/editable.go +++ b/pkg/cmd/pr/shared/editable.go @@ -10,6 +10,7 @@ import ( "github.com/cli/cli/v2/pkg/set" ) +// Editable collects the editable fields of an issue or pull request for interactive or flag-driven editing. type Editable struct { Title EditableString Body EditableString @@ -24,6 +25,7 @@ type Editable struct { Metadata api.RepoMetadataResult } +// EditableString holds a single string field that can be edited, along with its default value and valid options. type EditableString struct { Value string Default string @@ -31,6 +33,7 @@ type EditableString struct { Edited bool } +// EditableSlice holds a list field that supports adding and removing values, along with defaults and valid options. type EditableSlice struct { Value []string Add []string @@ -62,6 +65,7 @@ type EditableProjects struct { ProjectItems map[string]string } +// Dirty reports whether any field in the Editable has been modified. func (e Editable) Dirty() bool { return e.Title.Edited || e.Body.Edited || @@ -73,6 +77,7 @@ func (e Editable) Dirty() bool { e.Milestone.Edited } +// TitleValue returns a pointer to the edited title, or nil if the title was not edited. func (e Editable) TitleValue() *string { if !e.Title.Edited { return nil @@ -80,6 +85,7 @@ func (e Editable) TitleValue() *string { return &e.Title.Value } +// BodyValue returns a pointer to the edited body, or nil if the body was not edited. func (e Editable) BodyValue() *string { if !e.Body.Edited { return nil @@ -87,6 +93,7 @@ func (e Editable) BodyValue() *string { return &e.Body.Value } +// AssigneeIds resolves the edited assignees to their node IDs, applying any add/remove modifications. func (e Editable) AssigneeIds(client *api.Client, repo ghrepo.Interface) (*[]string, error) { if !e.Assignees.Edited { return nil, nil @@ -208,6 +215,7 @@ func (e Editable) ProjectV2Ids() (*[]string, *[]string, error) { return &addIds, &removeIds, nil } +// MilestoneId resolves the edited milestone title to its node ID, or returns an empty string to clear it. func (e Editable) MilestoneId() (*string, error) { if !e.Milestone.Edited { return nil, nil @@ -288,6 +296,7 @@ func (ep *EditableProjects) clone() EditableProjects { } } +// EditPrompter defines the prompting interface used by interactive editing surveys. type EditPrompter interface { Select(string, string, []string) (int, error) Input(string, string) (string, error) @@ -297,6 +306,7 @@ type EditPrompter interface { Confirm(string, bool) (bool, error) } +// EditFieldsSurvey interactively prompts the user to provide values for each edited field and confirms submission. func EditFieldsSurvey(p EditPrompter, editable *Editable, editorCommand string) error { var err error if editable.Title.Edited { @@ -396,6 +406,7 @@ func EditFieldsSurvey(p EditPrompter, editable *Editable, editorCommand string) return nil } +// FieldsToEditSurvey prompts the user to select which fields they want to edit and marks them on the Editable. func FieldsToEditSurvey(p EditPrompter, editable *Editable) error { contains := func(s []string, str string) bool { for _, v := range s { @@ -441,6 +452,7 @@ func FieldsToEditSurvey(p EditPrompter, editable *Editable) error { return nil } +// FetchOptions retrieves repository metadata (reviewers, assignees, labels, projects, milestones) needed for editing. func FetchOptions(client *api.Client, repo ghrepo.Interface, editable *Editable, projectV1Support gh.ProjectsV1Support) error { // Determine whether to fetch organization teams and reviewers. // Interactive reviewer editing (Edited true, but no Add/Remove slices) still needs diff --git a/pkg/cmd/pr/shared/editable_http.go b/pkg/cmd/pr/shared/editable_http.go index 8cd51c349..9011e7cda 100644 --- a/pkg/cmd/pr/shared/editable_http.go +++ b/pkg/cmd/pr/shared/editable_http.go @@ -9,6 +9,7 @@ import ( "golang.org/x/sync/errgroup" ) +// UpdateIssue applies the edited fields to an issue or pull request via the GitHub API, handling labels, projects, and other fields concurrently. func UpdateIssue(httpClient *http.Client, repo ghrepo.Interface, id string, isPR bool, options Editable) error { var wg errgroup.Group diff --git a/pkg/cmd/pr/shared/find_refs_resolution.go b/pkg/cmd/pr/shared/find_refs_resolution.go index 4b977c716..456f76aed 100644 --- a/pkg/cmd/pr/shared/find_refs_resolution.go +++ b/pkg/cmd/pr/shared/find_refs_resolution.go @@ -36,6 +36,7 @@ func NewQualifiedHeadRef(owner string, branchName string) QualifiedHeadRef { } } +// NewQualifiedHeadRefWithoutOwner creates a QualifiedHeadRef with only a branch name and no owner. func NewQualifiedHeadRefWithoutOwner(branchName string) QualifiedHeadRef { return QualifiedHeadRef{ owner: o.None[string](), @@ -74,6 +75,7 @@ func (r QualifiedHeadRef) String() string { return r.branchName } +// BranchName returns the branch name component of the qualified head ref. func (r QualifiedHeadRef) BranchName() string { return r.branchName } @@ -97,6 +99,7 @@ func (r PRFindRefs) QualifiedHeadRef() string { return r.qualifiedHeadRef.String() } +// UnqualifiedHeadRef returns the branch name without any owner prefix. func (r PRFindRefs) UnqualifiedHeadRef() string { return r.qualifiedHeadRef.BranchName() } @@ -109,10 +112,12 @@ func (r PRFindRefs) Matches(baseBranchName, qualifiedHeadRef string) bool { return headMatches && baseMatches } +// BaseRepo returns the base repository used for finding the pull request. func (r PRFindRefs) BaseRepo() ghrepo.Interface { return r.baseRepo } +// RemoteNameToRepoFn is a function type that resolves a git remote name to a repository interface. type RemoteNameToRepoFn func(remoteName string) (ghrepo.Interface, error) // PullRequestFindRefsResolver interrogates git configuration to try and determine @@ -122,6 +127,7 @@ type PullRequestFindRefsResolver struct { RemoteNameToRepoFn RemoteNameToRepoFn } +// NewPullRequestFindRefsResolver creates a PullRequestFindRefsResolver with the given git config client and remotes function. func NewPullRequestFindRefsResolver(gitConfigClient GitConfigClient, remotesFn func() (ghContext.Remotes, error)) PullRequestFindRefsResolver { return PullRequestFindRefsResolver{ GitConfigClient: gitConfigClient, @@ -245,6 +251,7 @@ type remoteToRepoResolver struct { remoteNameToRepo RemoteNameToRepoFn } +// NewRemoteToRepoResolver creates a remoteToRepoResolver that maps git remotes to repository interfaces. func NewRemoteToRepoResolver(remotesFn func() (ghContext.Remotes, error)) remoteToRepoResolver { return remoteToRepoResolver{ remoteNameToRepo: newRemoteNameToRepoFn(remotesFn), diff --git a/pkg/cmd/pr/shared/finder.go b/pkg/cmd/pr/shared/finder.go index 80f1e707d..4a7171341 100644 --- a/pkg/cmd/pr/shared/finder.go +++ b/pkg/cmd/pr/shared/finder.go @@ -26,6 +26,7 @@ import ( "golang.org/x/sync/errgroup" ) +// PRFinder finds a pull request by selector, returning the PR and its base repository. type PRFinder interface { Find(opts FindOptions) (*api.PullRequest, ghrepo.Interface, error) } @@ -35,6 +36,7 @@ type progressIndicator interface { StopProgressIndicator() } +// GitConfigClient provides access to git branch configuration and push settings. type GitConfigClient interface { ReadBranchConfig(ctx context.Context, branchName string) (git.BranchConfig, error) PushDefault(ctx context.Context) (git.PushDefault, error) @@ -55,6 +57,7 @@ type finder struct { branchName string } +// NewFinder creates a PRFinder backed by the given factory, or returns a test stub if one has been set. func NewFinder(factory *cmdutil.Factory) PRFinder { if finderForRunCommandStyleTests != nil { f := finderForRunCommandStyleTests @@ -92,6 +95,7 @@ func StubFinderForRunCommandStyleTests(t *testing.T, selector string, pr *api.Pu return finder } +// FindOptions specifies the parameters for finding a pull request. type FindOptions struct { // Selector can be a number with optional `#` prefix, a branch name with optional `:` prefix, or // a PR URL. @@ -108,6 +112,7 @@ type FindOptions struct { Detector fd.Detector } +// Find locates a pull request by number, URL, or branch name, fetching the requested GraphQL fields. func (f *finder) Find(opts FindOptions) (*api.PullRequest, ghrepo.Interface, error) { // If we have a URL, we don't need git stuff if len(opts.Fields) == 0 { @@ -619,14 +624,17 @@ func preloadPrChecks(client *http.Client, repo ghrepo.Interface, pr *api.PullReq return nil } +// NotFoundError indicates that no pull request matching the query was found. type NotFoundError struct { error } +// Unwrap returns the underlying error wrapped by NotFoundError. func (err *NotFoundError) Unwrap() error { return err.error } +// NewMockFinder creates a mockFinder for use in tests, returning the given PR and repo when Find is called. func NewMockFinder(selector string, pr *api.PullRequest, repo ghrepo.Interface) *mockFinder { var err error if pr == nil { @@ -649,6 +657,7 @@ type mockFinder struct { err error } +// Find returns the mocked pull request and repository, validating the selector and fields expectations. func (m *mockFinder) Find(opts FindOptions) (*api.PullRequest, ghrepo.Interface, error) { if m.err != nil { return nil, nil, m.err @@ -672,6 +681,7 @@ func (m *mockFinder) Find(opts FindOptions) (*api.PullRequest, ghrepo.Interface, return m.pr, m.repo, nil } +// ExpectFields sets the expected GraphQL fields that Find must be called with. func (m *mockFinder) ExpectFields(fields []string) { m.expectFields = fields } diff --git a/pkg/cmd/pr/shared/git_cached_config_client.go b/pkg/cmd/pr/shared/git_cached_config_client.go index aea25abee..b9bd6780e 100644 --- a/pkg/cmd/pr/shared/git_cached_config_client.go +++ b/pkg/cmd/pr/shared/git_cached_config_client.go @@ -8,11 +8,13 @@ import ( var _ GitConfigClient = &CachedBranchConfigGitConfigClient{} +// CachedBranchConfigGitConfigClient wraps a GitConfigClient to return a pre-fetched BranchConfig instead of querying git. type CachedBranchConfigGitConfigClient struct { CachedBranchConfig git.BranchConfig GitConfigClient } +// ReadBranchConfig returns the cached BranchConfig, ignoring the branch name argument. func (c CachedBranchConfigGitConfigClient) ReadBranchConfig(ctx context.Context, branchName string) (git.BranchConfig, error) { return c.CachedBranchConfig, nil } diff --git a/pkg/cmd/pr/shared/lister.go b/pkg/cmd/pr/shared/lister.go index cd140a950..98570c6ad 100644 --- a/pkg/cmd/pr/shared/lister.go +++ b/pkg/cmd/pr/shared/lister.go @@ -9,10 +9,12 @@ import ( api "github.com/cli/cli/v2/api" ) +// PRLister is the interface for listing pull requests from a repository. type PRLister interface { List(opt ListOptions) (*api.PullRequestAndTotalCount, error) } +// ListOptions specifies filtering and pagination options for listing pull requests. type ListOptions struct { BaseRepo ghrepo.Interface @@ -29,12 +31,14 @@ type lister struct { httpClient *http.Client } +// NewLister creates a PRLister that fetches pull requests via the GitHub GraphQL API. func NewLister(httpClient *http.Client) PRLister { return &lister{ httpClient: httpClient, } } +// List retrieves pull requests from the repository matching the given options. func (l *lister) List(opts ListOptions) (*api.PullRequestAndTotalCount, error) { type response struct { Repository struct { @@ -153,6 +157,7 @@ type mockLister struct { err error } +// NewMockLister creates a mock PRLister that returns the given result and error for testing. func NewMockLister(result *api.PullRequestAndTotalCount, err error) *mockLister { return &mockLister{ result: result, @@ -160,6 +165,7 @@ func NewMockLister(result *api.PullRequestAndTotalCount, err error) *mockLister } } +// List returns the preconfigured result and error, validating expected fields if set. func (m *mockLister) List(opt ListOptions) (*api.PullRequestAndTotalCount, error) { m.called = true if m.err != nil { @@ -176,6 +182,7 @@ func (m *mockLister) List(opt ListOptions) (*api.PullRequestAndTotalCount, error return m.result, m.err } +// ExpectFields sets the GraphQL fields that the mock expects to receive in List calls. func (m *mockLister) ExpectFields(fields []string) { m.expectFields = fields } diff --git a/pkg/cmd/pr/shared/params.go b/pkg/cmd/pr/shared/params.go index 784b68cf9..99795b173 100644 --- a/pkg/cmd/pr/shared/params.go +++ b/pkg/cmd/pr/shared/params.go @@ -13,6 +13,7 @@ import ( "github.com/google/shlex" ) +// WithPrAndIssueQueryParams appends issue or pull request metadata from state as query parameters to the given URL. func WithPrAndIssueQueryParams(client *api.Client, baseRepo ghrepo.Interface, baseURL string, state IssueMetadataState, projectsV1Support gh.ProjectsV1Support) (string, error) { u, err := url.Parse(baseURL) if err != nil { @@ -56,6 +57,7 @@ func ValidURL(urlStr string) bool { return len(urlStr) < 8192 } +// AddMetadataToIssueParams resolves metadata such as assignees, labels, projects, milestones, and reviewers into IDs and adds them to the params map. func AddMetadataToIssueParams(client *api.Client, baseRepo ghrepo.Interface, params map[string]interface{}, tb *IssueMetadataState, projectV1Support gh.ProjectsV1Support) error { if !tb.HasMetadata() { return nil @@ -139,6 +141,7 @@ func AddMetadataToIssueParams(client *api.Client, baseRepo ghrepo.Interface, par return nil } +// FilterOptions holds the filtering criteria used when listing issues or pull requests. type FilterOptions struct { Assignee string Author string @@ -155,6 +158,7 @@ type FilterOptions struct { State string } +// IsDefault reports whether the filter options represent the default state with no custom filters applied. func (opts *FilterOptions) IsDefault() bool { if opts.State != "open" { return false @@ -186,6 +190,7 @@ func (opts *FilterOptions) IsDefault() bool { return true } +// ListURLWithQuery builds a URL by appending a search query derived from FilterOptions to the given list URL. func ListURLWithQuery(listURL string, options FilterOptions, advancedIssueSearchSyntax bool) (string, error) { u, err := url.Parse(listURL) if err != nil { @@ -199,6 +204,7 @@ func ListURLWithQuery(listURL string, options FilterOptions, advancedIssueSearch return u.String(), nil } +// SearchQueryBuild constructs a GitHub search query string from the given FilterOptions. func SearchQueryBuild(options FilterOptions, advancedIssueSearchSyntax bool) string { var is, state string switch options.State { @@ -231,6 +237,7 @@ func SearchQueryBuild(options FilterOptions, advancedIssueSearchSyntax bool) str return query.AdvancedIssueSearchString() } +// QueryHasStateClause reports whether the search query contains an explicit state or merged filter clause. func QueryHasStateClause(searchQuery string) bool { argv, err := shlex.Split(searchQuery) if err != nil { @@ -253,6 +260,7 @@ type MeReplacer struct { login string } +// NewMeReplacer creates a MeReplacer that resolves @me to the currently authenticated user's login. func NewMeReplacer(apiClient *api.Client, hostname string) *MeReplacer { return &MeReplacer{ apiClient: apiClient, @@ -272,6 +280,7 @@ func (r *MeReplacer) currentLogin() (string, error) { return login, nil } +// Replace substitutes "@me" with the current user's login, returning other handles unchanged. func (r *MeReplacer) Replace(handle string) (string, error) { if handle == "@me" { return r.currentLogin() @@ -279,6 +288,7 @@ func (r *MeReplacer) Replace(handle string) (string, error) { return handle, nil } +// ReplaceSlice applies Replace to each element in the slice, substituting any "@me" occurrences. func (r *MeReplacer) ReplaceSlice(handles []string) ([]string, error) { res := make([]string, len(handles)) for i, h := range handles { diff --git a/pkg/cmd/pr/shared/preserve.go b/pkg/cmd/pr/shared/preserve.go index 948f81236..0733f1744 100644 --- a/pkg/cmd/pr/shared/preserve.go +++ b/pkg/cmd/pr/shared/preserve.go @@ -9,6 +9,7 @@ import ( "github.com/cli/cli/v2/pkg/iostreams" ) +// PreserveInput returns a cleanup function that saves issue or PR metadata state to a temporary file when a creation error occurs, enabling recovery with --recover. func PreserveInput(io *iostreams.IOStreams, state *IssueMetadataState, createErr *error) func() { return func() { if !state.IsDirty() { diff --git a/pkg/cmd/pr/shared/reaction_groups.go b/pkg/cmd/pr/shared/reaction_groups.go index 3448ace24..7771259cf 100644 --- a/pkg/cmd/pr/shared/reaction_groups.go +++ b/pkg/cmd/pr/shared/reaction_groups.go @@ -7,6 +7,7 @@ import ( "github.com/cli/cli/v2/api" ) +// ReactionGroupList formats a list of reaction groups into a human-readable string with emoji and counts. func ReactionGroupList(rgs api.ReactionGroups) string { var rs []string diff --git a/pkg/cmd/pr/shared/state.go b/pkg/cmd/pr/shared/state.go index b9f2c0293..94744ebd4 100644 --- a/pkg/cmd/pr/shared/state.go +++ b/pkg/cmd/pr/shared/state.go @@ -11,10 +11,13 @@ import ( type metadataStateType int const ( + // IssueMetadata indicates that the metadata state is for an issue. IssueMetadata metadataStateType = iota + // PRMetadata indicates that the metadata state is for a pull request. PRMetadata ) +// IssueMetadataState holds the editable metadata for an issue or pull request during creation or editing. type IssueMetadataState struct { Type metadataStateType @@ -38,14 +41,17 @@ type IssueMetadataState struct { dirty bool // whether user i/o has modified this } +// MarkDirty flags the state as having been modified by user input. func (tb *IssueMetadataState) MarkDirty() { tb.dirty = true } +// IsDirty reports whether the state has been modified by user input or has metadata set. func (tb *IssueMetadataState) IsDirty() bool { return tb.dirty || tb.HasMetadata() } +// HasMetadata reports whether any reviewers, assignees, labels, projects, or milestones are set. func (tb *IssueMetadataState) HasMetadata() bool { return len(tb.Reviewers) > 0 || len(tb.Assignees) > 0 || @@ -54,6 +60,7 @@ func (tb *IssueMetadataState) HasMetadata() bool { len(tb.Milestones) > 0 } +// FillFromJSON populates the given IssueMetadataState by reading and unmarshaling a JSON recovery file. func FillFromJSON(io *iostreams.IOStreams, recoverFile string, state *IssueMetadataState) error { var data []byte var err error diff --git a/pkg/cmd/pr/shared/survey.go b/pkg/cmd/pr/shared/survey.go index e350671b9..6399a5063 100644 --- a/pkg/cmd/pr/shared/survey.go +++ b/pkg/cmd/pr/shared/survey.go @@ -15,15 +15,23 @@ import ( "github.com/cli/cli/v2/pkg/surveyext" ) +// Action represents a user-selected action in a submission prompt. type Action int const ( + // SubmitAction indicates the user chose to submit. SubmitAction Action = iota + // PreviewAction indicates the user chose to continue in the browser. PreviewAction + // CancelAction indicates the user cancelled the operation. CancelAction + // MetadataAction indicates the user chose to add metadata. MetadataAction + // EditCommitMessageAction indicates the user chose to edit the commit message. EditCommitMessageAction + // EditCommitSubjectAction indicates the user chose to edit the commit subject. EditCommitSubjectAction + // SubmitDraftAction indicates the user chose to submit as a draft. SubmitDraftAction noMilestone = "(none)" @@ -35,6 +43,7 @@ const ( cancelLabel = "Cancel" ) +// Prompt defines the interface for interactive user prompts used during issue and PR creation. type Prompt interface { Input(prompt string, defaultValue string) (string, error) Select(prompt string, defaultValue string, options []string) (int, error) @@ -44,10 +53,12 @@ type Prompt interface { MultiSelectWithSearch(prompt, searchPrompt string, defaults []string, persistentOptions []string, searchFunc func(string) prompter.MultiSelectSearchResult) ([]string, error) } +// ConfirmIssueSubmission prompts the user to select a submission action for an issue. func ConfirmIssueSubmission(p Prompt, allowPreview bool, allowMetadata bool) (Action, error) { return confirmSubmission(p, allowPreview, allowMetadata, false, false) } +// ConfirmPRSubmission prompts the user to select a submission action for a pull request. func ConfirmPRSubmission(p Prompt, allowPreview, allowMetadata, isDraft bool) (Action, error) { return confirmSubmission(p, allowPreview, allowMetadata, true, isDraft) } @@ -89,6 +100,7 @@ func confirmSubmission(p Prompt, allowPreview, allowMetadata, allowDraft, isDraf } } +// BodySurvey prompts the user to edit the body of an issue or pull request using a markdown editor. func BodySurvey(p Prompt, state *IssueMetadataState, templateContent string) error { if templateContent != "" { if state.Body != "" { @@ -113,6 +125,7 @@ func BodySurvey(p Prompt, state *IssueMetadataState, templateContent string) err return nil } +// TitleSurvey prompts the user to enter a required title for an issue or pull request. func TitleSurvey(p Prompt, io *iostreams.IOStreams, state *IssueMetadataState) error { var err error result := "" @@ -135,6 +148,7 @@ func TitleSurvey(p Prompt, io *iostreams.IOStreams, state *IssueMetadataState) e return nil } +// MetadataFetcher fetches repository metadata such as assignees, labels, and projects for use in interactive prompts. type MetadataFetcher struct { IO *iostreams.IOStreams APIClient *api.Client @@ -142,6 +156,7 @@ type MetadataFetcher struct { State *IssueMetadataState } +// RepoMetadataFetch retrieves repository metadata based on the given input and caches the result in State. func (mf *MetadataFetcher) RepoMetadataFetch(input api.RepoMetadataInput) (*api.RepoMetadataResult, error) { mf.IO.StartProgressIndicator() metadataResult, err := api.RepoMetadata(mf.APIClient, mf.Repo, input) @@ -150,10 +165,12 @@ func (mf *MetadataFetcher) RepoMetadataFetch(input api.RepoMetadataInput) (*api. return metadataResult, err } +// RepoMetadataFetcher is the interface for fetching repository metadata used during metadata surveys. type RepoMetadataFetcher interface { RepoMetadataFetch(api.RepoMetadataInput) (*api.RepoMetadataResult, error) } +// MetadataSurvey interactively prompts the user to select and set metadata such as reviewers, assignees, labels, projects, and milestones. func MetadataSurvey(p Prompt, io *iostreams.IOStreams, baseRepo ghrepo.Interface, fetcher RepoMetadataFetcher, state *IssueMetadataState, projectsV1Support gh.ProjectsV1Support) error { isChosen := func(m string) bool { for _, c := range state.Metadata { @@ -360,15 +377,18 @@ func MetadataSurvey(p Prompt, io *iostreams.IOStreams, baseRepo ghrepo.Interface return nil } +// Editor is the interface for launching an external text editor. type Editor interface { Edit(filename, initialValue string) (string, error) } +// UserEditor opens the user's preferred text editor for editing content. type UserEditor struct { IO *iostreams.IOStreams Config func() (gh.Config, error) } +// Edit opens the user's configured editor with the given filename and initial content, returning the edited result. func (e *UserEditor) Edit(filename, initialValue string) (string, error) { editorCommand, err := cmdutil.DetermineEditor(e.Config) if err != nil { @@ -382,6 +402,7 @@ const editorHint = ` Please Enter the title on the first line and the body on subsequent lines. Lines below dotted lines will be ignored, and an empty title aborts the creation process.` +// TitledEditSurvey returns a function that opens an editor for the user to provide a title and body. func TitledEditSurvey(editor Editor) func(string, string) (string, string, error) { return func(initialTitle, initialBody string) (string, string, error) { initialValue := strings.Join([]string{initialTitle, initialBody, editorHintMarker, editorHint}, "\n") @@ -397,6 +418,7 @@ func TitledEditSurvey(editor Editor) func(string, string) (string, string, error } } +// InitEditorMode determines whether editor mode should be enabled based on flags and user configuration. func InitEditorMode(f *cmdutil.Factory, editorMode bool, webMode bool, canPrompt bool) (bool, error) { if err := cmdutil.MutuallyExclusive( "specify only one of `--editor` or `--web`", diff --git a/pkg/cmd/pr/shared/templates.go b/pkg/cmd/pr/shared/templates.go index b8c9ea719..1aca00371 100644 --- a/pkg/cmd/pr/shared/templates.go +++ b/pkg/cmd/pr/shared/templates.go @@ -26,34 +26,42 @@ type pullRequestTemplate struct { Gbody string `graphql:"body"` } +// Name returns the display name of the issue template. func (t *issueTemplate) Name() string { return t.Gname } +// NameForSubmit returns the template name to include when submitting an issue. func (t *issueTemplate) NameForSubmit() string { return t.Gname } +// Body returns the content of the issue template as bytes. func (t *issueTemplate) Body() []byte { return []byte(t.Gbody) } +// Title returns the default title of the issue template. func (t *issueTemplate) Title() string { return t.Gtitle } +// Name returns the filename of the pull request template. func (t *pullRequestTemplate) Name() string { return t.Gname } +// NameForSubmit returns an empty string since pull request templates do not have a submission name. func (t *pullRequestTemplate) NameForSubmit() string { return "" } +// Body returns the content of the pull request template as bytes. func (t *pullRequestTemplate) Body() []byte { return []byte(t.Gbody) } +// Title returns an empty string since pull request templates do not have a default title. func (t *pullRequestTemplate) Title() string { return "" } @@ -114,6 +122,7 @@ func listPullRequestTemplates(httpClient *http.Client, repo ghrepo.Interface) ([ return templates, nil } +// Template defines the interface for issue and pull request templates. type Template interface { Name() string NameForSubmit() string @@ -141,6 +150,7 @@ type templateManager struct { fetchError error } +// NewTemplateManager creates a templateManager that discovers issue or PR templates from the API and optionally the filesystem. func NewTemplateManager(httpClient *http.Client, repo ghrepo.Interface, p iprompter, dir string, allowFS bool, isPR bool) *templateManager { cachedClient := api.NewCachedHTTPClient(httpClient, time.Hour*24) return &templateManager{ @@ -167,6 +177,7 @@ func (m *templateManager) hasAPI() (bool, error) { return features.PullRequestTemplateQuery, nil } +// HasTemplates reports whether any templates are available for the repository. func (m *templateManager) HasTemplates() (bool, error) { if err := m.memoizedFetch(); err != nil { return false, err @@ -174,6 +185,7 @@ func (m *templateManager) HasTemplates() (bool, error) { return len(m.templates) > 0, nil } +// LegacyBody returns the body of the legacy template, or nil if no legacy template exists. func (m *templateManager) LegacyBody() []byte { if m.legacyTemplate == nil { return nil @@ -181,6 +193,7 @@ func (m *templateManager) LegacyBody() []byte { return m.legacyTemplate.Body() } +// Choose prompts the user to interactively select a template from the available options. func (m *templateManager) Choose() (Template, error) { if err := m.memoizedFetch(); err != nil { return nil, err @@ -210,6 +223,7 @@ func (m *templateManager) Choose() (Template, error) { return m.templates[selectedOption], nil } +// Select returns the template matching the given name, or an error if not found. func (m *templateManager) Select(name string) (Template, error) { if err := m.memoizedFetch(); err != nil { return nil, err @@ -294,18 +308,22 @@ type filesystemTemplate struct { path string } +// Name returns the display name extracted from the filesystem template file. func (t *filesystemTemplate) Name() string { return githubtemplate.ExtractName(t.path) } +// NameForSubmit returns an empty string since filesystem templates do not have a submission name. func (t *filesystemTemplate) NameForSubmit() string { return "" } +// Body returns the contents of the filesystem template file. func (t *filesystemTemplate) Body() []byte { return githubtemplate.ExtractContents(t.path) } +// Title returns the title extracted from the filesystem template file. func (t *filesystemTemplate) Title() string { return githubtemplate.ExtractTitle(t.path) } diff --git a/pkg/cmd/pr/status/status.go b/pkg/cmd/pr/status/status.go index 60202594f..c2fb4826d 100644 --- a/pkg/cmd/pr/status/status.go +++ b/pkg/cmd/pr/status/status.go @@ -23,6 +23,7 @@ import ( "github.com/spf13/cobra" ) +// StatusOptions holds the configuration for the pr status command. type StatusOptions struct { HttpClient func() (*http.Client, error) GitClient *git.Client @@ -39,6 +40,7 @@ type StatusOptions struct { Detector fd.Detector } +// NewCmdStatus creates the cobra command for showing the status of relevant pull requests. func NewCmdStatus(f *cmdutil.Factory, runF func(*StatusOptions) error) *cobra.Command { opts := &StatusOptions{ IO: f.IOStreams, diff --git a/pkg/cmd/pr/update-branch/update_branch.go b/pkg/cmd/pr/update-branch/update_branch.go index ca3b2c910..7a6be58f3 100644 --- a/pkg/cmd/pr/update-branch/update_branch.go +++ b/pkg/cmd/pr/update-branch/update_branch.go @@ -16,6 +16,7 @@ import ( "github.com/spf13/cobra" ) +// UpdateBranchOptions holds the configuration for the pr update-branch command. type UpdateBranchOptions struct { HttpClient func() (*http.Client, error) IO *iostreams.IOStreams @@ -27,6 +28,7 @@ type UpdateBranchOptions struct { Rebase bool } +// NewCmdUpdateBranch creates the cobra command for updating a pull request branch with the latest base branch changes. func NewCmdUpdateBranch(f *cmdutil.Factory, runF func(*UpdateBranchOptions) error) *cobra.Command { opts := &UpdateBranchOptions{ IO: f.IOStreams, diff --git a/pkg/cmd/pr/view/view.go b/pkg/cmd/pr/view/view.go index 564cce913..b34c8f1a5 100644 --- a/pkg/cmd/pr/view/view.go +++ b/pkg/cmd/pr/view/view.go @@ -20,6 +20,7 @@ import ( "github.com/spf13/cobra" ) +// ViewOptions holds the configuration for the pr view command. type ViewOptions struct { IO *iostreams.IOStreams Browser browser.Browser @@ -37,6 +38,7 @@ type ViewOptions struct { Now func() time.Time } +// NewCmdView creates the cobra command for viewing a pull request's details. func NewCmdView(f *cmdutil.Factory, runF func(*ViewOptions) error) *cobra.Command { opts := &ViewOptions{ IO: f.IOStreams,