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 :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.has_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: false, 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, 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 current_user.viewable_starred_projects 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]) 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, :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) 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 ::Projects::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