Skip to content

Commit e126d00

Browse files
committed
Improve assignment logic & code quality
1 parent f31656b commit e126d00

20 files changed

+1329
-786
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,6 @@ go.work.sum
3030
# Editor/IDE
3131
# .idea/
3232
# .vscode/
33+
34+
# added by lint-install
35+
out/

better-reviewers

263 KB
Binary file not shown.

bot_detection_test.go

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package main
22

33
import (
44
"context"
5+
"net/http"
56
"testing"
67
)
78

@@ -21,7 +22,7 @@ func TestIsUserBot(t *testing.T) {
2122
{"Bot with bot- prefix", "bot-user", true},
2223
{"Bot with bot_ prefix", "bot_scanner", true},
2324
{"Bot with .bot suffix", "scanner.bot", true},
24-
25+
2526
// Specific known bots
2627
{"GitHub Actions", "github-actions", true},
2728
{"GitHub Actions with bracket", "github-actions[bot]", true},
@@ -35,7 +36,7 @@ func TestIsUserBot(t *testing.T) {
3536
{"Jenkins", "jenkins", true},
3637
{"Mergify", "mergify[bot]", true},
3738
{"Stale bot", "stale[bot]", true},
38-
39+
3940
// Organization/service patterns
4041
{"Octo STS", "octo-sts", true},
4142
{"Octocat", "octocat", true},
@@ -53,7 +54,7 @@ func TestIsUserBot(t *testing.T) {
5354
{"Admin account", "cluster-admin", true},
5455
{"Security account", "security-scanner", true},
5556
{"Compliance account", "compliance-checker", true},
56-
57+
5758
// Valid human users
5859
{"Regular user", "johndoe", false},
5960
{"User with dash", "john-doe", false},
@@ -64,7 +65,7 @@ func TestIsUserBot(t *testing.T) {
6465
{"PR author", "ajayk", false},
6566
{"Reviewer", "tstromberg", false},
6667
{"Another reviewer", "vavilen84", false},
67-
68+
6869
// Edge cases - users that might look like bots but aren't
6970
{"User with 'bot' in name", "abbott", false},
7071
{"User with 'test' in name", "atestuser", false},
@@ -87,23 +88,27 @@ func TestIsUserBot(t *testing.T) {
8788
func TestIsValidReviewer(t *testing.T) {
8889
// Create a mock GitHub client that returns specific user types
8990
mockClient := &GitHubClient{
90-
userCache: newUserCache(),
91+
httpClient: &http.Client{},
92+
userCache: &userCache{users: make(map[string]*userInfo)},
93+
token: "test-token",
9194
}
92-
95+
9396
// Pre-populate the cache with test data
9497
mockClient.userCache.users["octo-sts"] = &userInfo{login: "octo-sts", userType: userTypeOrg}
9598
mockClient.userCache.users["github"] = &userInfo{login: "github", userType: userTypeOrg}
9699
mockClient.userCache.users["dependabot[bot]"] = &userInfo{login: "dependabot[bot]", userType: userTypeBot}
100+
mockClient.userCache.users["github-actions"] = &userInfo{login: "github-actions", userType: userTypeBot}
101+
mockClient.userCache.users["deploy-service"] = &userInfo{login: "deploy-service", userType: userTypeBot}
97102
mockClient.userCache.users["johndoe"] = &userInfo{login: "johndoe", userType: userTypeUser}
98103
mockClient.userCache.users["sergiodj"] = &userInfo{login: "sergiodj", userType: userTypeUser}
99-
104+
100105
rf := &ReviewerFinder{
101106
client: mockClient,
102107
}
103-
108+
104109
ctx := context.Background()
105110
pr := &PullRequest{Owner: "test", Repository: "repo"}
106-
111+
107112
tests := []struct {
108113
name string
109114
username string
@@ -115,12 +120,12 @@ func TestIsValidReviewer(t *testing.T) {
115120
{"Bot with API confirmation", "dependabot[bot]", false},
116121
{"Pattern-based bot", "github-actions", false},
117122
{"Service account", "deploy-service", false},
118-
123+
119124
// Should be valid
120125
{"Regular user", "johndoe", true},
121126
{"Contributor", "sergiodj", true},
122127
}
123-
128+
124129
for _, tt := range tests {
125130
t.Run(tt.name, func(t *testing.T) {
126131
got := rf.isValidReviewer(ctx, pr, tt.username)
@@ -159,7 +164,7 @@ func TestGetUserType(t *testing.T) {
159164
wantUserType: userTypeUser,
160165
},
161166
}
162-
167+
163168
// These would be integration tests with a mock HTTP client
164169
// For now, we're testing the core logic
165170
for _, tt := range tests {
@@ -168,4 +173,4 @@ func TestGetUserType(t *testing.T) {
168173
// Implementation would require mocking the HTTP client
169174
})
170175
}
171-
}
176+
}

cache.go

Lines changed: 41 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -7,53 +7,68 @@ import (
77

88
// cacheEntry holds a cached value with expiration.
99
type cacheEntry struct {
10-
value interface{}
10+
value any
1111
expiration time.Time
1212
}
1313

1414
// cache provides thread-safe caching with TTL.
1515
type cache struct {
16-
mu sync.RWMutex
1716
entries map[string]cacheEntry
17+
mu sync.RWMutex
1818
ttl time.Duration
1919
}
2020

21-
// newCache creates a new cache with the given TTL.
22-
func newCache(ttl time.Duration) *cache {
23-
return &cache{
24-
entries: make(map[string]cacheEntry),
25-
ttl: ttl,
26-
}
27-
}
28-
29-
// get retrieves a value from cache if not expired.
30-
func (c *cache) get(key string) (interface{}, bool) {
21+
// value retrieves a value from cache if not expired.
22+
func (c *cache) value(key string) (any, bool) {
3123
c.mu.RLock()
32-
defer c.mu.RUnlock()
33-
3424
entry, exists := c.entries[key]
35-
if !exists || time.Now().After(entry.expiration) {
25+
c.mu.RUnlock()
26+
27+
if !exists {
28+
return nil, false
29+
}
30+
31+
// Check expiration without holding the lock
32+
if time.Now().After(entry.expiration) {
33+
// Remove expired entry
34+
c.mu.Lock()
35+
delete(c.entries, key)
36+
c.mu.Unlock()
3637
return nil, false
3738
}
38-
39+
3940
return entry.value, true
4041
}
4142

4243
// set stores a value in cache with TTL.
43-
func (c *cache) set(key string, value interface{}) {
44+
func (c *cache) set(key string, value any) {
45+
c.setWithTTL(key, value, c.ttl)
46+
}
47+
48+
// setWithTTL stores a value in cache with custom TTL.
49+
func (c *cache) setWithTTL(key string, value any, ttl time.Duration) {
4450
c.mu.Lock()
4551
defer c.mu.Unlock()
46-
52+
4753
c.entries[key] = cacheEntry{
4854
value: value,
49-
expiration: time.Now().Add(c.ttl),
55+
expiration: time.Now().Add(ttl),
5056
}
5157
}
5258

53-
// clear removes all entries from cache.
54-
func (c *cache) clear() {
55-
c.mu.Lock()
56-
defer c.mu.Unlock()
57-
58-
c.entries = make(map[string]cacheEntry)
59-
}
59+
// cleanupExpired periodically removes expired entries.
60+
func (c *cache) cleanupExpired() {
61+
ticker := time.NewTicker(5 * time.Minute)
62+
defer ticker.Stop()
63+
64+
for range ticker.C {
65+
c.mu.Lock()
66+
now := time.Now()
67+
for key, entry := range c.entries {
68+
if now.After(entry.expiration) {
69+
delete(c.entries, key)
70+
}
71+
}
72+
c.mu.Unlock()
73+
}
74+
}

constants.go

Lines changed: 59 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,75 @@ package main
22

33
import "time"
44

5-
// Selection methods for reviewer choice tracking
5+
// Selection methods for reviewer choice tracking.
66
const (
77
selectionAssignee = "assignee-expert"
88
selectionAuthorOverlap = "author-overlap"
9-
selectionAuthorDirectory = "author-directory"
9+
selectionAuthorDirectory = "author-directory"
1010
selectionAuthorProject = "author-project"
1111
selectionReviewerCommenter = "reviewer-commenter"
1212
selectionReviewerOverlap = "reviewer-overlap"
1313
selectionReviewerDirectory = "reviewer-directory"
1414
selectionReviewerProject = "reviewer-project"
1515
)
1616

17-
// Configuration constants
17+
// Configuration constants.
1818
const (
1919
httpTimeout = 120 // seconds
2020
maxRetries = 3
21-
retryDelay = 2 // seconds
22-
nearbyLines = 3 // lines within this distance count as "nearby"
23-
maxFilesToAnalyze = 10
24-
maxHistoricalPRs = 50 // With caching, we can afford more lookups
25-
maxRecentPRs = 20 // Reasonable limit for recent PRs
26-
cacheTTL = 15 * time.Minute // Cache results for 15 minutes
27-
)
21+
retryDelay = 2 // seconds
22+
nearbyLines = 3 // lines within this distance count as "nearby"
23+
maxFilesToAnalyze = 3 // Focus on 3 files with largest delta to reduce API calls
24+
maxHistoricalPRs = 2 // Limit to 2 PRs per file to reduce API calls
25+
maxRecentPRs = 50 // Increased to get better candidate pool
26+
cacheTTL = 24 * time.Hour // Default cache TTL for most items
27+
prCacheTTL = 20 * 24 * time.Hour // Cache PRs for 20 days (use updated_at to invalidate)
28+
searchCacheTTL = 15 * time.Minute // Cache search results for 15 minutes
29+
30+
// Specific cache TTLs for different data types
31+
userTypeCacheTTL = 30 * 24 * time.Hour // User type never changes
32+
repoContributorsCacheTTL = 4 * time.Hour // 4 hours - catch people returning from vacation
33+
directoryOwnersCacheTTL = 3 * 24 * time.Hour // Directory ownership changes slowly
34+
recentPRsCacheTTL = 1 * time.Hour // Recent PRs for active repos
35+
fileHistoryCacheTTL = 3 * 24 * time.Hour // File history changes slowly
36+
37+
// API and pagination limits.
38+
perPageLimit = 100 // GitHub API per_page limit
39+
40+
// Analysis parameters.
41+
topReviewersLimit = 10 // Number of top reviewers to find
42+
progressBarWidth = 40 // Width of progress bar in characters
43+
44+
// Overlap scoring parameters.
45+
overlapDecayDays = 30.0 // Days for recency weight decay
46+
nearbyMatchWeight = 0.5 // Weight for nearby line matches
47+
48+
// Recency window parameters (in days).
49+
recencyWindow1 = 4 // First window: 4 days
50+
recencyWindow2 = 8 // Second window: 8 days
51+
recencyWindow3 = 16 // Third window: 16 days
52+
recencyWindow4 = 32 // Fourth window: 32 days
53+
selectionRolls = 10 // Number of random rolls for weighted selection
54+
55+
// Scoring parameters.
56+
topCandidatesToLog = 5 // Number of top candidates to log
57+
maxContextScore = 100 // Maximum context score for candidates
58+
59+
// Scoring weights (must sum to 100)
60+
fileOverlapWeight = 40.0 // Weight for file overlap score
61+
recencyWeight = 35.0 // Weight for recency score
62+
expertiseWeight = 25.0 // Weight for domain expertise score
63+
64+
// File significance multipliers
65+
prodCodeMultiplier = 1.5 // Production code vs test code
66+
criticalFileMultiplier = 1.3 // Main.go, handlers, etc.
67+
refactoringMultiplier = 1.2 // More deletions than additions
68+
69+
// Retry parameters handled by external library
70+
71+
// PR URL parsing.
72+
minURLParts = 4 // Minimum parts in PR URL
73+
74+
// GraphQL constants.
75+
graphQLNodes = "nodes" // Common GraphQL field name
76+
)

0 commit comments

Comments
 (0)