X Tutup
Skip to content

Commit 00cb921

Browse files
authored
Merge pull request cli#2953 from cristiand391/add-repo-list
Add `repo list` command
2 parents da2a732 + e27a77f commit 00cb921

File tree

7 files changed

+1004
-0
lines changed

7 files changed

+1004
-0
lines changed
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
{
2+
"data": {
3+
"repositoryOwner": {
4+
"login": "octocat",
5+
"repositories": {
6+
"totalCount": 3,
7+
"nodes": [
8+
{
9+
"nameWithOwner": "octocat/hello-world",
10+
"description": "My first repository",
11+
"isFork": false,
12+
"isPrivate": false,
13+
"isArchived": false,
14+
"pushedAt": "2021-02-19T06:34:58Z"
15+
},
16+
{
17+
"nameWithOwner": "octocat/cli",
18+
"description": "GitHub CLI",
19+
"isFork": true,
20+
"isPrivate": false,
21+
"isArchived": false,
22+
"pushedAt": "2021-02-19T06:06:06Z"
23+
},
24+
{
25+
"nameWithOwner": "octocat/testing",
26+
"description": null,
27+
"isFork": false,
28+
"isPrivate": true,
29+
"isArchived": false,
30+
"pushedAt": "2021-02-11T22:32:05Z"
31+
}
32+
],
33+
"pageInfo": {
34+
"hasNextPage": false,
35+
"endCursor": ""
36+
}
37+
}
38+
}
39+
}
40+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
{
2+
"data": {
3+
"search": {
4+
"repositoryCount": 3,
5+
"nodes": [
6+
{
7+
"nameWithOwner": "octocat/hello-world",
8+
"description": "My first repository",
9+
"isFork": false,
10+
"isPrivate": false,
11+
"isArchived": false,
12+
"pushedAt": "2021-02-19T06:34:58Z"
13+
},
14+
{
15+
"nameWithOwner": "octocat/cli",
16+
"description": "GitHub CLI",
17+
"isFork": true,
18+
"isPrivate": false,
19+
"isArchived": false,
20+
"pushedAt": "2021-02-19T06:06:06Z"
21+
},
22+
{
23+
"nameWithOwner": "octocat/testing",
24+
"description": null,
25+
"isFork": false,
26+
"isPrivate": true,
27+
"isArchived": false,
28+
"pushedAt": "2021-02-11T22:32:05Z"
29+
}
30+
],
31+
"pageInfo": {
32+
"hasNextPage": false,
33+
"endCursor": ""
34+
}
35+
}
36+
}
37+
}

pkg/cmd/repo/list/http.go

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
package list
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"net/http"
7+
"reflect"
8+
"strings"
9+
"time"
10+
11+
"github.com/cli/cli/internal/ghinstance"
12+
"github.com/shurcooL/githubv4"
13+
"github.com/shurcooL/graphql"
14+
)
15+
16+
type Repository struct {
17+
NameWithOwner string
18+
Description string
19+
IsFork bool
20+
IsPrivate bool
21+
IsArchived bool
22+
PushedAt time.Time
23+
}
24+
25+
func (r Repository) Info() string {
26+
var tags []string
27+
28+
if r.IsPrivate {
29+
tags = append(tags, "private")
30+
} else {
31+
tags = append(tags, "public")
32+
}
33+
if r.IsFork {
34+
tags = append(tags, "fork")
35+
}
36+
if r.IsArchived {
37+
tags = append(tags, "archived")
38+
}
39+
40+
return strings.Join(tags, ", ")
41+
}
42+
43+
type RepositoryList struct {
44+
Owner string
45+
Repositories []Repository
46+
TotalCount int
47+
FromSearch bool
48+
}
49+
50+
type FilterOptions struct {
51+
Visibility string // private, public
52+
Fork bool
53+
Source bool
54+
Language string
55+
Archived bool
56+
NonArchived bool
57+
}
58+
59+
func listRepos(client *http.Client, hostname string, limit int, owner string, filter FilterOptions) (*RepositoryList, error) {
60+
if filter.Language != "" || filter.Archived || filter.NonArchived {
61+
return searchRepos(client, hostname, limit, owner, filter)
62+
}
63+
64+
perPage := limit
65+
if perPage > 100 {
66+
perPage = 100
67+
}
68+
69+
variables := map[string]interface{}{
70+
"perPage": githubv4.Int(perPage),
71+
"endCursor": (*githubv4.String)(nil),
72+
}
73+
74+
if filter.Visibility != "" {
75+
variables["privacy"] = githubv4.RepositoryPrivacy(strings.ToUpper(filter.Visibility))
76+
} else {
77+
variables["privacy"] = (*githubv4.RepositoryPrivacy)(nil)
78+
}
79+
80+
if filter.Fork {
81+
variables["fork"] = githubv4.Boolean(true)
82+
} else if filter.Source {
83+
variables["fork"] = githubv4.Boolean(false)
84+
} else {
85+
variables["fork"] = (*githubv4.Boolean)(nil)
86+
}
87+
88+
var ownerConnection string
89+
if owner == "" {
90+
ownerConnection = `graphql:"repositoryOwner: viewer"`
91+
} else {
92+
ownerConnection = `graphql:"repositoryOwner(login: $owner)"`
93+
variables["owner"] = githubv4.String(owner)
94+
}
95+
96+
type repositoryOwner struct {
97+
Login string
98+
Repositories struct {
99+
Nodes []Repository
100+
TotalCount int
101+
PageInfo struct {
102+
HasNextPage bool
103+
EndCursor string
104+
}
105+
} `graphql:"repositories(first: $perPage, after: $endCursor, privacy: $privacy, isFork: $fork, ownerAffiliations: OWNER, orderBy: { field: PUSHED_AT, direction: DESC })"`
106+
}
107+
query := reflect.StructOf([]reflect.StructField{
108+
{
109+
Name: "RepositoryOwner",
110+
Type: reflect.TypeOf(repositoryOwner{}),
111+
Tag: reflect.StructTag(ownerConnection),
112+
},
113+
})
114+
115+
gql := graphql.NewClient(ghinstance.GraphQLEndpoint(hostname), client)
116+
listResult := RepositoryList{}
117+
pagination:
118+
for {
119+
result := reflect.New(query)
120+
err := gql.QueryNamed(context.Background(), "RepositoryList", result.Interface(), variables)
121+
if err != nil {
122+
return nil, err
123+
}
124+
125+
owner := result.Elem().FieldByName("RepositoryOwner").Interface().(repositoryOwner)
126+
listResult.TotalCount = owner.Repositories.TotalCount
127+
listResult.Owner = owner.Login
128+
129+
for _, repo := range owner.Repositories.Nodes {
130+
listResult.Repositories = append(listResult.Repositories, repo)
131+
if len(listResult.Repositories) >= limit {
132+
break pagination
133+
}
134+
}
135+
136+
if !owner.Repositories.PageInfo.HasNextPage {
137+
break
138+
}
139+
variables["endCursor"] = githubv4.String(owner.Repositories.PageInfo.EndCursor)
140+
}
141+
142+
return &listResult, nil
143+
}
144+
145+
func searchRepos(client *http.Client, hostname string, limit int, owner string, filter FilterOptions) (*RepositoryList, error) {
146+
type query struct {
147+
Search struct {
148+
RepositoryCount int
149+
Nodes []struct {
150+
Repository Repository `graphql:"...on Repository"`
151+
}
152+
PageInfo struct {
153+
HasNextPage bool
154+
EndCursor string
155+
}
156+
} `graphql:"search(type: REPOSITORY, query: $query, first: $perPage, after: $endCursor)"`
157+
}
158+
159+
perPage := limit
160+
if perPage > 100 {
161+
perPage = 100
162+
}
163+
164+
variables := map[string]interface{}{
165+
"query": githubv4.String(searchQuery(owner, filter)),
166+
"perPage": githubv4.Int(perPage),
167+
"endCursor": (*githubv4.String)(nil),
168+
}
169+
170+
gql := graphql.NewClient(ghinstance.GraphQLEndpoint(hostname), client)
171+
listResult := RepositoryList{FromSearch: true}
172+
pagination:
173+
for {
174+
var result query
175+
err := gql.QueryNamed(context.Background(), "RepositoryListSearch", &result, variables)
176+
if err != nil {
177+
return nil, err
178+
}
179+
180+
listResult.TotalCount = result.Search.RepositoryCount
181+
for _, node := range result.Search.Nodes {
182+
if listResult.Owner == "" {
183+
idx := strings.IndexRune(node.Repository.NameWithOwner, '/')
184+
listResult.Owner = node.Repository.NameWithOwner[:idx]
185+
}
186+
listResult.Repositories = append(listResult.Repositories, node.Repository)
187+
if len(listResult.Repositories) >= limit {
188+
break pagination
189+
}
190+
}
191+
192+
if !result.Search.PageInfo.HasNextPage {
193+
break
194+
}
195+
variables["endCursor"] = githubv4.String(result.Search.PageInfo.EndCursor)
196+
}
197+
198+
return &listResult, nil
199+
}
200+
201+
func searchQuery(owner string, filter FilterOptions) string {
202+
queryParts := []string{"sort:updated-desc"}
203+
if owner == "" {
204+
queryParts = append(queryParts, "user:@me")
205+
} else {
206+
queryParts = append(queryParts, "user:"+owner)
207+
}
208+
209+
if filter.Fork {
210+
queryParts = append(queryParts, "fork:only")
211+
} else if filter.Source {
212+
queryParts = append(queryParts, "fork:false")
213+
} else {
214+
queryParts = append(queryParts, "fork:true")
215+
}
216+
217+
if filter.Language != "" {
218+
queryParts = append(queryParts, fmt.Sprintf("language:%q", filter.Language))
219+
}
220+
221+
switch filter.Visibility {
222+
case "public":
223+
queryParts = append(queryParts, "is:public")
224+
case "private":
225+
queryParts = append(queryParts, "is:private")
226+
}
227+
228+
if filter.Archived {
229+
queryParts = append(queryParts, "archived:true")
230+
} else if filter.NonArchived {
231+
queryParts = append(queryParts, "archived:false")
232+
}
233+
234+
return strings.Join(queryParts, " ")
235+
}

0 commit comments

Comments
 (0)
X Tutup