github.com/abdfnx/gh-api@v0.0.0-20210414084727-f5432eec23b8/pkg/cmd/repo/list/http.go (about) 1 package list 2 3 import ( 4 "context" 5 "net/http" 6 "reflect" 7 "strings" 8 "time" 9 10 "github.com/abdfnx/gh-api/internal/ghinstance" 11 "github.com/abdfnx/gh-api/pkg/githubsearch" 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 q := githubsearch.NewQuery() 203 q.SortBy(githubsearch.UpdatedAt, githubsearch.Desc) 204 205 if owner == "" { 206 q.OwnedBy("@me") 207 } else { 208 q.OwnedBy(owner) 209 } 210 211 if filter.Fork { 212 q.OnlyForks() 213 } else { 214 q.IncludeForks(!filter.Source) 215 } 216 217 if filter.Language != "" { 218 q.SetLanguage(filter.Language) 219 } 220 221 switch filter.Visibility { 222 case "public": 223 q.SetVisibility(githubsearch.Public) 224 case "private": 225 q.SetVisibility(githubsearch.Private) 226 } 227 228 if filter.Archived { 229 q.SetArchived(true) 230 } else if filter.NonArchived { 231 q.SetArchived(false) 232 } 233 234 return q.String() 235 }