Add repo list command

This commit is contained in:
Cristian Dominguez 2021-02-11 19:43:08 -03:00
parent 962791bf27
commit 9a149d7694
3 changed files with 402 additions and 0 deletions

176
pkg/cmd/repo/list/http.go Normal file
View file

@ -0,0 +1,176 @@
package list
import (
"fmt"
"strings"
"github.com/cli/cli/api"
"github.com/shurcooL/githubv4"
)
type RepositoryList struct {
Repositories []Repository
TotalCount int
}
func listRepos(client *api.Client, hostname string, limit int, owner string, filter FilterOptions) (*RepositoryList, error) {
type reposBlock struct {
TotalCount int
RepositoryCount int
Nodes []Repository
PageInfo struct {
HasNextPage bool
EndCursor string
}
}
type response struct {
RepositoryOwner struct {
Repositories reposBlock
}
Search reposBlock
}
fragment := `
fragment repo on Repository {
nameWithOwner
description
isFork
isPrivate
isArchived
updatedAt
}`
// If `--archived` wasn't specified, use `repositoryOwner.repositores`
query := fragment + `
query RepoList($owner: String!, $per_page: Int!, $endCursor: String, $fork: Boolean, $privacy: RepositoryPrivacy) {
repositoryOwner(login: $owner) {
repositories(
first: $per_page,
after: $endCursor,
privacy: $privacy,
isFork: $fork,
ownerAffiliations: OWNER,
orderBy: { field: UPDATED_AT, direction: DESC }) {
totalCount
nodes {
...repo
}
pageInfo {
hasNextPage
endCursor
}
}
}
}`
perPage := limit
if perPage > 100 {
perPage = 100
}
variables := map[string]interface{}{
"per_page": githubv4.Int(perPage),
"endCursor": (*githubv4.String)(nil),
}
hasArchivedFilter := filter.Archived
if hasArchivedFilter {
// If `--archived` was specified, use the `search` API rather than
// `repositoryOwner.repositories`
query = fragment + `
query RepoList($per_page: Int!, $endCursor: String, $query: String!) {
search(first: $per_page, after:$endCursor, type: REPOSITORY, query: $query) {
repositoryCount
nodes {
... on Repository {
...repo
}
}
pageInfo {
hasNextPage
endCursor
}
}
}`
search := []string{fmt.Sprintf("user:%s archived:true fork:true sort:updated-desc", owner)}
switch filter.Visibility {
case "private":
search = append(search, "is:private")
case "public":
search = append(search, "is:public")
default:
search = append(search, "is:all")
}
variables["query"] = strings.Join(search, " ")
} else {
variables["owner"] = githubv4.String(owner)
if filter.Visibility != "" {
variables["privacy"] = githubv4.RepositoryPrivacy(strings.ToUpper(filter.Visibility))
} else {
variables["privacy"] = (*githubv4.RepositoryPrivacy)(nil)
}
if filter.Fork {
variables["fork"] = githubv4.Boolean(true)
} else if filter.Source {
variables["fork"] = githubv4.Boolean(false)
} else {
variables["fork"] = (*githubv4.Boolean)(nil)
}
}
var repos []Repository
var totalCount int
var result response
pagination:
for {
err := client.GraphQL(hostname, query, variables, &result)
if err != nil {
return nil, err
}
if hasArchivedFilter {
repos = append(repos, result.Search.Nodes...)
} else {
repos = append(repos, result.RepositoryOwner.Repositories.Nodes...)
}
if len(repos) >= limit {
if len(repos) > limit {
repos = repos[:limit]
}
break pagination
}
if !result.RepositoryOwner.Repositories.PageInfo.HasNextPage {
if !result.Search.PageInfo.HasNextPage {
break
}
}
variables["endCursor"] = githubv4.String(result.RepositoryOwner.Repositories.PageInfo.EndCursor)
if hasArchivedFilter {
variables["endCursor"] = githubv4.String(result.Search.PageInfo.EndCursor)
}
}
totalCount = result.RepositoryOwner.Repositories.TotalCount
if hasArchivedFilter {
totalCount = result.Search.RepositoryCount
}
listResult := &RepositoryList{
Repositories: repos,
TotalCount: totalCount,
}
return listResult, nil
}

224
pkg/cmd/repo/list/list.go Normal file
View file

