github.com/ungtb10d/cli/v2@v2.0.0-20221110210412-98537dd9d6a1/pkg/cmd/search/repos/repos.go (about)

     1  package repos
     2  
     3  import (
     4  	"fmt"
     5  	"strings"
     6  	"time"
     7  
     8  	"github.com/MakeNowJust/heredoc"
     9  	"github.com/ungtb10d/cli/v2/internal/browser"
    10  	"github.com/ungtb10d/cli/v2/internal/text"
    11  	"github.com/ungtb10d/cli/v2/pkg/cmd/search/shared"
    12  	"github.com/ungtb10d/cli/v2/pkg/cmdutil"
    13  	"github.com/ungtb10d/cli/v2/pkg/iostreams"
    14  	"github.com/ungtb10d/cli/v2/pkg/search"
    15  	"github.com/ungtb10d/cli/v2/utils"
    16  	"github.com/spf13/cobra"
    17  )
    18  
    19  type ReposOptions struct {
    20  	Browser  browser.Browser
    21  	Exporter cmdutil.Exporter
    22  	IO       *iostreams.IOStreams
    23  	Now      time.Time
    24  	Query    search.Query
    25  	Searcher search.Searcher
    26  	WebMode  bool
    27  }
    28  
    29  func NewCmdRepos(f *cmdutil.Factory, runF func(*ReposOptions) error) *cobra.Command {
    30  	var order string
    31  	var sort string
    32  	opts := &ReposOptions{
    33  		Browser: f.Browser,
    34  		IO:      f.IOStreams,
    35  		Query:   search.Query{Kind: search.KindRepositories},
    36  	}
    37  
    38  	cmd := &cobra.Command{
    39  		Use:   "repos [<query>]",
    40  		Short: "Search for repositories",
    41  		Long: heredoc.Doc(`
    42  			Search for repositories on GitHub.
    43  
    44  			The command supports constructing queries using the GitHub search syntax,
    45  			using the parameter and qualifier flags, or a combination of the two.
    46  
    47  			GitHub search syntax is documented at:
    48  			<https://docs.github.com/search-github/searching-on-github/searching-for-repositories>
    49      `),
    50  		Example: heredoc.Doc(`
    51  			# search repositories matching set of keywords "cli" and "shell"
    52  			$ gh search repos cli shell
    53  
    54  			# search repositories matching phrase "vim plugin"
    55  			$ gh search repos "vim plugin"
    56  
    57  			# search repositories public repos in the microsoft organization
    58  			$ gh search repos --owner=microsoft --visibility=public
    59  
    60  			# search repositories with a set of topics
    61  			$ gh search repos --topic=unix,terminal
    62  
    63  			# search repositories by coding language and number of good first issues
    64  			$ gh search repos --language=go --good-first-issues=">=10"
    65  
    66  			# search repositories without topic "linux"
    67  			$ gh search repos -- -topic:linux
    68      `),
    69  		RunE: func(c *cobra.Command, args []string) error {
    70  			if len(args) == 0 && c.Flags().NFlag() == 0 {
    71  				return cmdutil.FlagErrorf("specify search keywords or flags")
    72  			}
    73  			if opts.Query.Limit < 1 || opts.Query.Limit > shared.SearchMaxResults {
    74  				return cmdutil.FlagErrorf("`--limit` must be between 1 and 1000")
    75  			}
    76  			if c.Flags().Changed("order") {
    77  				opts.Query.Order = order
    78  			}
    79  			if c.Flags().Changed("sort") {
    80  				opts.Query.Sort = sort
    81  			}
    82  			opts.Query.Keywords = args
    83  			if runF != nil {
    84  				return runF(opts)
    85  			}
    86  			var err error
    87  			opts.Searcher, err = shared.Searcher(f)
    88  			if err != nil {
    89  				return err
    90  			}
    91  			return reposRun(opts)
    92  		},
    93  	}
    94  
    95  	// Output flags
    96  	cmdutil.AddJSONFlags(cmd, &opts.Exporter, search.RepositoryFields)
    97  	cmd.Flags().BoolVarP(&opts.WebMode, "web", "w", false, "Open the search query in the web browser")
    98  
    99  	// Query parameter flags
   100  	cmd.Flags().IntVarP(&opts.Query.Limit, "limit", "L", 30, "Maximum number of repositories to fetch")
   101  	cmdutil.StringEnumFlag(cmd, &order, "order", "", "desc", []string{"asc", "desc"}, "Order of repositories returned, ignored unless '--sort' flag is specified")
   102  	cmdutil.StringEnumFlag(cmd, &sort, "sort", "", "best-match", []string{"forks", "help-wanted-issues", "stars", "updated"}, "Sort fetched repositories")
   103  
   104  	// Query qualifier flags
   105  	cmdutil.NilBoolFlag(cmd, &opts.Query.Qualifiers.Archived, "archived", "", "Filter based on archive state")
   106  	cmd.Flags().StringVar(&opts.Query.Qualifiers.Created, "created", "", "Filter based on created at `date`")
   107  	cmd.Flags().StringVar(&opts.Query.Qualifiers.Followers, "followers", "", "Filter based on `number` of followers")
   108  	cmdutil.StringEnumFlag(cmd, &opts.Query.Qualifiers.Fork, "include-forks", "", "", []string{"false", "true", "only"}, "Include forks in fetched repositories")
   109  	cmd.Flags().StringVar(&opts.Query.Qualifiers.Forks, "forks", "", "Filter on `number` of forks")
   110  	cmd.Flags().StringVar(&opts.Query.Qualifiers.GoodFirstIssues, "good-first-issues", "", "Filter on `number` of issues with the 'good first issue' label")
   111  	cmd.Flags().StringVar(&opts.Query.Qualifiers.HelpWantedIssues, "help-wanted-issues", "", "Filter on `number` of issues with the 'help wanted' label")
   112  	cmdutil.StringSliceEnumFlag(cmd, &opts.Query.Qualifiers.In, "match", "", nil, []string{"name", "description", "readme"}, "Restrict search to specific field of repository")
   113  	cmdutil.StringSliceEnumFlag(cmd, &opts.Query.Qualifiers.Is, "visibility", "", nil, []string{"public", "private", "internal"}, "Filter based on visibility")
   114  	cmd.Flags().StringVar(&opts.Query.Qualifiers.Language, "language", "", "Filter based on the coding language")
   115  	cmd.Flags().StringSliceVar(&opts.Query.Qualifiers.License, "license", nil, "Filter based on license type")
   116  	cmd.Flags().StringVar(&opts.Query.Qualifiers.Pushed, "updated", "", "Filter on last updated at `date`")
   117  	cmd.Flags().StringVar(&opts.Query.Qualifiers.Size, "size", "", "Filter on a size range, in kilobytes")
   118  	cmd.Flags().StringVar(&opts.Query.Qualifiers.Stars, "stars", "", "Filter on `number` of stars")
   119  	cmd.Flags().StringSliceVar(&opts.Query.Qualifiers.Topic, "topic", nil, "Filter on topic")
   120  	cmd.Flags().StringVar(&opts.Query.Qualifiers.Topics, "number-topics", "", "Filter on `number` of topics")
   121  	cmd.Flags().StringVar(&opts.Query.Qualifiers.User, "owner", "", "Filter on owner")
   122  
   123  	return cmd
   124  }
   125  
   126  func reposRun(opts *ReposOptions) error {
   127  	io := opts.IO
   128  	if opts.WebMode {
   129  		url := opts.Searcher.URL(opts.Query)
   130  		if io.IsStdoutTTY() {
   131  			fmt.Fprintf(io.ErrOut, "Opening %s in your browser.\n", text.DisplayURL(url))
   132  		}
   133  		return opts.Browser.Browse(url)
   134  	}
   135  	io.StartProgressIndicator()
   136  	result, err := opts.Searcher.Repositories(opts.Query)
   137  	io.StopProgressIndicator()
   138  	if err != nil {
   139  		return err
   140  	}
   141  	if len(result.Items) == 0 && opts.Exporter == nil {
   142  		return cmdutil.NewNoResultsError("no repositories matched your search")
   143  	}
   144  	if err := io.StartPager(); err == nil {
   145  		defer io.StopPager()
   146  	} else {
   147  		fmt.Fprintf(io.ErrOut, "failed to start pager: %v\n", err)
   148  	}
   149  	if opts.Exporter != nil {
   150  		return opts.Exporter.Write(io, result.Items)
   151  	}
   152  
   153  	return displayResults(io, opts.Now, result)
   154  }
   155  
   156  func displayResults(io *iostreams.IOStreams, now time.Time, results search.RepositoriesResult) error {
   157  	if now.IsZero() {
   158  		now = time.Now()
   159  	}
   160  	cs := io.ColorScheme()
   161  	//nolint:staticcheck // SA1019: utils.NewTablePrinter is deprecated: use internal/tableprinter
   162  	tp := utils.NewTablePrinter(io)
   163  	for _, repo := range results.Items {
   164  		tags := []string{visibilityLabel(repo)}
   165  		if repo.IsFork {
   166  			tags = append(tags, "fork")
   167  		}
   168  		if repo.IsArchived {
   169  			tags = append(tags, "archived")
   170  		}
   171  		info := strings.Join(tags, ", ")
   172  		infoColor := cs.Gray
   173  		if repo.IsPrivate {
   174  			infoColor = cs.Yellow
   175  		}
   176  		tp.AddField(repo.FullName, nil, cs.Bold)
   177  		description := repo.Description
   178  		tp.AddField(text.RemoveExcessiveWhitespace(description), nil, nil)
   179  		tp.AddField(info, nil, infoColor)
   180  		if tp.IsTTY() {
   181  			tp.AddField(text.FuzzyAgoAbbr(now, repo.UpdatedAt), nil, cs.Gray)
   182  		} else {
   183  			tp.AddField(repo.UpdatedAt.Format(time.RFC3339), nil, nil)
   184  		}
   185  		tp.EndRow()
   186  	}
   187  	if io.IsStdoutTTY() {
   188  		header := fmt.Sprintf("Showing %d of %d repositories\n\n", len(results.Items), results.Total)
   189  		fmt.Fprintf(io.Out, "\n%s", header)
   190  	}
   191  	return tp.Render()
   192  }
   193  
   194  func visibilityLabel(repo search.Repository) string {
   195  	if repo.Visibility != "" {
   196  		return repo.Visibility
   197  	} else if repo.IsPrivate {
   198  		return "private"
   199  	}
   200  	return "public"
   201  }