Skip to content

Commit d57c869

Browse files
authored
Merge pull request #32 from git-pkgs/add-rate-limit
Add rate limit support
2 parents f1d4109 + 164d7cc commit d57c869

14 files changed

Lines changed: 413 additions & 2 deletions

bitbucket/rate_limit.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package bitbucket
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
forge "github.com/git-pkgs/forge"
8+
)
9+
10+
func (f *bitbucketForge) GetRateLimit(ctx context.Context) (*forge.RateLimit, error) {
11+
return nil, fmt.Errorf("getting rate limit: %w", forge.ErrNotSupported)
12+
}

bitbucket/rate_limit_test.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package bitbucket
2+
3+
import (
4+
"context"
5+
"errors"
6+
"testing"
7+
8+
forge "github.com/git-pkgs/forge"
9+
)
10+
11+
func TestBitbucketRateLimitNotSupported(t *testing.T) {
12+
f := New("test-token", nil)
13+
_, err := f.GetRateLimit(context.Background())
14+
if !errors.Is(err, forge.ErrNotSupported) {
15+
t.Fatalf("expected ErrNotSupported, got %v", err)
16+
}
17+
}

forge.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ type Forge interface {
4545
Secrets() SecretService
4646
Notifications() NotificationService
4747
Reviews() ReviewService
48+
GetRateLimit(ctx context.Context) (*RateLimit, error)
4849
}
4950

5051
// Client routes requests to the appropriate Forge based on the URL domain.

forges_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,10 @@ func (m *mockForge) Reviews() ReviewService {
477477
return &mockReviewService{}
478478
}
479479

480+
func (m *mockForge) GetRateLimit(_ context.Context) (*RateLimit, error) {
481+
return nil, ErrNotSupported
482+
}
483+
480484
type mockRepoService struct {
481485
repo *Repository
482486
repos []Repository

gitea/gitea.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ import (
99
)
1010

1111
type giteaForge struct {
12-
client *gitea.Client
12+
client *gitea.Client
13+
baseURL string
14+
token string
15+
httpClient *http.Client
1316
}
1417

1518
// New creates a Gitea/Forgejo forge backend.
@@ -22,7 +25,7 @@ func New(baseURL, token string, hc *http.Client) forge.Forge {
2225
opts = append(opts, gitea.SetHTTPClient(hc))
2326
}
2427
c, _ := gitea.NewClient(baseURL, opts...)
25-
return &giteaForge{client: c}
28+
return &giteaForge{client: c, baseURL: baseURL, token: token, httpClient: hc}
2629
}
2730

2831
type giteaRepoService struct {

gitea/rate_limit.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package gitea
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"net/http"
8+
"time"
9+
10+
forge "github.com/git-pkgs/forge"
11+
)
12+
13+
type giteaRateLimitResponse struct {
14+
Resources struct {
15+
Core struct {
16+
Limit int `json:"limit"`
17+
Remaining int `json:"remaining"`
18+
Reset int64 `json:"reset"`
19+
} `json:"core"`
20+
} `json:"resources"`
21+
}
22+
23+
func (f *giteaForge) GetRateLimit(ctx context.Context) (*forge.RateLimit, error) {
24+
url := f.baseURL + "/api/v1/rate_limit"
25+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
26+
if err != nil {
27+
return nil, err
28+
}
29+
if f.token != "" {
30+
req.Header.Set("Authorization", "token "+f.token)
31+
}
32+
33+
hc := f.httpClient
34+
if hc == nil {
35+
hc = http.DefaultClient
36+
}
37+
38+
resp, err := hc.Do(req)
39+
if err != nil {
40+
return nil, err
41+
}
42+
defer func() { _ = resp.Body.Close() }()
43+
44+
if resp.StatusCode == http.StatusNotFound {
45+
return nil, fmt.Errorf("getting rate limit: %w", forge.ErrNotSupported)
46+
}
47+
if resp.StatusCode >= 400 {
48+
return nil, &forge.HTTPError{StatusCode: resp.StatusCode, URL: url}
49+
}
50+
51+
var result giteaRateLimitResponse
52+
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
53+
return nil, err
54+
}
55+
56+
core := result.Resources.Core
57+
var reset time.Time
58+
if core.Reset > 0 {
59+
reset = time.Unix(core.Reset, 0)
60+
}
61+
62+
return &forge.RateLimit{
63+
Limit: core.Limit,
64+
Remaining: core.Remaining,
65+
Reset: reset,
66+
}, nil
67+
}

