github.com/ungtb10d/cli/v2@v2.0.0-20221110210412-98537dd9d6a1/pkg/cmd/repo/list/list.go (about) 1 package list 2 3 import ( 4 "fmt" 5 "net/http" 6 "strings" 7 "time" 8 9 "github.com/spf13/cobra" 10 11 "github.com/ungtb10d/cli/v2/api" 12 "github.com/ungtb10d/cli/v2/internal/config" 13 fd "github.com/ungtb10d/cli/v2/internal/featuredetection" 14 "github.com/ungtb10d/cli/v2/internal/text" 15 "github.com/ungtb10d/cli/v2/pkg/cmdutil" 16 "github.com/ungtb10d/cli/v2/pkg/iostreams" 17 "github.com/ungtb10d/cli/v2/utils" 18 ) 19 20 type ListOptions struct { 21 HttpClient func() (*http.Client, error) 22 Config func() (config.Config, error) 23 IO *iostreams.IOStreams 24 Exporter cmdutil.Exporter 25 Detector fd.Detector 26 27 Limit int 28 Owner string 29 30 Visibility string 31 Fork bool 32 Source bool 33 Language string 34 Topic []string 35 Archived bool 36 NonArchived bool 37 38 Now func() time.Time 39 } 40 41 func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Command { 42 opts := ListOptions{ 43 IO: f.IOStreams, 44 Config: f.Config, 45 HttpClient: f.HttpClient, 46 Now: time.Now, 47 } 48 49 var ( 50 flagPublic bool 51 flagPrivate bool 52 ) 53 54 cmd := &cobra.Command{ 55 Use: "list [<owner>]", 56 Args: cobra.MaximumNArgs(1), 57 Short: "List repositories owned by user or organization", 58 Aliases: []string{"ls"}, 59 RunE: func(c *cobra.Command, args []string) error { 60 if opts.Limit < 1 { 61 return cmdutil.FlagErrorf("invalid limit: %v", opts.Limit) 62 } 63 64 if err := cmdutil.MutuallyExclusive("specify only one of `--public`, `--private`, or `--visibility`", flagPublic, flagPrivate, opts.Visibility != ""); err != nil { 65 return err 66 } 67 if opts.Source && opts.Fork { 68 return cmdutil.FlagErrorf("specify only one of `--source` or `--fork`") 69 } 70 if opts.Archived && opts.NonArchived { 71 return cmdutil.FlagErrorf("specify only one of `--archived` or `--no-archived`") 72 } 73 74 if flagPrivate { 75 opts.Visibility = "private" 76 } else if flagPublic { 77 opts.Visibility = "public" 78 } 79 80 if len(args) > 0 { 81 opts.Owner = args[0] 82 } 83 84 if runF != nil { 85 return runF(&opts) 86 } 87 return listRun(&opts) 88 }, 89 } 90 91 cmd.Flags().IntVarP(&opts.Limit, "limit", "L", 30, "Maximum number of repositories to list") 92 cmd.Flags().BoolVar(&opts.Source, "source", false, "Show only non-forks") 93 cmd.Flags().BoolVar(&opts.Fork, "fork", false, "Show only forks") 94 cmd.Flags().StringVarP(&opts.Language, "language", "l", "", "Filter by primary coding language") 95 cmd.Flags().StringSliceVarP(&opts.Topic, "topic", "", nil, "Filter by topic") 96 cmdutil.StringEnumFlag(cmd, &opts.Visibility, "visibility", "", "", []string{"public", "private", "internal"}, "Filter by repository visibility") 97 cmd.Flags().BoolVar(&opts.Archived, "archived", false, "Show only archived repositories") 98 cmd.Flags().BoolVar(&opts.NonArchived, "no-archived", false, "Omit archived repositories") 99 cmdutil.AddJSONFlags(cmd, &opts.Exporter, api.RepositoryFields) 100 101 cmd.Flags().BoolVar(&flagPrivate, "private", false, "Show only private repositories") 102 cmd.Flags().BoolVar(&flagPublic, "public", false, "Show only public repositories") 103 _ = cmd.Flags().MarkDeprecated("public", "use `--visibility=public` instead") 104 _ = cmd.Flags().MarkDeprecated("private", "use `--visibility=private` instead") 105 106 return cmd 107 } 108 109 var defaultFields = []string{"nameWithOwner", "description", "isPrivate", "isFork", "isArchived", "createdAt", "pushedAt"} 110 111 func listRun(opts *ListOptions) error { 112 httpClient, err := opts.HttpClient() 113 if err != nil { 114 return err 115 } 116 117 cfg, err := opts.Config() 118 if err != nil { 119 return err 120 } 121 122 host, _ := cfg.DefaultHost() 123 124 if opts.Detector == nil { 125 cachedClient := api.NewCachedHTTPClient(httpClient, time.Hour*24) 126 opts.Detector = fd.NewDetector(cachedClient, host) 127 } 128 features, err := opts.Detector.RepositoryFeatures() 129 if err != nil { 130 return err 131 } 132 133 fields := defaultFields 134 if features.VisibilityField { 135 fields = append(defaultFields, "visibility") 136 } 137 138 filter := FilterOptions{ 139 Visibility: opts.Visibility, 140 Fork: opts.Fork, 141 Source: opts.Source, 142 Language: opts.Language, 143 Topic: opts.Topic, 144 Archived: opts.Archived, 145 NonArchived: opts.NonArchived, 146 Fields: fields, 147 } 148 if opts.Exporter != nil { 149 filter.Fields = opts.Exporter.Fields() 150 } 151 152 listResult, err := listRepos(httpClient, host, opts.Limit, opts.Owner, filter) 153 if err != nil { 154 return err 155 } 156 157 if err := opts.IO.StartPager(); err != nil { 158 fmt.Fprintf(opts.IO.ErrOut, "error starting pager: %v\n", err) 159 } 160 defer opts.IO.StopPager() 161 162 if opts.Exporter != nil { 163 return opts.Exporter.Write(opts.IO, listResult.Repositories) 164 } 165 166 cs := opts.IO.ColorScheme() 167 //nolint:staticcheck // SA1019: utils.NewTablePrinter is deprecated: use internal/tableprinter 168 tp := utils.NewTablePrinter(opts.IO) 169 170 for _, repo := range listResult.Repositories { 171 info := repoInfo(repo) 172 infoColor := cs.Gray 173 174 if repo.IsPrivate { 175 infoColor = cs.Yellow 176 } 177 178 t := repo.PushedAt 179 if repo.PushedAt == nil { 180 t = &repo.CreatedAt 181 } 182 183 tp.AddField(repo.NameWithOwner, nil, cs.Bold) 184 tp.AddField(text.RemoveExcessiveWhitespace(repo.Description), nil, nil) 185 tp.AddField(info, nil, infoColor) 186 if tp.IsTTY() { 187 tp.AddField(text.FuzzyAgoAbbr(opts.Now(), *t), nil, cs.Gray) 188 } else { 189 tp.AddField(t.Format(time.RFC3339), nil, nil) 190 } 191 tp.EndRow() 192 } 193 194 if listResult.FromSearch && opts.Limit > 1000 { 195 fmt.Fprintln(opts.IO.ErrOut, "warning: this query uses the Search API which is capped at 1000 results maximum") 196 } 197 if opts.IO.IsStdoutTTY() { 198 hasFilters := filter.Visibility != "" || filter.Fork || filter.Source || filter.Language != "" || len(filter.Topic) > 0 199 title := listHeader(listResult.Owner, len(listResult.Repositories), listResult.TotalCount, hasFilters) 200 fmt.Fprintf(opts.IO.Out, "\n%s\n\n", title) 201 } 202 203 return tp.Render() 204 } 205 206 func listHeader(owner string, matchCount, totalMatchCount int, hasFilters bool) string { 207 if totalMatchCount == 0 { 208 if hasFilters { 209 return "No results match your search" 210 } else if owner != "" { 211 return "There are no repositories in @" + owner 212 } 213 return "No results" 214 } 215 216 var matchStr string 217 if hasFilters { 218 matchStr = " that match your search" 219 } 220 return fmt.Sprintf("Showing %d of %d repositories in @%s%s", matchCount, totalMatchCount, owner, matchStr) 221 } 222 223 func repoInfo(r api.Repository) string { 224 tags := []string{visibilityLabel(r)} 225 226 if r.IsFork { 227 tags = append(tags, "fork") 228 } 229 if r.IsArchived { 230 tags = append(tags, "archived") 231 } 232 233 return strings.Join(tags, ", ") 234 } 235 236 func visibilityLabel(repo api.Repository) string { 237 if repo.Visibility != "" { 238 return strings.ToLower(repo.Visibility) 239 } else if repo.IsPrivate { 240 return "private" 241 } 242 return "public" 243 }