Enable filtering repo list by coding language
This commit is contained in:
parent
f75144dd1f
commit
5da8301d5d
5 changed files with 308 additions and 10 deletions
37
pkg/cmd/repo/list/fixtures/repoSearch.json
Normal file
37
pkg/cmd/repo/list/fixtures/repoSearch.json
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
{
|
||||
"data": {
|
||||
"search": {
|
||||
"repositoryCount": 3,
|
||||
"nodes": [
|
||||
{
|
||||
"nameWithOwner": "octocat/hello-world",
|
||||
"description": "My first repository",
|
||||
"isFork": false,
|
||||
"isPrivate": false,
|
||||
"isArchived": false,
|
||||
"pushedAt": "2021-02-19T06:34:58Z"
|
||||
},
|
||||
{
|
||||
"nameWithOwner": "octocat/cli",
|
||||
"description": "GitHub CLI",
|
||||
"isFork": true,
|
||||
"isPrivate": false,
|
||||
"isArchived": false,
|
||||
"pushedAt": "2021-02-19T06:06:06Z"
|
||||
},
|
||||
{
|
||||
"nameWithOwner": "octocat/testing",
|
||||
"description": null,
|
||||
"isFork": false,
|
||||
"isPrivate": true,
|
||||
"isArchived": false,
|
||||
"pushedAt": "2021-02-11T22:32:05Z"
|
||||
}
|
||||
],
|
||||
"pageInfo": {
|
||||
"hasNextPage": false,
|
||||
"endCursor": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package list
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
|
@ -45,7 +46,18 @@ type RepositoryList struct {
|
|||
TotalCount int
|
||||
}
|
||||
|
||||
type FilterOptions struct {
|
||||
Visibility string // private, public
|
||||
Fork bool
|
||||
Source bool
|
||||
Language string
|
||||
}
|
||||
|
||||
func listRepos(client *http.Client, hostname string, limit int, owner string, filter FilterOptions) (*RepositoryList, error) {
|
||||
if filter.Language != "" {
|
||||
return searchRepos(client, hostname, limit, owner, filter)
|
||||
}
|
||||
|
||||
perPage := limit
|
||||
if perPage > 100 {
|
||||
perPage = 100
|
||||
|
|
@ -97,11 +109,11 @@ func listRepos(client *http.Client, hostname string, limit int, owner string, fi
|
|||
},
|
||||
})
|
||||
|
||||
gql := graphql.NewClient(ghinstance.GraphQLEndpoint(hostname), client)
|
||||
listResult := RepositoryList{}
|
||||
pagination:
|
||||
for {
|
||||
result := reflect.New(query)
|
||||
gql := graphql.NewClient(ghinstance.GraphQLEndpoint(hostname), client)
|
||||
err := gql.QueryNamed(context.Background(), "RepositoryList", result.Interface(), variables)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -126,3 +138,89 @@ pagination:
|
|||
|
||||
return &listResult, nil
|
||||
}
|
||||
|
||||
func searchRepos(client *http.Client, hostname string, limit int, owner string, filter FilterOptions) (*RepositoryList, error) {
|
||||
type query struct {
|
||||
Search struct {
|
||||
RepositoryCount int
|
||||
Nodes []struct {
|
||||
Repository Repository `graphql:"...on Repository"`
|
||||
}
|
||||
PageInfo struct {
|
||||
HasNextPage bool
|
||||
EndCursor string
|
||||
}
|
||||
} `graphql:"search(type: REPOSITORY, query: $query, first: $perPage, after: $endCursor)"`
|
||||
}
|
||||
|
||||
perPage := limit
|
||||
if perPage > 100 {
|
||||
perPage = 100
|
||||
}
|
||||
|
||||
variables := map[string]interface{}{
|
||||
"query": githubv4.String(searchQuery(owner, filter)),
|
||||
"perPage": githubv4.Int(perPage),
|
||||
"endCursor": (*githubv4.String)(nil),
|
||||
}
|
||||
|
||||
gql := graphql.NewClient(ghinstance.GraphQLEndpoint(hostname), client)
|
||||
listResult := RepositoryList{}
|
||||
pagination:
|
||||
for {
|
||||
var result query
|
||||
err := gql.QueryNamed(context.Background(), "RepositoryListSearch", &result, variables)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
listResult.TotalCount = result.Search.RepositoryCount
|
||||
for _, node := range result.Search.Nodes {
|
||||
if listResult.Owner == "" {
|
||||
idx := strings.IndexRune(node.Repository.NameWithOwner, '/')
|
||||
listResult.Owner = node.Repository.NameWithOwner[:idx]
|
||||
}
|
||||
listResult.Repositories = append(listResult.Repositories, node.Repository)
|
||||
if len(listResult.Repositories) >= limit {
|
||||
break pagination
|
||||
}
|
||||
}
|
||||
|
||||
if !result.Search.PageInfo.HasNextPage {
|
||||
break
|
||||
}
|
||||
variables["endCursor"] = githubv4.String(result.Search.PageInfo.EndCursor)
|
||||
}
|
||||
|
||||
return &listResult, nil
|
||||
}
|
||||
|
||||
func searchQuery(owner string, filter FilterOptions) string {
|
||||
queryParts := []string{"sort:updated-desc"}
|
||||
if owner == "" {
|
||||
queryParts = append(queryParts, "user:@me")
|
||||
} else {
|
||||
queryParts = append(queryParts, "user:"+owner)
|
||||
}
|
||||
|
||||
if filter.Fork {
|
||||
queryParts = append(queryParts, "fork:only")
|
||||
} else if filter.Source {
|
||||
queryParts = append(queryParts, "fork:false")
|
||||
} else {
|
||||
queryParts = append(queryParts, "fork:true")
|
||||
}
|
||||
|
||||
if filter.Language != "" {
|
||||
queryParts = append(queryParts, fmt.Sprintf("language:%q", filter.Language))
|
||||
}
|
||||
|
||||
switch filter.Visibility {
|
||||
case "public":
|
||||
queryParts = append(queryParts, "is:public")
|
||||
case "private":
|
||||
queryParts = append(queryParts, "is:private")
|
||||
}
|
||||
|
||||
return strings.Join(queryParts, " ")
|
||||
}
|
||||
|
|
|
|||
141
pkg/cmd/repo/list/http_test.go
Normal file
141
pkg/cmd/repo/list/http_test.go
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
package list
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/pkg/httpmock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_listReposWithLanguage(t *testing.T) {
|
||||
reg := httpmock.Registry{}
|
||||
defer reg.Verify(t)
|
||||
|
||||
var searchData struct {
|
||||
Query string
|
||||
Variables map[string]interface{}
|
||||
}
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query RepositoryListSearch\b`),
|
||||
func(req *http.Request) (*http.Response, error) {
|
||||
jsonData, err := ioutil.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = json.Unmarshal(jsonData, &searchData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
respBody, err := os.Open("./fixtures/repoSearch.json")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &http.Response{
|
||||
StatusCode: 200,
|
||||
Request: req,
|
||||
Body: respBody,
|
||||
}, nil
|
||||
},
|
||||
)
|
||||
|
||||
client := http.Client{Transport: ®}
|
||||
res, err := listRepos(&client, "github.com", 10, "", FilterOptions{
|
||||
Language: "go",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, 3, res.TotalCount)
|
||||
assert.Equal(t, "octocat", res.Owner)
|
||||
assert.Equal(t, "octocat/hello-world", res.Repositories[0].NameWithOwner)
|
||||
|
||||
assert.Equal(t, float64(10), searchData.Variables["perPage"])
|
||||
assert.Equal(t, `sort:updated-desc user:@me fork:true language:"go"`, searchData.Variables["query"])
|
||||
}
|
||||
|
||||
func Test_searchQuery(t *testing.T) {
|
||||
type args struct {
|
||||
owner string
|
||||
filter FilterOptions
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "blank",
|
||||
want: "sort:updated-desc user:@me fork:true",
|
||||
},
|
||||
{
|
||||
name: "in org",
|
||||
args: args{
|
||||
owner: "cli",
|
||||
},
|
||||
want: "sort:updated-desc user:cli fork:true",
|
||||
},
|
||||
{
|
||||
name: "only public",
|
||||
args: args{
|
||||
owner: "",
|
||||
filter: FilterOptions{
|
||||
Visibility: "public",
|
||||
},
|
||||
},
|
||||
want: "sort:updated-desc user:@me fork:true is:public",
|
||||
},
|
||||
{
|
||||
name: "only private",
|
||||
args: args{
|
||||
owner: "",
|
||||
filter: FilterOptions{
|
||||
Visibility: "private",
|
||||
},
|
||||
},
|
||||
want: "sort:updated-desc user:@me fork:true is:private",
|
||||
},
|
||||
{
|
||||
name: "only forks",
|
||||
args: args{
|
||||
owner: "",
|
||||
filter: FilterOptions{
|
||||
Fork: true,
|
||||
},
|
||||
},
|
||||
want: "sort:updated-desc user:@me fork:only",
|
||||
},
|
||||
{
|
||||
name: "no forks",
|
||||
args: args{
|
||||
owner: "",
|
||||
filter: FilterOptions{
|
||||
Source: true,
|
||||
},
|
||||
},
|
||||
want: "sort:updated-desc user:@me fork:false",
|
||||
},
|
||||
{
|
||||
name: "with language",
|
||||
args: args{
|
||||
owner: "",
|
||||
filter: FilterOptions{
|
||||
Language: "ruby",
|
||||
},
|
||||
},
|
||||
want: "sort:updated-desc user:@me fork:true language:\"ruby\"",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := searchQuery(tt.args.owner, tt.args.filter); got != tt.want {
|
||||
t.Errorf("searchQuery() = %q, want %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -13,12 +13,6 @@ import (
|
|||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type FilterOptions struct {
|
||||
Visibility string // private, public
|
||||
Fork bool
|
||||
Source bool
|
||||
}
|
||||
|
||||
type ListOptions struct {
|
||||
HttpClient func() (*http.Client, error)
|
||||
IO *iostreams.IOStreams
|
||||
|
|
@ -29,6 +23,7 @@ type ListOptions struct {
|
|||
Visibility string
|
||||
Fork bool
|
||||
Source bool
|
||||
Language string
|
||||
|
||||
Now func() time.Time
|
||||
}
|
||||
|
|
@ -83,6 +78,7 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman
|
|||
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")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
|
@ -97,6 +93,7 @@ func listRun(opts *ListOptions) error {
|
|||
Visibility: opts.Visibility,
|
||||
Fork: opts.Fork,
|
||||
Source: opts.Source,
|
||||
Language: opts.Language,
|
||||
}
|
||||
|
||||
listResult, err := listRepos(httpClient, ghinstance.OverridableDefault(), opts.Limit, opts.Owner, filter)
|
||||
|
|
@ -132,7 +129,7 @@ func listRun(opts *ListOptions) error {
|
|||
}
|
||||
|
||||
if opts.IO.IsStdoutTTY() {
|
||||
hasFilters := filter.Visibility != "" || filter.Fork || filter.Source
|
||||
hasFilters := filter.Visibility != "" || filter.Fork || filter.Source || filter.Language != ""
|
||||
title := listHeader(listResult.Owner, len(listResult.Repositories), listResult.TotalCount, hasFilters)
|
||||
fmt.Fprintf(opts.IO.Out, "\n%s\n\n", title)
|
||||
}
|
||||
|
|
@ -144,9 +141,15 @@ func listHeader(owner string, matchCount, totalMatchCount int, hasFilters bool)
|
|||
if totalMatchCount == 0 {
|
||||
if hasFilters {
|
||||
return "No results match your search"
|
||||
} else if owner != "" {
|
||||
return "There are no repositories in @" + owner
|
||||
}
|
||||
return "There are no repositories in @" + owner
|
||||
return "No results"
|
||||
}
|
||||
|
||||
return fmt.Sprintf("Showing %d of %d repositories in @%s", matchCount, totalMatchCount, owner)
|
||||
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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ func TestNewCmdList(t *testing.T) {
|
|||
Visibility: "",
|
||||
Fork: false,
|
||||
Source: false,
|
||||
Language: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -44,6 +45,7 @@ func TestNewCmdList(t *testing.T) {
|
|||
Visibility: "",
|
||||
Fork: false,
|
||||
Source: false,
|
||||
Language: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -55,6 +57,7 @@ func TestNewCmdList(t *testing.T) {
|
|||
Visibility: "",
|
||||
Fork: false,
|
||||
Source: false,
|
||||
Language: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -66,6 +69,7 @@ func TestNewCmdList(t *testing.T) {
|
|||
Visibility: "public",
|
||||
Fork: false,
|
||||
Source: false,
|
||||
Language: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -77,6 +81,7 @@ func TestNewCmdList(t *testing.T) {
|
|||
Visibility: "private",
|
||||
Fork: false,
|
||||
Source: false,
|
||||
Language: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -88,6 +93,7 @@ func TestNewCmdList(t *testing.T) {
|
|||
Visibility: "",
|
||||
Fork: true,
|
||||
Source: false,
|
||||
Language: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -99,6 +105,19 @@ func TestNewCmdList(t *testing.T) {
|
|||
Visibility: "",
|
||||
Fork: false,
|
||||
Source: true,
|
||||
Language: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with language",
|
||||
cli: "-l go",
|
||||
wants: ListOptions{
|
||||
Limit: 30,
|
||||
Owner: "",
|
||||
Visibility: "",
|
||||
Fork: false,
|
||||
Source: false,
|
||||
Language: "go",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue