Switch repo list to query via graphql package
Also order results by PUSHED_AT instead of UPDATED_AT to match the web interface.
This commit is contained in:
parent
2284ef43d0
commit
4da02614ed
4 changed files with 75 additions and 142 deletions
|
|
@ -10,7 +10,7 @@
|
|||
"isFork": false,
|
||||
"isPrivate": false,
|
||||
"isArchived": false,
|
||||
"updatedAt": "2021-02-19T06:34:58Z"
|
||||
"pushedAt": "2021-02-19T06:34:58Z"
|
||||
},
|
||||
{
|
||||
"nameWithOwner": "octocat/cli",
|
||||
|
|
@ -18,7 +18,7 @@
|
|||
"isFork": true,
|
||||
"isPrivate": false,
|
||||
"isArchived": false,
|
||||
"updatedAt": "2021-02-19T06:06:06Z"
|
||||
"pushedAt": "2021-02-19T06:06:06Z"
|
||||
},
|
||||
{
|
||||
"nameWithOwner": "octocat/testing",
|
||||
|
|
@ -26,7 +26,7 @@
|
|||
"isFork": false,
|
||||
"isPrivate": true,
|
||||
"isArchived": false,
|
||||
"updatedAt": "2021-02-11T22:32:05Z"
|
||||
"pushedAt": "2021-02-11T22:32:05Z"
|
||||
}
|
||||
],
|
||||
"pageInfo": {
|
||||
|
|
|
|||
|
|
@ -1,59 +1,62 @@
|
|||
package list
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cli/cli/api"
|
||||
"github.com/cli/cli/internal/ghinstance"
|
||||
"github.com/shurcooL/githubv4"
|
||||
"github.com/shurcooL/graphql"
|
||||
)
|
||||
|
||||
type Repository struct {
|
||||
NameWithOwner string
|
||||
Description string
|
||||
IsFork bool
|
||||
IsPrivate bool
|
||||
IsArchived bool
|
||||
PushedAt time.Time
|
||||
}
|
||||
|
||||
func (r Repository) Info() 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, ", ")
|
||||
}
|
||||
|
||||
type RepositoryList struct {
|
||||
Repositories []Repository
|
||||
TotalCount int
|
||||
}
|
||||
|
||||
func listRepos(client *api.Client, hostname string, limit int, owner string, filter FilterOptions) (*RepositoryList, error) {
|
||||
type response struct {
|
||||
func listRepos(client *http.Client, hostname string, limit int, owner string, filter FilterOptions) (*RepositoryList, error) {
|
||||
type query struct {
|
||||
RepositoryOwner struct {
|
||||
Repositories struct {
|
||||
TotalCount int
|
||||
RepositoryCount int
|
||||
Nodes []Repository
|
||||
PageInfo struct {
|
||||
Nodes []Repository
|
||||
TotalCount int
|
||||
PageInfo struct {
|
||||
HasNextPage bool
|
||||
EndCursor string
|
||||
}
|
||||
}
|
||||
}
|
||||
} `graphql:"repositories(first: $perPage, after: $endCursor, privacy: $privacy, isFork: $fork, ownerAffiliations: OWNER, orderBy: { field: PUSHED_AT, direction: DESC })"`
|
||||
} `graphql:"repositoryOwner(login: $owner)"`
|
||||
}
|
||||
|
||||
query := `
|
||||
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 {
|
||||
nameWithOwner
|
||||
description
|
||||
isFork
|
||||
isPrivate
|
||||
isArchived
|
||||
updatedAt
|
||||
}
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
endCursor
|
||||
}
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
perPage := limit
|
||||
if perPage > 100 {
|
||||
perPage = 100
|
||||
|
|
@ -61,7 +64,7 @@ func listRepos(client *api.Client, hostname string, limit int, owner string, fil
|
|||
|
||||
variables := map[string]interface{}{
|
||||
"owner": githubv4.String(owner),
|
||||
"per_page": githubv4.Int(perPage),
|
||||
"perPage": githubv4.Int(perPage),
|
||||
"endCursor": (*githubv4.String)(nil),
|
||||
}
|
||||
|
||||
|
|
@ -79,40 +82,29 @@ func listRepos(client *api.Client, hostname string, limit int, owner string, fil
|
|||
variables["fork"] = (*githubv4.Boolean)(nil)
|
||||
}
|
||||
|
||||
var repos []Repository
|
||||
var totalCount int
|
||||
|
||||
var result response
|
||||
|
||||
listResult := RepositoryList{}
|
||||
pagination:
|
||||
for {
|
||||
err := client.GraphQL(hostname, query, variables, &result)
|
||||
var result query
|
||||
gql := graphql.NewClient(ghinstance.GraphQLEndpoint(hostname), client)
|
||||
err := gql.QueryNamed(context.Background(), "RepositoryList", &result, variables)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
repos = append(repos, result.RepositoryOwner.Repositories.Nodes...)
|
||||
|
||||
if len(repos) >= limit {
|
||||
if len(repos) > limit {
|
||||
repos = repos[:limit]
|
||||
listResult.TotalCount = result.RepositoryOwner.Repositories.TotalCount
|
||||
for _, repo := range result.RepositoryOwner.Repositories.Nodes {
|
||||
listResult.Repositories = append(listResult.Repositories, repo)
|
||||
if len(listResult.Repositories) >= limit {
|
||||
break pagination
|
||||
}
|
||||
break pagination
|
||||
}
|
||||
|
||||
if !result.RepositoryOwner.Repositories.PageInfo.HasNextPage {
|
||||
break
|
||||
}
|
||||
|
||||
variables["endCursor"] = githubv4.String(result.RepositoryOwner.Repositories.PageInfo.EndCursor)
|
||||
}
|
||||
|
||||
totalCount = result.RepositoryOwner.Repositories.TotalCount
|
||||
|
||||
listResult := &RepositoryList{
|
||||
Repositories: repos,
|
||||
TotalCount: totalCount,
|
||||
}
|
||||
|
||||
return listResult, nil
|
||||
return &listResult, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ package list
|
|||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cli/cli/api"
|
||||
|
|
@ -45,14 +44,12 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman
|
|||
var (
|
||||
flagPublic bool
|
||||
flagPrivate bool
|
||||
flagSource bool
|
||||
flagFork bool
|
||||
)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "list",
|
||||
Use: "list [<owner>]",
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
Short: "List repositories from a user or organization",
|
||||
Short: "List repositories owned by 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)}
|
||||
|
|
@ -61,7 +58,7 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman
|
|||
if flagPrivate && flagPublic {
|
||||
return &cmdutil.FlagError{Err: fmt.Errorf("specify only one of `--public` or `--private`")}
|
||||
}
|
||||
if flagSource && flagFork {
|
||||
if opts.Source && opts.Fork {
|
||||
return &cmdutil.FlagError{Err: fmt.Errorf("specify only one of `--source` or `--fork`")}
|
||||
}
|
||||
|
||||
|
|
@ -71,9 +68,6 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman
|
|||
opts.Visibility = "public"
|
||||
}
|
||||
|
||||
opts.Fork = flagFork
|
||||
opts.Source = flagSource
|
||||
|
||||
if len(args) > 0 {
|
||||
opts.Owner = args[0]
|
||||
}
|
||||
|
|
@ -88,8 +82,8 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman
|
|||
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(&flagFork, "fork", false, "Show only forks")
|
||||
cmd.Flags().BoolVar(&opts.Source, "source", false, "Show only non-forks")
|
||||
cmd.Flags().BoolVar(&opts.Fork, "fork", false, "Show only forks")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
|
@ -118,58 +112,36 @@ func listRun(opts *ListOptions) error {
|
|||
Source: opts.Source,
|
||||
}
|
||||
|
||||
listResult, err := listRepos(apiClient, ghinstance.OverridableDefault(), opts.Limit, owner, filter)
|
||||
listResult, err := listRepos(httpClient, 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
|
||||
|
||||
matchCount := len(listResult.Repositories)
|
||||
now := opts.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)
|
||||
|
||||
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(nameWithOwner, nil, cs.Bold)
|
||||
tp.AddField(text.ReplaceExcessiveWhitespace(description), nil, nil)
|
||||
tp.AddField(info, nil, infoColor)
|
||||
tp.AddField(utils.FuzzyAgoAbbr(now, repo.UpdatedAt), nil, nil)
|
||||
tp.AddField(utils.FuzzyAgoAbbr(now, repo.PushedAt), nil, cs.Gray)
|
||||
} else {
|
||||
tp.AddField(nameWithOwner, nil, nil)
|
||||
tp.AddField(text.ReplaceExcessiveWhitespace(description), nil, nil)
|
||||
tp.AddField(visibility, nil, nil)
|
||||
tp.AddField(updatedAt, nil, nil)
|
||||
tp.AddField(repo.PushedAt.Format(time.RFC3339), nil, nil)
|
||||
}
|
||||
tp.EndRow()
|
||||
}
|
||||
|
||||
if isTerminal {
|
||||
hasFilters := filter.Visibility != "" || filter.Fork || filter.Source
|
||||
title := listHeader(owner, matchCount, listResult.TotalCount, hasFilters)
|
||||
title := listHeader(owner, len(listResult.Repositories), listResult.TotalCount, hasFilters)
|
||||
fmt.Fprintf(opts.IO.Out, "\n%s\n\n", title)
|
||||
}
|
||||
|
||||
|
|
@ -186,34 +158,3 @@ func listHeader(owner string, matchCount, totalMatchCount int, hasFilters bool)
|
|||
|
||||
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
|
||||
var tags []string
|
||||
|
||||
if r.IsPrivate {
|
||||
tags = append(tags, "private")
|
||||
}
|
||||
if r.IsFork {
|
||||
tags = append(tags, "fork")
|
||||
}
|
||||
if r.IsArchived {
|
||||
tags = append(tags, "archived")
|
||||
}
|
||||
|
||||
if len(tags) > 0 {
|
||||
tags[0] = strings.Title(tags[0])
|
||||
info = strings.Join(tags, ", ")
|
||||
}
|
||||
|
||||
return info
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ func TestRepoList_nontty(t *testing.T) {
|
|||
httpmock.StringResponse(`{"data":{"viewer":{"login":"octocat"}}}`),
|
||||
)
|
||||
httpReg.Register(
|
||||
httpmock.GraphQL(`query RepoList\b`),
|
||||
httpmock.GraphQL(`query RepositoryList\b`),
|
||||
httpmock.FileResponse("./fixtures/repoList.json"),
|
||||
)
|
||||
|
||||
|
|
@ -84,9 +84,9 @@ func TestRepoList_nontty(t *testing.T) {
|
|||
assert.Equal(t, "", stderr.String())
|
||||
|
||||
assert.Equal(t, heredoc.Doc(`
|
||||
octocat/hello-world My first repository Public 2021-02-19T06:34:58Z
|
||||
octocat/cli GitHub CLI Public 2021-02-19T06:06:06Z
|
||||
octocat/testing Private 2021-02-11T22:32:05Z
|
||||
octocat/hello-world My first repository public 2021-02-19T06:34:58Z
|
||||
octocat/cli GitHub CLI public, fork 2021-02-19T06:06:06Z
|
||||
octocat/testing private 2021-02-11T22:32:05Z
|
||||
`), stdout.String())
|
||||
}
|
||||
|
||||
|
|
@ -104,7 +104,7 @@ func TestRepoList_tty(t *testing.T) {
|
|||
httpmock.StringResponse(`{"data":{"viewer":{"login":"octocat"}}}`),
|
||||
)
|
||||
httpReg.Register(
|
||||
httpmock.GraphQL(`query RepoList\b`),
|
||||
httpmock.GraphQL(`query RepositoryList\b`),
|
||||
httpmock.FileResponse("./fixtures/repoList.json"),
|
||||
)
|
||||
|
||||
|
|
@ -129,9 +129,9 @@ func TestRepoList_tty(t *testing.T) {
|
|||
|
||||
Showing 3 of 3 repositories in @octocat
|
||||
|
||||
octocat/hello-world My first repository 8h
|
||||
octocat/cli GitHub CLI Fork 8h
|
||||
octocat/testing Private 7d
|
||||
octocat/hello-world My first repository public 8h
|
||||
octocat/cli GitHub CLI public, fork 8h
|
||||
octocat/testing private 7d
|
||||
`), stdout.String())
|
||||
}
|
||||
|
||||
|
|
@ -144,10 +144,10 @@ func TestRepoList_filtering(t *testing.T) {
|
|||
httpmock.StringResponse(`{"data":{"viewer":{"login":"octocat"}}}`),
|
||||
)
|
||||
http.Register(
|
||||
httpmock.GraphQL(`query RepoList\b`),
|
||||
httpmock.GraphQL(`query RepositoryList\b`),
|
||||
httpmock.GraphQLQuery(`{}`, func(_ string, params map[string]interface{}) {
|
||||
assert.Equal(t, "PRIVATE", params["privacy"])
|
||||
assert.Equal(t, float64(2), params["per_page"])
|
||||
assert.Equal(t, float64(2), params["perPage"])
|
||||
}),
|
||||
)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue