cli/pkg/cmd/repo/list/list.go
2021-10-21 11:40:20 -04:00

219 lines
5.5 KiB
Go

package list
import (
"fmt"
"net/http"
"strings"
"time"
"github.com/cli/cli/v2/api"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/cli/cli/v2/pkg/text"
"github.com/cli/cli/v2/utils"
"github.com/spf13/cobra"
)
type ListOptions struct {
HttpClient func() (*http.Client, error)
Config func() (config.Config, error)
IO *iostreams.IOStreams
Exporter cmdutil.Exporter
Limit int
Owner string
Visibility string
Fork bool
Source bool
Language string
Topic string
Archived bool
NonArchived bool
Now func() time.Time
}
func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Command {
opts := ListOptions{
IO: f.IOStreams,
Config: f.Config,
HttpClient: f.HttpClient,
Now: time.Now,
}
var (
flagPublic bool
flagPrivate bool
)
cmd := &cobra.Command{
Use: "list [<owner>]",
Args: cobra.MaximumNArgs(1),
Short: "List repositories owned by user or organization",
RunE: func(c *cobra.Command, args []string) error {
if opts.Limit < 1 {
return cmdutil.FlagErrorf("invalid limit: %v", opts.Limit)
}
if flagPrivate && flagPublic {
return cmdutil.FlagErrorf("specify only one of `--public` or `--private`")
}
if opts.Source && opts.Fork {
return cmdutil.FlagErrorf("specify only one of `--source` or `--fork`")
}
if opts.Archived && opts.NonArchived {
return cmdutil.FlagErrorf("specify only one of `--archived` or `--no-archived`")
}
if flagPrivate {
opts.Visibility = "private"
} else if flagPublic {
opts.Visibility = "public"
}
if len(args) > 0 {
opts.Owner = args[0]
}
if runF != nil {
return runF(&opts)
}
return listRun(&opts)
},
}
cmd.Flags().IntVarP(&opts.Limit, "limit", "L", 30, "Maximum number of repositories to list")
cmd.Flags().BoolVar(&flagPrivate, "private", false, "Show only private repositories")
cmd.Flags().BoolVar(&flagPublic, "public", false, "Show only public repositories")
cmd.Flags().BoolVar(&opts.Source, "source", false, "Show only non-forks")
cmd.Flags().BoolVar(&opts.Fork, "fork", false, "Show only forks")
cmd.Flags().StringVarP(&opts.Language, "language", "l", "", "Filter by primary coding language")
cmd.Flags().StringVar(&opts.Topic, "topic", "", "Filter by topic")
cmd.Flags().BoolVar(&opts.Archived, "archived", false, "Show only archived repositories")
cmd.Flags().BoolVar(&opts.NonArchived, "no-archived", false, "Omit archived repositories")
cmdutil.AddJSONFlags(cmd, &opts.Exporter, api.RepositoryFields)
return cmd
}
var defaultFields = []string{"nameWithOwner", "description", "isPrivate", "isFork", "isArchived", "createdAt", "pushedAt"}
func listRun(opts *ListOptions) error {
httpClient, err := opts.HttpClient()
if err != nil {
return err
}
filter := FilterOptions{
Visibility: opts.Visibility,
Fork: opts.Fork,
Source: opts.Source,
Language: opts.Language,
Topic: opts.Topic,
Archived: opts.Archived,
NonArchived: opts.NonArchived,
Fields: defaultFields,
}
if opts.Exporter != nil {
filter.Fields = opts.Exporter.Fields()
}
cfg, err := opts.Config()
if err != nil {
return err
}
host, err := cfg.DefaultHost()
if err != nil {
return err
}
listResult, err := listRepos(httpClient, host, opts.Limit, opts.Owner, filter)
if err != nil {
return err
}
if err := opts.IO.StartPager(); 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.Repositories)
}
cs := opts.IO.ColorScheme()
tp := utils.NewTablePrinter(opts.IO)
now := opts.Now()
for _, repo := range listResult.Repositories {
info := repoInfo(repo)
infoColor := cs.Gray
if repo.IsPrivate {
infoColor = cs.Yellow
}
t := repo.PushedAt
if repo.PushedAt == nil {
t = &repo.CreatedAt
}
tp.AddField(repo.NameWithOwner, nil, cs.Bold)
tp.AddField(text.ReplaceExcessiveWhitespace(repo.Description), nil, nil)
tp.AddField(info, nil, infoColor)
if tp.IsTTY() {
tp.AddField(utils.FuzzyAgoAbbr(now, *t), nil, cs.Gray)
} else {
tp.AddField(t.Format(time.RFC3339), nil, nil)
}
tp.EndRow()
}
if listResult.FromSearch && opts.Limit > 1000 {
fmt.Fprintln(opts.IO.ErrOut, "warning: this query uses the Search API which is capped at 1000 results maximum")
}
if opts.IO.IsStdoutTTY() {
hasFilters := filter.Visibility != "" || filter.Fork || filter.Source || filter.Language != "" || filter.Topic != ""
title := listHeader(listResult.Owner, len(listResult.Repositories), listResult.TotalCount, hasFilters)
fmt.Fprintf(opts.IO.Out, "\n%s\n\n", title)
}
return tp.Render()
}
func listHeader(owner string, matchCount, totalMatchCount int, hasFilters bool) string {
if totalMatchCount == 0 {
if hasFilters {
return "No results match your search"
} else if owner != "" {
return "There are no repositories in @" + owner
}
return "No results"
}
var matchStr string
if hasFilters {
matchStr = " that match your search"
}
return fmt.Sprintf("Showing %d of %d repositories in @%s%s", matchCount, totalMatchCount, owner, matchStr)
}
func repoInfo(r api.Repository) string {
var tags []string
if r.IsPrivate {
tags = append(tags, "private")
} else {
tags = append(tags, "public")
}
if r.IsFork {
tags = append(tags, "fork")
}
if r.IsArchived {
tags = append(tags, "archived")
}
return strings.Join(tags, ", ")
}