090ca9c33e
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
479 lines
18 KiB
Ruby
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
|