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 }