gitea/rate_limit_test.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package gitea
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"errors"
7+
"net/http"
8+
"net/http/httptest"
9+
"testing"
10+
11+
forge "github.com/git-pkgs/forge"
12+
)
13+
14+
func TestGiteaGetRateLimit(t *testing.T) {
15+
mux := http.NewServeMux()
16+
mux.HandleFunc("GET /api/v1/version", giteaVersionHandler)
17+
mux.HandleFunc("GET /api/v1/rate_limit", func(w http.ResponseWriter, r *http.Request) {
18+
_ = json.NewEncoder(w).Encode(map[string]any{
19+
"resources": map[string]any{
20+
"core": map[string]any{
21+
"limit": 100,
22+
"remaining": 98,
23+
"reset": 1717243200,
24+
},
25+
},
26+
})
27+
})
28+
29+
srv := httptest.NewServer(mux)
30+
defer srv.Close()
31+
32+
f := New(srv.URL, "test-token", nil)
33+
rl, err := f.GetRateLimit(context.Background())
34+
if err != nil {
35+
t.Fatalf("unexpected error: %v", err)
36+
}
37+
38+
assertEqualInt(t, "Limit", 100, rl.Limit)
39+
assertEqualInt(t, "Remaining", 98, rl.Remaining)
40+
if rl.Reset.Unix() != 1717243200 {
41+
t.Errorf("Reset: want unix 1717243200, got %d", rl.Reset.Unix())
42+
}
43+
}
44+
45+
func TestGiteaGetRateLimitNotSupported(t *testing.T) {
46+
mux := http.NewServeMux()
47+
mux.HandleFunc("GET /api/v1/version", giteaVersionHandler)
48+
mux.HandleFunc("GET /api/v1/rate_limit", func(w http.ResponseWriter, r *http.Request) {
49+
w.WriteHeader(http.StatusNotFound)
50+
})
51+
52+
srv := httptest.NewServer(mux)
53+
defer srv.Close()
54+
55+
f := New(srv.URL, "test-token", nil)
56+
_, err := f.GetRateLimit(context.Background())
57+
if !errors.Is(err, forge.ErrNotSupported) {
58+
t.Fatalf("expected ErrNotSupported, got %v", err)
59+
}
60+
}

github/rate_limit.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package github
2+
3+
import (
4+
"context"
5+
6+
forge "github.com/git-pkgs/forge"
7+
)
8+
9+
func (f *gitHubForge) GetRateLimit(ctx context.Context) (*forge.RateLimit, error) {
10+
limits, _, err := f.client.RateLimit.Get(ctx)
11+
if err != nil {
12+
return nil, err
13+
}
14+
15+
if limits == nil || limits.Core == nil {
16+
return &forge.RateLimit{}, nil
17+
}
18+
19+
core := limits.Core
20+
return &forge.RateLimit{
21+
Limit: core.Limit,
22+
Remaining: core.Remaining,
23+
Reset: core.Reset.Time,
24+
}, nil
25+
}

github/rate_limit_test.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package github
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"net/http"
7+
"net/http/httptest"
8+
"testing"
9+
"time"
10+
11+
"github.com/google/go-github/v82/github"
12+
)
13+
14+
func TestGitHubGetRateLimit(t *testing.T) {
15+
resetTime := time.Date(2024, 6, 1, 12, 0, 0, 0, time.UTC)
16+
17+
mux := http.NewServeMux()
18+
mux.HandleFunc("GET /api/v3/rate_limit", func(w http.ResponseWriter, r *http.Request) {
19+
_ = json.NewEncoder(w).Encode(map[string]any{
20+
"resources": map[string]any{
21+
"core": map[string]any{
22+
"limit": 5000,
23+
"remaining": 4999,
24+
"reset": resetTime.Unix(),
25+
},
26+
},
27+
})
28+
})
29+
30+
srv := httptest.NewServer(mux)
31+
defer srv.Close()
32+
33+
c := github.NewClient(nil)
34+
c, _ = c.WithEnterpriseURLs(srv.URL+"/api/v3", srv.URL+"/api/v3")
35+
f := &gitHubForge{client: c}
36+
37+
rl, err := f.GetRateLimit(context.Background())
38+
if err != nil {
39+
t.Fatalf("unexpected error: %v", err)
40+
}
41+
42+
assertEqualInt(t, "Limit", 5000, rl.Limit)
43+
assertEqualInt(t, "Remaining", 4999, rl.Remaining)
44+
if !rl.Reset.Equal(resetTime) {
45+
t.Errorf("Reset: want %v, got %v", resetTime, rl.Reset)
46+
}
47+
}

gitlab/rate_limit.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package gitlab
2+
3+
import (
4+
"context"
5+
"strconv"
6+
"time"
7+
8+
forge "github.com/git-pkgs/forge"
9+
)
10+
11+
func (f *gitLabForge) GetRateLimit(ctx context.Context) (*forge.RateLimit, error) {
12+
// GitLab has no dedicated rate limit endpoint. Rate limit info comes
13+
// from response headers on any API call, so we make a lightweight request.
14+
_, resp, err := f.client.Version.GetVersion()
15+
if err != nil {
16+
return nil, err
17+
}
18+
19+
limit, _ := strconv.Atoi(resp.Header.Get("RateLimit-Limit"))
20+
remaining, _ := strconv.Atoi(resp.Header.Get("RateLimit-Remaining"))
21+
resetUnix, _ := strconv.ParseInt(resp.Header.Get("RateLimit-Reset"), 10, 64)
22+
23+
var reset time.Time
24+
if resetUnix > 0 {
25+
reset = time.Unix(resetUnix, 0)
26+
}
27+
28+
return &forge.RateLimit{
29+
Limit: limit,
30+
Remaining: remaining,
31+
Reset: reset,
32+
}, nil
33+
}

0 commit comments

Comments
 (0)