Skip to content

Commit b2db0cc

Browse files
authored
Merge pull request #351 from drone/ritek/PIPE-32185-new
feat: [PIPE-32185]: handle deprecated cross-workspace API endpoints
2 parents 41cf654 + 3ecb23a commit b2db0cc

17 files changed

+2987
-64
lines changed

scm/driver/bitbucket/content_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,7 @@ func TestContentList(t *testing.T) {
244244
func TestContentListWithUrlInput(t *testing.T) {
245245
defer gock.Off()
246246

247-
mockNextPageUri := "https://api.bitbucket.org/2.0/repositories/atlassian/atlaskit/src/master/packages/activity?pageLen=3&page=RPfL"
247+
mockNextPageUri := "https://api.bitbucket.org/2.0/repositories/atlassian/atlaskit/src/master/packages/activity?pagelen=3&page=RPfL"
248248

249249
gock.New(mockNextPageUri).
250250
Reply(200).

scm/driver/bitbucket/org.go

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ import (
1111
"github.com/drone/go-scm/scm"
1212
)
1313

14+
const (
15+
avatarURLTemplate = "https://bitbucket.org/account/%s/avatar/32/"
16+
)
17+
1418
type organizationService struct {
1519
client *wrapper
1620
}
@@ -27,33 +31,43 @@ func (s *organizationService) FindMembership(ctx context.Context, name, username
2731
}
2832

2933
func (s *organizationService) List(ctx context.Context, opts scm.ListOptions) ([]*scm.Organization, *scm.Response, error) {
30-
path := fmt.Sprintf("2.0/workspaces?%s", encodeListRoleOptions(opts))
31-
out := new(organizationList)
34+
path := fmt.Sprintf("2.0/user/workspaces?%s", encodeListRoleOptions(opts))
35+
out := new(workspaceAccessList)
3236
res, err := s.client.do(ctx, "GET", path, nil, out)
3337
copyPagination(out.pagination, res)
34-
return convertOrganizationList(out), res, err
38+
return convertWorkspaceAccessList(out), res, err
3539
}
3640

37-
func convertOrganizationList(from *organizationList) []*scm.Organization {
41+
func convertWorkspaceAccessList(from *workspaceAccessList) []*scm.Organization {
3842
to := []*scm.Organization{}
39-
for _, v := range from.Values {
40-
to = append(to, convertOrganization(v))
43+
for _, value := range from.Values {
44+
if value.Workspace != nil {
45+
to = append(to, convertWorkspace(value.Workspace))
46+
}
4147
}
4248
return to
4349
}
4450

45-
type organizationList struct {
46-
pagination
47-
Values []*organization `json:"values"`
48-
}
49-
5051
type organization struct {
5152
Login string `json:"slug"`
5253
}
5354

5455
func convertOrganization(from *organization) *scm.Organization {
5556
return &scm.Organization{
5657
Name: from.Login,
57-
Avatar: fmt.Sprintf("https://bitbucket.org/account/%s/avatar/32/", from.Login),
58+
Avatar: fmt.Sprintf(avatarURLTemplate, from.Login),
59+
}
60+
}
61+
62+
func convertWorkspace(workspace *workspace) *scm.Organization {
63+
avatar := ""
64+
if workspace.Links.Avatar.Href != "" {
65+
avatar = workspace.Links.Avatar.Href
66+
} else {
67+
avatar = fmt.Sprintf(avatarURLTemplate, workspace.Slug)
68+
}
69+
return &scm.Organization{
70+
Name: workspace.Slug,
71+
Avatar: avatar,
5872
}
5973
}

scm/driver/bitbucket/org_test.go

Lines changed: 118 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,13 @@ func TestOrganizationList(t *testing.T) {
4545
defer gock.Off()
4646

4747
gock.New("https://api.bitbucket.org").
48-
Get("/2.0/workspaces").
48+
Get("/2.0/user/workspaces").
4949
MatchParam("pagelen", "30").
5050
MatchParam("page", "1").
51+
MatchParam("role", "member").
5152
Reply(200).
5253
Type("application/json").
53-
File("testdata/teams.json")
54+
File("testdata/user_workspaces.json")
5455

5556
client, _ := New("https://api.bitbucket.org")
5657
got, _, err := client.Organizations.List(context.Background(), scm.ListOptions{Size: 30, Page: 1})
@@ -59,11 +60,125 @@ func TestOrganizationList(t *testing.T) {
5960
}
6061

6162
want := []*scm.Organization{}
62-
raw, _ := ioutil.ReadFile("testdata/teams.json.golden")
63+
raw, _ := ioutil.ReadFile("testdata/user_workspaces.json.golden")
6364
json.Unmarshal(raw, &want)
6465

6566
if diff := cmp.Diff(got, want); diff != "" {
6667
t.Errorf("Unexpected Results")
6768
t.Log(diff)
6869
}
6970
}
71+
72+
func TestConvertWorkspace(t *testing.T) {
73+
tests := []struct {
74+
name string
75+
workspace *workspace
76+
want *scm.Organization
77+
}{
78+
{
79+
name: "workspace with avatar link",
80+
workspace: &workspace{
81+
Slug: "my-workspace",
82+
Links: struct {
83+
Avatar link `json:"avatar"`
84+
}{
85+
Avatar: link{Href: "https://bitbucket.org/account/my-workspace/avatar/32/"},
86+
},
87+
},
88+
want: &scm.Organization{
89+
Name: "my-workspace",
90+
Avatar: "https://bitbucket.org/account/my-workspace/avatar/32/",
91+
},
92+
},
93+
{
94+
name: "workspace without avatar link",
95+
workspace: &workspace{
96+
Slug: "test-workspace",
97+
Links: struct {
98+
Avatar link `json:"avatar"`
99+
}{
100+
Avatar: link{Href: ""},
101+
},
102+
},
103+
want: &scm.Organization{
104+
Name: "test-workspace",
105+
Avatar: "https://bitbucket.org/account/test-workspace/avatar/32/",
106+
},
107+
},
108+
}
109+
110+
for _, tt := range tests {
111+
t.Run(tt.name, func(t *testing.T) {
112+
got := convertWorkspace(tt.workspace)
113+
if diff := cmp.Diff(tt.want, got); diff != "" {
114+
t.Errorf("convertWorkspace() mismatch (-want +got):\n%s", diff)
115+
}
116+
})
117+
}
118+
}
119+
120+
func TestConvertWorkspaceAccessList(t *testing.T) {
121+
tests := []struct {
122+
name string
123+
from *workspaceAccessList
124+
want []*scm.Organization
125+
}{
126+
{
127+
name: "valid workspaces",
128+
from: &workspaceAccessList{
129+
Values: []*workspaceAccess{
130+
{
131+
Workspace: &workspace{
132+
Slug: "workspace1",
133+
Links: struct {
134+
Avatar link `json:"avatar"`
135+
}{
136+
Avatar: link{Href: "https://bitbucket.org/account/workspace1/avatar/32/"},
137+
},
138+
},
139+
},
140+
{
141+
Workspace: &workspace{
142+
Slug: "workspace2",
143+
Links: struct {
144+
Avatar link `json:"avatar"`
145+
}{
146+
Avatar: link{Href: ""},
147+
},
148+
},
149+
},
150+
},
151+
},
152+
want: []*scm.Organization{
153+
{
154+
Name: "workspace1",
155+
Avatar: "https://bitbucket.org/account/workspace1/avatar/32/",
156+
},
157+
{
158+
Name: "workspace2",
159+
Avatar: "https://bitbucket.org/account/workspace2/avatar/32/",
160+
},
161+
},
162+
},
163+
{
164+
name: "nil workspace",
165+
from: &workspaceAccessList{
166+
Values: []*workspaceAccess{
167+
{
168+
Workspace: nil,
169+
},
170+
},
171+
},
172+
want: []*scm.Organization{},
173+
},
174+
}
175+
176+
for _, tt := range tests {
177+
t.Run(tt.name, func(t *testing.T) {
178+
got := convertWorkspaceAccessList(tt.from)
179+
if diff := cmp.Diff(tt.want, got); diff != "" {
180+
t.Errorf("convertWorkspaceAccessList() mismatch (-want +got):\n%s", diff)
181+
}
182+
})
183+
}
184+
}

scm/driver/bitbucket/repo.go

Lines changed: 132 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"context"
99
"fmt"
1010
"net/url"
11+
"strings"
1112
"time"
1213

1314
"github.com/drone/go-scm/scm"
@@ -87,34 +88,108 @@ func (s *repositoryService) FindHook(ctx context.Context, repo string, id string
8788

8889
// FindPerms returns the repository permissions.
8990
func (s *repositoryService) FindPerms(ctx context.Context, repo string) (*scm.Perm, *scm.Response, error) {
90-
path := fmt.Sprintf("2.0/user/permissions/repositories?q=repository.full_name=%q", repo)
91-
out := new(perms)
92-
res, err := s.client.do(ctx, "GET", path, nil, out)
93-
return convertPerms(out), res, err
91+
// First, try to fetch using the repo identifier provided
92+
perm, res, err := s.findPermsWithIdentifier(ctx, repo)
93+
if err == nil {
94+
return perm, res, nil
95+
}
96+
97+
// If that fails and repo isn't in "workspace/repo" format,
98+
// search all workspaces as a fallback
99+
if !strings.Contains(repo, "/") {
100+
return s.findPermsAcrossWorkspaces(ctx, repo)
101+
}
102+
103+
// Repo is in "workspace/repo" format and fetch failed
104+
return nil, res, err
94105
}
95106

96-
// List returns the user repository list.
107+
// findPermsWithIdentifier attempts to fetch permissions using either the
108+
// extracted workspace from the client URL or the workspace in "workspace/repo" format.
109+
func (s *repositoryService) findPermsWithIdentifier(ctx context.Context, repo string) (*scm.Perm, *scm.Response, error) {
110+
workspace := s.client.extractWorkspaceFromURL()
111+
repoSlug := repo
112+
113+
// If workspace is available from URL, use it and only extract repo slug
114+
if workspace != "" {
115+
if strings.Contains(repo, "/") {
116+
_, repoSlug = scm.Split(repo)
117+
}
118+
return s.fetchRepoPerms(ctx, workspace, repoSlug)
119+
}
120+
121+
// If repo is in "workspace/repo" format, use the workspace from repo identifier
122+
if strings.Contains(repo, "/") {
123+
workspace, repoSlug = scm.Split(repo)
124+
return s.fetchRepoPerms(ctx, workspace, repoSlug)
125+
}
126+
127+
// No workspace available
128+
return nil, nil, fmt.Errorf("unable to determine workspace for repository %s", repo)
129+
}
130+
131+
// findPermsAcrossWorkspaces searches all user workspaces for the repository
132+
// and returns permissions if the user has access.
133+
func (s *repositoryService) findPermsAcrossWorkspaces(ctx context.Context, repoSlug string) (*scm.Perm, *scm.Response, error) {
134+
workspaces, err := s.client.fetchAllWorkspaces(ctx)
135+
if err != nil {
136+
return nil, nil, err
137+
}
138+
139+
for _, workspace := range workspaces {
140+
perm, res, err := s.fetchRepoPerms(ctx, workspace, repoSlug)
141+
if err != nil {
142+
// If it's a 404, the repo doesn't exist in this workspace
143+
if res != nil && res.Status == 404 {
144+
continue
145+
}
146+
// For other errors (network, auth, etc.), return immediately
147+
return nil, res, err
148+
}
149+
150+
// Return permissions if user has any access to the repository
151+
if perm.Pull || perm.Push || perm.Admin {
152+
return perm, res, nil
153+
}
154+
}
155+
156+
return nil, nil, fmt.Errorf("repository %s not found in any workspace", repoSlug)
157+
}
158+
159+
// List returns the user repository list using workspace-aware pagination.
97160
func (s *repositoryService) List(ctx context.Context, opts scm.ListOptions) ([]*scm.Repository, *scm.Response, error) {
98-
path := fmt.Sprintf("2.0/repositories?%s", encodeListRoleOptions(opts))
99161
if opts.URL != "" {
100-
path = opts.URL
162+
out := new(repositories)
163+
res, err := s.client.do(ctx, "GET", opts.URL, nil, &out)
164+
if err != nil {
165+
return nil, res, err
166+
}
167+
copyPagination(out.pagination, res)
168+
return convertRepositoryList(out), res, err
101169
}
102-
out := new(repositories)
103-
res, err := s.client.do(ctx, "GET", path, nil, &out)
104-
copyPagination(out.pagination, res)
105-
return convertRepositoryList(out), res, err
170+
171+
if opts.Page == 0 {
172+
opts.Page = 1
173+
}
174+
return s.client.fetchReposWithPagination(ctx, encodeListRoleOptions(opts), opts.Page, opts.Size)
106175
}
107176

108177
// ListV2 returns the user repository list based on the searchTerm passed.
109178
func (s *repositoryService) ListV2(ctx context.Context, opts scm.RepoListOptions) ([]*scm.Repository, *scm.Response, error) {
110-
path := fmt.Sprintf("2.0/repositories?%s", encodeRepoListOptions(opts))
111179
if opts.ListOptions.URL != "" {
112-
path = opts.ListOptions.URL
180+
out := new(repositories)
181+
res, err := s.client.do(ctx, "GET", opts.ListOptions.URL, nil, &out)
182+
if err != nil {
183+
return nil, res, err
184+
}
185+
copyPagination(out.pagination, res)
186+
return convertRepositoryList(out), res, err
113187
}
114-
out := new(repositories)
115-
res, err := s.client.do(ctx, "GET", path, nil, &out)
116-
copyPagination(out.pagination, res)
117-
return convertRepositoryList(out), res, err
188+
189+
if opts.ListOptions.Page == 0 {
190+
opts.ListOptions.Page = 1
191+
}
192+
return s.client.fetchReposWithPagination(ctx, encodeRepoListOptions(opts), opts.ListOptions.Page, opts.ListOptions.Size)
118193
}
119194

120195
func (s *repositoryService) ListNamespace(ctx context.Context, namespace string, opts scm.ListOptions) ([]*scm.Repository, *scm.Response, error) {
@@ -391,3 +466,43 @@ func convertFromState(from scm.State) string {
391466
return "FAILED"
392467
}
393468
}
469+
470+
// workspaceRepoPerms represents the response from
471+
// GET /2.0/workspaces/{workspace}/permissions/repositories/{repo_slug}
472+
type workspaceRepoPerms struct {
473+
Values []*workspaceRepoPerm `json:"values"`
474+
}
475+
476+
type workspaceRepoPerm struct {
477+
Permission string `json:"permission"` // "admin", "write", "read"
478+
}
479+
480+
func convertWorkspaceRepoPerms(from *workspaceRepoPerms) *scm.Perm {
481+
to := new(scm.Perm)
482+
if len(from.Values) == 0 {
483+
return to
484+
}
485+
switch from.Values[0].Permission {
486+
case "admin":
487+
to.Pull = true
488+
to.Push = true
489+
to.Admin = true
490+
case "write":
491+
to.Pull = true
492+
to.Push = true
493+
default:
494+
to.Pull = true
495+
}
496+
return to
497+
}
498+
499+
// fetchRepoPerms fetches repository permissions for a given workspace and repo slug.
500+
func (s *repositoryService) fetchRepoPerms(ctx context.Context, workspace, repoSlug string) (*scm.Perm, *scm.Response, error) {
501+
path := fmt.Sprintf("2.0/workspaces/%s/permissions/repositories/%s", workspace, repoSlug)
502+
out := new(workspaceRepoPerms)
503+
res, err := s.client.do(ctx, "GET", path, nil, out)
504+
if err != nil {
505+
return nil, res, err
506+
}
507+
return convertWorkspaceRepoPerms(out), res, nil
508+
}

0 commit comments

Comments
 (0)