package list import ( "fmt" "net/http" "strconv" "strings" "time" "github.com/MakeNowJust/heredoc" "github.com/cli/cli/v2/api" "github.com/cli/cli/v2/internal/browser" fd "github.com/cli/cli/v2/internal/featuredetection" "github.com/cli/cli/v2/internal/ghrepo" "github.com/cli/cli/v2/internal/tableprinter" "github.com/cli/cli/v2/internal/text" "github.com/cli/cli/v2/pkg/cmd/pr/shared" "github.com/cli/cli/v2/pkg/cmdutil" "github.com/cli/cli/v2/pkg/iostreams" "github.com/spf13/cobra" ) type ListOptions struct { HttpClient func() (*http.Client, error) IO *iostreams.IOStreams BaseRepo func() (ghrepo.Interface, error) Browser browser.Browser Detector fd.Detector WebMode bool LimitResults int Exporter cmdutil.Exporter State string BaseBranch string HeadBranch string Labels []string Author string Assignee string Search string Draft *bool Now func() time.Time } func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Command { opts := &ListOptions{ IO: f.IOStreams, HttpClient: f.HttpClient, Browser: f.Browser, Now: time.Now, } var appAuthor string cmd := &cobra.Command{ Use: "list", Short: "List pull requests in a repository", // TODO advancedIssueSearchCleanup // Update the links and remove the mention at GHES 3.17 version. Long: heredoc.Docf(` List pull requests in a GitHub repository. By default, this only lists open PRs. The search query syntax is documented here: On supported GitHub hosts, advanced issue search syntax can be used in the %[1]s--search%[1]s query. For more information about advanced issue search, see: `, "`"), Example: heredoc.Doc(` # List PRs authored by you $ gh pr list --author "@me" # List PRs with a specific head branch name $ gh pr list --head "typo" # List only PRs with all of the given labels $ gh pr list --label bug --label "priority 1" # Filter PRs using search syntax $ gh pr list --search "status:success review:required" # Find a PR that introduced a given commit $ gh pr list --search "" --state merged `), Aliases: []string{"ls"}, Args: cmdutil.NoArgsQuoteReminder, RunE: func(cmd *cobra.Command, args []string) error { // support `-R, --repo` override opts.BaseRepo = f.BaseRepo if opts.LimitResults < 1 { return cmdutil.FlagErrorf("invalid value for --limit: %v", opts.LimitResults) } if cmd.Flags().Changed("author") && cmd.Flags().Changed("app") { return cmdutil.FlagErrorf("specify only `--author` or `--app`") } if cmd.Flags().Changed("app") { opts.Author = fmt.Sprintf("app/%s", appAuthor) } if runF != nil { return runF(opts) } return listRun(opts) }, } cmd.Flags().BoolVarP(&opts.WebMode, "web", "w", false, "List pull requests in the web browser") cmd.Flags().IntVarP(&opts.LimitResults, "limit", "L", 30, "Maximum number of items to fetch") cmdutil.StringEnumFlag(cmd, &opts.State, "state", "s", "open", []string{"open", "closed", "merged", "all"}, "Filter by state") cmd.Flags().StringVarP(&opts.BaseBranch, "base", "B", "", "Filter by base branch") cmd.Flags().StringVarP(&opts.HeadBranch, "head", "H", "", `Filter by head branch (":" syntax not supported)`) cmd.Flags().StringSliceVarP(&opts.Labels, "label", "l", nil, "Filter by label") cmd.Flags().StringVarP(&opts.Author, "author", "A", "", "Filter by author") cmd.Flags().StringVar(&appAuthor, "app", "", "Filter by GitHub App author") cmd.Flags().StringVarP(&opts.Assignee, "assignee", "a", "", "Filter by assignee") cmd.Flags().StringVarP(&opts.Search, "search", "S", "", "Search pull requests with `query`") cmdutil.NilBoolFlag(cmd, &opts.Draft, "draft", "d", "Filter by draft state") cmdutil.AddJSONFlags(cmd, &opts.Exporter, api.PullRequestFields) _ = cmdutil.RegisterBranchCompletionFlags(f.GitClient, cmd, "base", "head") return cmd } var defaultFields = []string{ "number", "title", "state", "url", "headRefName", "headRepositoryOwner", "isCrossRepository", "isDraft", "createdAt", } func listRun(opts *ListOptions) error { httpClient, err := opts.HttpClient() if err != nil { return err } baseRepo, err := opts.BaseRepo() if err != nil { return err } if opts.Detector == nil { cachedClient := api.NewCachedHTTPClient(httpClient, time.Hour*24) opts.Detector = fd.NewDetector(cachedClient, baseRepo.RepoHost()) } prState := strings.ToLower(opts.State) if prState == "open" && shared.QueryHasStateClause(opts.Search) { prState = "" } filters := shared.FilterOptions{ Entity: "pr", State: prState, Author: opts.Author, Assignee: opts.Assignee, Labels: opts.Labels, BaseBranch: opts.BaseBranch, HeadBranch: opts.HeadBranch, Search: opts.Search, Draft: opts.Draft, Fields: defaultFields, } if opts.Exporter != nil { filters.Fields = opts.Exporter.Fields() } if opts.WebMode { prListURL := ghrepo.GenerateRepoURL(baseRepo, "pulls") // TODO advancedSearchFuture // As of August 2025, the advanced issue search syntax is not supported // in Pull Requests tab of repositories. When it's supported we can // change the argument to true. openURL, err := shared.ListURLWithQuery(prListURL, filters, false) if err != nil { return err } if opts.IO.IsStdoutTTY() { fmt.Fprintf(opts.IO.ErrOut, "Opening %s in your browser.\n", text.DisplayURL(openURL)) } return opts.Browser.Browse(openURL) } listResult, err := listPullRequests(httpClient, opts.Detector, baseRepo, filters, opts.LimitResults) if err != nil { return err } if len(listResult.PullRequests) == 0 && opts.Exporter == nil { return shared.ListNoResults(ghrepo.FullName(baseRepo), "pull request", !filters.IsDefault()) } err = opts.IO.StartPager() if err != nil { fmt.Fprintf(opts.IO.ErrOut, "error starting pager: %v\n", err) } defer opts.IO.StopPager() if opts.Exporter != nil { return opts.Exporter.Write(opts.IO, listResult.PullRequests) } if listResult.SearchCapped { fmt.Fprintln(opts.IO.ErrOut, "warning: this query uses the Search API which is capped at 1000 results maximum") } if opts.IO.IsStdoutTTY() { title := shared.ListHeader(ghrepo.FullName(baseRepo), "pull request", len(listResult.PullRequests), listResult.TotalCount, !filters.IsDefault()) fmt.Fprintf(opts.IO.Out, "\n%s\n\n", title) } cs := opts.IO.ColorScheme() isTTY := opts.IO.IsStdoutTTY() headers := []string{ "ID", "TITLE", "BRANCH", } if !isTTY { headers = append(headers, "STATE") } headers = append(headers, "CREATED AT") table := tableprinter.New(opts.IO, tableprinter.WithHeader(headers...)) for _, pr := range listResult.PullRequests { prNum := strconv.Itoa(pr.Number) if isTTY { prNum = "#" + prNum } table.AddField(prNum, tableprinter.WithColor(cs.ColorFromString(shared.ColorForPRState(pr)))) table.AddField(text.RemoveExcessiveWhitespace(pr.Title)) table.AddField(pr.HeadLabel(), tableprinter.WithColor(cs.Cyan)) if !isTTY { table.AddField(shared.PrStateWithDraft(&pr)) } table.AddTimeField(opts.Now(), pr.CreatedAt, cs.Muted) table.EndRow() } err = table.Render() if err != nil { return err } return nil }