diff --git a/scm/client.go b/scm/client.go index ca3d1cafc..0d05b3b9a 100644 --- a/scm/client.go +++ b/scm/client.go @@ -75,6 +75,19 @@ type ( PageListOptions ListOptions } + // RepoListOptions specifies optional repo search term and pagination + // parameters. + RepoListOptions struct { + ListOptions + RepoSearchTerm + } + + // RepoSearchTerm specifies searchable parameters. + RepoSearchTerm struct { + RepoName string + User string + } + // ListOptions specifies optional pagination // parameters. ListOptions struct { diff --git a/scm/driver/azure/git_test.go b/scm/driver/azure/git_test.go index 846e2b97b..f150dc38e 100644 --- a/scm/driver/azure/git_test.go +++ b/scm/driver/azure/git_test.go @@ -130,7 +130,7 @@ func TestGitListBranchesV2(t *testing.T) { Get("/ORG/PROJ/_apis/git/repositories/REPOID/"). Reply(200). Type("application/json"). - File("testdata/branchesFilter.json") + File("testdata/branches_filter.json") client := NewDefault("ORG", "PROJ") got, _, err := client.Git.ListBranchesV2(context.Background(), "REPOID", scm.BranchListOptions{SearchTerm: "main"}) @@ -140,7 +140,7 @@ func TestGitListBranchesV2(t *testing.T) { } want := []*scm.Reference{} - raw, _ := ioutil.ReadFile("testdata/branchesFilter.json.golden") + raw, _ := ioutil.ReadFile("testdata/branches_filter.json.golden") _ = json.Unmarshal(raw, &want) if diff := cmp.Diff(got, want); diff != "" { diff --git a/scm/driver/azure/repo.go b/scm/driver/azure/repo.go index 5d0427fa4..edfd4d876 100644 --- a/scm/driver/azure/repo.go +++ b/scm/driver/azure/repo.go @@ -56,6 +56,12 @@ func (s *RepositoryService) List(ctx context.Context, opts scm.ListOptions) ([]* return convertRepositoryList(out), res, err } +// ListV2 returns the user repository list. +func (s *RepositoryService) ListV2(ctx context.Context, opts scm.RepoListOptions) ([]*scm.Repository, *scm.Response, error) { + // Azure does not support search filters, hence calling List api without search filtering + return s.List(ctx, opts.ListOptions) +} + // ListHooks returns a list or repository hooks. func (s *RepositoryService) ListHooks(ctx context.Context, repo string, opts scm.ListOptions) ([]*scm.Hook, *scm.Response, error) { // https://docs.microsoft.com/en-us/rest/api/azure/devops/hooks/subscriptions/list?view=azure-devops-rest-6.0 diff --git a/scm/driver/azure/testdata/branchesFilter.json b/scm/driver/azure/testdata/branches_filter.json similarity index 100% rename from scm/driver/azure/testdata/branchesFilter.json rename to scm/driver/azure/testdata/branches_filter.json diff --git a/scm/driver/azure/testdata/branchesFilter.json.golden b/scm/driver/azure/testdata/branches_filter.json.golden similarity index 100% rename from scm/driver/azure/testdata/branchesFilter.json.golden rename to scm/driver/azure/testdata/branches_filter.json.golden diff --git a/scm/driver/bitbucket/git_test.go b/scm/driver/bitbucket/git_test.go index 275707a0b..74928583f 100644 --- a/scm/driver/bitbucket/git_test.go +++ b/scm/driver/bitbucket/git_test.go @@ -216,20 +216,19 @@ func TestGitListBranchesV2(t *testing.T) { MatchParam("pagelen", "30"). Reply(200). Type("application/json"). - File("testdata/branchesFilter.json") + File("testdata/branches_filter.json") client, _ := New("https://api.bitbucket.org") - got, res, err := client.Git.ListBranchesV2(context.Background(), "atlassian/stash-example-plugin", scm.BranchListOptions{SearchTerm: "mast", PageListOptions: struct { - URL string - Page int - Size int - }{Page: 1, Size: 30}}) + got, res, err := client.Git.ListBranchesV2(context.Background(), "atlassian/stash-example-plugin", scm.BranchListOptions{ + SearchTerm: "mast", + PageListOptions: scm.ListOptions{Page: 1, Size: 30}, + }) if err != nil { t.Error(err) } want := []*scm.Reference{} - raw, _ := ioutil.ReadFile("testdata/branchesFilter.json.golden") + raw, _ := ioutil.ReadFile("testdata/branches_filter.json.golden") json.Unmarshal(raw, &want) if diff := cmp.Diff(got, want); diff != "" { diff --git a/scm/driver/bitbucket/repo.go b/scm/driver/bitbucket/repo.go index cccae00d5..5929f6080 100644 --- a/scm/driver/bitbucket/repo.go +++ b/scm/driver/bitbucket/repo.go @@ -105,6 +105,18 @@ func (s *repositoryService) List(ctx context.Context, opts scm.ListOptions) ([]* return convertRepositoryList(out), res, err } +// ListV2 returns the user repository list based on the searchTerm passed. +func (s *repositoryService) ListV2(ctx context.Context, opts scm.RepoListOptions) ([]*scm.Repository, *scm.Response, error) { + path := fmt.Sprintf("2.0/repositories?%s", encodeRepoListOptions(opts)) + if opts.ListOptions.URL != "" { + path = opts.ListOptions.URL + } + out := new(repositories) + res, err := s.client.do(ctx, "GET", path, nil, &out) + copyPagination(out.pagination, res) + return convertRepositoryList(out), res, err +} + // ListHooks returns a list or repository hooks. func (s *repositoryService) ListHooks(ctx context.Context, repo string, opts scm.ListOptions) ([]*scm.Hook, *scm.Response, error) { path := fmt.Sprintf("2.0/repositories/%s/hooks?%s", repo, encodeListOptions(opts)) diff --git a/scm/driver/bitbucket/repo_test.go b/scm/driver/bitbucket/repo_test.go index 8b8efd79d..6c1f02b1b 100644 --- a/scm/driver/bitbucket/repo_test.go +++ b/scm/driver/bitbucket/repo_test.go @@ -136,6 +136,37 @@ func TestRepositoryList(t *testing.T) { } } +func TestRepositoryListV2(t *testing.T) { + defer gock.Off() + + gock.New("https://api.bitbucket.org"). + Get("/2.0/repositories"). + MatchParam("q", "name~\\\"plugin1\\\""). + MatchParam("role", "member"). + Reply(200). + Type("application/json"). + File("testdata/repos_filter.json") + + got := []*scm.Repository{} + opts := scm.RepoListOptions{RepoSearchTerm: scm.RepoSearchTerm{RepoName: "plugin1"}} + client, _ := New("https://api.bitbucket.org") + + repos, _, err := client.Repositories.ListV2(context.Background(), opts) + if err != nil { + t.Error(err) + } + got = append(got, repos...) + + want := []*scm.Repository{} + raw, _ := ioutil.ReadFile("testdata/repos_filter.json.golden") + json.Unmarshal(raw, &want) + + if diff := cmp.Diff(got, want); diff != "" { + t.Errorf("Unexpected Results") + t.Log(diff) + } +} + func TestStatusList(t *testing.T) { defer gock.Off() diff --git a/scm/driver/bitbucket/testdata/branchesFilter.json b/scm/driver/bitbucket/testdata/branches_filter.json similarity index 100% rename from scm/driver/bitbucket/testdata/branchesFilter.json rename to scm/driver/bitbucket/testdata/branches_filter.json diff --git a/scm/driver/bitbucket/testdata/branchesFilter.json.golden b/scm/driver/bitbucket/testdata/branches_filter.json.golden similarity index 100% rename from scm/driver/bitbucket/testdata/branchesFilter.json.golden rename to scm/driver/bitbucket/testdata/branches_filter.json.golden diff --git a/scm/driver/bitbucket/testdata/repos_filter.json b/scm/driver/bitbucket/testdata/repos_filter.json new file mode 100644 index 000000000..df610e1f4 --- /dev/null +++ b/scm/driver/bitbucket/testdata/repos_filter.json @@ -0,0 +1,110 @@ +{ + "pagelen": 1, + "values": [ + { + "scm": "git", + "website": "", + "has_wiki": false, + "uuid": "{7dd600e6-0d9c-4801-b967-cb4cc17359ff}", + "links": { + "watchers": { + "href": "https:\/\/api.bitbucket.org\/2.0\/repositories\/atlassian\/stash-example-plugin1\/watchers" + }, + "branches": { + "href": "https:\/\/api.bitbucket.org\/2.0\/repositories\/atlassian\/stash-example-plugin1\/refs\/branches" + }, + "tags": { + "href": "https:\/\/api.bitbucket.org\/2.0\/repositories\/atlassian\/stash-example-plugin1\/refs\/tags" + }, + "commits": { + "href": "https:\/\/api.bitbucket.org\/2.0\/repositories\/atlassian\/stash-example-plugin1\/commits" + }, + "clone": [ + { + "href": "https:\/\/brydzewski@bitbucket.org\/atlassian\/stash-example-plugin1.git", + "name": "https" + }, + { + "href": "git@bitbucket.org:atlassian\/stash-example-plugin1.git", + "name": "ssh" + } + ], + "self": { + "href": "https:\/\/api.bitbucket.org\/2.0\/repositories\/atlassian\/stash-example-plugin1" + }, + "source": { + "href": "https:\/\/api.bitbucket.org\/2.0\/repositories\/atlassian\/stash-example-plugin1\/src" + }, + "html": { + "href": "https:\/\/bitbucket.org\/atlassian\/stash-example-plugin1" + }, + "avatar": { + "href": "https:\/\/bytebucket.org\/ravatar\/%7B7dd600e6-0d9c-4801-b967-cb4cc17359ff%7D?ts=default" + }, + "hooks": { + "href": "https:\/\/api.bitbucket.org\/2.0\/repositories\/atlassian\/stash-example-plugin1\/hooks" + }, + "forks": { + "href": "https:\/\/api.bitbucket.org\/2.0\/repositories\/atlassian\/stash-example-plugin1\/forks" + }, + "downloads": { + "href": "https:\/\/api.bitbucket.org\/2.0\/repositories\/atlassian\/stash-example-plugin1\/downloads" + }, + "pullrequests": { + "href": "https:\/\/api.bitbucket.org\/2.0\/repositories\/atlassian\/stash-example-plugin1\/pullrequests" + } + }, + "fork_policy": "allow_forks", + "name": "stash-example-plugin1", + "project": { + "key": "PROJ", + "type": "project", + "uuid": "{8b56daff-dbc7-4cae-a7a3-1228c526906b}", + "links": { + "self": { + "href": "https:\/\/api.bitbucket.org\/2.0\/teams\/atlassian\/projects\/PROJ" + }, + "html": { + "href": "https:\/\/bitbucket.org\/account\/user\/atlassian\/projects\/PROJ" + }, + "avatar": { + "href": "https:\/\/bitbucket.org\/account\/user\/atlassian\/projects\/PROJ\/avatar\/32" + } + }, + "name": "Project: Atlassian" + }, + "language": "", + "created_on": "2013-04-15T03:05:05.595458+00:00", + "mainbranch": { + "type": "branch", + "name": "master" + }, + "full_name": "atlassian\/stash-example-plugin1", + "has_issues": false, + "owner": { + "username": "atlassian", + "display_name": "Atlassian", + "type": "team", + "uuid": "{02b941e3-cfaa-40f9-9a58-cec53e20bdc3}", + "links": { + "self": { + "href": "https:\/\/api.bitbucket.org\/2.0\/teams\/atlassian" + }, + "html": { + "href": "https:\/\/bitbucket.org\/atlassian\/" + }, + "avatar": { + "href": "https:\/\/bitbucket.org\/account\/atlassian\/avatar\/32\/" + } + } + }, + "updated_on": "2018-04-01T16:36:35.970175+00:00", + "size": 1116345, + "type": "repository", + "slug": "stash-example-plugin1", + "is_private": true, + "description": "Examples on how to decorate various pages around Stash." + } + ], + "next": "https:\/\/api.bitbucket.org\/2.0\/repositories?pagelen=1&after=PLACEHOLDER&role=member" +} \ No newline at end of file diff --git a/scm/driver/bitbucket/testdata/repos_filter.json.golden b/scm/driver/bitbucket/testdata/repos_filter.json.golden new file mode 100644 index 000000000..cf1ace3ba --- /dev/null +++ b/scm/driver/bitbucket/testdata/repos_filter.json.golden @@ -0,0 +1,15 @@ +[ + { + "ID": "{7dd600e6-0d9c-4801-b967-cb4cc17359ff}", + "Namespace": "atlassian", + "Name": "stash-example-plugin1", + "Perm": null, + "Branch": "master", + "Private": true, + "Clone": "https://bitbucket.org/atlassian/stash-example-plugin1.git", + "CloneSSH": "git@bitbucket.org:atlassian/stash-example-plugin1.git", + "Link": "https://bitbucket.org/atlassian/stash-example-plugin1", + "Created": "2013-04-15T03:05:05.595458Z", + "Updated": "2018-04-01T16:36:35.970175Z" + } +] \ No newline at end of file diff --git a/scm/driver/bitbucket/util.go b/scm/driver/bitbucket/util.go index b53ba777c..e43ea71cf 100644 --- a/scm/driver/bitbucket/util.go +++ b/scm/driver/bitbucket/util.go @@ -34,11 +34,13 @@ func encodeBranchListOptions(opts scm.BranchListOptions) string { sb.WriteString("\"") params.Set("q", sb.String()) } - if opts.PageListOptions.Page != 0 { - params.Set("page", strconv.Itoa(opts.PageListOptions.Page)) - } - if opts.PageListOptions.Size != 0 { - params.Set("pagelen", strconv.Itoa(opts.PageListOptions.Size)) + if opts.PageListOptions != (scm.ListOptions{}) { + if opts.PageListOptions.Page != 0 { + params.Set("page", strconv.Itoa(opts.PageListOptions.Page)) + } + if opts.PageListOptions.Size != 0 { + params.Set("pagelen", strconv.Itoa(opts.PageListOptions.Size)) + } } return params.Encode() } @@ -66,6 +68,27 @@ func encodeListRoleOptions(opts scm.ListOptions) string { return params.Encode() } +func encodeRepoListOptions(opts scm.RepoListOptions) string { + params := url.Values{} + if opts.RepoSearchTerm.RepoName != "" { + var sb strings.Builder + sb.WriteString("name~\"") + sb.WriteString(opts.RepoSearchTerm.RepoName) + sb.WriteString("\"") + params.Set("q", sb.String()) + } + if opts.ListOptions != (scm.ListOptions{}) { + if opts.ListOptions.Page != 0 { + params.Set("page", strconv.Itoa(opts.ListOptions.Page)) + } + if opts.ListOptions.Size != 0 { + params.Set("pagelen", strconv.Itoa(opts.ListOptions.Size)) + } + } + params.Set("role", "member") + return params.Encode() +} + func encodeCommitListOptions(opts scm.CommitListOptions) string { params := url.Values{} if opts.Page != 0 { diff --git a/scm/driver/gitea/repo.go b/scm/driver/gitea/repo.go index 4a58ef2bc..a678273bf 100644 --- a/scm/driver/gitea/repo.go +++ b/scm/driver/gitea/repo.go @@ -46,6 +46,11 @@ func (s *repositoryService) List(ctx context.Context, opts scm.ListOptions) ([]* return convertRepositoryList(out), res, err } +func (s *repositoryService) ListV2(ctx context.Context, opts scm.RepoListOptions) ([]*scm.Repository, *scm.Response, error) { + // gitea does not support search filters, hence calling List api without search filtering + return s.List(ctx, opts.ListOptions) +} + func (s *repositoryService) ListHooks(ctx context.Context, repo string, opts scm.ListOptions) ([]*scm.Hook, *scm.Response, error) { path := fmt.Sprintf("api/v1/repos/%s/hooks?%s", repo, encodeListOptions(opts)) out := []*hook{} diff --git a/scm/driver/gitee/repo.go b/scm/driver/gitee/repo.go index fa14ab8ea..72c60f567 100644 --- a/scm/driver/gitee/repo.go +++ b/scm/driver/gitee/repo.go @@ -45,6 +45,10 @@ func (s *RepositoryService) List(ctx context.Context, opts scm.ListOptions) ([]* res, err := s.client.do(ctx, "GET", path, nil, &out) return convertRepositoryList(out), res, err } +func (s *RepositoryService) ListV2(ctx context.Context, opts scm.RepoListOptions) ([]*scm.Repository, *scm.Response, error) { + // gitee does not support search filters, hence calling List api without search filtering + return s.List(ctx, opts.ListOptions) +} func (s *RepositoryService) ListHooks(ctx context.Context, repo string, opts scm.ListOptions) ([]*scm.Hook, *scm.Response, error) { path := fmt.Sprintf("repos/%s/hooks?%s", repo, encodeListOptions(opts)) diff --git a/scm/driver/github/repo.go b/scm/driver/github/repo.go index cc781cb92..c9f09b7bc 100644 --- a/scm/driver/github/repo.go +++ b/scm/driver/github/repo.go @@ -40,6 +40,10 @@ type repository struct { } `json:"permissions"` } +type searchRepositoryList struct { + Repositories []*repository `json:"items"` +} + type hook struct { ID int `json:"id,omitempty"` Name string `json:"name"` @@ -110,6 +114,14 @@ func (s *RepositoryService) List(ctx context.Context, opts scm.ListOptions) ([]* return convertRepositoryList(out), res, err } +// ListV2 returns the user repository list based on the searchTerm passed. +func (s *RepositoryService) ListV2(ctx context.Context, opts scm.RepoListOptions) ([]*scm.Repository, *scm.Response, error) { + path := fmt.Sprintf("search/repositories?%s", encodeRepoListOptions(opts)) + out := new(searchRepositoryList) + res, err := s.client.do(ctx, "GET", path, nil, &out) + return convertRepositoryList(out.Repositories), res, err +} + // List returns the github app installation repository list. func (s *RepositoryService) ListByInstallation(ctx context.Context, opts scm.ListOptions) ([]*scm.Repository, *scm.Response, error) { path := fmt.Sprintf("installation/repositories?%s", encodeListOptions(opts)) diff --git a/scm/driver/github/repo_test.go b/scm/driver/github/repo_test.go index cd1d7f8b3..6af3dba6a 100644 --- a/scm/driver/github/repo_test.go +++ b/scm/driver/github/repo_test.go @@ -131,6 +131,48 @@ func TestRepositoryList(t *testing.T) { t.Run("Page", testPage(res)) } +func TestRepositoryListV2(t *testing.T) { + defer gock.Off() + + gock.New("https://api.github.com"). + Get("/search/repositories"). + MatchParam("q", "testRepoin:name"). + MatchParam("q", "user:user123"). + MatchParam("page", "1"). + MatchParam("per_page", "30"). + Reply(200). + Type("application/json"). + SetHeaders(mockHeaders). + SetHeaders(mockPageHeaders). + File("testdata/repos_filter.json") + + client := NewDefault() + got, res, err := client.Repositories.ListV2(context.Background(), scm.RepoListOptions{ + ListOptions: scm.ListOptions{Page: 1, Size: 30}, + RepoSearchTerm: scm.RepoSearchTerm{ + RepoName: "testRepo", + User: "user123", + }, + }) + if err != nil { + t.Error(err) + return + } + + want := []*scm.Repository{} + raw, _ := ioutil.ReadFile("testdata/repos_filter.json.golden") + _ = json.Unmarshal(raw, &want) + + if diff := cmp.Diff(got, want); diff != "" { + t.Errorf("Unexpected Results") + t.Log(diff) + } + + t.Run("Request", testRequest(res)) + t.Run("Rate", testRate(res)) + t.Run("Page", testPage(res)) +} + func TestGithubAppInstallationList(t *testing.T) { defer gock.Off() diff --git a/scm/driver/github/testdata/repos_filter.json b/scm/driver/github/testdata/repos_filter.json new file mode 100644 index 000000000..5e724c4a7 --- /dev/null +++ b/scm/driver/github/testdata/repos_filter.json @@ -0,0 +1,107 @@ +{ + "total_count": 5, + "incomplete_results": false, + "items": [ + { + "id": 508719340, + "node_id": "R_kgDOHlJw7A", + "name": "testRepo2", + "full_name": "user123/testRepo2", + "private": false, + "owner": { + "login": "user123", + "id": 103414561, + "node_id": "U_kgDOBin7IQ", + "avatar_url": "https://avatars.githubusercontent.com/u/103414561?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/user123", + "html_url": "https://github.com/user123", + "followers_url": "https://api.github.com/users/user123/followers", + "following_url": "https://api.github.com/users/user123/following{/other_user}", + "gists_url": "https://api.github.com/users/user123/gists{/gist_id}", + "starred_url": "https://api.github.com/users/user123/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/user123/subscriptions", + "organizations_url": "https://api.github.com/users/user123/orgs", + "repos_url": "https://api.github.com/users/user123/repos", + "events_url": "https://api.github.com/users/user123/events{/privacy}", + "received_events_url": "https://api.github.com/users/user123/received_events", + "type": "User", + "site_admin": false + }, + "html_url": "https://github.com/user123/testRepo2", + "description": null, + "fork": false, + "url": "https://api.github.com/repos/user123/testRepo2", + "forks_url": "https://api.github.com/repos/user123/testRepo2/forks", + "keys_url": "https://api.github.com/repos/user123/testRepo2/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/user123/testRepo2/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/user123/testRepo2/teams", + "hooks_url": "https://api.github.com/repos/user123/testRepo2/hooks", + "issue_events_url": "https://api.github.com/repos/user123/testRepo2/issues/events{/number}", + "events_url": "https://api.github.com/repos/user123/testRepo2/events", + "assignees_url": "https://api.github.com/repos/user123/testRepo2/assignees{/user}", + "branches_url": "https://api.github.com/repos/user123/testRepo2/branches{/branch}", + "tags_url": "https://api.github.com/repos/user123/testRepo2/tags", + "blobs_url": "https://api.github.com/repos/user123/testRepo2/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/user123/testRepo2/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/user123/testRepo2/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/user123/testRepo2/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/user123/testRepo2/statuses/{sha}", + "languages_url": "https://api.github.com/repos/user123/testRepo2/languages", + "stargazers_url": "https://api.github.com/repos/user123/testRepo2/stargazers", + "contributors_url": "https://api.github.com/repos/user123/testRepo2/contributors", + "subscribers_url": "https://api.github.com/repos/user123/testRepo2/subscribers", + "subscription_url": "https://api.github.com/repos/user123/testRepo2/subscription", + "commits_url": "https://api.github.com/repos/user123/testRepo2/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/user123/testRepo2/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/user123/testRepo2/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/user123/testRepo2/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/user123/testRepo2/contents/{+path}", + "compare_url": "https://api.github.com/repos/user123/testRepo2/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/user123/testRepo2/merges", + "archive_url": "https://api.github.com/repos/user123/testRepo2/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/user123/testRepo2/downloads", + "issues_url": "https://api.github.com/repos/user123/testRepo2/issues{/number}", + "pulls_url": "https://api.github.com/repos/user123/testRepo2/pulls{/number}", + "milestones_url": "https://api.github.com/repos/user123/testRepo2/milestones{/number}", + "notifications_url": "https://api.github.com/repos/user123/testRepo2/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/user123/testRepo2/labels{/name}", + "releases_url": "https://api.github.com/repos/user123/testRepo2/releases{/id}", + "deployments_url": "https://api.github.com/repos/user123/testRepo2/deployments", + "created_at": "2022-06-29T14:11:36Z", + "updated_at": "2023-06-27T07:10:05Z", + "pushed_at": "2023-06-07T05:36:36Z", + "git_url": "git://github.com/user123/testRepo2.git", + "ssh_url": "git@github.com:user123/testRepo2.git", + "clone_url": "https://github.com/user123/testRepo2.git", + "svn_url": "https://github.com/user123/testRepo2", + "homepage": null, + "size": 53, + "stargazers_count": 0, + "watchers_count": 0, + "language": null, + "has_issues": true, + "has_projects": true, + "has_downloads": true, + "has_wiki": true, + "has_pages": false, + "has_discussions": false, + "forks_count": 0, + "mirror_url": null, + "archived": false, + "disabled": false, + "open_issues_count": 2, + "license": null, + "allow_forking": true, + "is_template": false, + "web_commit_signoff_required": false, + "topics": [], + "visibility": "public", + "forks": 0, + "open_issues": 2, + "watchers": 0, + "default_branch": "main", + "score": 1.0 + } + ] +} \ No newline at end of file diff --git a/scm/driver/github/testdata/repos_filter.json.golden b/scm/driver/github/testdata/repos_filter.json.golden new file mode 100644 index 000000000..bb164132f --- /dev/null +++ b/scm/driver/github/testdata/repos_filter.json.golden @@ -0,0 +1,20 @@ +[ + { + "ID": "508719340", + "Namespace": "user123", + "Name": "testRepo2", + "Perm": { + "Pull": false, + "Push": false, + "Admin": false + }, + "Branch": "main", + "Private": false, + "Visibility": 1, + "Clone": "https://github.com/user123/testRepo2.git", + "CloneSSH": "git@github.com:user123/testRepo2.git", + "Link": "https://github.com/user123/testRepo2", + "Created": "2022-06-29T14:11:36Z", + "Updated": "2023-06-27T07:10:05Z" + } +] \ No newline at end of file diff --git a/scm/driver/github/util.go b/scm/driver/github/util.go index 5d5fb4741..d06e474e0 100644 --- a/scm/driver/github/util.go +++ b/scm/driver/github/util.go @@ -7,6 +7,7 @@ package github import ( "net/url" "strconv" + "strings" "github.com/drone/go-scm/scm" ) @@ -22,6 +23,33 @@ func encodeListOptions(opts scm.ListOptions) string { return params.Encode() } +func encodeRepoListOptions(opts scm.RepoListOptions) string { + var sb strings.Builder + if opts.RepoSearchTerm != (scm.RepoSearchTerm{}) { + if opts.RepoSearchTerm.RepoName != "" { + sb.WriteString("q=") + sb.WriteString(opts.RepoSearchTerm.RepoName) + sb.WriteString("in:name+user:") + sb.WriteString(opts.RepoSearchTerm.User) + } else { + sb.WriteString("q=") + sb.WriteString("user:") + sb.WriteString(opts.RepoSearchTerm.User) + } + } + if opts.ListOptions != (scm.ListOptions{}) { + if opts.ListOptions.Page != 0 { + sb.WriteString("&page=") + sb.WriteString(strconv.Itoa(opts.ListOptions.Page)) + } + if opts.ListOptions.Size != 0 { + sb.WriteString("&per_page=") + sb.WriteString(strconv.Itoa(opts.ListOptions.Size)) + } + } + return sb.String() +} + func encodeCommitListOptions(opts scm.CommitListOptions) string { params := url.Values{} if opts.Page != 0 { diff --git a/scm/driver/gitlab/git_test.go b/scm/driver/gitlab/git_test.go index ee4595398..49fefee66 100644 --- a/scm/driver/gitlab/git_test.go +++ b/scm/driver/gitlab/git_test.go @@ -215,7 +215,7 @@ func TestGitListBranchesWithBranchFilter(t *testing.T) { Type("application/json"). SetHeaders(mockHeaders). SetHeaders(mockPageHeaders). - File("testdata/branchesFilter.json") + File("testdata/branches_filter.json") client := NewDefault() got, res, err := client.Git.ListBranchesV2(context.Background(), "diaspora/diaspora", scm.BranchListOptions{SearchTerm: "mast"}) @@ -225,7 +225,7 @@ func TestGitListBranchesWithBranchFilter(t *testing.T) { } want := []*scm.Reference{} - raw, _ := ioutil.ReadFile("testdata/branchesFilter.json.golden") + raw, _ := ioutil.ReadFile("testdata/branches_filter.json.golden") json.Unmarshal(raw, &want) if diff := cmp.Diff(got, want); diff != "" { diff --git a/scm/driver/gitlab/repo.go b/scm/driver/gitlab/repo.go index 23a04ffc6..dc6f39a7e 100644 --- a/scm/driver/gitlab/repo.go +++ b/scm/driver/gitlab/repo.go @@ -94,6 +94,14 @@ func (s *repositoryService) List(ctx context.Context, opts scm.ListOptions) ([]* return convertRepositoryList(out), res, err } +func (s *repositoryService) ListV2(ctx context.Context, opts scm.RepoListOptions) ([]*scm.Repository, *scm.Response, error) { + // We pass the repo searchTerm in the query params and gitlab filters repos based on this search term + path := fmt.Sprintf("api/v4/projects?%s", encodeRepoListOptions(opts)) + out := []*repository{} + res, err := s.client.do(ctx, "GET", path, nil, &out) + return convertRepositoryList(out), res, err +} + func (s *repositoryService) ListHooks(ctx context.Context, repo string, opts scm.ListOptions) ([]*scm.Hook, *scm.Response, error) { path := fmt.Sprintf("api/v4/projects/%s/hooks?%s", encode(repo), encodeListOptions(opts)) out := []*hook{} diff --git a/scm/driver/gitlab/repo_test.go b/scm/driver/gitlab/repo_test.go index 48e21db6b..293e523ff 100644 --- a/scm/driver/gitlab/repo_test.go +++ b/scm/driver/gitlab/repo_test.go @@ -163,6 +163,45 @@ func TestRepositoryList(t *testing.T) { t.Run("Page", testPage(res)) } +func TestRepositoryListV2(t *testing.T) { + defer gock.Off() + + gock.New("https://gitlab.com"). + Get("/api/v4/projects"). + MatchParam("search", "diaspora"). + MatchParam("page", "1"). + MatchParam("per_page", "30"). + MatchParam("membership", "true"). + Reply(200). + Type("application/json"). + SetHeaders(mockHeaders). + SetHeaders(mockPageHeaders). + File("testdata/repos_filter.json") + + client := NewDefault() + got, res, err := client.Repositories.ListV2(context.Background(), scm.RepoListOptions{ + ListOptions: scm.ListOptions{Page: 1, Size: 30}, + RepoSearchTerm: scm.RepoSearchTerm{RepoName: "diaspora"}, + }) + if err != nil { + t.Error(err) + return + } + + want := []*scm.Repository{} + raw, _ := ioutil.ReadFile("testdata/repos_filter.json.golden") + json.Unmarshal(raw, &want) + + if diff := cmp.Diff(got, want); diff != "" { + t.Errorf("Unexpected Results") + t.Log(diff) + } + + t.Run("Request", testRequest(res)) + t.Run("Rate", testRate(res)) + t.Run("Page", testPage(res)) +} + func TestStatusList(t *testing.T) { defer gock.Off() diff --git a/scm/driver/gitlab/testdata/branchesFilter.json b/scm/driver/gitlab/testdata/branches_filter.json similarity index 100% rename from scm/driver/gitlab/testdata/branchesFilter.json rename to scm/driver/gitlab/testdata/branches_filter.json diff --git a/scm/driver/gitlab/testdata/branchesFilter.json.golden b/scm/driver/gitlab/testdata/branches_filter.json.golden similarity index 100% rename from scm/driver/gitlab/testdata/branchesFilter.json.golden rename to scm/driver/gitlab/testdata/branches_filter.json.golden diff --git a/scm/driver/gitlab/testdata/repos_filter.json b/scm/driver/gitlab/testdata/repos_filter.json new file mode 100644 index 000000000..13f8b4de3 --- /dev/null +++ b/scm/driver/gitlab/testdata/repos_filter.json @@ -0,0 +1,59 @@ +[ + { + "id": 178504, + "description": "", + "default_branch": "master", + "tag_list": [], + "ssh_url_to_repo": "git@gitlab.com:diaspora/diaspora.git", + "http_url_to_repo": "https://gitlab.com/diaspora/diaspora.git", + "web_url": "https://gitlab.com/diaspora/diaspora", + "name": "Diaspora", + "name_with_namespace": "diaspora / Diaspora", + "path": "diaspora", + "path_with_namespace": "diaspora/diaspora", + "avatar_url": null, + "star_count": 0, + "forks_count": 0, + "created_at": "2015-03-03T18:37:05.387Z", + "last_activity_at": "2015-03-03T18:37:20.795Z", + "_links": { + "self": "http://gitlab.com/api/v4/projects/178504", + "issues": "http://gitlab.com/api/v4/projects/178504/issues", + "merge_requests": "http://gitlab.com/api/v4/projects/178504/merge_requests", + "repo_branches": "http://gitlab.com/api/v4/projects/178504/repository/branches", + "labels": "http://gitlab.com/api/v4/projects/178504/labels", + "events": "http://gitlab.com/api/v4/projects/178504/events", + "members": "http://gitlab.com/api/v4/projects/178504/members" + }, + "archived": false, + "visibility": "public", + "resolve_outdated_diff_discussions": null, + "container_registry_enabled": null, + "issues_enabled": true, + "merge_requests_enabled": true, + "wiki_enabled": true, + "jobs_enabled": true, + "snippets_enabled": false, + "shared_runners_enabled": true, + "lfs_enabled": true, + "creator_id": 57658, + "namespace": { + "id": 120836, + "name": "diaspora", + "path": "diaspora", + "kind": "group", + "full_path": "diaspora", + "parent_id": null + }, + "import_status": "finished", + "open_issues_count": 0, + "public_jobs": true, + "ci_config_path": null, + "shared_with_groups": [], + "only_allow_merge_if_pipeline_succeeds": false, + "request_access_enabled": true, + "only_allow_merge_if_all_discussions_are_resolved": null, + "printing_merge_request_link_enabled": true, + "approvals_before_merge": 0 + } +] \ No newline at end of file diff --git a/scm/driver/gitlab/testdata/repos_filter.json.golden b/scm/driver/gitlab/testdata/repos_filter.json.golden new file mode 100644 index 000000000..5780a5ebd --- /dev/null +++ b/scm/driver/gitlab/testdata/repos_filter.json.golden @@ -0,0 +1,20 @@ +[ + { + "ID": "178504", + "Namespace": "diaspora", + "Name": "diaspora", + "Perm": { + "Pull": true, + "Push": false, + "Admin": false + }, + "Branch": "master", + "Private": false, + "Visibility": 1, + "Clone": "https://gitlab.com/diaspora/diaspora.git", + "CloneSSH": "git@gitlab.com:diaspora/diaspora.git", + "Link": "https://gitlab.com/diaspora/diaspora", + "Created": "0001-01-01T00:00:00Z", + "Updated": "0001-01-01T00:00:00Z" + } +] \ No newline at end of file diff --git a/scm/driver/gitlab/util.go b/scm/driver/gitlab/util.go index 55f106f71..eb3f06893 100644 --- a/scm/driver/gitlab/util.go +++ b/scm/driver/gitlab/util.go @@ -29,11 +29,13 @@ func encodeBranchListOptions(opts scm.BranchListOptions) string { if opts.SearchTerm != "" { params.Set("search", opts.SearchTerm) } - if opts.PageListOptions.Page != 0 { - params.Set("page", strconv.Itoa(opts.PageListOptions.Page)) - } - if opts.PageListOptions.Size != 0 { - params.Set("per_page", strconv.Itoa(opts.PageListOptions.Size)) + if opts.PageListOptions != (scm.ListOptions{}) { + if opts.PageListOptions.Page != 0 { + params.Set("page", strconv.Itoa(opts.PageListOptions.Page)) + } + if opts.PageListOptions.Size != 0 { + params.Set("per_page", strconv.Itoa(opts.PageListOptions.Size)) + } } return params.Encode() } @@ -61,6 +63,25 @@ func encodeMemberListOptions(opts scm.ListOptions) string { return params.Encode() } +func encodeRepoListOptions(opts scm.RepoListOptions) string { + params := url.Values{} + params.Set("membership", "true") + if opts.RepoSearchTerm != (scm.RepoSearchTerm{}) { + if opts.RepoSearchTerm.RepoName != "" { + params.Set("search", opts.RepoSearchTerm.RepoName) + } + } + if opts.ListOptions != (scm.ListOptions{}) { + if opts.ListOptions.Page != 0 { + params.Set("page", strconv.Itoa(opts.ListOptions.Page)) + } + if opts.ListOptions.Size != 0 { + params.Set("per_page", strconv.Itoa(opts.ListOptions.Size)) + } + } + return params.Encode() +} + func encodeCommitListOptions(opts scm.CommitListOptions) string { params := url.Values{} if opts.Page != 0 { diff --git a/scm/driver/gogs/repo.go b/scm/driver/gogs/repo.go index d2a29fb3c..5657b5ee7 100644 --- a/scm/driver/gogs/repo.go +++ b/scm/driver/gogs/repo.go @@ -45,6 +45,11 @@ func (s *repositoryService) List(ctx context.Context, _ scm.ListOptions) ([]*scm return convertRepositoryList(out), res, err } +func (s *repositoryService) ListV2(ctx context.Context, opts scm.RepoListOptions) ([]*scm.Repository, *scm.Response, error) { + // Azure does not support search filters, hence calling List api without search filtering + return s.List(ctx, opts.ListOptions) +} + func (s *repositoryService) ListHooks(ctx context.Context, repo string, _ scm.ListOptions) ([]*scm.Hook, *scm.Response, error) { path := fmt.Sprintf("api/v1/repos/%s/hooks", repo) out := []*hook{} diff --git a/scm/driver/harness/repo.go b/scm/driver/harness/repo.go index a55089516..da4237555 100644 --- a/scm/driver/harness/repo.go +++ b/scm/driver/harness/repo.go @@ -52,6 +52,11 @@ func (s *repositoryService) List(ctx context.Context, opts scm.ListOptions) ([]* return convertRepositoryList(out), res, err } +func (s *repositoryService) ListV2(ctx context.Context, opts scm.RepoListOptions) ([]*scm.Repository, *scm.Response, error) { + // harness does not support search filters, hence calling List api without search filtering + return s.List(ctx, opts.ListOptions) +} + func (s *repositoryService) ListHooks(ctx context.Context, repo string, opts scm.ListOptions) ([]*scm.Hook, *scm.Response, error) { harnessURI := buildHarnessURI(s.client.account, s.client.organization, s.client.project, repo) path := fmt.Sprintf("api/v1/repos/%s/webhooks?sort=display_name&order=asc&%s", harnessURI, encodeListOptions(opts)) diff --git a/scm/driver/stash/git_test.go b/scm/driver/stash/git_test.go index 720e8fa6f..f93952ace 100644 --- a/scm/driver/stash/git_test.go +++ b/scm/driver/stash/git_test.go @@ -155,7 +155,7 @@ func TestGitListBranchesWithBranchFilter(t *testing.T) { MatchParam("filterText", "mast"). Reply(200). Type("application/json"). - File("testdata/branchesFilter.json") + File("testdata/branches_filter.json") client, _ := New("http://example.com:7990") got, _, err := client.Git.ListBranchesV2(context.Background(), "PRJ/my-repo", scm.BranchListOptions{SearchTerm: "mast"}) @@ -164,7 +164,7 @@ func TestGitListBranchesWithBranchFilter(t *testing.T) { } want := []*scm.Reference{} - raw, _ := ioutil.ReadFile("testdata/branchesFilter.json.golden") + raw, _ := ioutil.ReadFile("testdata/branches_filter.json.golden") _ = json.Unmarshal(raw, &want) if diff := cmp.Diff(got, want); diff != "" { diff --git a/scm/driver/stash/repo.go b/scm/driver/stash/repo.go index 070bfd373..33ba4bc42 100644 --- a/scm/driver/stash/repo.go +++ b/scm/driver/stash/repo.go @@ -180,6 +180,18 @@ func (s *repositoryService) List(ctx context.Context, opts scm.ListOptions) ([]* return convertRepositoryList(out), res, err } +// ListV2 returns the user repository list based on the searchTerm passed. +func (s *repositoryService) ListV2(ctx context.Context, opts scm.RepoListOptions) ([]*scm.Repository, *scm.Response, error) { + path := fmt.Sprintf("rest/api/1.0/repos?%s", encodeRepoListOptions(opts)) + out := new(repositories) + res, err := s.client.do(ctx, "GET", path, nil, &out) + if res != nil && !out.pagination.LastPage.Bool { + res.Page.First = 1 + res.Page.Next = opts.ListOptions.Page + 1 + } + return convertRepositoryList(out), res, err +} + // listWrite returns the user repository list. func (s *repositoryService) listWrite(ctx context.Context, repo string) ([]*scm.Repository, *scm.Response, error) { _, name := scm.Split(repo) diff --git a/scm/driver/stash/repo_test.go b/scm/driver/stash/repo_test.go index ee337dc24..ff7138df1 100644 --- a/scm/driver/stash/repo_test.go +++ b/scm/driver/stash/repo_test.go @@ -319,6 +319,47 @@ func TestRepositoryList(t *testing.T) { } } +func TestRepositoryListV2(t *testing.T) { + defer gock.Off() + + gock.New("http://example.com:7990"). + Get("/rest/api/1.0/repos"). + MatchParam("name", "quux"). + MatchParam("limit", "25"). + MatchParam("start", "50"). + MatchParam("permission", "REPO_READ"). + Reply(200). + Type("application/json"). + File("testdata/repos_filter.json") + + client, _ := New("http://example.com:7990") + got, res, err := client.Repositories.ListV2(context.Background(), scm.RepoListOptions{ + ListOptions: scm.ListOptions{Page: 3, Size: 25}, + RepoSearchTerm: scm.RepoSearchTerm{ + RepoName: "quux", + }, + }) + if err != nil { + t.Error(err) + } + + if got, want := res.Page.First, 1; got != want { + t.Errorf("Want Page.First %d, got %d", want, got) + } + if got, want := res.Page.Next, 4; got != want { + t.Errorf("Want Page.Next %d, got %d", want, got) + } + + want := []*scm.Repository{} + raw, _ := ioutil.ReadFile("testdata/repos_filter.json.golden") + _ = json.Unmarshal(raw, &want) + + if diff := cmp.Diff(got, want); diff != "" { + t.Errorf("Unexpected Results") + t.Log(diff) + } +} + func TestStatusList(t *testing.T) { client, _ := New("http://example.com:7990") _, _, err := client.Repositories.ListStatus(context.Background(), "PRJ/my-repo", "a6e5e7d797edf751cbd839d6bd4aef86c941eec9", scm.ListOptions{Size: 30, Page: 1}) diff --git a/scm/driver/stash/testdata/branchesFilter.json b/scm/driver/stash/testdata/branches_filter.json similarity index 100% rename from scm/driver/stash/testdata/branchesFilter.json rename to scm/driver/stash/testdata/branches_filter.json diff --git a/scm/driver/stash/testdata/branchesFilter.json.golden b/scm/driver/stash/testdata/branches_filter.json.golden similarity index 100% rename from scm/driver/stash/testdata/branchesFilter.json.golden rename to scm/driver/stash/testdata/branches_filter.json.golden diff --git a/scm/driver/stash/testdata/repos_filter.json b/scm/driver/stash/testdata/repos_filter.json new file mode 100644 index 000000000..80bb1c9a4 --- /dev/null +++ b/scm/driver/stash/testdata/repos_filter.json @@ -0,0 +1,49 @@ +{ + "size": 25, + "limit": 25, + "isLastPage": false, + "values": [ + { + "slug": "quux", + "id": 2, + "name": "quux", + "scmId": "git", + "state": "AVAILABLE", + "statusMessage": "Available", + "forkable": true, + "project": { + "key": "PRJ", + "id": 2, + "name": "different_name", + "public": false, + "type": "NORMAL", + "links": { + "self": [ + { + "href": "http://example.com:7990/projects/PRJ" + } + ] + } + }, + "public": false, + "links": { + "clone": [ + { + "href": "ssh://git@example.com:7999/prj/quux.git", + "name": "ssh" + }, + { + "href": "http://jcitizen@example.com:7990/scm/prj/quux.git", + "name": "http" + } + ], + "self": [ + { + "href": "http://example.com:7990/projects/PRJ/repos/quux/browse" + } + ] + } + } + ], + "start": 0 +} \ No newline at end of file diff --git a/scm/driver/stash/testdata/repos_filter.json.golden b/scm/driver/stash/testdata/repos_filter.json.golden new file mode 100644 index 000000000..34ff2307b --- /dev/null +++ b/scm/driver/stash/testdata/repos_filter.json.golden @@ -0,0 +1,15 @@ +[ + { + "ID": "2", + "Namespace": "PRJ", + "Name": "quux", + "Perm": null, + "Branch": "master", + "Private": true, + "Clone": "http://example.com:7990/scm/prj/quux.git", + "CloneSSH": "ssh://git@example.com:7999/prj/quux.git", + "Link": "http://example.com:7990/projects/PRJ/repos/quux/browse", + "Created": "0001-01-01T00:00:00Z", + "Updated": "0001-01-01T00:00:00Z" + } +] \ No newline at end of file diff --git a/scm/driver/stash/util.go b/scm/driver/stash/util.go index 0de82e3fa..1dbcb71e8 100644 --- a/scm/driver/stash/util.go +++ b/scm/driver/stash/util.go @@ -30,13 +30,15 @@ func encodeBranchListOptions(opts scm.BranchListOptions) string { if opts.SearchTerm != "" { params.Set("filterText", opts.SearchTerm) } - if opts.PageListOptions.Page > 1 { - params.Set("start", strconv.Itoa( - (opts.PageListOptions.Page-1)*opts.PageListOptions.Size), - ) - } - if opts.PageListOptions.Size != 0 { - params.Set("limit", strconv.Itoa(opts.PageListOptions.Size)) + if opts.PageListOptions != (scm.ListOptions{}) { + if opts.PageListOptions.Page > 1 { + params.Set("start", strconv.Itoa( + (opts.PageListOptions.Page-1)*opts.PageListOptions.Size), + ) + } + if opts.PageListOptions.Size != 0 { + params.Set("limit", strconv.Itoa(opts.PageListOptions.Size)) + } } return params.Encode() } @@ -55,6 +57,27 @@ func encodeListRoleOptions(opts scm.ListOptions) string { return params.Encode() } +func encodeRepoListOptions(opts scm.RepoListOptions) string { + params := url.Values{} + if opts.RepoSearchTerm != (scm.RepoSearchTerm{}) { + if opts.RepoSearchTerm.RepoName != "" { + params.Set("name", opts.RepoSearchTerm.RepoName) + } + } + if opts.ListOptions != (scm.ListOptions{}) { + if opts.ListOptions.Page > 1 { + params.Set("start", strconv.Itoa( + (opts.ListOptions.Page-1)*opts.ListOptions.Size), + ) + } + if opts.ListOptions.Size != 0 { + params.Set("limit", strconv.Itoa(opts.ListOptions.Size)) + } + } + params.Set("permission", "REPO_READ") + return params.Encode() +} + func encodePullRequestListOptions(opts scm.PullRequestListOptions) string { params := url.Values{} if opts.Page > 1 { diff --git a/scm/repo.go b/scm/repo.go index b62b53755..1e6218fdc 100644 --- a/scm/repo.go +++ b/scm/repo.go @@ -120,6 +120,9 @@ type ( // List returns a list of repositories. List(context.Context, ListOptions) ([]*Repository, *Response, error) + // ListV2 returns a list of repositories based on the searchTerm passed. + ListV2(context.Context, RepoListOptions) ([]*Repository, *Response, error) + // ListHooks returns a list or repository hooks. ListHooks(context.Context, string, ListOptions) ([]*Hook, *Response, error)