Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions bitbucket/bitbucket.go
Original file line number Diff line number Diff line change
Expand Up @@ -216,8 +216,12 @@ func (s *bitbucketRepoService) List(ctx context.Context, owner string, opts forg
if perPage <= 0 {
perPage = 100
}
if opts.Limit > 0 && perPage > opts.Limit {
perPage = opts.Limit
}

var all []forge.Repository
matched := 0
url := fmt.Sprintf("%s/repositories/%s?pagelen=%d", bitbucketAPI, owner, perPage)

for url != "" {
Expand All @@ -229,12 +233,19 @@ func (s *bitbucketRepoService) List(ctx context.Context, owner string, opts forg
return nil, err
}
for _, bb := range page.Values {
all = append(all, convertBitbucketRepo(bb))
repo := convertBitbucketRepo(bb)
all = append(all, repo)
if forge.RepoMatchesFilters(repo, opts) {
matched++
}
}
if opts.Limit > 0 && matched >= opts.Limit {
break
}
url = page.Next
}

return forge.FilterRepos(all, opts), nil
return forge.CapRepos(forge.FilterRepos(all, opts), opts), nil
}

func (s *bitbucketRepoService) Create(ctx context.Context, opts forge.CreateRepoOpts) (*forge.Repository, error) {
Expand Down
45 changes: 45 additions & 0 deletions bitbucket/bitbucket_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package bitbucket
import (
"context"
"encoding/json"
"fmt"
forge "github.com/git-pkgs/forge"
"net/http"
"net/http/httptest"
Expand Down Expand Up @@ -211,3 +212,47 @@ func TestBitbucketListTags(t *testing.T) {
assertEqual(t, "Tag[1].Name", "v0.1.0", tags[1].Name)
assertEqual(t, "Tag[1].Commit", "fff666", tags[1].Commit)
}

func TestBitbucketListReposStopsPaginatingAtLimit(t *testing.T) {
var requests int
mux := http.NewServeMux()
mux.HandleFunc("GET /2.0/repositories/atlassian", func(w http.ResponseWriter, r *http.Request) {
requests++
n := requests
_ = json.NewEncoder(w).Encode(map[string]any{
"values": []bbRepository{
{
Slug: "repo",
FullName: "atlassian/repo",
Owner: &struct {
Username string `json:"username"`
DisplayName string `json:"display_name"`
}{Username: "atlassian"},
},
},
// Always advertise a next page so the test fails loudly if the
// implementation keeps following it past the requested limit.
"next": fmt.Sprintf("%s?page=%d", r.URL.Path, n+1),
})
})

srv := httptest.NewServer(mux)
defer srv.Close()

origAPI := bitbucketAPI
defer func() { setBitbucketAPI(origAPI) }()
setBitbucketAPI(srv.URL + "/2.0")

f := New("test-token", nil)

repos, err := f.Repos().List(context.Background(), "atlassian", forge.ListRepoOpts{Limit: 1})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(repos) != 1 {
t.Fatalf("expected 1 repo, got %d", len(repos))
}
if requests != 1 {
t.Fatalf("expected List to stop after 1 request once Limit was reached, got %d requests", requests)
}
}
57 changes: 38 additions & 19 deletions forge.go
Original file line number Diff line number Diff line change
Expand Up @@ -219,36 +219,55 @@ func (c *Client) ListRepositories(ctx context.Context, domain, owner string, opt
return f.Repos().List(ctx, owner, opts)
}

// RepoMatchesFilters reports whether a repository satisfies the archived and
// fork filters requested in opts. Backends can use this to decide, while
// paginating, whether a repository counts toward opts.Limit.
func RepoMatchesFilters(r Repository, opts ListRepoOpts) bool {
switch opts.Archived {
case ArchivedExclude:
if r.Archived {
return false
}
case ArchivedOnly:
if !r.Archived {
return false
}
}
switch opts.Forks {
case ForkExclude:
if r.Fork {
return false
}
case ForkOnly:
if !r.Fork {
return false
}
}
return true
}

// FilterRepos applies archived and fork filters to a slice of repositories.
func FilterRepos(repos []Repository, opts ListRepoOpts) []Repository {
n := 0
for _, r := range repos {
switch opts.Archived {
case ArchivedExclude:
if r.Archived {
continue
}
case ArchivedOnly:
if !r.Archived {
continue
}
}
switch opts.Forks {
case ForkExclude:
if r.Fork {
continue
}
case ForkOnly:
if !r.Fork {
continue
}
if !RepoMatchesFilters(r, opts) {
continue
}
repos[n] = r
n++
}
return repos[:n]
}

// CapRepos truncates repos to at most opts.Limit entries. A Limit of 0 or
// less means unlimited and the slice is returned unchanged.
func CapRepos(repos []Repository, opts ListRepoOpts) []Repository {
if opts.Limit > 0 && len(repos) > opts.Limit {
return repos[:opts.Limit]
}
return repos
}

// FetchTagsFromPURL fetches git tags using a PURL's repository_url qualifier.
func (c *Client) FetchTagsFromPURL(ctx context.Context, p *purl.PURL) ([]Tag, error) {
repoURL := p.RepositoryURL()
Expand Down
33 changes: 26 additions & 7 deletions gitea/gitea.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,21 +106,25 @@ func (s *giteaRepoService) Get(ctx context.Context, owner, repo string) (*forge.

func (s *giteaRepoService) List(ctx context.Context, owner string, opts forge.ListRepoOpts) ([]forge.Repository, error) {
perPage := pageSize(opts.PerPage)
if opts.Limit > 0 && opts.Limit < perPage {
perPage = pageSize(opts.Limit)
}

// Try org endpoint first, fall back to user on 404.
repos, err := s.listOrgRepos(ctx, owner, perPage)
repos, err := s.listOrgRepos(ctx, owner, perPage, opts)
if err != nil {
repos, err = s.listUserRepos(ctx, owner, perPage)
repos, err = s.listUserRepos(ctx, owner, perPage, opts)
if err != nil {
return nil, err
}
}

return forge.FilterRepos(repos, opts), nil
return forge.CapRepos(forge.FilterRepos(repos, opts), opts), nil
}

func (s *giteaRepoService) listOrgRepos(_ context.Context, owner string, perPage int) ([]forge.Repository, error) {
func (s *giteaRepoService) listOrgRepos(_ context.Context, owner string, perPage int, opts forge.ListRepoOpts) ([]forge.Repository, error) {
var all []forge.Repository
matched := 0
page := 1
for {
gRepos, resp, err := s.client.ListOrgRepos(owner, gitea.ListOrgReposOptions{
Expand All @@ -133,7 +137,14 @@ func (s *giteaRepoService) listOrgRepos(_ context.Context, owner string, perPage
return nil, err
}
for _, r := range gRepos {
all = append(all, convertGiteaRepo(r))
repo := convertGiteaRepo(r)
all = append(all, repo)
if forge.RepoMatchesFilters(repo, opts) {
matched++
}
}
if opts.Limit > 0 && matched >= opts.Limit {
break
}
if lastPage(resp, len(gRepos), perPage) {
break
Expand All @@ -143,8 +154,9 @@ func (s *giteaRepoService) listOrgRepos(_ context.Context, owner string, perPage
return all, nil
}

func (s *giteaRepoService) listUserRepos(_ context.Context, owner string, perPage int) ([]forge.Repository, error) {
func (s *giteaRepoService) listUserRepos(_ context.Context, owner string, perPage int, opts forge.ListRepoOpts) ([]forge.Repository, error) {
var all []forge.Repository
matched := 0
page := 1
for {
gRepos, resp, err := s.client.ListUserRepos(owner, gitea.ListReposOptions{
Expand All @@ -157,7 +169,14 @@ func (s *giteaRepoService) listUserRepos(_ context.Context, owner string, perPag
return nil, err
}
for _, r := range gRepos {
all = append(all, convertGiteaRepo(r))
repo := convertGiteaRepo(r)
all = append(all, repo)
if forge.RepoMatchesFilters(repo, opts) {
matched++
}
}
if opts.Limit > 0 && matched >= opts.Limit {
break
}
if lastPage(resp, len(gRepos), perPage) {
break
Expand Down
35 changes: 35 additions & 0 deletions gitea/gitea_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -319,3 +319,38 @@ func TestGiteaListTags(t *testing.T) {
assertEqual(t, "Tag[1].Name", "v2.0.0", tags[1].Name)
assertEqual(t, "Tag[1].Commit", "ddd444", tags[1].Commit)
}

func TestGiteaListReposStopsPaginatingAtLimit(t *testing.T) {
var requests int
mux := http.NewServeMux()
mux.HandleFunc("GET /api/v1/version", giteaVersionHandler)
mux.HandleFunc("GET /api/v1/orgs/testorg/repos", func(w http.ResponseWriter, r *http.Request) {
requests++
page := 1
_, _ = fmt.Sscan(r.URL.Query().Get("page"), &page)

w.Header().Set("Link", fmt.Sprintf(`<?page=1>; rel="first", <?page=%d>; rel="next"`, page+1))
_ = json.NewEncoder(w).Encode([]map[string]any{
{
"full_name": fmt.Sprintf("testorg/repo-%d", page),
"name": fmt.Sprintf("repo-%d", page),
"owner": map[string]any{"login": "testorg"},
},
})
})

srv := httptest.NewServer(mux)
defer srv.Close()

f := New(srv.URL, "", nil)
repos, err := f.Repos().List(context.Background(), "testorg", forge.ListRepoOpts{Limit: 1})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(repos) != 1 {
t.Fatalf("expected 1 repo, got %d", len(repos))
}
if requests != 1 {
t.Fatalf("expected List to stop after 1 request once Limit was reached, got %d requests", requests)
}
}
33 changes: 26 additions & 7 deletions github/github.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,21 +113,25 @@ func (s *gitHubRepoService) List(ctx context.Context, owner string, opts forge.L
if perPage <= 0 {
perPage = defaultPageSize
}
if opts.Limit > 0 && perPage > opts.Limit {
perPage = opts.Limit
}

// Try org endpoint first, fall back to user on 404.
repos, err := s.listOrgRepos(ctx, owner, perPage)
repos, err := s.listOrgRepos(ctx, owner, perPage, opts)
if err != nil {
repos, err = s.listUserRepos(ctx, owner, perPage)
repos, err = s.listUserRepos(ctx, owner, perPage, opts)
if err != nil {
return nil, err
}
}

return forge.FilterRepos(repos, opts), nil
return forge.CapRepos(forge.FilterRepos(repos, opts), opts), nil
}

func (s *gitHubRepoService) listOrgRepos(ctx context.Context, owner string, perPage int) ([]forge.Repository, error) {
func (s *gitHubRepoService) listOrgRepos(ctx context.Context, owner string, perPage int, opts forge.ListRepoOpts) ([]forge.Repository, error) {
var all []forge.Repository
matched := 0
ghOpts := &github.RepositoryListByOrgOptions{
ListOptions: github.ListOptions{PerPage: perPage},
}
Expand All @@ -140,7 +144,14 @@ func (s *gitHubRepoService) listOrgRepos(ctx context.Context, owner string, perP
return nil, err
}
for _, r := range ghRepos {
all = append(all, convertGitHubRepo(r))
repo := convertGitHubRepo(r)
all = append(all, repo)
if forge.RepoMatchesFilters(repo, opts) {
matched++
}
}
if opts.Limit > 0 && matched >= opts.Limit {
break
}
if resp.NextPage == 0 {
break
Expand All @@ -150,8 +161,9 @@ func (s *gitHubRepoService) listOrgRepos(ctx context.Context, owner string, perP
return all, nil
}

func (s *gitHubRepoService) listUserRepos(ctx context.Context, owner string, perPage int) ([]forge.Repository, error) {
func (s *gitHubRepoService) listUserRepos(ctx context.Context, owner string, perPage int, opts forge.ListRepoOpts) ([]forge.Repository, error) {
var all []forge.Repository
matched := 0
ghOpts := &github.RepositoryListByUserOptions{
ListOptions: github.ListOptions{PerPage: perPage},
}
Expand All @@ -164,7 +176,14 @@ func (s *gitHubRepoService) listUserRepos(ctx context.Context, owner string, per
return nil, err
}
for _, r := range ghRepos {
all = append(all, convertGitHubRepo(r))
repo := convertGitHubRepo(r)
all = append(all, repo)
if forge.RepoMatchesFilters(repo, opts) {
matched++
}
}
if opts.Limit > 0 && matched >= opts.Limit {
break
}
if resp.NextPage == 0 {
break
Expand Down
37 changes: 37 additions & 0 deletions github/github_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -344,3 +344,40 @@ func TestGitHubDeleteRepoNotFound(t *testing.T) {
t.Fatalf("expected forge.ErrNotFound, got %v", err)
}
}

func TestGitHubListReposStopsPaginatingAtLimit(t *testing.T) {
var requests int
mux := http.NewServeMux()
mux.HandleFunc("GET /api/v3/orgs/myorg/repos", func(w http.ResponseWriter, r *http.Request) {
requests++
page := r.URL.Query().Get("page")
if page == "" {
page = "1"
}
w.Header().Set("Link", `<http://example.com?page=`+page+`>; rel="next"`)
_ = json.NewEncoder(w).Encode([]*github.Repository{
{
FullName: ptr("myorg/repo-" + page),
Name: ptr("repo-" + page),
Owner: &github.User{Login: ptr("myorg")},
Archived: ptrBool(false),
Fork: ptrBool(false),
},
})
})

srv := httptest.NewServer(mux)
defer srv.Close()

s := newTestGitHubRepoService(srv)
repos, err := s.List(context.Background(), "myorg", forge.ListRepoOpts{Limit: 1})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(repos) != 1 {
t.Fatalf("expected 1 repo, got %d", len(repos))
}
if requests != 1 {
t.Fatalf("expected List to stop after 1 request once Limit was reached, got %d requests", requests)
}
}
Loading