Skip to content

Commit eda395f

Browse files
authored
Merge pull request #35 from git-pkgs/add-list-forks
Add list forks support
2 parents 8717241 + bb70e70 commit eda395f

12 files changed

Lines changed: 513 additions & 0 deletions

File tree

bitbucket/bitbucket.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,36 @@ func (s *bitbucketRepoService) Fork(ctx context.Context, owner, repo string, opt
311311
return &result, nil
312312
}
313313

314+
func (s *bitbucketRepoService) ListForks(ctx context.Context, owner, repo string, opts forge.ListForksOpts) ([]forge.Repository, error) {
315+
perPage := opts.PerPage
316+
if perPage <= 0 {
317+
perPage = 100
318+
}
319+
320+
var all []forge.Repository
321+
url := fmt.Sprintf("%s/repositories/%s/%s/forks?pagelen=%d", bitbucketAPI, owner, repo, perPage)
322+
323+
for url != "" {
324+
var page bbReposResponse
325+
if err := s.getJSON(ctx, url, &page); err != nil {
326+
return nil, err
327+
}
328+
for _, bb := range page.Values {
329+
all = append(all, convertBitbucketRepo(bb))
330+
}
331+
if opts.Limit > 0 && len(all) >= opts.Limit {
332+
break
333+
}
334+
url = page.Next
335+
}
336+
337+
if opts.Limit > 0 && len(all) > opts.Limit {
338+
all = all[:opts.Limit]
339+
}
340+
341+
return all, nil
342+
}
343+
314344
func (s *bitbucketRepoService) ListTags(ctx context.Context, owner, repo string) ([]forge.Tag, error) {
315345
var allTags []forge.Tag
316346
url := fmt.Sprintf("%s/repositories/%s/%s/refs/tags?pagelen=100", bitbucketAPI, owner, repo)

bitbucket/forks_test.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package bitbucket
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
forge "github.com/git-pkgs/forge"
7+
"net/http"
8+
"net/http/httptest"
9+
"testing"
10+
)
11+
12+
func TestBitbucketListForks(t *testing.T) {
13+
mux := http.NewServeMux()
14+
mux.HandleFunc("GET /2.0/repositories/atlassian/stash/forks", func(w http.ResponseWriter, r *http.Request) {
15+
_ = json.NewEncoder(w).Encode(map[string]any{
16+
"values": []map[string]any{
17+
{
18+
"full_name": "alice/stash",
19+
"name": "stash",
20+
"language": "Java",
21+
"created_on": "2024-01-01T00:00:00Z",
22+
"updated_on": "2024-06-01T00:00:00Z",
23+
"links": map[string]any{
24+
"html": map[string]string{"href": "https://bitbucket.org/alice/stash"},
25+
"clone": []map[string]string{{"href": "https://bitbucket.org/alice/stash.git", "name": "https"}},
26+
},
27+
"owner": map[string]any{"nickname": "alice"},
28+
},
29+
},
30+
"next": "",
31+
})
32+
})
33+
34+
srv := httptest.NewServer(mux)
35+
defer srv.Close()
36+
37+
oldAPI := bitbucketAPI
38+
setBitbucketAPI(srv.URL + "/2.0")
39+
defer func() { bitbucketAPI = oldAPI }()
40+
41+
f := New("token", nil)
42+
forks, err := f.Repos().ListForks(context.Background(), "atlassian", "stash", forge.ListForksOpts{})
43+
if err != nil {
44+
t.Fatalf("unexpected error: %v", err)
45+
}
46+
if len(forks) != 1 {
47+
t.Fatalf("expected 1 fork, got %d", len(forks))
48+
}
49+
if forks[0].FullName != "alice/stash" {
50+
t.Errorf("expected full_name alice/stash, got %s", forks[0].FullName)
51+
}
52+
}

