Feature/opportunity ranking#436
Conversation
📝 WalkthroughWalkthroughThis PR introduces a complete new feature for analyzing GitHub repositories and computing contribution opportunity scores. The system adds a CLI command that fetches open issues and contributor statistics from a specified repository, classifies issues by label (good-first-issue, help-wanted, bug, etc.), and computes a weighted opportunity score combining issue classification, issue age, and contributor count. ChangesOpportunity Analysis Feature
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Suggested labels
Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 14
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@cmd/opportunities.go`:
- Around line 3-8: The build fails because strings.SplitN is used in this file
but the "strings" package is not imported; update the import block in
cmd/opportunities.go to include "strings" so functions referencing
strings.SplitN (used near the code that parses opportunity identifiers) compile
successfully—add "strings" alongside "context", "fmt", and the other imports.
- Around line 19-23: The current validation only checks parts length and allows
empty segments like "owner/" or "/repo"; update the validation around the parts
parsing (variables parts, owner, repo) to also ensure parts[0] and parts[1] are
non-empty and return a clear error (e.g., fmt.Errorf("invalid repository format,
expected owner/repo with non-empty owner and repo")) when either is empty so
invalid inputs are rejected early in the CLI path where ctx :=
context.Background() is created.
In `@internal/opportunity/classifier.go`:
- Around line 21-38: The current loop in classifier.go returns on the first
matching label which makes classification order-dependent; instead scan all
issue.Labels and compute a deterministic result by applying a precedence list
(good-first > help-wanted > bug > enhancement > docs), e.g., track which of the
known classifications are present while iterating (use issue.Labels and the
ClassificationGoodFirstIssue, ClassificationHelpWanted, ClassificationBug,
ClassificationEnhancement, ClassificationDocumentation symbols), then after the
loop return the highest-precedence classification (or default) so an issue with
multiple labels always yields the same classification.
- Around line 25-37: Normalize the label string before the switch in classify
logic: replace name := *lbl.Name with a normalized value (e.g.,
strings.TrimSpace(strings.ToLower(*lbl.Name))) so matches like "Good First
Issue" or "help wanted " hit the expected cases; add the strings import if
missing and keep the existing switch cases (ClassificationGoodFirstIssue,
ClassificationHelpWanted, ClassificationBug, ClassificationEnhancement,
ClassificationDocumentation) using the normalized name.
- Line 3: The build breaks because the package import
"github.com/google/go-github/v55/github" (used in
internal/opportunity/classifier.go, score.go, github_service.go, engine.go) is
not declared in go.mod/go.sum; fix by adding the module to go.mod and updating
go.sum—run a module fetch such as `go get github.com/google/go-github/v55` and
then `go mod tidy` (or equivalent tooling) so the dependency and matching v55
version are recorded and resolved before merging.
In `@internal/opportunity/engine.go`:
- Around line 3-8: Remove the unused time import from the import block in
internal/opportunity/engine.go: locate the import list that currently includes
"time" alongside "fmt", "context", and "github.com/google/go-github/v55/github"
and delete the "time" entry so the package builds cleanly (no other code changes
needed unless you actually need time elsewhere in functions within this file
such as in any function in the same package).
- Around line 41-52: The local ad-hoc scoring (variables goodFirst, helpWanted
and the formula score := goodFirst*2 + helpWanted + len(contributors)) must be
replaced with the package's shared scorer from score.go so we use the canonical
weights, age bonus, and contributor cap; remove the manual Classify/count loop
and instead call the shared scoring function defined in
internal/opportunity/score.go (the scorer used elsewhere in the package) passing
the same issues and contributors, and assign its return to score so this code
uses the same scoring contract as the rest of the package.
- Line 5: Remove the unused time import in internal/opportunity/engine.go; add a
require line for github.com/google/go-github v55 to go.mod and run go mod tidy
so the module is recorded (and update go.sum). In engine.go update Run to call
the existing computeScore function from internal/opportunity/score.go (instead
of computing the score inline) and ensure it supplies any required parameters;
also either use the Engine.e.client inside Run (e.g., to fetch GitHub data) or
remove the unused e.client field/parameter from NewEngine so there are no unused
fields.
In `@internal/opportunity/github_service.go`:
- Around line 53-61: In ListContributors (the loop calling
s.client.Repositories.ListContributorsStats) currently returns immediately on
err != nil, which prevents retrying for GitHub's 202 case; change the error
handling to detect github.AcceptedError (type *github.AcceptedError) and treat
it like a non-fatal retry: if err is an AcceptedError (or resp.StatusCode ==
202) then wait (respecting ctx.Done()) and continue the loop, otherwise return
the error; ensure the wait honors ctx cancellation (select on ctx.Done() vs
time.After) so the method exits promptly when context is cancelled.
- Around line 17-31: ListIssues is returning both issues and PRs (because
github.Issue objects for PRs have PullRequestLinks set), which inflates scoring;
modify internal/opportunity/github_service.go inside GitHubService.ListIssues to
skip any issue where issue.PullRequestLinks != nil before appending to all,
keeping pagination logic intact. Also add the missing time import in
internal/opportunity/score.go (ensure the file imports "time") so calls to
time.Since compile. Reference: GitHubService.ListIssues and the score.go usage
of time.Since.
- Around line 13-15: NewGitHubService currently constructs a github client with
no timeout; change NewGitHubService to create the client with an http.Client
that sets a reasonable Timeout (or accept/require a context with deadline) so
API calls cannot hang indefinitely. When listing issues with Issues.ListByRepo
(used in GitHubService methods that enumerate issues), filter out pull requests
by checking each issue's PullRequest field (skip if iss.PullRequest != nil)
before classifying/scoping. For contributor stats handling, stop relying on
resp.StatusCode==202 after err==nil and instead detect GitHub "accepted"
responses by checking the error type using errors.As(err,
new(*github.AcceptedError)) (and retry on that condition); apply this change in
the method that fetches contributor stats in GitHubService. Ensure you update
any callers (e.g., functions that pass context.Background()) to pass a context
with timeout if you choose the context-deadline approach.
In `@internal/opportunity/score.go`:
- Around line 27-52: The score logic in engine.go is bypassing computeScore;
replace the inline calculation (goodFirst*2 + helpWanted + len(contributors))
with a call to computeScore, passing the issues slice and the contributors slice
to computeScore(issues, contributors), and remove the duplicated weighting/age
logic there; ensure the types match (issues as []*github.Issue and contributors
as []*github.ContributorStats) or adapt computeScore signature accordingly and
import/update any packages needed so the CLI uses the centralized computeScore
implementation.
- Around line 30-36: Guard against nil entries in the issues slice before
dereferencing fields: in the loop over issues (for _, iss := range issues) add a
nil-check (if iss == nil { continue }) before calling calculateWeight(iss) and
before accessing iss.CreatedAt so you don't panic on nil pointers; update
references around calculateWeight, iss, and the CreatedAt ageDays calculation to
early-continue when iss is nil.
- Line 3: The file is missing the time package import used by computeScore (it
calls time.Since); update the import block to include "time" (either add a
separate import or include it in the existing import group with
"github.com/google/go-github/v55/github") so computeScore can call time.Since
and the package will compile.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 695b8af3-73cc-45d3-bef0-68b5cc125c26
📒 Files selected for processing (5)
cmd/opportunities.gointernal/opportunity/classifier.gointernal/opportunity/engine.gointernal/opportunity/github_service.gointernal/opportunity/score.go
| import ( | ||
| "context" | ||
| "fmt" | ||
| "github.com/spf13/cobra" | ||
| "github.com/agnivo988/Repo-lyzer/internal/opportunity" | ||
| ) |
There was a problem hiding this comment.
Missing strings import causes build failure.
strings.SplitN is used on Line 18, but strings is not imported in Lines 3-8, so this file won’t compile.
Proposed fix
import (
"context"
"fmt"
+ "strings"
"github.com/spf13/cobra"
"github.com/agnivo988/Repo-lyzer/internal/opportunity"
)Also applies to: 18-18
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@cmd/opportunities.go` around lines 3 - 8, The build fails because
strings.SplitN is used in this file but the "strings" package is not imported;
update the import block in cmd/opportunities.go to include "strings" so
functions referencing strings.SplitN (used near the code that parses opportunity
identifiers) compile successfully—add "strings" alongside "context", "fmt", and
the other imports.
| if len(parts) != 2 { | ||
| return fmt.Errorf("invalid repository format, expected owner/repo") | ||
| } | ||
| owner, repo = parts[0], parts[1] | ||
| ctx := context.Background() |
There was a problem hiding this comment.
Reject empty owner/repo segments during CLI validation.
The current check accepts inputs like owner/ or /repo. This passes invalid values downstream and fails later with noisier errors; validate non-empty owner and repo at this boundary.
Proposed fix
parts := strings.SplitN(ownerRepo, "/", 2)
- if len(parts) != 2 {
+ if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
return fmt.Errorf("invalid repository format, expected owner/repo")
}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@cmd/opportunities.go` around lines 19 - 23, The current validation only
checks parts length and allows empty segments like "owner/" or "/repo"; update
the validation around the parts parsing (variables parts, owner, repo) to also
ensure parts[0] and parts[1] are non-empty and return a clear error (e.g.,
fmt.Errorf("invalid repository format, expected owner/repo with non-empty owner
and repo")) when either is empty so invalid inputs are rejected early in the CLI
path where ctx := context.Background() is created.
| @@ -0,0 +1,40 @@ | |||
| package opportunity | |||
|
|
|||
| import "github.com/google/go-github/v55/github" | |||
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify missing module declaration for go-github v55 in manifest files.
set -euo pipefail
fd -a '^go\.mod$' | xargs -I{} sh -c 'echo "== {} =="; sed -n "1,220p" "{}"'
fd -a '^go\.sum$' | xargs -I{} sh -c 'echo "== {} (go-github entries) =="; rg -n "github.com/google/go-github" "{}" || true'Repository: agnivo988/Repo-lyzer
Length of output: 2515
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the exact import in the referenced file
if [ -f internal/opportunity/classifier.go ]; then
echo "== internal/opportunity/classifier.go =="
sed -n '1,120p' internal/opportunity/classifier.go | nl -ba | sed -n '1,80p'
else
echo "Missing file: internal/opportunity/classifier.go"
fi
# Check whether any go-github (any version) is referenced anywhere in the repo
echo "== Searching for github.com/google/go-github =="
rg -n "github\.com/google/go-github" . || true
# Check go.mod/go.sum for any go-github versions (not just v55)
echo "== Checking go.mod for github.com/google/go-github =="
rg -n "github\.com/google/go-github" go.mod || true
echo "== Checking go.sum for github.com/google/go-github =="
rg -n "github\.com/google/go-github" go.sum || trueRepository: agnivo988/Repo-lyzer
Length of output: 166
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Show referenced import lines (no nl dependency)
if [ -f internal/opportunity/classifier.go ]; then
echo "== internal/opportunity/classifier.go =="
cat -n internal/opportunity/classifier.go | sed -n '1,120p'
else
echo "Missing file: internal/opportunity/classifier.go"
fi
echo "== Searching for github.com/google/go-github =="
rg -n "github\.com/google/go-github" . || true
echo "== Checking go.mod for github.com/google/go-github =="
rg -n "github\.com/google/go-github" go.mod || true
echo "== Checking go.sum for github.com/google/go-github =="
rg -n "github\.com/google/go-github" go.sum || trueRepository: agnivo988/Repo-lyzer
Length of output: 2195
Build is blocked: github.com/google/go-github/v55 is imported but not declared in go.mod / go.sum
internal/opportunity/classifier.goimportsgithub.com/google/go-github/v55/github(same v55 import is also used ininternal/opportunity/score.go,internal/opportunity/github_service.go, andinternal/opportunity/engine.go).go.modandgo.sumcontain nogithub.com/google/go-githubentries, so module resolution/typecheck will fail.
Add github.com/google/go-github/v55 to go.mod and update go.sum (e.g., via go get ... and go mod tidy) before merge.
🧰 Tools
🪛 GitHub Actions: CI/CD Pipeline / test
[error] 3-3: go test ./... failed during package setup: no required module provides package github.com/google/go-github/v55/github. Run go get github.com/google/go-github/v55/github and/or add the dependency to go.mod.
🪛 GitHub Check: test
[failure] 3-3:
no required module provides package github.com/google/go-github/v55/github; to add it:
[failure] 3-3:
no required module provides package github.com/google/go-github/v55/github; to add it:
[failure] 3-3:
no required module provides package github.com/google/go-github/v55/github; to add it:
🪛 golangci-lint (2.12.2)
[error] 3-3: could not import github.com/google/go-github/v55/github (internal/opportunity/classifier.go:3:8: no required module provides package github.com/google/go-github/v55/github; to add it:
go get github.com/google/go-github/v55/github)
(typecheck)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@internal/opportunity/classifier.go` at line 3, The build breaks because the
package import "github.com/google/go-github/v55/github" (used in
internal/opportunity/classifier.go, score.go, github_service.go, engine.go) is
not declared in go.mod/go.sum; fix by adding the module to go.mod and updating
go.sum—run a module fetch such as `go get github.com/google/go-github/v55` and
then `go mod tidy` (or equivalent tooling) so the dependency and matching v55
version are recorded and resolved before merging.
| for _, lbl := range issue.Labels { | ||
| if lbl == nil || lbl.Name == nil { | ||
| continue | ||
| } | ||
| name := *lbl.Name | ||
| switch name { | ||
| case "good first issue", "good-first-issue": | ||
| return ClassificationGoodFirstIssue | ||
| case "help wanted", "help-wanted": | ||
| return ClassificationHelpWanted | ||
| case "bug": | ||
| return ClassificationBug | ||
| case "enhancement", "feature": | ||
| return ClassificationEnhancement | ||
| case "documentation", "docs": | ||
| return ClassificationDocumentation | ||
| } | ||
| } |
There was a problem hiding this comment.
Avoid order-dependent classification when multiple labels are present.
Returning on first matched label makes classification depend on GitHub label order. An issue with both bug and good first issue may be classified as bug and lose opportunity scoring weight. Prefer deterministic precedence (e.g., good-first > help-wanted > bug > enhancement > docs) across all labels.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@internal/opportunity/classifier.go` around lines 21 - 38, The current loop in
classifier.go returns on the first matching label which makes classification
order-dependent; instead scan all issue.Labels and compute a deterministic
result by applying a precedence list (good-first > help-wanted > bug >
enhancement > docs), e.g., track which of the known classifications are present
while iterating (use issue.Labels and the ClassificationGoodFirstIssue,
ClassificationHelpWanted, ClassificationBug, ClassificationEnhancement,
ClassificationDocumentation symbols), then after the loop return the
highest-precedence classification (or default) so an issue with multiple labels
always yields the same classification.
| name := *lbl.Name | ||
| switch name { | ||
| case "good first issue", "good-first-issue": | ||
| return ClassificationGoodFirstIssue | ||
| case "help wanted", "help-wanted": | ||
| return ClassificationHelpWanted | ||
| case "bug": | ||
| return ClassificationBug | ||
| case "enhancement", "feature": | ||
| return ClassificationEnhancement | ||
| case "documentation", "docs": | ||
| return ClassificationDocumentation | ||
| } |
There was a problem hiding this comment.
Normalize label names before matching to avoid false “other” classifications.
switch name is case/whitespace sensitive, so labels like Good First Issue or help wanted will miss and fall through to other, undercounting opportunities downstream in internal/opportunity/engine.go (Line 41-46 there).
Suggested fix
+import (
+ "strings"
+ "github.com/google/go-github/v55/github"
+)
-
-import "github.com/google/go-github/v55/github"
@@
- name := *lbl.Name
+ name := strings.TrimSpace(strings.ToLower(*lbl.Name))
switch name {🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@internal/opportunity/classifier.go` around lines 25 - 37, Normalize the label
string before the switch in classify logic: replace name := *lbl.Name with a
normalized value (e.g., strings.TrimSpace(strings.ToLower(*lbl.Name))) so
matches like "Good First Issue" or "help wanted " hit the expected cases; add
the strings import if missing and keep the existing switch cases
(ClassificationGoodFirstIssue, ClassificationHelpWanted, ClassificationBug,
ClassificationEnhancement, ClassificationDocumentation) using the normalized
name.
| func (s *GitHubService) ListIssues(ctx context.Context, owner, repo string, state string) ([]*github.Issue, error) { | ||
| var all []*github.Issue | ||
| opt := &github.IssueListByRepoOptions{State: state, ListOptions: github.ListOptions{PerPage: 100}} | ||
| for { | ||
| issues, resp, err := s.client.Issues.ListByRepo(ctx, owner, repo, opt) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| all = append(all, issues...) | ||
| if resp.NextPage == 0 { | ||
| break | ||
| } | ||
| opt.Page = resp.NextPage | ||
| } | ||
| return all, nil |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# List relevant files
ls -la internal/opportunity || true
ls -la internal/opportunity/github_service.go internal/opportunity/classifier.go internal/opportunity/score.go 2>/dev/null || true
# Show the exact ListIssues implementation
echo "==== internal/opportunity/github_service.go (context) ===="
sed -n '1,200p' internal/opportunity/github_service.go
# Show classifier logic that consumes issues
echo "==== internal/opportunity/classifier.go (context) ===="
sed -n '1,220p' internal/opportunity/classifier.go
# Show scoring logic that consumes issues
echo "==== internal/opportunity/score.go (context) ===="
sed -n '1,240p' internal/opportunity/score.go
# Find all call sites of ListIssues
echo "==== Call sites of ListIssues ===="
rg -n "ListIssues\\(" -S internal/opportunity || true
# Find if PRs are filtered elsewhere (PullRequestLinks or IsPullRequest fields)
echo "==== Search for PullRequestLinks / IsPullRequest usage ===="
rg -n "PullRequestLinks|IsPullRequest|pull request|PullRequest" internal/opportunity || true
# Also search for any helper that normalizes issue vs PR
echo "==== Search for IssueListByRepoOptions handling ===="
rg -n "IssueListByRepoOptions|ListByRepo\\(" internal/opportunity || trueRepository: agnivo988/Repo-lyzer
Length of output: 6692
🏁 Script executed:
#!/bin/bash
set -euo pipefail
sed -n '1,220p' internal/opportunity/engine.go
rg -n "computeScore\\(|Classify\\(|PullRequestLinks|pull_request" internal/opportunity -SRepository: agnivo988/Repo-lyzer
Length of output: 2009
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "==== internal/opportunity/score.go (full) ===="
cat -n internal/opportunity/score.goRepository: agnivo988/Repo-lyzer
Length of output: 1894
Filter pull requests out of ListIssues (and fix score.go missing time import).
Issues.ListByRepoincludes PRs, andEngine.Runclassifies/counts everything returned byListIssuesbased on labels, inflating the opportunity score for labeled PRs—skip entries whereissue.PullRequestLinks != nil.internal/opportunity/score.gocallstime.Sincebut doesn’t importtime, so it won’t compile.
Suggested fix
func (s *GitHubService) ListIssues(ctx context.Context, owner, repo string, state string) ([]*github.Issue, error) {
var all []*github.Issue
opt := &github.IssueListByRepoOptions{State: state, ListOptions: github.ListOptions{PerPage: 100}}
for {
issues, resp, err := s.client.Issues.ListByRepo(ctx, owner, repo, opt)
if err != nil {
return nil, err
}
- all = append(all, issues...)
+ for _, issue := range issues {
+ if issue == nil || issue.PullRequestLinks != nil {
+ continue
+ }
+ all = append(all, issue)
+ }
if resp.NextPage == 0 {
break
}
opt.Page = resp.NextPage
}
return all, nil
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| func (s *GitHubService) ListIssues(ctx context.Context, owner, repo string, state string) ([]*github.Issue, error) { | |
| var all []*github.Issue | |
| opt := &github.IssueListByRepoOptions{State: state, ListOptions: github.ListOptions{PerPage: 100}} | |
| for { | |
| issues, resp, err := s.client.Issues.ListByRepo(ctx, owner, repo, opt) | |
| if err != nil { | |
| return nil, err | |
| } | |
| all = append(all, issues...) | |
| if resp.NextPage == 0 { | |
| break | |
| } | |
| opt.Page = resp.NextPage | |
| } | |
| return all, nil | |
| func (s *GitHubService) ListIssues(ctx context.Context, owner, repo string, state string) ([]*github.Issue, error) { | |
| var all []*github.Issue | |
| opt := &github.IssueListByRepoOptions{State: state, ListOptions: github.ListOptions{PerPage: 100}} | |
| for { | |
| issues, resp, err := s.client.Issues.ListByRepo(ctx, owner, repo, opt) | |
| if err != nil { | |
| return nil, err | |
| } | |
| for _, issue := range issues { | |
| if issue == nil || issue.PullRequestLinks != nil { | |
| continue | |
| } | |
| all = append(all, issue) | |
| } | |
| if resp.NextPage == 0 { | |
| break | |
| } | |
| opt.Page = resp.NextPage | |
| } | |
| return all, nil | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@internal/opportunity/github_service.go` around lines 17 - 31, ListIssues is
returning both issues and PRs (because github.Issue objects for PRs have
PullRequestLinks set), which inflates scoring; modify
internal/opportunity/github_service.go inside GitHubService.ListIssues to skip
any issue where issue.PullRequestLinks != nil before appending to all, keeping
pagination logic intact. Also add the missing time import in
internal/opportunity/score.go (ensure the file imports "time") so calls to
time.Since compile. Reference: GitHubService.ListIssues and the score.go usage
of time.Since.
| for { | ||
| stats, resp, err := s.client.Repositories.ListContributorsStats(ctx, owner, repo) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| if resp.StatusCode == 202 { | ||
| // GitHub is computing statistics, wait briefly. | ||
| time.Sleep(2 * time.Second) | ||
| continue |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Find the target file and print the relevant region with line numbers
ls -la internal/opportunity/github_service.go
sed -n '1,140p' internal/opportunity/github_service.go | cat -n
# Also search for the ListContributorsStats usage and any existing retry/Accepted handling
rg -n "ListContributorsStats|AcceptedError|StatusCode == 202|time\\.Sleep" internal/opportunity/github_service.goRepository: agnivo988/Repo-lyzer
Length of output: 2730
🌐 Web query:
go-github v55 ListContributorsStats 202 AcceptedError
💡 Result:
In the go-github library, the ListContributorsStats method is used to retrieve contributor statistics for a repository [1][2]. When this method is called, GitHub may return a 202 Accepted status code if the statistics have not yet been cached or computed for the repository [1][3][4]. In go-github v55 (and other versions), this 202 status is surfaced to the caller as an *AcceptedError [1][2]. This design ensures that users are explicitly informed that the requested data is not yet available and that a background job has been triggered on the GitHub side to compute the statistics [2][4]. To handle this in your code, you should check for the presence of an *AcceptedError using errors.As. A common pattern is to wait briefly before retrying the request [1][2]: stats, _, err:= client.Repositories.ListContributorsStats(ctx, org, repo) if errors.As(err, new(*github.AcceptedError)) { // The request was accepted; statistics are being computed. // Wait for a short duration (e.g., a few seconds) and retry. log.Println("Statistics are being computed on the GitHub side. Please retry later.") } It is worth noting that as of June 2026, some developers have reported instances where the /stats/contributors endpoint may persistently return a 202 status code without progressing to a 200 OK, which may indicate a broader service issue for specific repositories rather than a typical caching delay [5].
Citations:
- 1: https://github.com/google/go-github/blob/master/github/repos_stats.go
- 2: https://pkg.go.dev/github.com/google/go-github/v55/github
- 3: https://docs2.lfe.io/v3/repos/statistics/
- 4: Make empty responses due to 202 Accepted status code more visible. google/go-github#486
- 5: https://devactivity.com/insights/github-api-hiccup-stats-contributors-endpoint-stuck-on-202-impacting-git-analysis-tools/
Handle *github.AcceptedError in ListContributors; current resp.StatusCode == 202 retry never runs.
In internal/opportunity/github_service.go, ListContributors returns immediately on err != nil, but go-github v55 surfaces a 202 Accepted from Repositories.ListContributorsStats as *github.AcceptedError, so the resp.StatusCode == 202 branch is unreachable. Catch AcceptedError and retry while honoring ctx.Done().
Suggested fix
import (
"context"
+ "errors"
"time"
"github.com/google/go-github/v55/github"
)
@@
func (s *GitHubService) ListContributors(ctx context.Context, owner, repo string) ([]*github.ContributorStats, error) {
// GitHub provides contributor stats endpoint; may be delayed, handle 202 response.
for {
- stats, resp, err := s.client.Repositories.ListContributorsStats(ctx, owner, repo)
+ stats, _, err := s.client.Repositories.ListContributorsStats(ctx, owner, repo)
if err != nil {
+ var accepted *github.AcceptedError
+ if errors.As(err, &accepted) {
+ select {
+ case <-ctx.Done():
+ return nil, ctx.Err()
+ case <-time.After(2 * time.Second):
+ continue
+ }
+ }
return nil, err
}
- if resp.StatusCode == 202 {
- // GitHub is computing statistics, wait briefly.
- time.Sleep(2 * time.Second)
- continue
- }
return stats, nil
}
}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@internal/opportunity/github_service.go` around lines 53 - 61, In
ListContributors (the loop calling s.client.Repositories.ListContributorsStats)
currently returns immediately on err != nil, which prevents retrying for
GitHub's 202 case; change the error handling to detect github.AcceptedError
(type *github.AcceptedError) and treat it like a non-fatal retry: if err is an
AcceptedError (or resp.StatusCode == 202) then wait (respecting ctx.Done()) and
continue the loop, otherwise return the error; ensure the wait honors ctx
cancellation (select on ctx.Done() vs time.After) so the method exits promptly
when context is cancelled.
| @@ -0,0 +1,52 @@ | |||
| package opportunity | |||
|
|
|||
| import "github.com/google/go-github/v55/github" | |||
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify unresolved time symbol in score.go
set -euo pipefail
fd -a 'score.go$' | xargs -I{} sh -c 'echo "== {} =="; sed -n "1,120p" "{}"'Repository: agnivo988/Repo-lyzer
Length of output: 3011
Fix missing time import in internal/opportunity/score.go
computeScore calls time.Since (line 35), but the file’s import block only includes github.com/google/go-github/v55/github, so the package won’t compile.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@internal/opportunity/score.go` at line 3, The file is missing the time
package import used by computeScore (it calls time.Since); update the import
block to include "time" (either add a separate import or include it in the
existing import group with "github.com/google/go-github/v55/github") so
computeScore can call time.Since and the package will compile.
| // computeScore aggregates issue scores and contributor count into a final contribution score. | ||
| func computeScore(issues []*github.Issue, contributors [] *github.ContributorStats) int { | ||
| total := 0 | ||
| for _, iss := range issues { | ||
| weight := calculateWeight(iss) | ||
| // simple age penalty: newer issues get a small bonus (max 2 points) | ||
| ageDays := 0 | ||
| if iss.CreatedAt != nil { | ||
| ageDays = int(time.Since(*iss.CreatedAt).Hours() / 24) | ||
| } | ||
| bonus := 0 | ||
| if ageDays < 30 { | ||
| bonus = 2 | ||
| } else if ageDays < 90 { | ||
| bonus = 1 | ||
| } | ||
| total += weight + bonus | ||
| } | ||
| // Add a point per contributor (capped at 50 for sanity) | ||
| contribPoints := len(contributors) | ||
| if contribPoints > 50 { | ||
| contribPoints = 50 | ||
| } | ||
| total += contribPoints | ||
| return total | ||
| } |
There was a problem hiding this comment.
Scoring utility appears unwired from the runtime path.
computeScore is implemented here, but internal/opportunity/engine.go (Line 48 there) still computes goodFirst*2 + helpWanted + len(contributors) directly and never calls this function. As-is, new weighting/age logic won’t affect CLI output.
🧰 Tools
🪛 golangci-lint (2.12.2)
[error] 35-35: undefined: time
(typecheck)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@internal/opportunity/score.go` around lines 27 - 52, The score logic in
engine.go is bypassing computeScore; replace the inline calculation (goodFirst*2
+ helpWanted + len(contributors)) with a call to computeScore, passing the
issues slice and the contributors slice to computeScore(issues, contributors),
and remove the duplicated weighting/age logic there; ensure the types match
(issues as []*github.Issue and contributors as []*github.ContributorStats) or
adapt computeScore signature accordingly and import/update any packages needed
so the CLI uses the centralized computeScore implementation.
| for _, iss := range issues { | ||
| weight := calculateWeight(iss) | ||
| // simple age penalty: newer issues get a small bonus (max 2 points) | ||
| ageDays := 0 | ||
| if iss.CreatedAt != nil { | ||
| ageDays = int(time.Since(*iss.CreatedAt).Hours() / 24) | ||
| } |
There was a problem hiding this comment.
Guard against nil issues before dereferencing CreatedAt.
issues is a slice of pointers; if any element is nil, iss.CreatedAt panics at runtime. Add if iss == nil { continue } before accessing fields.
Suggested fix
for _, iss := range issues {
+ if iss == nil {
+ continue
+ }
weight := calculateWeight(iss)
// simple age penalty: newer issues get a small bonus (max 2 points)
ageDays := 0📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| for _, iss := range issues { | |
| weight := calculateWeight(iss) | |
| // simple age penalty: newer issues get a small bonus (max 2 points) | |
| ageDays := 0 | |
| if iss.CreatedAt != nil { | |
| ageDays = int(time.Since(*iss.CreatedAt).Hours() / 24) | |
| } | |
| for _, iss := range issues { | |
| if iss == nil { | |
| continue | |
| } | |
| weight := calculateWeight(iss) | |
| // simple age penalty: newer issues get a small bonus (max 2 points) | |
| ageDays := 0 | |
| if iss.CreatedAt != nil { | |
| ageDays = int(time.Since(*iss.CreatedAt).Hours() / 24) | |
| } |
🧰 Tools
🪛 golangci-lint (2.12.2)
[error] 35-35: undefined: time
(typecheck)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@internal/opportunity/score.go` around lines 30 - 36, Guard against nil
entries in the issues slice before dereferencing fields: in the loop over issues
(for _, iss := range issues) add a nil-check (if iss == nil { continue }) before
calling calculateWeight(iss) and before accessing iss.CreatedAt so you don't
panic on nil pointers; update references around calculateWeight, iss, and the
CreatedAt ageDays calculation to early-continue when iss is nil.
Advanced scoring utilities (score.go), weight & age‑bonus logic, contributor‑count contribution. Will later host difficulty estimation and a ranked “Top N” list.
Summary by CodeRabbit
opportunitiescommand to analyze GitHub repositories and calculate a Contribution Opportunity Score for the specified owner/repo