77 "fmt"
88 "io"
99 "net/http"
10- "net/url"
10+ "slices"
11+ "strings"
1112 "time"
1213)
1314
@@ -17,9 +18,10 @@ const (
1718 apiBaseURL = "https://api.github.com"
1819 apiVersionHeader = "2022-11-28"
1920 acceptHeader = "application/vnd.github+json"
21+ fetchPageSize = 100
22+ maxFetchPages = 10
2023)
2124
22- // repo is the JSON shape returned by both the list and search endpoints.
2325type repo struct {
2426 Name string `json:"name"`
2527 FullName string `json:"full_name"`
@@ -59,11 +61,11 @@ func New() *Client {
5961// ListReposPage returns one page of repositories accessible by the token.
6062// Pass page=1 to start; use Page.NextPage for subsequent calls (0 means done).
6163//
62- // When search is non-empty the GitHub Search API is used so that filtering
63- // happens server -side. When search is empty the standard list endpoint is used .
64+ // When search is non-empty all accessible repos are fetched from /user/repos
65+ // and filtered client -side so that org repos are included in results .
6466func (c * Client ) ListReposPage (ctx context.Context , token string , page , perPage int , search string ) (Page , error ) {
6567 if search != "" {
66- return c .searchReposPage (ctx , token , page , perPage , search )
68+ return c .searchReposPage (ctx , token , perPage , search )
6769 }
6870 return c .listReposPage (ctx , token , page , perPage )
6971}
@@ -84,31 +86,66 @@ func (c *Client) listReposPage(ctx context.Context, token string, page, perPage
8486 return Page {Repos : out , NextPage : nextPage }, nil
8587}
8688
87- func (c * Client ) searchReposPage (ctx context.Context , token string , page , perPage int , search string ) (Page , error ) {
88- type searchResponse struct {
89- TotalCount int `json:"total_count"`
90- Items []repo `json:"items"`
91- }
89+ // searchReposPage fetches repos from /user/repos in batches and filters
90+ // client-side by name/description. This avoids the search API which returns
91+ // all public repos on GitHub.
92+ func (c * Client ) searchReposPage (ctx context.Context , token string , perPage int , search string ) (Page , error ) {
93+ lower := strings .ToLower (search )
94+ var matched []Repo
9295
93- q := url . QueryEscape ( search + " user:@me" )
94- u := fmt .Sprintf ("%s/search/repositories?q=%s& per_page=%d&page=%d" , apiBaseURL , q , perPage , page )
96+ for p := 1 ; p <= maxFetchPages ; p ++ {
97+ u := fmt .Sprintf ("%s/user/repos? per_page=%d&page=%d" , apiBaseURL , fetchPageSize , p )
9598
96- var result searchResponse
97- if err := c .doJSON (ctx , token , u , & result ); err != nil {
98- return Page {}, err
99+ var rows []repo
100+ if err := c .doJSON (ctx , token , u , & rows ); err != nil {
101+ return Page {}, err
102+ }
103+
104+ for _ , r := range rows {
105+ nameLower := strings .ToLower (r .Name )
106+ descLower := strings .ToLower (r .Description )
107+ if strings .Contains (nameLower , lower ) || strings .Contains (descLower , lower ) {
108+ matched = append (matched , convertRepo (r ))
109+ }
110+ }
111+
112+ if len (rows ) < fetchPageSize {
113+ break
114+ }
99115 }
100116
101- out := convertRepos ( result . Items )
102- nextPage := 0
103- if len (result . Items ) == perPage {
104- nextPage = page + 1
117+ sortByNameRelevance ( matched , lower )
118+
119+ if len (matched ) > perPage {
120+ matched = matched [: perPage ]
105121 }
106- return Page {Repos : out , NextPage : nextPage }, nil
122+ return Page {Repos : matched }, nil
123+ }
124+
125+ func sortByNameRelevance (repos []Repo , lower string ) {
126+ slices .SortStableFunc (repos , func (a , b Repo ) int {
127+ aName := strings .ToLower (a .Name )
128+ bName := strings .ToLower (b .Name )
129+ aExact := aName == lower
130+ bExact := bName == lower
131+ if aExact != bExact {
132+ if aExact {
133+ return - 1
134+ }
135+ return 1
136+ }
137+ aHas := strings .Contains (aName , lower )
138+ bHas := strings .Contains (bName , lower )
139+ if aHas != bHas {
140+ if aHas {
141+ return - 1
142+ }
143+ return 1
144+ }
145+ return 0
146+ })
107147}
108148
109- // doJSON performs a GET request with standard GitHub headers and decodes the
110- // JSON response into dst. It enforces a response body size limit to prevent
111- // unbounded reads from a misbehaving upstream.
112149func (c * Client ) doJSON (ctx context.Context , token , rawURL string , dst any ) error {
113150 req , err := http .NewRequestWithContext (ctx , http .MethodGet , rawURL , nil )
114151 if err != nil {
@@ -136,17 +173,21 @@ func (c *Client) doJSON(ctx context.Context, token, rawURL string, dst any) erro
136173 return nil
137174}
138175
176+ func convertRepo (r repo ) Repo {
177+ return Repo {
178+ Name : r .Name ,
179+ FullName : r .FullName ,
180+ HTMLURL : r .HTMLURL ,
181+ Private : r .Private ,
182+ DefaultBranch : r .DefaultBranch ,
183+ Description : r .Description ,
184+ }
185+ }
186+
139187func convertRepos (rows []repo ) []Repo {
140188 out := make ([]Repo , len (rows ))
141189 for i , r := range rows {
142- out [i ] = Repo {
143- Name : r .Name ,
144- FullName : r .FullName ,
145- HTMLURL : r .HTMLURL ,
146- Private : r .Private ,
147- DefaultBranch : r .DefaultBranch ,
148- Description : r .Description ,
149- }
190+ out [i ] = convertRepo (r )
150191 }
151192 return out
152193}
0 commit comments