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  }