@ -0,0 +1,224 @@
package list
import (
"fmt"
"net/http"
"strings"
"time"
"unicode"
"github.com/cli/cli/api"
"github.com/cli/cli/internal/ghinstance"
"github.com/cli/cli/pkg/cmdutil"
"github.com/cli/cli/pkg/iostreams"
"github.com/cli/cli/pkg/text"
"github.com/cli/cli/utils"
"github.com/spf13/cobra"
)
type FilterOptions struct {
Visibility string // private, public
Fork bool
Source bool
Archived bool
}
type ListOptions struct {
HttpClient func() (*http.Client, error)
IO *iostreams.IOStreams
Limit int
Owner string
Visibility string
Fork bool
Source bool
Archived bool
}
func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Command {
opts := ListOptions{
IO: f.IOStreams,
HttpClient: f.HttpClient,
}
var (
flagPublic bool
flagPrivate bool
flagSource bool
flagFork bool
flagArchived bool
)
cmd := &cobra.Command{
Use: "list",
Args: cobra.MaximumNArgs(1),
Short: "List repositories from a user or organization",
RunE: func(c *cobra.Command, args []string) error {
if opts.Limit < 1 {
return &cmdutil.FlagError{Err: fmt.Errorf("invalid limit: %v", opts.Limit)}
}
if flagPrivate && flagPublic {
return &cmdutil.FlagError{Err: fmt.Errorf("specify only one of `--public` or `--private`")}
}
if flagSource && flagFork {
return &cmdutil.FlagError{Err: fmt.Errorf("specify only one of `--source` or `--fork`")}
}
if flagPrivate {
opts.Visibility = "private"
} else if flagPublic {
opts.Visibility = "public"
}
opts.Archived = flagArchived
opts.Fork = flagFork
opts.Source = flagSource
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(&flagSource, "source", false, "Show only source repositories")
cmd.Flags().BoolVar(&flagArchived, "archived", false, "Show only archived repositories")
cmd.Flags().BoolVar(&flagFork, "fork", false, "Show only forks")
return cmd
}
func listRun(opts *ListOptions) error {
httpClient, err := opts.HttpClient()
if err != nil {
return err
}
apiClient := api.NewClientFromHTTP(httpClient)
isTerminal := opts.IO.IsStdoutTTY()
owner := opts.Owner
if owner == "" {
owner, err = api.CurrentLoginName(apiClient, ghinstance.OverridableDefault())
if err != nil {
return err
}
}
filter := FilterOptions{
Visibility: opts.Visibility,
Fork: opts.Fork,
Source: opts.Source,
Archived: opts.Archived,
}
listResult, err := listRepos(apiClient, ghinstance.OverridableDefault(), opts.Limit, owner, filter)
if err != nil {
return err
}
cs := opts.IO.ColorScheme()
tp := utils.NewTablePrinter(opts.IO)
notArchived := (filter.Fork || filter.Source) && !filter.Archived
matchCount := len(listResult.Repositories)
now := time.Now()
for _, repo := range listResult.Repositories {
if notArchived && repo.IsArchived {
matchCount--
listResult.TotalCount--
continue
}
nameWithOwner := repo.NameWithOwner
info := repo.Info()
infoColor := cs.Gray
visibility := "Public"
if repo.IsPrivate {
infoColor = cs.Yellow
visibility = "Private"
}
description := repo.Description
updatedAt := repo.UpdatedAt.Format(time.RFC3339)
if tp.IsTTY() {
tp.AddField(nameWithOwner, nil, cs.Bold)
tp.AddField(info, nil, infoColor)
tp.AddField(text.ReplaceExcessiveWhitespace(description), nil, nil)
tp.AddField(utils.FuzzyAgoAbbr(now, repo.UpdatedAt), nil, nil)
} else {
tp.AddField(nameWithOwner, nil, nil)
tp.AddField(visibility, nil, nil)
tp.AddField(text.ReplaceExcessiveWhitespace(description), nil, nil)
tp.AddField(updatedAt, nil, nil)
}
tp.EndRow()
}
if isTerminal {
hasFilters := filter.Visibility != "" || filter.Fork || filter.Source || filter.Archived
title := listHeader(owner, matchCount, 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"
}
return "There are no repositories in @" + owner
}
return fmt.Sprintf("Showing %d of %d repositories in @%s", matchCount, totalMatchCount, owner)
}
type Repository struct {
NameWithOwner string
Description string
IsFork bool
IsPrivate bool
IsArchived bool
UpdatedAt time.Time
}
func (r Repository) Info() string {
var info string
if r.IsPrivate {
info = "private"
}
if r.IsFork {
info += " fork"
}
if r.IsArchived {
info += " archived"
}
if info != "" {
info = strings.TrimPrefix(info, " ")
infoRunes := []rune(info)
infoRunes[0] = unicode.ToUpper(infoRunes[0])
info = fmt.Sprintf("(%s)", string(infoRunes))
}
return info
}

View file

@ -7,6 +7,7 @@ import (
creditsCmd "github.com/cli/cli/pkg/cmd/repo/credits"
repoForkCmd "github.com/cli/cli/pkg/cmd/repo/fork"
gardenCmd "github.com/cli/cli/pkg/cmd/repo/garden"
repoListCmd "github.com/cli/cli/pkg/cmd/repo/list"
repoViewCmd "github.com/cli/cli/pkg/cmd/repo/view"
"github.com/cli/cli/pkg/cmdutil"
"github.com/spf13/cobra"
@ -36,6 +37,7 @@ func NewCmdRepo(f *cmdutil.Factory) *cobra.Command {
cmd.AddCommand(repoForkCmd.NewCmdFork(f, nil))
cmd.AddCommand(repoCloneCmd.NewCmdClone(f, nil))
cmd.AddCommand(repoCreateCmd.NewCmdCreate(f, nil))
cmd.AddCommand(repoListCmd.NewCmdList(f, nil))
cmd.AddCommand(creditsCmd.NewCmdRepoCredits(f, nil))
cmd.AddCommand(gardenCmd.NewCmdGarden(f, nil))