Skip to content
Open
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
115 changes: 83 additions & 32 deletions backend/plugins/bitbucket/api/remote_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,13 @@ func listBitbucketWorkspaces(
err errors.Error,
) {
var res *http.Response
// /user/permissions/workspaces was removed by Bitbucket CHANGE-2770; /workspaces
// lists the current user's workspaces and is the supported replacement.
res, err = apiClient.Get(
"/user/permissions/workspaces",
"/workspaces",
url.Values{
"sort": {"workspace.slug"},
"fields": {"values.workspace.slug,values.workspace.name,pagelen,page,size"},
"sort": {"slug"},
"fields": {"values.slug,values.name,pagelen,page,size"},
"page": {fmt.Sprintf("%v", page.Page)},
"pagelen": {fmt.Sprintf("%v", page.PageLen)},
},
Expand All @@ -98,9 +100,9 @@ func listBitbucketWorkspaces(
for _, r := range resBody.Values {
children = append(children, dsmodels.DsRemoteApiScopeListEntry[models.BitbucketRepo]{
Type: api.RAS_ENTRY_TYPE_GROUP,
Id: r.Workspace.Slug,
Name: r.Workspace.Name,
FullName: r.Workspace.Name,
Id: r.Slug,
Name: r.Name,
FullName: r.Name,
})
}
return
Expand Down Expand Up @@ -152,46 +154,95 @@ func listBitbucketRepos(
return
}

// searchBitbucketRepos searches repositories by name across the user's workspaces.
// The cross-workspace GET /repositories?role=member was removed by Bitbucket
// CHANGE-2770, so we enumerate workspaces and query the workspace-scoped
// GET /repositories/{workspace} endpoint for each, aggregating up to PageSize hits.
func searchBitbucketRepos(
apiClient plugin.ApiClient,
params *dsmodels.DsRemoteApiScopeSearchParams,
) (
children []dsmodels.DsRemoteApiScopeListEntry[models.BitbucketRepo],
err errors.Error,
) {
var res *http.Response
res, err = apiClient.Get(
"/repositories",
url.Values{
"sort": {"name"},
"fields": {"values.name,values.full_name,values.language,values.description,values.owner.display_name,values.created_on,values.updated_on,values.links.clone,values.links.html,pagelen,page,size"},
"role": {"member"},
"q": {fmt.Sprintf(`full_name~"%s"`, params.Search)},
"page": {fmt.Sprintf("%v", params.Page)},
"pagelen": {fmt.Sprintf("%v", params.PageSize)},
},
nil,
)
if err != nil {
return nil, err
pageSize := params.PageSize
if pageSize == 0 {
pageSize = 100
}
var resBody models.ReposResponse
err = api.UnmarshalResponse(res, &resBody)

workspaces, err := listAllBitbucketWorkspaces(apiClient)
if err != nil {
return
return nil, err
}
for _, r := range resBody.Values {
children = append(children, dsmodels.DsRemoteApiScopeListEntry[models.BitbucketRepo]{
Type: api.RAS_ENTRY_TYPE_SCOPE,
Id: r.FullName,
Name: r.Name,
FullName: r.FullName,
Data: r.ConvertApiScope(),
})

for _, workspace := range workspaces {
if len(children) >= pageSize {
break
}
var res *http.Response
res, err = apiClient.Get(
fmt.Sprintf("/repositories/%s", workspace),
url.Values{
"sort": {"name"},
"fields": {"values.name,values.full_name,values.language,values.description,values.owner.display_name,values.created_on,values.updated_on,values.links.clone,values.links.html,pagelen,page,size"},
"q": {fmt.Sprintf(`name~"%s"`, params.Search)},
"pagelen": {fmt.Sprintf("%v", pageSize)},
},
nil,
)
if err != nil {
return nil, err
}
var resBody models.ReposResponse
err = api.UnmarshalResponse(res, &resBody)
if err != nil {
return
}
for _, r := range resBody.Values {
children = append(children, dsmodels.DsRemoteApiScopeListEntry[models.BitbucketRepo]{
Type: api.RAS_ENTRY_TYPE_SCOPE,
Id: r.FullName,
Name: r.Name,
FullName: r.FullName,
Data: r.ConvertApiScope(),
})
}
}
return
}

// listAllBitbucketWorkspaces returns every workspace slug accessible to the
// authenticated user, following pagination of GET /2.0/workspaces.
func listAllBitbucketWorkspaces(apiClient plugin.ApiClient) ([]string, errors.Error) {
var slugs []string
for page := 1; ; page++ {
res, err := apiClient.Get(
"/workspaces",
url.Values{
"sort": {"slug"},
"fields": {"values.slug,pagelen,page,size"},
"page": {fmt.Sprintf("%v", page)},
"pagelen": {"100"},
},
nil,
)
if err != nil {
return nil, err
}
var resBody models.WorkspaceResponse
if err = api.UnmarshalResponse(res, &resBody); err != nil {
return nil, err
}
for _, w := range resBody.Values {
slugs = append(slugs, w.Slug)
}
if len(resBody.Values) == 0 || page*resBody.Pagelen >= resBody.Size {
break
}
}
return slugs, nil
}

// RemoteScopes list all available scopes on the remote server
// @Summary list all available scopes on the remote server
// @Description list all available scopes on the remote server
Expand Down
17 changes: 5 additions & 12 deletions backend/plugins/bitbucket/models/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,27 +117,20 @@ type WorkspaceResponse struct {
Values []GroupResponse `json:"values"`
}

// GroupResponse maps an entry from GET /2.0/workspaces. The cross-workspace
// GET /2.0/user/permissions/workspaces was removed by Bitbucket CHANGE-2770,
// so slug/name now live at the top level instead of under a nested workspace.
type GroupResponse struct {
//Type string `json:"type"`
//Permission string `json:"permission"`
//LastAccessed time.Time `json:"last_accessed"`
//AddedOn time.Time `json:"added_on"`
Workspace WorkspaceItem `json:"workspace"`
}

type WorkspaceItem struct {
//Type string `json:"type"`
//Uuid string `json:"uuid"`
Slug string `json:"slug" group:"id"`
Name string `json:"name" group:"name"`
}

func (p GroupResponse) GroupId() string {
return p.Workspace.Slug
return p.Slug
}

func (p GroupResponse) GroupName() string {
return p.Workspace.Name
return p.Name
}

type ReposResponse struct {
Expand Down