Skip to content

Commit 0afc674

Browse files
committed
fix merge conflict
2 parents 0fc82f6 + 8807805 commit 0afc674

File tree

7 files changed

+337
-28
lines changed

7 files changed

+337
-28
lines changed

README.md

Lines changed: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -20,30 +20,39 @@ Lives in your menubar like a tiny waterfowl of productivity shame, watching your
2020
- **🧠 Smart turn-based assignment** - knows who is blocking a PR, knows when tests are failing, etc.
2121
- **⭐ Auto-start** on login (macOS)
2222
- **🔔 Auto-open** incoming PRs in your browser (off by default, rate-limited)
23+
- **🎯 Org Filtering** for orgs you may not care about in a home or work context
2324

24-
You can also visit the web-based equivalent at https://dash.ready-to-review.dev/
25+
You can also visit the web-based dashboard at https://dash.ready-to-review.dev/
2526

26-
## macOS Quick Start ⚡ (How to Get Honked At)
27+
## Dependencies
2728

28-
### Option 1: Authenticating using GitHub CLI
29+
* [go](https://go.dev/) 1.23.4 or higher
30+
* [gh](https://cli.github.com/), AKA the GitHub command-line utility
2931

30-
Install dependencies: the [GitHub CLI, aka "gh"](https://cli.github.com/) and [Go](https://go.dev/):
32+
## macOS Quick Start ⚡ (Get Honked At)
33+
34+
Install dependencies:
3135

3236
```bash
3337
brew install gh go
34-
gh auth login
3538
```
3639

37-
Then summon the goose:
40+
Confirm that `gh` is properly authenticated:
41+
42+
```
43+
gh auth status || gh auth login
44+
```
45+
46+
Build & run:
3847

3948
```bash
4049
git clone https://github.com/ready-to-review/goose.git
4150
cd goose && make run
4251
```
4352

44-
`make run` will cause the goose to implant itself into `/Applications/Review Goose.app` for future use. To be persistently annoyed by the goose every time you start your computer, click the `Start at Login` menu item.
53+
This will will cause the goose to implant itself into `/Applications/Review Goose.app` for future invocations. To be persistently annoyed every time you login, click the `Start at Login` menu item.
4554

46-
### Option 2: Using a fine-grained access token
55+
### Using a fine-grained access token
4756

4857
If you want more control over which repositories the goose can access, you can use a [fine-grained personal access token](https://github.com/settings/personal-access-tokens/new) with the following permissions:
4958

@@ -53,33 +62,31 @@ If you want more control over which repositories the goose can access, you can u
5362
You can then use the token like so:
5463

5564
```bash
56-
export GITHUB_TOKEN=your_token_here
57-
git clone https://github.com/ready-to-review/goose.git
58-
cd goose && make run
65+
env GITHUB_TOKEN=your_token_here goose
5966
```
6067

61-
We don't yet try to persist fine-grained tokens to disk - PR's welcome!
68+
We don't yet persist fine-grained tokens to disk - PR's welcome!
6269

6370
## Known Issues
6471

65-
- Blocking logic isn't 100% accurate - issues welcome!
72+
- Visual notifications won't work on macOS until we release signed binaries.
73+
- Blocking turn logic isn't 100% accurate - open an issue if you find something.
6674
- The goose may not stop honking until you review your PRs
67-
- Visual notifications won't work on macOS until we sign the binary
6875
- Linux, BSD, and Windows support is implemented but untested
6976

7077
## Pricing
7178

72-
The Goose is part of the [codeGROOVE](https://codegroove.dev) developer acceleration platform:
73-
- **FREE forever** for open-source or public repositories
74-
- Coming soon: GitHub Sponsors gain access to private repos ($2.56/mo recommended)
79+
- Review Goose is free forever for public repositories ❤️
80+
- Private repo access will soon be a supporter-only feature to ensure the goose is fed. ($2.56/mo is our recommendation)
7581

7682
## Privacy
7783

7884
- Your GitHub token is used to authenticate against GitHub and codeGROOVE's API for state-machine & natural-language processing
79-
- Your GitHub token is never stored or logged,
80-
- PR metadata may be locally or remotely cached for up to 20 days (performance)
85+
- Your GitHub token is never stored or logged.
86+
- PR metadata may be cached locally & remotely for up to 20 days
87+
- No data is resold to anyone. We don't even want it.
8188
- No telemetry is collected
8289

8390
---
8491

85-
Built with ❤️ by [codeGROOVE](https://codegroove.dev/) - PRs welcome!
92+
Built with 🪿 by [codeGROOVE](https://codegroove.dev/) - PRs welcome!

cmd/goose/filtering_test.go

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
package main
2+
3+
import (
4+
"testing"
5+
"time"
6+
)
7+
8+
// TestCountPRsWithHiddenOrgs tests that PRs from hidden orgs are not counted
9+
func TestCountPRsWithHiddenOrgs(t *testing.T) {
10+
app := &App{
11+
incoming: []PR{
12+
{Repository: "org1/repo1", NeedsReview: true, UpdatedAt: time.Now()},
13+
{Repository: "org2/repo2", NeedsReview: true, UpdatedAt: time.Now()},
14+
{Repository: "org3/repo3", NeedsReview: true, UpdatedAt: time.Now()},
15+
},
16+
outgoing: []PR{
17+
{Repository: "org1/repo4", IsBlocked: true, UpdatedAt: time.Now()},
18+
{Repository: "org2/repo5", IsBlocked: true, UpdatedAt: time.Now()},
19+
},
20+
hiddenOrgs: map[string]bool{
21+
"org2": true, // Hide org2
22+
},
23+
hideStaleIncoming: false,
24+
}
25+
26+
counts := app.countPRs()
27+
28+
// Should only count PRs from org1 and org3, not org2
29+
if counts.IncomingTotal != 2 {
30+
t.Errorf("IncomingTotal = %d, want 2 (org2 should be hidden)", counts.IncomingTotal)
31+
}
32+
if counts.IncomingBlocked != 2 {
33+
t.Errorf("IncomingBlocked = %d, want 2 (org2 should be hidden)", counts.IncomingBlocked)
34+
}
35+
if counts.OutgoingTotal != 1 {
36+
t.Errorf("OutgoingTotal = %d, want 1 (org2 should be hidden)", counts.OutgoingTotal)
37+
}
38+
if counts.OutgoingBlocked != 1 {
39+
t.Errorf("OutgoingBlocked = %d, want 1 (org2 should be hidden)", counts.OutgoingBlocked)
40+
}
41+
}
42+
43+
// TestCountPRsWithStalePRs tests that stale PRs are not counted when hideStaleIncoming is true
44+
func TestCountPRsWithStalePRs(t *testing.T) {
45+
now := time.Now()
46+
staleTime := now.Add(-100 * 24 * time.Hour) // 100 days ago
47+
recentTime := now.Add(-1 * time.Hour) // 1 hour ago
48+
49+
app := &App{
50+
incoming: []PR{
51+
{Repository: "org1/repo1", NeedsReview: true, UpdatedAt: staleTime},
52+
{Repository: "org1/repo2", NeedsReview: true, UpdatedAt: recentTime},
53+
{Repository: "org2/repo3", NeedsReview: false, UpdatedAt: staleTime},
54+
},
55+
outgoing: []PR{
56+
{Repository: "org1/repo4", IsBlocked: true, UpdatedAt: staleTime},
57+
{Repository: "org1/repo5", IsBlocked: true, UpdatedAt: recentTime},
58+
},
59+
hiddenOrgs: map[string]bool{},
60+
hideStaleIncoming: true, // Hide stale PRs
61+
}
62+
63+
counts := app.countPRs()
64+
65+
// Should only count recent PRs
66+
if counts.IncomingTotal != 1 {
67+
t.Errorf("IncomingTotal = %d, want 1 (stale PRs should be hidden)", counts.IncomingTotal)
68+
}
69+
if counts.IncomingBlocked != 1 {
70+
t.Errorf("IncomingBlocked = %d, want 1 (stale PRs should be hidden)", counts.IncomingBlocked)
71+
}
72+
if counts.OutgoingTotal != 1 {
73+
t.Errorf("OutgoingTotal = %d, want 1 (stale PRs should be hidden)", counts.OutgoingTotal)
74+
}
75+
if counts.OutgoingBlocked != 1 {
76+
t.Errorf("OutgoingBlocked = %d, want 1 (stale PRs should be hidden)", counts.OutgoingBlocked)
77+
}
78+
}
79+
80+
// TestCountPRsWithBothFilters tests that both filters work together
81+
func TestCountPRsWithBothFilters(t *testing.T) {
82+
now := time.Now()
83+
staleTime := now.Add(-100 * 24 * time.Hour)
84+
recentTime := now.Add(-1 * time.Hour)
85+
86+
app := &App{
87+
incoming: []PR{
88+
{Repository: "org1/repo1", NeedsReview: true, UpdatedAt: recentTime}, // Should be counted
89+
{Repository: "org2/repo2", NeedsReview: true, UpdatedAt: recentTime}, // Hidden org
90+
{Repository: "org3/repo3", NeedsReview: true, UpdatedAt: staleTime}, // Stale
91+
{Repository: "org1/repo4", NeedsReview: false, UpdatedAt: recentTime}, // Not blocked
92+
},
93+
outgoing: []PR{
94+
{Repository: "org1/repo5", IsBlocked: true, UpdatedAt: recentTime}, // Should be counted
95+
{Repository: "org2/repo6", IsBlocked: true, UpdatedAt: recentTime}, // Hidden org
96+
{Repository: "org3/repo7", IsBlocked: true, UpdatedAt: staleTime}, // Stale
97+
},
98+
hiddenOrgs: map[string]bool{
99+
"org2": true,
100+
},
101+
hideStaleIncoming: true,
102+
}
103+
104+
counts := app.countPRs()
105+
106+
// Should only count org1/repo1 (incoming) and org1/repo5 (outgoing)
107+
if counts.IncomingTotal != 2 {
108+
t.Errorf("IncomingTotal = %d, want 2", counts.IncomingTotal)
109+
}
110+
if counts.IncomingBlocked != 1 {
111+
t.Errorf("IncomingBlocked = %d, want 1", counts.IncomingBlocked)
112+
}
113+
if counts.OutgoingTotal != 1 {
114+
t.Errorf("OutgoingTotal = %d, want 1", counts.OutgoingTotal)
115+
}
116+
if counts.OutgoingBlocked != 1 {
117+
t.Errorf("OutgoingBlocked = %d, want 1", counts.OutgoingBlocked)
118+
}
119+
}
120+
121+
// TestExtractOrgFromRepo tests the org extraction function
122+
func TestExtractOrgFromRepo(t *testing.T) {
123+
tests := []struct {
124+
repo string
125+
name string
126+
want string
127+
}{
128+
{
129+
name: "standard repo path",
130+
repo: "microsoft/vscode",
131+
want: "microsoft",
132+
},
133+
{
134+
name: "single segment",
135+
repo: "justarepo",
136+
want: "justarepo",
137+
},
138+
{
139+
name: "empty string",
140+
repo: "",
141+
want: "",
142+
},
143+
{
144+
name: "nested path",
145+
repo: "org/repo/subpath",
146+
want: "org",
147+
},
148+
}
149+
150+
for _, tt := range tests {
151+
t.Run(tt.name, func(t *testing.T) {
152+
if got := extractOrgFromRepo(tt.repo); got != tt.want {
153+
t.Errorf("extractOrgFromRepo(%q) = %q, want %q", tt.repo, got, tt.want)
154+
}
155+
})
156+
}
157+
}

cmd/goose/github.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,15 @@ import (
2020
"golang.org/x/oauth2"
2121
)
2222

23+
// extractOrgFromRepo extracts the organization name from a repository path like "org/repo".
24+
func extractOrgFromRepo(repo string) string {
25+
parts := strings.Split(repo, "/")
26+
if len(parts) >= 1 {
27+
return parts[0]
28+
}
29+
return ""
30+
}
31+
2332
// initClients initializes GitHub and Turn API clients.
2433
func (app *App) initClients(ctx context.Context) error {
2534
token, err := app.token(ctx)
@@ -321,6 +330,17 @@ func (app *App) fetchPRsInternal(ctx context.Context, waitForTurn bool) (incomin
321330
}
322331
repo := strings.TrimPrefix(issue.GetRepositoryURL(), "https://api.github.com/repos/")
323332

333+
// Extract org and track it (but don't filter here)
334+
org := extractOrgFromRepo(repo)
335+
if org != "" {
336+
app.mu.Lock()
337+
if !app.seenOrgs[org] {
338+
log.Printf("[ORG] Discovered new organization: %s", org)
339+
}
340+
app.seenOrgs[org] = true
341+
app.mu.Unlock()
342+
}
343+
324344
pr := PR{
325345
Title: issue.GetTitle(),
326346
URL: issue.GetHTMLURL(),

cmd/goose/main.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@ type App struct {
9898
enableAutoBrowser bool
9999
hideStaleIncoming bool
100100
noCache bool
101+
hiddenOrgs map[string]bool
102+
seenOrgs map[string]bool
101103
}
102104

103105
func loadCurrentUser(ctx context.Context, app *App) {
@@ -220,6 +222,8 @@ func main() {
220222
enableAutoBrowser: false, // Default to false for safety
221223
browserRateLimiter: NewBrowserRateLimiter(browserOpenDelay, maxBrowserOpensMinute, maxBrowserOpensDay),
222224
startTime: time.Now(),
225+
seenOrgs: make(map[string]bool),
226+
hiddenOrgs: make(map[string]bool),
223227
}
224228

225229
// Load saved settings
@@ -627,6 +631,12 @@ func (app *App) checkForNewlyBlockedPRs(ctx context.Context) {
627631
}
628632
}
629633

634+
// Get hidden orgs for filtering
635+
hiddenOrgs := make(map[string]bool)
636+
for org, hidden := range app.hiddenOrgs {
637+
hiddenOrgs[org] = hidden
638+
}
639+
630640
// Log any removed entries
631641
removedCount := 0
632642
for url := range app.blockedPRTimes {
@@ -656,6 +666,12 @@ func (app *App) checkForNewlyBlockedPRs(ctx context.Context) {
656666

657667
// Check incoming PRs
658668
for i := range incoming {
669+
// Skip PRs from hidden orgs for notifications
670+
org := extractOrgFromRepo(incoming[i].Repository)
671+
if org != "" && hiddenOrgs[org] {
672+
continue
673+
}
674+
659675
if !incoming[i].NeedsReview {
660676
continue
661677
}
@@ -700,6 +716,12 @@ func (app *App) checkForNewlyBlockedPRs(ctx context.Context) {
700716

701717
// Check outgoing PRs
702718
for i := range outgoing {
719+
// Skip PRs from hidden orgs for notifications
720+
org := extractOrgFromRepo(outgoing[i].Repository)
721+
if org != "" && hiddenOrgs[org] {
722+
continue
723+
}
724+
703725
if !outgoing[i].IsBlocked {
704726
continue
705727
}

0 commit comments

Comments
 (0)