@@ -145,7 +145,7 @@ impl GithubClient {
145145 ///
146146 /// Returns an error on network failure or if the response cannot be
147147 /// deserialised as `T`.
148- async fn get < T : for < ' de > Deserialize < ' de > > ( & self , url : & str ) -> Result < T > {
148+ async fn get < T : for < ' de > Deserialize < ' de > > ( & self , url : & str ) -> Result < Option < T > > {
149149 let resp = self
150150 . client
151151 . get ( url)
@@ -157,6 +157,9 @@ impl GithubClient {
157157 . with_context ( || format ! ( "GET {url} failed" ) ) ?;
158158
159159 let status = resp. status ( ) ;
160+ if status == reqwest:: StatusCode :: NOT_FOUND {
161+ return Ok ( None ) ;
162+ }
160163 if !status. is_success ( ) {
161164 let body = resp. text ( ) . await . unwrap_or_default ( ) ;
162165 bail ! ( "GET {url} returned {status}: {body}" ) ;
@@ -165,14 +168,15 @@ impl GithubClient {
165168 resp. json :: < T > ( )
166169 . await
167170 . with_context ( || format ! ( "Failed to deserialise response from {url}" ) )
171+ . map ( Some )
168172 }
169173
170174 /// Fetches repository metadata.
171175 ///
172176 /// # Errors
173177 ///
174178 /// Returns an error if the API call fails.
175- async fn repo_info ( & self , owner : & str , repo : & str ) -> Result < RepoInfo > {
179+ async fn repo_info ( & self , owner : & str , repo : & str ) -> Result < Option < RepoInfo > > {
176180 let url = format ! ( "https://api.github.com/repos/{owner}/{repo}" ) ;
177181 self . get :: < RepoInfo > ( & url) . await
178182 }
@@ -183,16 +187,18 @@ impl GithubClient {
183187 /// # Errors
184188 ///
185189 /// Returns an error if the API call fails.
186- async fn contributor_count ( & self , owner : & str , repo : & str ) -> Result < usize > {
190+ async fn contributor_count ( & self , owner : & str , repo : & str ) -> Result < Option < usize > > {
187191 let url =
188192 format ! ( "https://api.github.com/repos/{owner}/{repo}/contributors?per_page=100&anon=0" ) ;
189- let contributors = self . get :: < Vec < Contributor > > ( & url) . await ?;
193+ let Some ( contributors) = self . get :: < Vec < Contributor > > ( & url) . await ? else {
194+ return Ok ( None ) ;
195+ } ;
190196 // Exclude bot accounts from the contributor count.
191197 let human_count = contributors
192198 . iter ( )
193199 . filter ( |c| c. account_type != "Bot" )
194200 . count ( ) ;
195- Ok ( human_count)
201+ Ok ( Some ( human_count) )
196202 }
197203
198204 /// Lists all comments on a PR/issue.
@@ -202,7 +208,9 @@ impl GithubClient {
202208 /// Returns an error if the API call fails.
203209 async fn list_pr_comments ( & self , repo : & str , pr : u64 ) -> Result < Vec < IssueComment > > {
204210 let url = format ! ( "https://api.github.com/repos/{repo}/issues/{pr}/comments?per_page=100" ) ;
205- self . get :: < Vec < IssueComment > > ( & url) . await
211+ self . get :: < Vec < IssueComment > > ( & url)
212+ . await ?
213+ . with_context ( || format ! ( "PR {pr} not found in {repo}" ) )
206214 }
207215
208216 /// Creates a new PR comment.
@@ -310,19 +318,20 @@ async fn check_tool(client: &GithubClient, tool: &ToolEntry) -> Result<ToolRepor
310318 let contributors_result = client. contributor_count ( & owner, & repo) . await ;
311319
312320 let stars_check = match & repo_result {
313- Ok ( info) => {
321+ Ok ( Some ( info) ) => {
314322 let s = info. stargazers_count ;
315323 if s >= MIN_STARS {
316324 CheckResult :: Pass ( format ! ( "{s} stars" ) )
317325 } else {
318326 CheckResult :: Fail ( format ! ( "{s} stars (minimum is {MIN_STARS})" ) )
319327 }
320328 }
329+ Ok ( None ) => CheckResult :: Skip ( "repository not found" . into ( ) ) ,
321330 Err ( e) => CheckResult :: Fail ( format ! ( "Could not fetch repo info: {e}" ) ) ,
322331 } ;
323332
324333 let age_check = match & repo_result {
325- Ok ( info) => {
334+ Ok ( Some ( info) ) => {
326335 let age = Utc :: now ( ) . signed_duration_since ( info. created_at ) ;
327336 let days = age. num_days ( ) ;
328337 let months = days / 30 ;
@@ -335,11 +344,12 @@ async fn check_tool(client: &GithubClient, tool: &ToolEntry) -> Result<ToolRepor
335344 ) )
336345 }
337346 }
347+ Ok ( None ) => CheckResult :: Skip ( "repository not found" . into ( ) ) ,
338348 Err ( _) => CheckResult :: Skip ( "Could not determine age (repo info unavailable)" . into ( ) ) ,
339349 } ;
340350
341351 let contributors_check = match contributors_result {
342- Ok ( count) => {
352+ Ok ( Some ( count) ) => {
343353 if count >= MIN_CONTRIBUTORS {
344354 CheckResult :: Pass ( format ! ( "{count} contributors" ) )
345355 } else {
@@ -348,16 +358,22 @@ async fn check_tool(client: &GithubClient, tool: &ToolEntry) -> Result<ToolRepor
348358 ) )
349359 }
350360 }
361+ Ok ( None ) => CheckResult :: Skip ( "repository not found" . into ( ) ) ,
351362 Err ( e) => CheckResult :: Fail ( format ! ( "Could not fetch contributors: {e}" ) ) ,
352363 } ;
353364
365+ let repo_not_found = matches ! ( repo_result, Ok ( None ) ) ;
366+ let note = repo_not_found. then_some (
367+ "The source URL returned a 404. Please check that the repository exists and is public." ,
368+ ) ;
369+
354370 Ok ( ToolReport {
355371 name : tool. name . to_string ( ) ,
356372 source,
357373 stars : stars_check,
358374 contributors : contributors_check,
359375 age : age_check,
360- note : None ,
376+ note : note . map ( str :: to_owned ) ,
361377 } )
362378 } else {
363379 // No source or non-GitHub source. This is fine for proprietary or
0 commit comments