forges_test.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -540,6 +540,12 @@ func (m *mockRepoService) Fork(_ context.Context, owner, repo string, opts ForkR
540540
return m.repo, nil
541541
}
542542

543+
func (m *mockRepoService) ListForks(_ context.Context, owner, repo string, opts ListForksOpts) ([]Repository, error) {
544+
m.lastOwner = owner
545+
m.lastRepo = repo
546+
return m.repos, nil
547+
}
548+
543549
func (m *mockRepoService) ListTags(_ context.Context, owner, repo string) ([]Tag, error) {
544550
m.lastOwner = owner
545551
m.lastRepo = repo

gitea/forks_test.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package gitea
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
forge "github.com/git-pkgs/forge"
7+
"net/http"
8+
"net/http/httptest"
9+
"testing"
10+
"time"
11+
)
12+
13+
func TestGiteaListForks(t *testing.T) {
14+
mux := http.NewServeMux()
15+
mux.HandleFunc("GET /api/v1/version", giteaVersionHandler)
16+
mux.HandleFunc("GET /api/v1/repos/testorg/testrepo/forks", func(w http.ResponseWriter, r *http.Request) {
17+
_ = json.NewEncoder(w).Encode([]map[string]any{
18+
{
19+
"full_name": "alice/testrepo",
20+
"name": "testrepo",
21+
"html_url": "https://codeberg.org/alice/testrepo",
22+
"default_branch": "main",
23+
"fork": true,
24+
"archived": false,
25+
"private": false,
26+
"owner": map[string]any{"login": "alice"},
27+
"created_at": time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC).Format(time.RFC3339),
28+
"updated_at": time.Date(2024, 6, 1, 0, 0, 0, 0, time.UTC).Format(time.RFC3339),
29+
},
30+
{
31+
"full_name": "bob/testrepo",
32+
"name": "testrepo",
33+
"html_url": "https://codeberg.org/bob/testrepo",
34+
"default_branch": "main",
35+
"fork": true,
36+
"archived": false,
37+
"private": false,
38+
"owner": map[string]any{"login": "bob"},
39+
"created_at": time.Date(2024, 2, 1, 0, 0, 0, 0, time.UTC).Format(time.RFC3339),
40+
"updated_at": time.Date(2024, 6, 1, 0, 0, 0, 0, time.UTC).Format(time.RFC3339),
41+
},
42+
})
43+
})
44+
45+
srv := httptest.NewServer(mux)
46+
defer srv.Close()
47+
48+
f := New(srv.URL, "test-token", nil)
49+
forks, err := f.Repos().ListForks(context.Background(), "testorg", "testrepo", forge.ListForksOpts{})
50+
if err != nil {
51+
t.Fatalf("unexpected error: %v", err)
52+
}
53+
if len(forks) != 2 {
54+
t.Fatalf("expected 2 forks, got %d", len(forks))
55+
}
56+
assertEqual(t, "forks[0].FullName", "alice/testrepo", forks[0].FullName)
57+
assertEqual(t, "forks[1].FullName", "bob/testrepo", forks[1].FullName)
58+
}
59+
60+
func TestGiteaListForksNotFound(t *testing.T) {
61+
mux := http.NewServeMux()
62+
mux.HandleFunc("GET /api/v1/version", giteaVersionHandler)
63+
mux.HandleFunc("GET /api/v1/repos/testorg/nope/forks", func(w http.ResponseWriter, r *http.Request) {
64+
w.WriteHeader(http.StatusNotFound)
65+
_ = json.NewEncoder(w).Encode(map[string]string{"message": "Not Found"})
66+
})
67+
68+
srv := httptest.NewServer(mux)
69+
defer srv.Close()
70+
71+
f := New(srv.URL, "test-token", nil)
72+
_, err := f.Repos().ListForks(context.Background(), "testorg", "nope", forge.ListForksOpts{})
73+
if err != forge.ErrNotFound {
74+
t.Fatalf("expected forge.ErrNotFound, got %v", err)
75+
}
76+
}

