Add repo list command
This commit is contained in:
parent
962791bf27
commit
9a149d7694
3 changed files with 402 additions and 0 deletions
176
pkg/cmd/repo/list/http.go
Normal file
176
pkg/cmd/repo/list/http.go
Normal 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
224
pkg/cmd/repo/list/list.go
Normal 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
|
||||
}
|
||||
|
|
@ -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))
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue