Skip to content

Commit 3f45c50

Browse files
authored
Make --forge-type skip network detection for custom hosts (#83)
* Make --forge-type skip network detection for custom hosts * Surface transport error when forge type detection fails * Use Link headers for gitea pagination so server-clamped page sizes don't truncate results
1 parent b641660 commit 3f45c50

20 files changed

Lines changed: 202 additions & 90 deletions

detect.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@ func DetectForgeType(ctx context.Context, domain string, hc ...*http.Client) (Fo
1919
baseURL := "https://" + domain
2020

2121
ft, err := detectFromHeaders(ctx, client, baseURL)
22-
if err == nil && ft != Unknown {
22+
if err != nil {
23+
return Unknown, fmt.Errorf("could not detect forge type for %s: %w", baseURL, err)
24+
}
25+
if ft != Unknown {
2326
return ft, nil
2427
}
2528

detect_test.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package forges
2+
3+
import (
4+
"context"
5+
"errors"
6+
"net/http"
7+
"strings"
8+
"testing"
9+
)
10+
11+
type errTransport struct{ err error }
12+
13+
func (t errTransport) RoundTrip(*http.Request) (*http.Response, error) { return nil, t.err }
14+
15+
func TestDetectForgeTypeSurfacesTransportError(t *testing.T) {
16+
netErr := errors.New("dial tcp: lookup forge.invalid: no such host")
17+
hc := &http.Client{Transport: errTransport{err: netErr}}
18+
19+
_, err := DetectForgeType(context.Background(), "forge.invalid", hc)
20+
if err == nil {
21+
t.Fatal("expected error")
22+
}
23+
if !strings.Contains(err.Error(), netErr.Error()) {
24+
t.Fatalf("expected transport error to be surfaced, got: %v", err)
25+
}
26+
}

gitea/branches.go

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,7 @@ func (f *giteaForge) Branches() forge.BranchService {
1717
}
1818

1919
func (s *giteaBranchService) List(ctx context.Context, owner, repo string, opts forge.ListBranchOpts) ([]forge.Branch, error) {
20-
perPage := opts.PerPage
21-
if perPage <= 0 {
22-
perPage = 30
23-
}
20+
perPage := pageSize(opts.PerPage)
2421
page := opts.Page
2522
if page <= 0 {
2623
page = 1
@@ -47,7 +44,7 @@ func (s *giteaBranchService) List(ctx context.Context, owner, repo string, opts
4744
}
4845
all = append(all, branch)
4946
}
50-
if len(branches) < perPage || (opts.Limit > 0 && len(all) >= opts.Limit) {
47+
if lastPage(resp, len(branches), perPage) || (opts.Limit > 0 && len(all) >= opts.Limit) {
5148
break
5249
}
5350
page++

gitea/ci.go

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -70,10 +70,7 @@ func convertGiteaWorkflowJob(j *gitea.ActionWorkflowJob) forge.CIJob {
7070
}
7171

7272
func (s *giteaCIService) ListRuns(_ context.Context, owner, repo string, opts forge.ListCIRunOpts) ([]forge.CIRun, error) {
73-
perPage := opts.PerPage
74-
if perPage <= 0 {
75-
perPage = 20
76-
}
73+
perPage := pageSize(opts.PerPage)
7774
page := opts.Page
7875
if page <= 0 {
7976
page = 1
@@ -104,7 +101,7 @@ func (s *giteaCIService) ListRuns(_ context.Context, owner, repo string, opts fo
104101
for _, r := range resp.WorkflowRuns {
105102
all = append(all, convertGiteaWorkflowRun(r))
106103
}
107-
if len(resp.WorkflowRuns) < perPage || (opts.Limit > 0 && len(all) >= opts.Limit) {
104+
if lastPage(httpResp, len(resp.WorkflowRuns), perPage) || (opts.Limit > 0 && len(all) >= opts.Limit) {
108105
break
109106
}
110107
gOpts.Page++

gitea/collaborators.go

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,7 @@ func (f *giteaForge) Collaborators() forge.CollaboratorService {
1717
}
1818

1919
func (s *giteaCollaboratorService) List(ctx context.Context, owner, repo string, opts forge.ListCollaboratorOpts) ([]forge.Collaborator, error) {
20-
perPage := opts.PerPage
21-
if perPage <= 0 {
22-
perPage = 50
23-
}
20+
perPage := pageSize(opts.PerPage)
2421
page := opts.Page
2522
if page <= 0 {
2623
page = 1
@@ -47,7 +44,7 @@ func (s *giteaCollaboratorService) List(ctx context.Context, owner, repo string,
4744
Permission: perm,
4845
})
4946
}
50-
if len(users) < perPage || (opts.Limit > 0 && len(all) >= opts.Limit) {
47+
if lastPage(resp, len(users), perPage) || (opts.Limit > 0 && len(all) >= opts.Limit) {
5148
break
5249
}
5350
page++

gitea/commit_statuses.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,30 @@ import (
1010

1111
const defaultPageSize = 50
1212

13+
// pageSize caps the requested page size at Gitea's default MAX_RESPONSE_ITEMS.
14+
// Servers clamp larger values, which breaks len(results) < perPage loop exits.
15+
func pageSize(perPage int) int {
16+
if perPage <= 0 || perPage > defaultPageSize {
17+
return defaultPageSize
18+
}
19+
return perPage
20+
}
21+
22+
// lastPage reports whether a paginated response was the final page. It trusts
23+
// the SDK-parsed Link headers when the server sent any, since Gitea clamps the
24+
// page size to MAX_RESPONSE_ITEMS and a clamped page would otherwise look like
25+
// a short final page. Falls back to the short-page heuristic when no Link
26+
// header was sent.
27+
func lastPage(resp *gitea.Response, got, perPage int) bool {
28+
if got == 0 {
29+
return true
30+
}
31+
if resp != nil && (resp.FirstPage > 0 || resp.PrevPage > 0 || resp.NextPage > 0 || resp.LastPage > 0) {
32+
return resp.NextPage == 0
33+
}
34+
return got < perPage
35+
}
36+
1337
type giteaCommitStatusService struct {
1438
client *gitea.Client
1539
}

gitea/deploy_keys.go

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,7 @@ func (f *giteaForge) DeployKeys() forge.DeployKeyService {
1717
}
1818

1919
func (s *giteaDeployKeyService) List(ctx context.Context, owner, repo string, opts forge.ListDeployKeyOpts) ([]forge.DeployKey, error) {
20-
perPage := opts.PerPage
21-
if perPage <= 0 {
22-
perPage = 30
23-
}
20+
perPage := pageSize(opts.PerPage)
2421
page := opts.Page
2522
if page <= 0 {
2623
page = 1
@@ -46,7 +43,7 @@ func (s *giteaDeployKeyService) List(ctx context.Context, owner, repo string, op
4643
CreatedAt: k.Created,
4744
})
4845
}
49-
if len(keys) < perPage || (opts.Limit > 0 && len(all) >= opts.Limit) {
46+
if lastPage(resp, len(keys), perPage) || (opts.Limit > 0 && len(all) >= opts.Limit) {
5047
break
5148
}
5249
page++

gitea/gitea.go

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -100,10 +100,7 @@ func (s *giteaRepoService) Get(ctx context.Context, owner, repo string) (*forge.
100100
}
101101

102102
func (s *giteaRepoService) List(ctx context.Context, owner string, opts forge.ListRepoOpts) ([]forge.Repository, error) {
103-
perPage := opts.PerPage
104-
if perPage <= 0 {
105-
perPage = defaultPageSize
106-
}
103+
perPage := pageSize(opts.PerPage)
107104

108105
// Try org endpoint first, fall back to user on 404.
109106
repos, err := s.listOrgRepos(ctx, owner, perPage)
@@ -133,7 +130,7 @@ func (s *giteaRepoService) listOrgRepos(_ context.Context, owner string, perPage
133130
for _, r := range gRepos {
134131
all = append(all, convertGiteaRepo(r))
135132
}
136-
if len(gRepos) < perPage {
133+
if lastPage(resp, len(gRepos), perPage) {
137134
break
138135
}
139136
page++
@@ -157,7 +154,7 @@ func (s *giteaRepoService) listUserRepos(_ context.Context, owner string, perPag
157154
for _, r := range gRepos {
158155
all = append(all, convertGiteaRepo(r))
159156
}
160-
if len(gRepos) < perPage {
157+
if lastPage(resp, len(gRepos), perPage) {
161158
break
162159
}
163160
page++
@@ -300,10 +297,7 @@ func (s *giteaRepoService) Fork(ctx context.Context, owner, repo string, opts fo
300297
}
301298

302299
func (s *giteaRepoService) ListForks(ctx context.Context, owner, repo string, opts forge.ListForksOpts) ([]forge.Repository, error) {
303-
perPage := opts.PerPage
304-
if perPage <= 0 {
305-
perPage = defaultPageSize
306-
}
300+
perPage := pageSize(opts.PerPage)
307301
page := opts.Page
308302
if page <= 0 {
309303
page = 1
@@ -323,7 +317,7 @@ func (s *giteaRepoService) ListForks(ctx context.Context, owner, repo string, op
323317
for _, r := range forks {
324318
all = append(all, convertGiteaRepo(r))
325319
}
326-
if len(forks) < perPage || (opts.Limit > 0 && len(all) >= opts.Limit) {
320+
if lastPage(resp, len(forks), perPage) || (opts.Limit > 0 && len(all) >= opts.Limit) {
327321
break
328322
}
329323
page++
@@ -369,10 +363,7 @@ func (s *giteaRepoService) ListContributors(ctx context.Context, owner, repo str
369363
}
370364

371365
func (s *giteaRepoService) Search(ctx context.Context, opts forge.SearchRepoOpts) ([]forge.Repository, error) {
372-
perPage := opts.PerPage
373-
if perPage <= 0 {
374-
perPage = 30
375-
}
366+
perPage := pageSize(opts.PerPage)
376367
page := opts.Page
377368
if page <= 0 {
378369
page = 1

gitea/gitea_test.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
forge "github.com/git-pkgs/forge"
88
"net/http"
99
"net/http/httptest"
10+
"strings"
1011
"testing"
1112
"time"
1213
)
@@ -198,6 +199,53 @@ func TestGiteaListRepos(t *testing.T) {
198199
assertEqual(t, "repos[1].FullName", "testorg/repo-b", repos[1].FullName)
199200
}
200201

202+
func TestGiteaListReposPaginatesWhenServerClampsPageSize(t *testing.T) {
203+
const serverCap = 2
204+
total := 5
205+
206+
pages := (total + serverCap - 1) / serverCap
207+
208+
mux := http.NewServeMux()
209+
mux.HandleFunc("GET /api/v1/version", giteaVersionHandler)
210+
mux.HandleFunc("GET /api/v1/orgs/testorg/repos", func(w http.ResponseWriter, r *http.Request) {
211+
page := 1
212+
_, _ = fmt.Sscan(r.URL.Query().Get("page"), &page)
213+
214+
var links []string
215+
links = append(links, `<?page=1>; rel="first"`, fmt.Sprintf(`<?page=%d>; rel="last"`, pages))
216+
if page < pages {
217+
links = append(links, fmt.Sprintf(`<?page=%d>; rel="next"`, page+1))
218+
}
219+
if page > 1 {
220+
links = append(links, fmt.Sprintf(`<?page=%d>; rel="prev"`, page-1))
221+
}
222+
w.Header().Set("Link", strings.Join(links, ", "))
223+
224+
start := (page - 1) * serverCap
225+
var out []map[string]any
226+
for i := start; i < start+serverCap && i < total; i++ {
227+
out = append(out, map[string]any{
228+
"full_name": fmt.Sprintf("testorg/repo-%d", i),
229+
"name": fmt.Sprintf("repo-%d", i),
230+
"owner": map[string]any{"login": "testorg"},
231+
})
232+
}
233+
_ = json.NewEncoder(w).Encode(out)
234+
})
235+
236+
srv := httptest.NewServer(mux)
237+
defer srv.Close()
238+
239+
f := New(srv.URL, "", nil)
240+
repos, err := f.Repos().List(context.Background(), "testorg", forge.ListRepoOpts{PerPage: 100})
241+
if err != nil {
242+
t.Fatalf("unexpected error: %v", err)
243+
}
244+
if len(repos) != total {
245+
t.Fatalf("expected %d repos across pages, got %d", total, len(repos))
246+
}
247+
}
248+
201249
func TestGiteaListReposFallbackToUser(t *testing.T) {
202250
mux := http.NewServeMux()
203251
mux.HandleFunc("GET /api/v1/version", giteaVersionHandler)

gitea/issues.go

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -119,10 +119,7 @@ func (s *giteaIssueService) Get(ctx context.Context, owner, repo string, number
119119
}
120120

121121
func (s *giteaIssueService) List(ctx context.Context, owner, repo string, opts forge.ListIssueOpts) ([]forge.Issue, error) {
122-
perPage := opts.PerPage
123-
if perPage <= 0 {
124-
perPage = 30
125-
}
122+
perPage := pageSize(opts.PerPage)
126123
page := opts.Page
127124
if page <= 0 {
128125
page = 1
@@ -160,7 +157,7 @@ func (s *giteaIssueService) List(ctx context.Context, owner, repo string, opts f
160157
for _, i := range issues {
161158
all = append(all, convertGiteaIssue(i))
162159
}
163-
if len(issues) < perPage || (opts.Limit > 0 && len(all) >= opts.Limit) {
160+
if lastPage(resp, len(issues), perPage) || (opts.Limit > 0 && len(all) >= opts.Limit) {
164161
break
165162
}
166163
gOpts.Page++

0 commit comments

Comments
 (0)