55 "context"
66 "encoding/json"
77 "fmt"
8- "log "
8+ "io "
99 "net/http"
10+ "net/url"
1011 "os"
1112 "regexp"
1213 "strings"
@@ -26,27 +27,22 @@ const issueTemplateContent = `
2627
2728var issueTemplate = template .Must (template .New ("issue" ).Parse (issueTemplateContent ))
2829
29- // FIXME: use official github client
30- var reGithubRepo = regexp .MustCompile ("https://github.com/([a-zA-Z0-9-._]+)/([a-zA-Z0-9-._]+)$" )
31- var githubGETREPO = "https://api.github.com/repos%s"
32- var githubGETCOMMITS = "https://api.github.com/repos%s/commits"
30+ var reGithubRepo = regexp .MustCompile (`^https://github\.com/([-a-zA-Z0-9_.]+)/([-a-zA-Z0-9_.]+)/?$` )
31+ var githubGETREPO = "https://api.github.com/repos/%s/%s"
32+ var githubGETCOMMITS = "https://api.github.com/repos/%s/%s/commits"
3333var githubPOSTISSUES = "https://api.github.com/repos/avelino/awesome-go/issues"
34+ var awesomeGoGETISSUES = "https://api.github.com/repos/avelino/awesome-go/issues"
3435
35- // FIXME: use https
36- var awesomeGoGETISSUES = "http://api.github.com/repos/avelino/awesome-go/issues" //only returns open issues
37- // FIXME: variable has type Duration, but contains a number. we should use
38- //
39- // time.Hour * ... or change type of variable
40- var numberOfYears time.Duration = 1
36+ var numberOfYears = 1
4137var timeNow = time .Now ()
4238var issueTitle = fmt .Sprintf ("Investigate repositories with more than 1 year without update - %s" , timeNow .Format (time .DateOnly ))
4339
44- const deadLinkMessage = " this repository might no longer exist! (status code >= 400 returned)"
40+ const deadLinkMessage = " this repository might no longer exist! (status code 404/410 returned)"
4541const movedPermanently = " status code 301 received"
4642const status302 = " status code 302 received"
4743const archived = " repository has been archived"
44+ const noRecentCommits = " repository has not received any commits in over 1 year"
4845
49- // LIMIT specifies the max number of repositories that are added in a single run of the script
5046var LIMIT = 10
5147var ctr = 0
5248
@@ -77,30 +73,26 @@ func getRepositoriesFromBody(body string) []string {
7773 link = strings .ReplaceAll (link , movedPermanently , "" )
7874 link = strings .ReplaceAll (link , status302 , "" )
7975 link = strings .ReplaceAll (link , archived , "" )
76+ link = strings .ReplaceAll (link , noRecentCommits , "" )
8077 links [i ] = link
8178 }
82-
8379 return links
8480}
8581
8682func generateIssueBody (t * testing.T , repositories []string ) (string , error ) {
8783 t .Helper ()
88-
8984 buf := bytes .NewBuffer (nil )
9085 err := issueTemplate .Execute (buf , repositories )
9186 requireNoErr (t , err , "Failed to generate template" )
92-
9387 return buf .String (), nil
9488}
9589
9690func createIssue (t * testing.T , staleRepos []string , client * http.Client ) {
9791 t .Helper ()
98-
9992 if len (staleRepos ) == 0 {
100- log . Print ("NO STALE REPOSITORIES" )
93+ t . Log ("NO STALE REPOSITORIES" )
10194 return
10295 }
103-
10496 body , err := generateIssueBody (t , staleRepos )
10597 requireNoErr (t , err , "failed to generate issue body" )
10698
@@ -114,21 +106,29 @@ func createIssue(t *testing.T, staleRepos []string, client *http.Client) {
114106 req , err := http .NewRequest (http .MethodPost , githubPOSTISSUES , buf )
115107 requireNoErr (t , err , "failed to create request" )
116108
117- _ , roundTripErr := client .Do (req )
109+ resp , roundTripErr := client .Do (req )
118110 requireNoErr (t , roundTripErr , "failed to send request" )
111+ defer resp .Body .Close ()
112+
113+ if resp .StatusCode < 200 || resp .StatusCode >= 300 {
114+ respBody , _ := io .ReadAll (resp .Body )
115+ t .Fatalf ("create issue failed: status %d, body: %s" , resp .StatusCode , string (respBody ))
116+ }
119117}
120118
121119func getAllFlaggedRepositories (t * testing.T , client * http.Client ) map [string ]bool {
122120 t .Helper ()
123-
124121 req , err := http .NewRequest (http .MethodGet , awesomeGoGETISSUES , nil )
125122 requireNoErr (t , err , "failed to create request" )
126123
127124 res , err := client .Do (req )
128125 requireNoErr (t , err , "failed to send request" )
129-
130126 defer res .Body .Close ()
131127
128+ if res .StatusCode == http .StatusForbidden || res .StatusCode == http .StatusUnauthorized {
129+ t .Fatalf ("getAllFlaggedRepositories: rate limit or auth failure (status %d)" , res .StatusCode )
130+ }
131+
132132 var issues []issue
133133 requireNoErr (t , json .NewDecoder (res .Body ).Decode (& issues ), "failed to unmarshal response" )
134134
@@ -137,90 +137,92 @@ func getAllFlaggedRepositories(t *testing.T, client *http.Client) map[string]boo
137137 if issue .Title != issueTitle {
138138 continue
139139 }
140-
141140 repos := getRepositoriesFromBody (issue .Body )
142141 for _ , repo := range repos {
143142 addressedRepositories [repo ] = true
144143 }
145144 }
146-
147145 return addressedRepositories
148146}
149147
150- func checkRepoAvailability (toRun bool , href string , client * http.Client ) ([]string , bool ) {
148+ func checkRepoAvailability (t * testing.T , toRun bool , href string , client * http.Client ) (warnings []string , aborted bool ) {
149+ t .Helper ()
151150 if ! toRun {
152151 return nil , false
153152 }
154153
155- ownerRepo := strings .ReplaceAll (href , "https://github.com" , "" )
156- apiCall := fmt .Sprintf (githubGETREPO , ownerRepo )
154+ matches := reGithubRepo .FindStringSubmatch (href )
155+ if len (matches ) < 3 {
156+ t .Logf ("Invalid github repo url format: %s" , href )
157+ return nil , false
158+ }
159+ owner , repo := matches [1 ], matches [2 ]
160+ apiCall := fmt .Sprintf (githubGETREPO , owner , repo )
161+
157162 req , err := http .NewRequest (http .MethodGet , apiCall , nil )
158163 if err != nil {
159- log . Printf ("Failed at repository %s\n " , href )
164+ t . Logf ("Failed to create request for repository %s\n " , href )
160165 return nil , false
161166 }
162167
163168 resp , err := client .Do (req )
164169 if err != nil {
165- log . Printf ("Failed at repository %s\n " , href )
170+ t . Logf ("Failed at repository %s\n " , href )
166171 return nil , false
167172 }
168-
169173 defer resp .Body .Close ()
170174
175+ if resp .StatusCode == http .StatusForbidden || resp .StatusCode == http .StatusUnauthorized {
176+ t .Logf ("GitHub API rate limit exceeded or unauthorized (Status: %d). Signal abort." , resp .StatusCode )
177+ return nil , true
178+ }
179+
171180 var repoResp struct {
172181 Archived bool `json:"archived"`
173182 }
174-
175183 if err := json .NewDecoder (resp .Body ).Decode (& repoResp ); err != nil {
176184 return nil , false
177185 }
178186
179- var isRepoAdded bool
180-
181- var warnings []string
182187 if resp .StatusCode == http .StatusMovedPermanently {
183188 warnings = append (warnings , href + movedPermanently )
184- log .Printf ("%s returned %d" , href , resp .StatusCode )
185- isRepoAdded = true
186- }
187-
188- if resp .StatusCode == http .StatusFound && ! isRepoAdded {
189+ t .Logf ("%s returned %d" , href , resp .StatusCode )
190+ } else if resp .StatusCode == http .StatusFound {
189191 warnings = append (warnings , href + status302 )
190- log .Printf ("%s returned %d" , href , resp .StatusCode )
191- isRepoAdded = true
192- }
193-
194- if resp .StatusCode >= http .StatusBadRequest && ! isRepoAdded {
192+ t .Logf ("%s returned %d" , href , resp .StatusCode )
193+ } else if resp .StatusCode == http .StatusNotFound || resp .StatusCode == http .StatusGone {
195194 warnings = append (warnings , href + deadLinkMessage )
196- log .Printf ("%s might not exist!" , href )
197- isRepoAdded = true
195+ t .Logf ("%s might not exist! (Status: %d)" , href , resp .StatusCode )
198196 }
199197
200- if repoResp .Archived && ! isRepoAdded {
198+ if repoResp .Archived && len ( warnings ) == 0 {
201199 warnings = append (warnings , href + archived )
202- log .Printf ("%s is archived!" , href )
203- isRepoAdded = true
200+ t .Logf ("%s is archived!" , href )
204201 }
205202
206- // FIXME: expression `(len(warnings) > 0) == isRepoAdded` is always true.
207- return warnings , isRepoAdded
203+ return warnings , false
208204}
209205
210- func checkRepoCommitActivity (toRun bool , href string , client * http.Client ) ([]string , bool ) {
206+ func checkRepoCommitActivity (t * testing.T , toRun bool , href string , client * http.Client ) (warnings []string , aborted bool ) {
207+ t .Helper ()
211208 if ! toRun {
212209 return nil , false
213210 }
214211
215- ownerRepo := strings .ReplaceAll (href , "https://github.com" , "" )
216- apiCall := fmt .Sprintf (githubGETCOMMITS , ownerRepo )
212+ matches := reGithubRepo .FindStringSubmatch (href )
213+ if len (matches ) < 3 {
214+ return nil , false
215+ }
216+ owner , repo := matches [1 ], matches [2 ]
217+ apiCall := fmt .Sprintf (githubGETCOMMITS , owner , repo )
218+
217219 req , err := http .NewRequest (http .MethodGet , apiCall , nil )
218220 if err != nil {
219- log . Printf ("Failed at repository %s\n " , href )
221+ t . Logf ("Failed to create request for repository %s\n " , href )
220222 return nil , false
221223 }
222224
223- since := timeNow .Add (- 1 * 365 * 24 * numberOfYears * time .Hour )
225+ since := timeNow .Add (- 1 * time . Duration ( numberOfYears ) * 365 * 24 * time .Hour )
224226 sinceQuery := since .Format (time .RFC3339 )
225227
226228 q := req .URL .Query ()
@@ -229,29 +231,27 @@ func checkRepoCommitActivity(toRun bool, href string, client *http.Client) ([]st
229231
230232 resp , err := client .Do (req )
231233 if err != nil {
232- log . Printf ("Failed at repository %s\n " , href )
234+ t . Logf ("Failed at repository %s\n " , href )
233235 return nil , false
234236 }
235-
236237 defer resp .Body .Close ()
237238
239+ if resp .StatusCode == http .StatusForbidden || resp .StatusCode == http .StatusUnauthorized {
240+ t .Logf ("GitHub API rate limit exceeded or unauthorized (Status: %d). Signal abort." , resp .StatusCode )
241+ return nil , true
242+ }
243+
238244 var respObj []map [string ]interface {}
239- // FIXME: handle error in all that cases
240245 if err := json .NewDecoder (resp .Body ).Decode (& respObj ); err != nil {
241246 return nil , false
242247 }
243248
244- var warnings []string
245- var isRepoAdded bool
246- isAged := len (respObj ) == 0
247- if isAged {
248- log .Printf ("%s has not had a commit in a while" , href )
249- warnings = append (warnings , href )
250- isRepoAdded = true
249+ if len (respObj ) == 0 {
250+ t .Logf ("%s has not had a commit in a while" , href )
251+ warnings = append (warnings , href + noRecentCommits )
251252 }
252253
253- // FIXME: expression `(len(warnings) > 0) == isRepoAdded` is always true.
254- return warnings , isRepoAdded
254+ return warnings , false
255255}
256256
257257func TestStaleRepository (t * testing.T ) {
@@ -263,15 +263,14 @@ func TestStaleRepository(t *testing.T) {
263263 }
264264
265265 if oauth == "" {
266- log . Print ("No oauth token found. Using unauthenticated client ..." )
266+ t . Log ("No oauth token found. Using unauthenticated client ..." )
267267 } else {
268268 tokenSource := & tokenSource {
269269 AccessToken : oauth ,
270270 }
271271 client = oauth2 .NewClient (context .Background (), tokenSource )
272272 }
273273
274- // FIXME: return addressedRepositories, no need to pass
275274 addressedRepositories := getAllFlaggedRepositories (t , client )
276275
277276 var staleRepos []string
@@ -280,32 +279,55 @@ func TestStaleRepository(t *testing.T) {
280279 EachWithBreak (func (_ int , s * goquery.Selection ) bool {
281280 href , ok := s .Attr ("href" )
282281 if ! ok {
283- log .Println ("expected to have href" )
282+ t .Log ("expected to have href" )
283+ return true
284+ }
285+
286+ if strings .HasPrefix (href , "#" ) {
284287 return true
285288 }
286289
290+ u , parseErr := url .Parse (href )
291+ if parseErr == nil {
292+ u .RawQuery = ""
293+ u .Fragment = ""
294+ href = u .String ()
295+ }
296+
287297 if ctr >= LIMIT && LIMIT != - 1 {
288- log . Print ("Max number of issues created" )
298+ t . Log ("Max number of issues created" )
289299 return false
290300 }
291301
292302 if _ , issueExists := addressedRepositories [href ]; issueExists {
293- log . Printf ("issue already exists for %s\n " , href )
303+ t . Logf ("issue already exists for %s\n " , href )
294304 return true
295305 }
296306
297307 if ! reGithubRepo .MatchString (href ) {
298- log .Printf ("%s non-github repo not currently handled" , href )
308+ t .Logf ("%s non-github repo not currently handled" , href )
309+ return true
299310 }
300311
301- // FIXME: this is `or` expres24sion. Probably we need `and`?
302- warnings , isRepoAdded := checkRepoAvailability (true , href , client )
303- staleRepos = append (staleRepos , warnings ... )
312+ var aborted bool
313+ var availWarnings []string
314+ availWarnings , aborted = checkRepoAvailability (t , true , href , client )
315+ if aborted {
316+ t .Logf ("Scan aborted due to API limits. Proceeding to create issue with %d records." , len (staleRepos ))
317+ return false
318+ }
319+ staleRepos = append (staleRepos , availWarnings ... )
304320
305- warnings , isRepoAdded = checkRepoCommitActivity (! isRepoAdded , href , client )
306- staleRepos = append (staleRepos , warnings ... )
321+ isRepoAdded := len (availWarnings ) > 0
322+ var commitWarnings []string
323+ commitWarnings , aborted = checkRepoCommitActivity (t , ! isRepoAdded , href , client )
324+ if aborted {
325+ t .Logf ("Scan aborted due to API limits. Proceeding to create issue with %d records." , len (staleRepos ))
326+ return false
327+ }
328+ staleRepos = append (staleRepos , commitWarnings ... )
307329
308- if isRepoAdded {
330+ if len ( availWarnings ) + len ( commitWarnings ) > 0 {
309331 ctr ++
310332 }
311333
0 commit comments