gitea/gitea.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,43 @@ func (s *giteaRepoService) Fork(ctx context.Context, owner, repo string, opts fo
293293
return &result, nil
294294
}
295295

296+
func (s *giteaRepoService) ListForks(ctx context.Context, owner, repo string, opts forge.ListForksOpts) ([]forge.Repository, error) {
297+
perPage := opts.PerPage
298+
if perPage <= 0 {
299+
perPage = 50
300+
}
301+
page := opts.Page
302+
if page <= 0 {
303+
page = 1
304+
}
305+
306+
var all []forge.Repository
307+
for {
308+
forks, resp, err := s.client.ListForks(owner, repo, gitea.ListForksOptions{
309+
ListOptions: gitea.ListOptions{Page: page, PageSize: perPage},
310+
})
311+
if err != nil {
312+
if resp != nil && resp.StatusCode == http.StatusNotFound {
313+
return nil, forge.ErrNotFound
314+
}
315+
return nil, err
316+
}
317+
for _, r := range forks {
318+
all = append(all, convertGiteaRepo(r))
319+
}
320+
if len(forks) < perPage || (opts.Limit > 0 && len(all) >= opts.Limit) {
321+
break
322+
}
323+
page++
324+
}
325+
326+
if opts.Limit > 0 && len(all) > opts.Limit {
327+
all = all[:opts.Limit]
328+
}
329+
330+
return all, nil
331+
}
332+
296333
func (s *giteaRepoService) ListTags(ctx context.Context, owner, repo string) ([]forge.Tag, error) {
297334
var allTags []forge.Tag
298335
page := 1

github/forks_test.go

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package github
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
forge "github.com/git-pkgs/forge"
7+
"net/http"
8+
"net/http/httptest"
9+
"testing"
10+
11+
"github.com/google/go-github/v82/github"
12+
)
13+
14+
func TestGitHubListForks(t *testing.T) {
15+
mux := http.NewServeMux()
16+
mux.HandleFunc("GET /api/v3/repos/octocat/hello-world/forks", func(w http.ResponseWriter, r *http.Request) {
17+
_ = json.NewEncoder(w).Encode([]*github.Repository{
18+
{FullName: ptr("alice/hello-world"), Name: ptr("hello-world"), Fork: ptrBool(true), Owner: &github.User{Login: ptr("alice")}},
19+
{FullName: ptr("bob/hello-world"), Name: ptr("hello-world"), Fork: ptrBool(true), Owner: &github.User{Login: ptr("bob")}},
20+
})
21+
})
22+
23+
srv := httptest.NewServer(mux)
24+
defer srv.Close()
25+
26+
c := github.NewClient(nil)
27+
c, _ = c.WithEnterpriseURLs(srv.URL+"/api/v3", srv.URL+"/api/v3")
28+
s := &gitHubRepoService{client: c}
29+
30+
forks, err := s.ListForks(context.Background(), "octocat", "hello-world", forge.ListForksOpts{})
31+
if err != nil {
32+
t.Fatalf("unexpected error: %v", err)
33+
}
34+
if len(forks) != 2 {
35+
t.Fatalf("expected 2 forks, got %d", len(forks))
36+
}
37+
assertEqual(t, "forks[0].FullName", "alice/hello-world", forks[0].FullName)
38+
assertEqual(t, "forks[1].FullName", "bob/hello-world", forks[1].FullName)
39+
}
40+
41+
func TestGitHubListForksNotFound(t *testing.T) {
42+
mux := http.NewServeMux()
43+
mux.HandleFunc("GET /api/v3/repos/octocat/nope/forks", func(w http.ResponseWriter, r *http.Request) {
44+
w.WriteHeader(http.StatusNotFound)
45+
_ = json.NewEncoder(w).Encode(map[string]string{"message": "Not Found"})
46+
})
47+
48+
srv := httptest.NewServer(mux)
49+
defer srv.Close()
50+
51+
c := github.NewClient(nil)
52+
c, _ = c.WithEnterpriseURLs(srv.URL+"/api/v3", srv.URL+"/api/v3")
53+
s := &gitHubRepoService{client: c}
54+
55+
_, err := s.ListForks(context.Background(), "octocat", "nope", forge.ListForksOpts{})
56+
if err != forge.ErrNotFound {
57+
t.Fatalf("expected forge.ErrNotFound, got %v", err)
58+
}
59+
}
60+
61+
func TestGitHubListForksWithLimit(t *testing.T) {
62+
mux := http.NewServeMux()
63+
mux.HandleFunc("GET /api/v3/repos/octocat/hello-world/forks", func(w http.ResponseWriter, r *http.Request) {
64+
_ = json.NewEncoder(w).Encode([]*github.Repository{
65+
{FullName: ptr("alice/hello-world"), Name: ptr("hello-world"), Fork: ptrBool(true), Owner: &github.User{Login: ptr("alice")}},
66+
{FullName: ptr("bob/hello-world"), Name: ptr("hello-world"), Fork: ptrBool(true), Owner: &github.User{Login: ptr("bob")}},
67+
{FullName: ptr("carol/hello-world"), Name: ptr("hello-world"), Fork: ptrBool(true), Owner: &github.User{Login: ptr("carol")}},
68+
})
69+
})
70+
71+
srv := httptest.NewServer(mux)
72+
defer srv.Close()
73+
74+
c := github.NewClient(nil)
75+
c, _ = c.WithEnterpriseURLs(srv.URL+"/api/v3", srv.URL+"/api/v3")
76+
s := &gitHubRepoService{client: c}
77+
78+
forks, err := s.ListForks(context.Background(), "octocat", "hello-world", forge.ListForksOpts{Limit: 2})
79+
if err != nil {
80+
t.Fatalf("unexpected error: %v", err)
81+
}
82+
if len(forks) != 2 {
83+
t.Fatalf("expected 2 forks (limit), got %d", len(forks))
84+
}
85+
}

