diff --git a/lib/gitlab/client.rb b/lib/gitlab/client.rb index fabaf7608..06c666dc3 100644 --- a/lib/gitlab/client.rb +++ b/lib/gitlab/client.rb @@ -45,6 +45,7 @@ class Client < API include RepositorySubmodules include ResourceLabelEvents include Runners + include Search include Services include Sidekiq include Snippets diff --git a/lib/gitlab/client/search.rb b/lib/gitlab/client/search.rb new file mode 100644 index 000000000..66324323d --- /dev/null +++ b/lib/gitlab/client/search.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +class Gitlab::Client + # Defines methods related to global searches, searching in projects and searching in groups. + # @see https://docs.gitlab.com/ce/api/search.html + module Search + # Search globally across the GitLab instance. + # + # @example + # Gitlab.search_globally('projects', 'gitlab') + # Gitlab.search_globally('issues', 'gitlab') + # Gitlab.search_globally('merge_requests', 'gitlab') + # Gitlab.search_globally('milestones', 'gitlab') + # Gitlab.search_globally('snippet_titles', 'gitlab') + # Gitlab.search_globally('snippet_blobs', 'gitlab') + # + # @param [String] scope The scope to search in. Currently these scopes are supported: projects, issues, merge_requests, milestones, snippet_titles, snippet_blobs. + # @param [String] search The search query. + # @return [Array] Returns a list of responses depending on the requested scope. + def search_globally(scope, search) + options = { scope: scope, search: search } + get('/search', query: options) + end + + # Search within the specified group. + # + # @example + # Gitlab.search_in_group(1, 'projects', 'gitlab') + # Gitlab.search_in_group(1, 'issues', 'gitlab') + # Gitlab.search_in_group(1, 'merge_requests', 'gitlab') + # Gitlab.search_in_group(1, 'milestones', 'gitlab') + # + # @param [Integer, String] group The ID or name of a group. + # @param [String] scope The scope to search in. Currently these scopes are supported: projects, issues, merge_requests, milestones. + # @param [String] search The search query. + # @return [Array] Returns a list of responses depending on the requested scope. + def search_in_group(group, scope, search) + options = { scope: scope, search: search } + get("/groups/#{url_encode group}/search", query: options) + end + + # Search within the specified project. + # + # @example + # Gitlab.search_in_project(1, 'issues', 'gitlab') + # Gitlab.search_in_project(1, 'merge_requests', 'gitlab') + # Gitlab.search_in_project(1, 'milestones', 'gitlab') + # Gitlab.search_in_project(1, 'notes', 'gitlab') + # Gitlab.search_in_project(1, 'wiki_blobs', 'gitlab') + # Gitlab.search_in_project(1, 'commits', 'gitlab') + # Gitlab.search_in_project(1, 'blobs', 'gitlab') + # + # @param [Integer, String] project The ID or name of a project. + # @param [String] scope The scope to search in. Currently these scopes are supported: issues, merge_requests, milestones, notes, wiki_blobs, commits, blobs. + # @param [String] search The search query. + # @return [Array] Returns a list of responses depending on the requested scope. + def search_in_project(project, scope, search) + options = { scope: scope, search: search } + get("/projects/#{url_encode project}/search", query: options) + end + end +end diff --git a/spec/fixtures/search_blobs_results.json b/spec/fixtures/search_blobs_results.json new file mode 100644 index 000000000..e2dad38bb --- /dev/null +++ b/spec/fixtures/search_blobs_results.json @@ -0,0 +1,12 @@ + +[ + { + "basename": "README", + "data": "```\n\n## Installation\n\nQuick start using the [pre-built", + "filename": "README.md", + "id": null, + "ref": "master", + "startline": 46, + "project_id": 6 + } +] diff --git a/spec/fixtures/search_commits_results.json b/spec/fixtures/search_commits_results.json new file mode 100644 index 000000000..a4f9e4148 --- /dev/null +++ b/spec/fixtures/search_commits_results.json @@ -0,0 +1,20 @@ + +[ + { + "id": "4109c2d872d5fdb1ed057400d103766aaea97f98", + "short_id": "4109c2d8", + "title": "goodbye $.browser", + "created_at": "2013-02-18T22:02:54.000Z", + "parent_ids": [ + "59d05353ab575bcc2aa958fe1782e93297de64c9" + ], + "message": "goodbye $.browser\n", + "author_name": "angus croll", + "author_email": "anguscroll@gmail.com", + "authored_date": "2013-02-18T22:02:54.000Z", + "committer_name": "angus croll", + "committer_email": "anguscroll@gmail.com", + "committed_date": "2013-02-18T22:02:54.000Z", + "project_id": 6 + } +] diff --git a/spec/fixtures/search_issues_results.json b/spec/fixtures/search_issues_results.json new file mode 100644 index 000000000..b01bfd411 --- /dev/null +++ b/spec/fixtures/search_issues_results.json @@ -0,0 +1,52 @@ +[ + { + "id": 83, + "iid": 1, + "project_id": 12, + "title": "Add file", + "description": "Add first file", + "state": "opened", + "created_at": "2018-01-24T06:02:15.514Z", + "updated_at": "2018-02-06T12:36:23.263Z", + "closed_at": null, + "labels":[], + "milestone": null, + "assignees": [{ + "id": 20, + "name": "Ceola Deckow", + "username": "sammy.collier", + "state": "active", + "avatar_url": "https://www.gravatar.com/avatar/c23d85a4f50e0ea76ab739156c639231?s=80&d=identicon", + "web_url": "http://localhost:3000/sammy.collier" + }], + "author": { + "id": 1, + "name": "Administrator", + "username": "root", + "state": "active", + "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", + "web_url": "http://localhost:3000/root" + }, + "assignee": { + "id": 20, + "name": "Ceola Deckow", + "username": "sammy.collier", + "state": "active", + "avatar_url": "https://www.gravatar.com/avatar/c23d85a4f50e0ea76ab739156c639231?s=80&d=identicon", + "web_url": "http://localhost:3000/sammy.collier" + }, + "user_notes_count": 0, + "upvotes": 0, + "downvotes": 0, + "due_date": null, + "confidential": false, + "discussion_locked": null, + "web_url": "http://localhost:3000/h5bp/7bp/subgroup-prj/issues/1", + "time_stats": { + "time_estimate": 0, + "total_time_spent": 0, + "human_time_estimate": null, + "human_total_time_spent": null + } + } +] diff --git a/spec/fixtures/search_merge_requests_results.json b/spec/fixtures/search_merge_requests_results.json new file mode 100644 index 000000000..ec3e108a1 --- /dev/null +++ b/spec/fixtures/search_merge_requests_results.json @@ -0,0 +1,66 @@ +[ + { + "id": 56, + "iid": 8, + "project_id": 6, + "title": "Add first file", + "description": "This is a test MR to add file", + "state": "opened", + "created_at": "2018-01-22T14:21:50.830Z", + "updated_at": "2018-02-06T12:40:33.295Z", + "target_branch": "master", + "source_branch": "jaja-test", + "upvotes": 0, + "downvotes": 0, + "author": { + "id": 1, + "name": "Administrator", + "username": "root", + "state": "active", + "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", + "web_url": "http://localhost:3000/root" + }, + "assignee": { + "id": 5, + "name": "Jacquelyn Kutch", + "username": "abigail", + "state": "active", + "avatar_url": "https://www.gravatar.com/avatar/3138c66095ee4bd11a508c2f7f7772da?s=80&d=identicon", + "web_url": "http://localhost:3000/abigail" + }, + "source_project_id": 6, + "target_project_id": 6, + "labels": [ + "ruby", + "tests" + ], + "work_in_progress": false, + "milestone": { + "id": 13, + "iid": 3, + "project_id": 6, + "title": "v2.0", + "description": "Qui aut qui eos dolor beatae itaque tempore molestiae.", + "state": "active", + "created_at": "2017-09-05T07:58:29.099Z", + "updated_at": "2017-09-05T07:58:29.099Z", + "due_date": null, + "start_date": null + }, + "merge_when_pipeline_succeeds": false, + "merge_status": "can_be_merged", + "sha": "78765a2d5e0a43585945c58e61ba2f822e4d090b", + "merge_commit_sha": null, + "user_notes_count": 0, + "discussion_locked": null, + "should_remove_source_branch": null, + "force_remove_source_branch": true, + "web_url": "http://localhost:3000/twitter/flight/merge_requests/8", + "time_stats": { + "time_estimate": 0, + "total_time_spent": 0, + "human_time_estimate": null, + "human_total_time_spent": null + } + } +] diff --git a/spec/fixtures/search_milestones_results.json b/spec/fixtures/search_milestones_results.json new file mode 100644 index 000000000..335e00625 --- /dev/null +++ b/spec/fixtures/search_milestones_results.json @@ -0,0 +1,14 @@ +[ + { + "id": 44, + "iid": 1, + "project_id": 12, + "title": "next release", + "description": "Next release milestone", + "state": "active", + "created_at": "2018-02-06T12:43:39.271Z", + "updated_at": "2018-02-06T12:44:01.298Z", + "due_date": "2018-04-18", + "start_date": "2018-02-04" + } +] diff --git a/spec/fixtures/search_notes_results.json b/spec/fixtures/search_notes_results.json new file mode 100644 index 000000000..f43a5f602 --- /dev/null +++ b/spec/fixtures/search_notes_results.json @@ -0,0 +1,21 @@ +[ + { + "id": 191, + "body": "Harum maxime consequuntur et et deleniti assumenda facilis.", + "attachment": null, + "author": { + "id": 23, + "name": "User 1", + "username": "user1", + "state": "active", + "avatar_url": "https://www.gravatar.com/avatar/111d68d06e2d317b5a59c2c6c5bad808?s=80&d=identicon", + "web_url": "http://localhost:3000/user1" + }, + "created_at": "2017-09-05T08:01:32.068Z", + "updated_at": "2017-09-05T08:01:32.068Z", + "system": false, + "noteable_id": 22, + "noteable_type": "Issue", + "noteable_iid": 2 + } +] diff --git a/spec/fixtures/search_projects_results.json b/spec/fixtures/search_projects_results.json new file mode 100644 index 000000000..6ee6b39af --- /dev/null +++ b/spec/fixtures/search_projects_results.json @@ -0,0 +1,20 @@ +[ + { + "id": 6, + "description": "Nobis sed ipsam vero quod cupiditate veritatis hic.", + "name": "Flight", + "name_with_namespace": "Twitter / Flight", + "path": "flight", + "path_with_namespace": "twitter/flight", + "created_at": "2017-09-05T07:58:01.621Z", + "default_branch": "master", + "tag_list":[], + "ssh_url_to_repo": "ssh://jarka@localhost:2222/twitter/flight.git", + "http_url_to_repo": "http://localhost:3000/twitter/flight.git", + "web_url": "http://localhost:3000/twitter/flight", + "avatar_url": null, + "star_count": 0, + "forks_count": 0, + "last_activity_at": "2018-01-31T09:56:30.902Z" + } +] diff --git a/spec/fixtures/search_snippet_blobs_results.json b/spec/fixtures/search_snippet_blobs_results.json new file mode 100644 index 000000000..75fd62ff3 --- /dev/null +++ b/spec/fixtures/search_snippet_blobs_results.json @@ -0,0 +1,20 @@ +[ + { + "id": 50, + "title": "Sample file", + "file_name": "file.rb", + "description": "Simple ruby file", + "author": { + "id": 1, + "name": "Administrator", + "username": "root", + "state": "active", + "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", + "web_url": "http://localhost:3000/root" + }, + "updated_at": "2018-02-06T12:49:29.104Z", + "created_at": "2017-11-28T08:20:18.071Z", + "project_id": 9, + "web_url": "http://localhost:3000/root/jira-test/snippets/50" + } +] diff --git a/spec/fixtures/search_snippet_titles_results.json b/spec/fixtures/search_snippet_titles_results.json new file mode 100644 index 000000000..75fd62ff3 --- /dev/null +++ b/spec/fixtures/search_snippet_titles_results.json @@ -0,0 +1,20 @@ +[ + { + "id": 50, + "title": "Sample file", + "file_name": "file.rb", + "description": "Simple ruby file", + "author": { + "id": 1, + "name": "Administrator", + "username": "root", + "state": "active", + "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", + "web_url": "http://localhost:3000/root" + }, + "updated_at": "2018-02-06T12:49:29.104Z", + "created_at": "2017-11-28T08:20:18.071Z", + "project_id": 9, + "web_url": "http://localhost:3000/root/jira-test/snippets/50" + } +] diff --git a/spec/fixtures/search_wiki_blobs_results.json b/spec/fixtures/search_wiki_blobs_results.json new file mode 100644 index 000000000..801c78527 --- /dev/null +++ b/spec/fixtures/search_wiki_blobs_results.json @@ -0,0 +1,12 @@ + +[ + { + "basename": "home", + "data": "hello\n\nand bye\n\nend", + "filename": "home.md", + "id": null, + "ref": "master", + "startline": 5, + "project_id": 6 + } +] diff --git a/spec/gitlab/client/search_spec.rb b/spec/gitlab/client/search_spec.rb new file mode 100644 index 000000000..66a4cff14 --- /dev/null +++ b/spec/gitlab/client/search_spec.rb @@ -0,0 +1,289 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Client do + describe '.search_globally' do + context 'when scope projects' do + before do + stub_get('/search', 'search_projects_results').with(query: { scope: 'projects', search: 'flight' }) + @search = Gitlab.search_globally('projects', 'flight') + end + + it 'gets the correct resource' do + expect(a_get('/search') + .with(query: { scope: 'projects', search: 'flight' })).to have_been_made + end + + it 'returns a paginated response of search results' do + expect(@search).to be_a Gitlab::PaginatedResponse + end + end + + context 'when scope issues' do + before do + stub_get('/search', 'search_issues_results').with(query: { scope: 'issues', search: 'file' }) + @search = Gitlab.search_globally('issues', 'file') + end + + it 'gets the correct resource' do + expect(a_get('/search') + .with(query: { scope: 'issues', search: 'file' })).to have_been_made + end + + it 'returns a paginated response of search results' do + expect(@search).to be_a Gitlab::PaginatedResponse + end + end + + context 'when scope merge_requests' do + before do + stub_get('/search', 'search_merge_requests_results').with(query: { scope: 'merge_requests', search: 'file' }) + @search = Gitlab.search_globally('merge_requests', 'file') + end + + it 'gets the correct resource' do + expect(a_get('/search') + .with(query: { scope: 'merge_requests', search: 'file' })).to have_been_made + end + + it 'returns a paginated response of search results' do + expect(@search).to be_a Gitlab::PaginatedResponse + end + end + + context 'when scope milestones' do + before do + stub_get('/search', 'search_milestones_results').with(query: { scope: 'milestones', search: 'release' }) + @search = Gitlab.search_globally('milestones', 'release') + end + + it 'gets the correct resource' do + expect(a_get('/search') + .with(query: { scope: 'milestones', search: 'release' })).to have_been_made + end + + it 'returns a paginated response of search results' do + expect(@search).to be_a Gitlab::PaginatedResponse + end + end + + context 'when scope snippet_titles' do + before do + stub_get('/search', 'search_snippet_titles_results').with(query: { scope: 'snippet_titles', search: 'sample' }) + @search = Gitlab.search_globally('snippet_titles', 'sample') + end + + it 'gets the correct resource' do + expect(a_get('/search') + .with(query: { scope: 'snippet_titles', search: 'sample' })).to have_been_made + end + + it 'returns a paginated response of search results' do + expect(@search).to be_a Gitlab::PaginatedResponse + end + end + + context 'when scope snippet_blobs' do + before do + stub_get('/search', 'search_snippet_blobs_results').with(query: { scope: 'snippet_blobs', search: 'test' }) + @search = Gitlab.search_globally('snippet_blobs', 'test') + end + + it 'gets the correct resource' do + expect(a_get('/search') + .with(query: { scope: 'snippet_blobs', search: 'test' })).to have_been_made + end + + it 'returns a paginated response of search results' do + expect(@search).to be_a Gitlab::PaginatedResponse + end + end + end + + describe '.search_in_group' do + context 'when scope projects' do + before do + stub_get('/groups/3/search', 'search_projects_results').with(query: { scope: 'projects', search: 'flight' }) + @search = Gitlab.search_in_group(3, 'projects', 'flight') + end + + it 'gets the correct resource' do + expect(a_get('/groups/3/search') + .with(query: { scope: 'projects', search: 'flight' })).to have_been_made + end + + it 'returns a paginated response of search results' do + expect(@search).to be_a Gitlab::PaginatedResponse + end + end + + context 'when scope issues' do + before do + stub_get('/groups/3/search', 'search_issues_results').with(query: { scope: 'issues', search: 'file' }) + @search = Gitlab.search_in_group(3, 'issues', 'file') + end + + it 'gets the correct resource' do + expect(a_get('/groups/3/search') + .with(query: { scope: 'issues', search: 'file' })).to have_been_made + end + + it 'returns a paginated response of search results' do + expect(@search).to be_a Gitlab::PaginatedResponse + end + end + + context 'when scope merge_requests' do + before do + stub_get('/groups/3/search', 'search_merge_requests_results').with(query: { scope: 'merge_requests', search: 'file' }) + @search = Gitlab.search_in_group(3, 'merge_requests', 'file') + end + + it 'gets the correct resource' do + expect(a_get('/groups/3/search') + .with(query: { scope: 'merge_requests', search: 'file' })).to have_been_made + end + + it 'returns a paginated response of search results' do + expect(@search).to be_a Gitlab::PaginatedResponse + end + end + + context 'when scope milestones' do + before do + stub_get('/groups/3/search', 'search_milestones_results').with(query: { scope: 'milestones', search: 'release' }) + @search = Gitlab.search_in_group(3, 'milestones', 'release') + end + + it 'gets the correct resource' do + expect(a_get('/groups/3/search') + .with(query: { scope: 'milestones', search: 'release' })).to have_been_made + end + + it 'returns a paginated response of search results' do + expect(@search).to be_a Gitlab::PaginatedResponse + end + end + end + + describe '.search_in_project' do + context 'when scope issues' do + before do + stub_get('/projects/12/search', 'search_issues_results').with(query: { scope: 'issues', search: 'file' }) + @search = Gitlab.search_in_project(12, 'issues', 'file') + end + + it 'gets the correct resource' do + expect(a_get('/projects/12/search') + .with(query: { scope: 'issues', search: 'file' })).to have_been_made + end + + it 'returns a paginated response of search results' do + expect(@search).to be_a Gitlab::PaginatedResponse + expect(@search[0].project_id).to eq(12) + end + end + + context 'when scope merge_requests' do + before do + stub_get('/projects/6/search', 'search_merge_requests_results').with(query: { scope: 'merge_requests', search: 'file' }) + @search = Gitlab.search_in_project(6, 'merge_requests', 'file') + end + + it 'gets the correct resource' do + expect(a_get('/projects/6/search') + .with(query: { scope: 'merge_requests', search: 'file' })).to have_been_made + end + + it 'returns a paginated response of search results' do + expect(@search).to be_a Gitlab::PaginatedResponse + expect(@search[0].project_id).to eq(6) + end + end + + context 'when scope milestones' do + before do + stub_get('/projects/12/search', 'search_milestones_results').with(query: { scope: 'milestones', search: 'release' }) + @search = Gitlab.search_in_project(12, 'milestones', 'release') + end + + it 'gets the correct resource' do + expect(a_get('/projects/12/search') + .with(query: { scope: 'milestones', search: 'release' })).to have_been_made + end + + it 'returns a paginated response of search results' do + expect(@search).to be_a Gitlab::PaginatedResponse + expect(@search[0].project_id).to eq(12) + end + end + + context 'when scope notes' do + before do + stub_get('/projects/6/search', 'search_notes_results').with(query: { scope: 'notes', search: 'maxime' }) + @search = Gitlab.search_in_project(6, 'notes', 'maxime') + end + + it 'gets the correct resource' do + expect(a_get('/projects/6/search') + .with(query: { scope: 'notes', search: 'maxime' })).to have_been_made + end + + it 'returns a paginated response of search results' do + expect(@search).to be_a Gitlab::PaginatedResponse + end + end + + context 'when scope wiki_blobs' do + before do + stub_get('/projects/6/search', 'search_wiki_blobs_results').with(query: { scope: 'wiki_blobs', search: 'bye' }) + @search = Gitlab.search_in_project(6, 'wiki_blobs', 'bye') + end + + it 'gets the correct resource' do + expect(a_get('/projects/6/search') + .with(query: { scope: 'wiki_blobs', search: 'bye' })).to have_been_made + end + + it 'returns a paginated response of search results' do + expect(@search).to be_a Gitlab::PaginatedResponse + expect(@search[0].project_id).to eq(6) + end + end + + context 'when scope commits' do + before do + stub_get('/projects/6/search', 'search_commits_results').with(query: { scope: 'commits', search: 'bye' }) + @search = Gitlab.search_in_project(6, 'commits', 'bye') + end + + it 'gets the correct resource' do + expect(a_get('/projects/6/search') + .with(query: { scope: 'commits', search: 'bye' })).to have_been_made + end + + it 'returns a paginated response of search results' do + expect(@search).to be_a Gitlab::PaginatedResponse + expect(@search[0].project_id).to eq(6) + end + end + + context 'when scope blobs' do + before do + stub_get('/projects/6/search', 'search_blobs_results').with(query: { scope: 'blobs', search: 'installation' }) + @search = Gitlab.search_in_project(6, 'blobs', 'installation') + end + + it 'gets the correct resource' do + expect(a_get('/projects/6/search') + .with(query: { scope: 'blobs', search: 'installation' })).to have_been_made + end + + it 'returns a paginated response of search results' do + expect(@search).to be_a Gitlab::PaginatedResponse + expect(@search[0].project_id).to eq(6) + end + end + end +end