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:
Mislav Marohnić 2021-02-27 13:17:59 +01:00
parent 2284ef43d0
commit 4da02614ed
4 changed files with 75 additions and 142 deletions

View file

@ -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": {

View file

@ -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
}

View file

@ -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
}

View file

@ -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"])
}),
)