github/github.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,46 @@ func (s *gitHubRepoService) Fork(ctx context.Context, owner, repo string, opts f
302302
return &result, nil
303303
}
304304

305+
func (s *gitHubRepoService) ListForks(ctx context.Context, owner, repo string, opts forge.ListForksOpts) ([]forge.Repository, error) {
306+
perPage := opts.PerPage
307+
if perPage <= 0 {
308+
perPage = 100
309+
}
310+
page := opts.Page
311+
if page <= 0 {
312+
page = 1
313+
}
314+
315+
ghOpts := &github.RepositoryListForksOptions{
316+
Sort: opts.Sort,
317+
ListOptions: github.ListOptions{PerPage: perPage, Page: page},
318+
}
319+
320+
var all []forge.Repository
321+
for {
322+
forks, resp, err := s.client.Repositories.ListForks(ctx, owner, repo, ghOpts)
323+
if err != nil {
324+
if resp != nil && resp.StatusCode == http.StatusNotFound {
325+
return nil, forge.ErrNotFound
326+
}
327+
return nil, err
328+
}
329+
for _, r := range forks {
330+
all = append(all, convertGitHubRepo(r))
331+
}
332+
if resp.NextPage == 0 || (opts.Limit > 0 && len(all) >= opts.Limit) {
333+
break
334+
}
335+
ghOpts.Page = resp.NextPage
336+
}
337+
338+
if opts.Limit > 0 && len(all) > opts.Limit {
339+
all = all[:opts.Limit]
340+
}
341+
342+
return all, nil
343+
}
344+
305345
func (s *gitHubRepoService) ListTags(ctx context.Context, owner, repo string) ([]forge.Tag, error) {
306346
var allTags []forge.Tag
307347
opts := &github.ListOptions{PerPage: 100}

0 commit comments

Comments
 (0)