gitlab-org--gitlab-foss/lib/api/v3/projects.rb
Jan Provaznik 090ca9c33e Use limit for search count queries
Search query is especially slow if a user searches a generic string
which matches many records, in such case search can take tens of
seconds or time out. To speed up the search query, we search only for
first 1000 records, if there is >1000 matching records we just display
"1000+" instead of precise total count supposing that with such amount
the exact count is not so important for the user.

Because for issues even limited search was not fast enough, 2-phase
approach is used for issues: first we use simpler/faster query to get
all public issues, if this exceeds the limit, we just return the limit.
If the amount of matching results is lower than limit, we re-run more
complex search query (which includes also confidential issues).
Re-running the complex query should be fast enough in such case because the
amount of matching issues is lower than limit.

Because exact total_count is now limited, this patch also switches to
to "prev/next" pagination.

Related #40540
2018-01-23 22:33:42 +01:00

479 lines
18 KiB
Ruby

module API
module V3
class Projects < Grape::API
include PaginationParams
before { authenticate_non_get! }
after_validation do
set_only_allow_merge_if_pipeline_succeeds!
end
helpers do
params :optional_params do
optional :description, type: String, desc: 'The description of the project'
optional :issues_enabled, type: Boolean, desc: 'Flag indication if the issue tracker is enabled'
optional :merge_requests_enabled, type: Boolean, desc: 'Flag indication if merge requests are enabled'
optional :wiki_enabled, type: Boolean, desc: 'Flag indication if the wiki is enabled'
optional :builds_enabled, type: Boolean, desc: 'Flag indication if builds are enabled'
optional :snippets_enabled, type: Boolean, desc: 'Flag indication if snippets are enabled'
optional :shared_runners_enabled, type: Boolean, desc: 'Flag indication if shared runners are enabled for that project'
optional :resolve_outdated_diff_discussions, type: Boolean, desc: 'Automatically resolve merge request diffs discussions on lines changed with a push'
optional :container_registry_enabled, type: Boolean, desc: 'Flag indication if the container registry is enabled for that project'
optional :lfs_enabled, type: Boolean, desc: 'Flag indication if Git LFS is enabled for that project'
optional :public, type: Boolean, desc: 'Create a public project. The same as visibility_level = 20.'
optional :visibility_level, type: Integer, values: [
Gitlab::VisibilityLevel::PRIVATE,
Gitlab::VisibilityLevel::INTERNAL,
Gitlab::VisibilityLevel::PUBLIC
], desc: 'Create a public project. The same as visibility_level = 20.'
optional :public_builds, type: Boolean, desc: 'Perform public builds'
optional :request_access_enabled, type: Boolean, desc: 'Allow users to request member access'
optional :only_allow_merge_if_build_succeeds, type: Boolean, desc: 'Only allow to merge if builds succeed'
optional :only_allow_merge_if_pipeline_succeeds, type: Boolean, desc: 'Only allow to merge if builds succeed'
optional :only_allow_merge_if_all_discussions_are_resolved, type: Boolean, desc: 'Only allow to merge if all discussions are resolved'
end
def map_public_to_visibility_level(attrs)
publik = attrs.delete(:public)
if !publik.nil? && !attrs[:visibility_level].present?
# Since setting the public attribute to private could mean either
# private or internal, use the more conservative option, private.
attrs[:visibility_level] = (publik == true) ? Gitlab::VisibilityLevel::PUBLIC : Gitlab::VisibilityLevel::PRIVATE
end
attrs
end
def set_only_allow_merge_if_pipeline_succeeds!
if params.key?(:only_allow_merge_if_build_succeeds)
params[:only_allow_merge_if_pipeline_succeeds] = params.delete(:only_allow_merge_if_build_succeeds)
end
end
end
resource :projects do
helpers do
params :collection_params do
use :sort_params
use :filter_params
use :pagination
optional :simple, type: Boolean, default: false,
desc: 'Return only the ID, URL, name, and path of each project'
end
params :sort_params do
optional :order_by, type: String, values: %w[id name path created_at updated_at last_activity_at],
default: 'created_at', desc: 'Return projects ordered by field'
optional :sort, type: String, values: %w[asc desc], default: 'desc',
desc: 'Return projects sorted in ascending and descending order'
end
params :filter_params do
optional :archived, type: Boolean, default: nil, desc: 'Limit by archived status'
optional :visibility, type: String, values: %w[public internal private],
desc: 'Limit by visibility'
optional :search, type: String, desc: 'Return list of authorized projects matching the search criteria'
end
params :statistics_params do
optional :statistics, type: Boolean, default: false, desc: 'Include project statistics'
end
params :create_params do
optional :namespace_id, type: Integer, desc: 'Namespace ID for the new project. Default to the user namespace.'
optional :import_url, type: String, desc: 'URL from which the project is imported'
end
def present_projects(projects, options = {})
options = options.reverse_merge(
with: ::API::V3::Entities::Project,
current_user: current_user,
simple: params[:simple]
)
projects = filter_projects(projects)
projects = projects.with_statistics if options[:statistics]
options[:with] = ::API::Entities::BasicProjectDetails if options[:simple]
present paginate(projects), options
end
end
desc 'Get a list of visible projects for authenticated user' do
success ::API::Entities::BasicProjectDetails
end
params do
use :collection_params
end
get '/visible' do
entity = current_user ? ::API::V3::Entities::ProjectWithAccess : ::API::Entities::BasicProjectDetails
present_projects ProjectsFinder.new(current_user: current_user).execute, with: entity
end
desc 'Get a projects list for authenticated user' do
success ::API::Entities::BasicProjectDetails
end
params do
use :collection_params
end
get do
authenticate!
present_projects current_user.authorized_projects.order_id_desc,
with: ::API::V3::Entities::ProjectWithAccess
end
desc 'Get an owned projects list for authenticated user' do
success ::API::Entities::BasicProjectDetails
end
params do
use :collection_params
use :statistics_params
end
get '/owned' do
authenticate!
present_projects current_user.owned_projects,
with: ::API::V3::Entities::ProjectWithAccess,
statistics: params[:statistics]
end
desc 'Gets starred project for the authenticated user' do
success ::API::Entities::BasicProjectDetails
end
params do
use :collection_params
end
get '/starred' do
authenticate!
present_projects ProjectsFinder.new(current_user: current_user, params: { starred: true }).execute
end
desc 'Get all projects for admin user' do
success ::API::Entities::BasicProjectDetails
end
params do
use :collection_params
use :statistics_params
end
get '/all' do
authenticated_as_admin!
present_projects Project.all, with: ::API::V3::Entities::ProjectWithAccess, statistics: params[:statistics]
end
desc 'Search for projects the current user has access to' do
success ::API::V3::Entities::Project
end
params do
requires :query, type: String, desc: 'The project name to be searched'
use :sort_params
use :pagination
end
get "/search/:query", requirements: { query: /[^\/]+/ } do
search_service = Search::GlobalService.new(current_user, search: params[:query]).execute
projects = search_service.objects('projects', params[:page], false)
projects = projects.reorder(params[:order_by] => params[:sort])
present paginate(projects), with: ::API::V3::Entities::Project
end
desc 'Create new project' do
success ::API::V3::Entities::Project
end
params do
optional :name, type: String, desc: 'The name of the project'
optional :path, type: String, desc: 'The path of the repository'
at_least_one_of :name, :path
use :optional_params
use :create_params
end
post do
attrs = map_public_to_visibility_level(declared_params(include_missing: false))
project = ::Projects::CreateService.new(current_user, attrs).execute
if project.saved?
present project, with: ::API::V3::Entities::Project,
user_can_admin_project: can?(current_user, :admin_project, project)
else
if project.errors[:limit_reached].present?
error!(project.errors[:limit_reached], 403)
end
render_validation_error!(project)
end
end
desc 'Create new project for a specified user. Only available to admin users.' do
success ::API::V3::Entities::Project
end
params do
requires :name, type: String, desc: 'The name of the project'
requires :user_id, type: Integer, desc: 'The ID of a user'
optional :default_branch, type: String, desc: 'The default branch of the project'
use :optional_params
use :create_params
end
post "user/:user_id" do
authenticated_as_admin!
user = User.find_by(id: params.delete(:user_id))
not_found!('User') unless user
attrs = map_public_to_visibility_level(declared_params(include_missing: false))
project = ::Projects::CreateService.new(user, attrs).execute
if project.saved?
present project, with: ::API::V3::Entities::Project,
user_can_admin_project: can?(current_user, :admin_project, project)
else
render_validation_error!(project)
end
end
end
params do
requires :id, type: String, desc: 'The ID of a project'
end
resource :projects, requirements: { id: %r{[^/]+} } do
desc 'Get a single project' do
success ::API::V3::Entities::ProjectWithAccess
end
get ":id" do
entity = current_user ? ::API::V3::Entities::ProjectWithAccess : ::API::Entities::BasicProjectDetails
present user_project, with: entity, current_user: current_user,
user_can_admin_project: can?(current_user, :admin_project, user_project)
end
desc 'Get events for a single project' do
success ::API::V3::Entities::Event
end
params do
use :pagination
end
get ":id/events" do
present paginate(user_project.events.recent), with: ::API::V3::Entities::Event
end
desc 'Fork new project for the current user or provided namespace.' do
success ::API::V3::Entities::Project
end
params do
optional :namespace, type: String, desc: 'The ID or name of the namespace that the project will be forked into'
end
post 'fork/:id' do
fork_params = declared_params(include_missing: false)
namespace_id = fork_params[:namespace]
if namespace_id.present?
fork_params[:namespace] = if namespace_id =~ /^\d+$/
Namespace.find_by(id: namespace_id)
else
Namespace.find_by_path_or_name(namespace_id)
end
unless fork_params[:namespace] && can?(current_user, :create_projects, fork_params[:namespace])
not_found!('Target Namespace')
end
end
forked_project = ::Projects::ForkService.new(user_project, current_user, fork_params).execute
if forked_project.errors.any?
conflict!(forked_project.errors.messages)
else
present forked_project, with: ::API::V3::Entities::Project,
user_can_admin_project: can?(current_user, :admin_project, forked_project)
end
end
desc 'Update an existing project' do
success ::API::V3::Entities::Project
end
params do
optional :name, type: String, desc: 'The name of the project'
optional :default_branch, type: String, desc: 'The default branch of the project'
optional :path, type: String, desc: 'The path of the repository'
use :optional_params
at_least_one_of :name, :description, :issues_enabled, :merge_requests_enabled,
:wiki_enabled, :builds_enabled, :snippets_enabled,
:shared_runners_enabled, :resolve_outdated_diff_discussions,
:container_registry_enabled, :lfs_enabled, :public, :visibility_level,
:public_builds, :request_access_enabled, :only_allow_merge_if_build_succeeds,
:only_allow_merge_if_all_discussions_are_resolved, :path,
:default_branch
end
put ':id' do
authorize_admin_project
attrs = map_public_to_visibility_level(declared_params(include_missing: false))
authorize! :rename_project, user_project if attrs[:name].present?
authorize! :change_visibility_level, user_project if attrs[:visibility_level].present?
result = ::Projects::UpdateService.new(user_project, current_user, attrs).execute
if result[:status] == :success
present user_project, with: ::API::V3::Entities::Project,
user_can_admin_project: can?(current_user, :admin_project, user_project)
else
render_validation_error!(user_project)
end
end
desc 'Archive a project' do
success ::API::V3::Entities::Project
end
post ':id/archive' do
authorize!(:archive_project, user_project)
user_project.archive!
present user_project, with: ::API::V3::Entities::Project
end
desc 'Unarchive a project' do
success ::API::V3::Entities::Project
end
post ':id/unarchive' do
authorize!(:archive_project, user_project)
user_project.unarchive!
present user_project, with: ::API::V3::Entities::Project
end
desc 'Star a project' do
success ::API::V3::Entities::Project
end
post ':id/star' do
if current_user.starred?(user_project)
not_modified!
else
current_user.toggle_star(user_project)
user_project.reload
present user_project, with: ::API::V3::Entities::Project
end
end
desc 'Unstar a project' do
success ::API::V3::Entities::Project
end
delete ':id/star' do
if current_user.starred?(user_project)
current_user.toggle_star(user_project)
user_project.reload
present user_project, with: ::API::V3::Entities::Project
else
not_modified!
end
end
desc 'Remove a project'
delete ":id" do
authorize! :remove_project, user_project
status(200)
::Projects::DestroyService.new(user_project, current_user, {}).async_execute
end
desc 'Mark this project as forked from another'
params do
requires :forked_from_id, type: String, desc: 'The ID of the project it was forked from'
end
post ":id/fork/:forked_from_id" do
authenticated_as_admin!
forked_from_project = find_project!(params[:forked_from_id])
not_found!("Source Project") unless forked_from_project
if user_project.forked_from_project.nil?
user_project.create_forked_project_link(forked_to_project_id: user_project.id, forked_from_project_id: forked_from_project.id)
::Projects::ForksCountService.new(forked_from_project).refresh_cache
else
render_api_error!("Project already forked", 409)
end
end
desc 'Remove a forked_from relationship'
delete ":id/fork" do
authorize! :remove_fork_project, user_project
if user_project.forked?
status(200)
user_project.forked_project_link.destroy
else
not_modified!
end
end
desc 'Share the project with a group' do
success ::API::Entities::ProjectGroupLink
end
params do
requires :group_id, type: Integer, desc: 'The ID of a group'
requires :group_access, type: Integer, values: Gitlab::Access.values, desc: 'The group access level'
optional :expires_at, type: Date, desc: 'Share expiration date'
end
post ":id/share" do
authorize! :admin_project, user_project
group = Group.find_by_id(params[:group_id])
unless group && can?(current_user, :read_group, group)
not_found!('Group')
end
unless user_project.allowed_to_share_with_group?
return render_api_error!("The project sharing with group is disabled", 400)
end
link = user_project.project_group_links.new(declared_params(include_missing: false))
if link.save
present link, with: ::API::Entities::ProjectGroupLink
else
render_api_error!(link.errors.full_messages.first, 409)
end
end
params do
requires :group_id, type: Integer, desc: 'The ID of the group'
end
delete ":id/share/:group_id" do
authorize! :admin_project, user_project
link = user_project.project_group_links.find_by(group_id: params[:group_id])
not_found!('Group Link') unless link
link.destroy
no_content!
end
desc 'Upload a file'
params do
requires :file, type: File, desc: 'The file to be uploaded'
end
post ":id/uploads" do
UploadService.new(user_project, params[:file]).execute
end
desc 'Get the users list of a project' do
success ::API::Entities::UserBasic
end
params do
optional :search, type: String, desc: 'Return list of users matching the search criteria'
use :pagination
end
get ':id/users' do
users = user_project.team.users
users = users.search(params[:search]) if params[:search].present?
present paginate(users), with: ::API::Entities::UserBasic
end
end
end
end
end