diff options
Diffstat (limited to 'lib')
53 files changed, 777 insertions, 185 deletions
diff --git a/lib/api/commits.rb b/lib/api/commits.rb index 9d23daafe95..8defc59224d 100644 --- a/lib/api/commits.rb +++ b/lib/api/commits.rb @@ -323,6 +323,22 @@ module API present paginate(commit.merge_requests), with: Entities::MergeRequestBasic end + + desc "Get a commit's GPG signature" do + success Entities::CommitSignature + end + params do + requires :sha, type: String, desc: 'A commit sha, or the name of a branch or tag' + end + get ':id/repository/commits/:sha/signature', requirements: API::COMMIT_ENDPOINT_REQUIREMENTS do + commit = user_project.commit(params[:sha]) + not_found! 'Commit' unless commit + + signature = commit.signature + not_found! 'GPG Signature' unless signature + + present signature, with: Entities::CommitSignature + end end end end diff --git a/lib/api/entities.rb b/lib/api/entities.rb index af788a4ed73..7c035990fb0 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -392,6 +392,13 @@ module API expose :project_id end + class CommitSignature < Grape::Entity + expose :gpg_key_id + expose :gpg_key_primary_keyid, :gpg_key_user_name, :gpg_key_user_email + expose :verification_status + expose :gpg_key_subkey_id + end + class BasicRef < Grape::Entity expose :type, :name end @@ -550,6 +557,15 @@ module API expose :time_stats, using: 'API::Entities::IssuableTimeStats' do |issue| issue end + + expose :merge_requests_count do |issue, options| + if options[:issuable_metadata] + # Avoids an N+1 query when metadata is included + options[:issuable_metadata][issue.id].merge_requests_count + else + issue.merge_requests_closing_issues.count + end + end end class Issue < IssueBasic @@ -1369,13 +1385,9 @@ module API class GitInfo < Grape::Entity expose :repo_url, :ref, :sha, :before_sha - expose :ref_type do |model| - if model.tag - 'tag' - else - 'branch' - end - end + expose :ref_type + expose :refspecs + expose :git_depth, as: :depth end class RunnerInfo < Grape::Entity diff --git a/lib/api/features.rb b/lib/api/features.rb index 835aac05905..4dc1834c644 100644 --- a/lib/api/features.rb +++ b/lib/api/features.rb @@ -42,6 +42,7 @@ module API requires :value, type: String, desc: '`true` or `false` to enable/disable, an integer for percentage of time' optional :feature_group, type: String, desc: 'A Feature group name' optional :user, type: String, desc: 'A GitLab username' + optional :group, type: String, desc: "A GitLab group's path, such as 'gitlab-org'" optional :project, type: String, desc: 'A projects path, like gitlab-org/gitlab-ce' end post ':name' do diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 2eb7b04711a..54cd4cd9cdb 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -299,6 +299,12 @@ module API items.search(text) end + def order_options_with_tie_breaker + order_options = { params[:order_by] => params[:sort] } + order_options['id'] ||= 'desc' + order_options + end + # error helpers def forbidden!(reason = nil) @@ -393,7 +399,7 @@ module API # rubocop: disable CodeReuse/ActiveRecord def reorder_projects(projects) - projects.reorder(params[:order_by] => params[:sort]) + projects.reorder(order_options_with_tie_breaker) end # rubocop: enable CodeReuse/ActiveRecord diff --git a/lib/api/helpers/internal_helpers.rb b/lib/api/helpers/internal_helpers.rb index 4eaaca96b49..fe78049af87 100644 --- a/lib/api/helpers/internal_helpers.rb +++ b/lib/api/helpers/internal_helpers.rb @@ -81,6 +81,14 @@ module API Gitlab::GlRepository.gl_repository(project, wiki?) end + def gl_project_path + if wiki? + project.wiki.full_path + else + project.full_path + end + end + # Return the repository depending on whether we want the wiki or the # regular repository def repository diff --git a/lib/api/helpers/pagination.rb b/lib/api/helpers/pagination.rb index de59c915d66..d00e61678b5 100644 --- a/lib/api/helpers/pagination.rb +++ b/lib/api/helpers/pagination.rb @@ -13,6 +13,33 @@ module API strategy.new(self).paginate(relation) end + class Base + private + + def per_page + @per_page ||= params[:per_page] + end + + def base_request_uri + @base_request_uri ||= URI.parse(request.url).tap do |uri| + uri.host = Gitlab.config.gitlab.host + uri.port = nil + end + end + + def build_page_url(query_params:) + base_request_uri.tap do |uri| + uri.query = query_params + end.to_s + end + + def page_href(next_page_params = {}) + query_params = params.merge(**next_page_params, per_page: per_page).to_query + + build_page_url(query_params: query_params) + end + end + class KeysetPaginationInfo attr_reader :relation, :request_context @@ -85,7 +112,7 @@ module API end end - class KeysetPaginationStrategy + class KeysetPaginationStrategy < Base attr_reader :request_context delegate :params, :header, :request, to: :request_context @@ -141,10 +168,6 @@ module API ] end - def per_page - params[:per_page] - end - def add_default_pagination_headers header 'X-Per-Page', per_page.to_s end @@ -154,22 +177,12 @@ module API header 'Link', link_for('next', next_page_params) end - def page_href(next_page_params) - request_url = request.url.split('?').first - request_params = params.dup - request_params[:per_page] = per_page - - request_params.merge!(next_page_params) if next_page_params - - "#{request_url}?#{request_params.to_query}" - end - def link_for(rel, next_page_params) %(<#{page_href(next_page_params)}>; rel="#{rel}") end end - class DefaultPaginationStrategy + class DefaultPaginationStrategy < Base attr_reader :request_context delegate :params, :header, :request, to: :request_context @@ -198,15 +211,13 @@ module API end end - # rubocop: disable CodeReuse/ActiveRecord def add_default_order(relation) if relation.is_a?(ActiveRecord::Relation) && relation.order_values.empty? - relation = relation.order(:id) + relation = relation.order(:id) # rubocop: disable CodeReuse/ActiveRecord end relation end - # rubocop: enable CodeReuse/ActiveRecord def add_pagination_headers(paginated_data) header 'X-Per-Page', paginated_data.limit_value.to_s @@ -222,27 +233,13 @@ module API end def pagination_links(paginated_data) - request_url = request.url.split('?').first - request_params = params.clone - request_params[:per_page] = paginated_data.limit_value - - links = [] - - request_params[:page] = paginated_data.prev_page - links << %(<#{request_url}?#{request_params.to_query}>; rel="prev") if request_params[:page] - - request_params[:page] = paginated_data.next_page - links << %(<#{request_url}?#{request_params.to_query}>; rel="next") if request_params[:page] - - request_params[:page] = 1 - links << %(<#{request_url}?#{request_params.to_query}>; rel="first") - - unless data_without_counts?(paginated_data) - request_params[:page] = total_pages(paginated_data) - links << %(<#{request_url}?#{request_params.to_query}>; rel="last") - end + [].tap do |links| + links << %(<#{page_href(page: paginated_data.prev_page)}>; rel="prev") if paginated_data.prev_page + links << %(<#{page_href(page: paginated_data.next_page)}>; rel="next") if paginated_data.next_page + links << %(<#{page_href(page: 1)}>; rel="first") - links.join(', ') + links << %(<#{page_href(page: total_pages(paginated_data))}>; rel="last") unless data_without_counts?(paginated_data) + end.join(', ') end def total_pages(paginated_data) diff --git a/lib/api/helpers/runner.rb b/lib/api/helpers/runner.rb index 16df8e830e1..ff73a49d5e8 100644 --- a/lib/api/helpers/runner.rb +++ b/lib/api/helpers/runner.rb @@ -26,7 +26,7 @@ module API end def get_runner_ip - { ip_address: request.env["HTTP_X_FORWARDED_FOR"] || request.ip } + { ip_address: env["action_dispatch.remote_ip"].to_s || request.ip } end def current_runner diff --git a/lib/api/internal.rb b/lib/api/internal.rb index 9488b3469d9..70b32f7d758 100644 --- a/lib/api/internal.rb +++ b/lib/api/internal.rb @@ -77,6 +77,7 @@ module API when ::Gitlab::GitAccessResult::Success payload = { gl_repository: gl_repository, + gl_project_path: gl_project_path, gl_id: Gitlab::GlId.gl_id(user), gl_username: user&.username, git_config_options: [], @@ -117,13 +118,7 @@ module API raise ActiveRecord::RecordNotFound.new("No key_id or user_id passed!") end - token_handler = Gitlab::LfsToken.new(actor) - - { - username: token_handler.actor_name, - lfs_token: token_handler.token, - repository_http_path: project.http_url_to_repo - } + Gitlab::LfsToken.new(actor).authentication_payload(project.http_url_to_repo) end # rubocop: enable CodeReuse/ActiveRecord diff --git a/lib/api/issues.rb b/lib/api/issues.rb index b3636c98550..f43f4d961d6 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -29,8 +29,7 @@ module API issues = IssuesFinder.new(current_user, args).execute .preload(:assignees, :labels, :notes, :timelogs, :project, :author, :closed_by) - - issues.reorder(args[:order_by] => args[:sort]) + issues.reorder(order_options_with_tie_breaker) end # rubocop: enable CodeReuse/ActiveRecord @@ -55,6 +54,7 @@ module API optional :scope, type: String, values: %w[created-by-me assigned-to-me created_by_me assigned_to_me all], desc: 'Return issues for the given scope: `created_by_me`, `assigned_to_me` or `all`' optional :my_reaction_emoji, type: String, desc: 'Return issues reacted by the authenticated user by the given emoji' + optional :confidential, type: Boolean, desc: 'Filter confidential or public issues' use :pagination use :issues_params_ee @@ -304,19 +304,14 @@ module API get ':id/issues/:issue_iid/related_merge_requests' do issue = find_project_issue(params[:issue_iid]) - merge_request_iids = ::Issues::ReferencedMergeRequestsService.new(user_project, current_user) + merge_requests = ::Issues::ReferencedMergeRequestsService.new(user_project, current_user) .execute(issue) .flatten - .map(&:iid) - - merge_requests = - if merge_request_iids.present? - MergeRequestsFinder.new(current_user, project_id: user_project.id, iids: merge_request_iids).execute - else - MergeRequest.none - end - present paginate(merge_requests), with: Entities::MergeRequestBasic, current_user: current_user, project: user_project + present paginate(::Kaminari.paginate_array(merge_requests)), + with: Entities::MergeRequestBasic, + current_user: current_user, + project: user_project end desc 'List merge requests closing issue' do diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index df46b4446ff..03f6684226f 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -38,7 +38,7 @@ module API args[:scope] = args[:scope].underscore if args[:scope] merge_requests = MergeRequestsFinder.new(current_user, args).execute - .reorder(args[:order_by] => args[:sort]) + .reorder(order_options_with_tie_breaker) merge_requests = paginate(merge_requests) .preload(:source_project, :target_project) @@ -369,11 +369,11 @@ module API merge_request.update(squash: params[:squash]) if params[:squash] - merge_params = { + merge_params = HashWithIndifferentAccess.new( commit_message: params[:merge_commit_message], squash_commit_message: params[:squash_commit_message], should_remove_source_branch: params[:should_remove_source_branch] - } + ) if merge_when_pipeline_succeeds && merge_request.head_pipeline && merge_request.head_pipeline.active? ::MergeRequests::MergeWhenPipelineSucceedsService diff --git a/lib/api/notes.rb b/lib/api/notes.rb index 1bdf7aeb119..f7bd092ce50 100644 --- a/lib/api/notes.rb +++ b/lib/api/notes.rb @@ -39,7 +39,7 @@ module API # at the DB query level (which we cannot in that case), the current # page can have less elements than :per_page even if # there's more than one page. - raw_notes = noteable.notes.with_metadata.reorder(params[:order_by] => params[:sort]) + raw_notes = noteable.notes.with_metadata.reorder(order_options_with_tie_breaker) notes = # paginate() only works with a relation. This could lead to a # mismatch between the pagination headers info and the actual notes diff --git a/lib/api/project_milestones.rb b/lib/api/project_milestones.rb index da31bcb8dac..ca24742b7a3 100644 --- a/lib/api/project_milestones.rb +++ b/lib/api/project_milestones.rb @@ -98,6 +98,23 @@ module API milestone_issuables_for(user_project, :merge_request) end + + desc 'Promote a milestone to group milestone' do + detail 'This feature was introduced in GitLab 11.9' + end + post ':id/milestones/:milestone_id/promote' do + begin + authorize! :admin_milestone, user_project + authorize! :admin_milestone, user_project.group + + milestone = user_project.milestones.find(params[:milestone_id]) + Milestones::PromoteService.new(user_project, current_user).execute(milestone) + + status(200) + rescue Milestones::PromoteService::PromoteMilestoneError => error + render_api_error!(error.message, 400) + end + end end end end diff --git a/lib/api/project_templates.rb b/lib/api/project_templates.rb index d05ddad7466..119902a189c 100644 --- a/lib/api/project_templates.rb +++ b/lib/api/project_templates.rb @@ -36,7 +36,10 @@ module API optional :project, type: String, desc: 'The project name to use when expanding placeholders in the template. Only affects licenses' optional :fullname, type: String, desc: 'The full name of the copyright holder to use when expanding placeholders in the template. Only affects licenses' end - get ':id/templates/:type/:name', requirements: { name: /[\w\.-]+/ } do + # The regex is needed to ensure a period (e.g. agpl-3.0) + # isn't confused with a format type. We also need to allow encoded + # values (e.g. C%2B%2B for C++), so allow % and + as well. + get ':id/templates/:type/:name', requirements: { name: /[\w%.+-]+/ } do template = TemplateFinder .build(params[:type], user_project, name: params[:name]) .execute diff --git a/lib/api/projects.rb b/lib/api/projects.rb index 6a93ef9f3ad..b23fe6cd4e7 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -258,6 +258,8 @@ module API end params do optional :namespace, type: String, desc: 'The ID or name of the namespace that the project will be forked into' + optional :path, type: String, desc: 'The path that will be assigned to the fork' + optional :name, type: String, desc: 'The name that will be assigned to the fork' end post ':id/fork' do Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42284') @@ -386,7 +388,11 @@ module API desc 'Get languages in project repository' get ':id/languages' do - user_project.repository.languages.map { |language| language.values_at(:label, :value) }.to_h + if user_project.repository_languages.present? + user_project.repository_languages.map { |l| [l.name, l.share] }.to_h + else + user_project.repository.languages.map { |language| language.values_at(:label, :value) }.to_h + end end desc 'Remove a project' diff --git a/lib/api/services.rb b/lib/api/services.rb index 163c7505a65..bda6be51553 100644 --- a/lib/api/services.rb +++ b/lib/api/services.rb @@ -431,7 +431,7 @@ module API { required: false, name: :jira_issue_transition_id, - type: Integer, + type: String, desc: 'The ID of a transition that moves issues to a closed state. You can find this number under the JIRA workflow administration (**Administration > Issues > Workflows**) by selecting **View** under **Operations** of the desired workflow of your project. The ID of each state can be found inside the parenthesis of each transition name under the **Transitions (id)** column ([see screenshot][trans]). By default, this ID is set to `2`' } ], @@ -592,6 +592,26 @@ module API desc: 'The description of the tracker' } ], + 'youtrack' => [ + { + required: true, + name: :project_url, + type: String, + desc: 'The project URL' + }, + { + required: true, + name: :issues_url, + type: String, + desc: 'The issues URL' + }, + { + required: false, + name: :description, + type: String, + desc: 'The description of the tracker' + } + ], 'slack' => [ CHAT_NOTIFICATION_SETTINGS, CHAT_NOTIFICATION_FLAGS, @@ -665,6 +685,7 @@ module API PrometheusService, PushoverService, RedmineService, + YoutrackService, SlackService, MattermostService, MicrosoftTeamsService, diff --git a/lib/api/users.rb b/lib/api/users.rb index 8ce09a8881b..7d88880d412 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -26,7 +26,7 @@ module API # rubocop: disable CodeReuse/ActiveRecord def reorder_users(users) if params[:order_by] && params[:sort] - users.reorder(params[:order_by] => params[:sort]) + users.reorder(order_options_with_tie_breaker) else users end diff --git a/lib/banzai/filter/relative_link_filter.rb b/lib/banzai/filter/relative_link_filter.rb index 93e6d6470f1..2745905c5ff 100644 --- a/lib/banzai/filter/relative_link_filter.rb +++ b/lib/banzai/filter/relative_link_filter.rb @@ -150,7 +150,10 @@ module Banzai end def uri_type(path) - @uri_types[path] ||= current_commit.uri_type(path) + # https://gitlab.com/gitlab-org/gitlab-ce/issues/58011 + Gitlab::GitalyClient.allow_n_plus_1_calls do + @uri_types[path] ||= current_commit.uri_type(path) + end end def current_commit diff --git a/lib/bitbucket_server/connection.rb b/lib/bitbucket_server/connection.rb index 9c14b26c65a..fbd451efb23 100644 --- a/lib/bitbucket_server/connection.rb +++ b/lib/bitbucket_server/connection.rb @@ -77,6 +77,7 @@ module BitbucketServer private def check_errors!(response) + return if ActionDispatch::Response::NO_CONTENT_CODES.include?(response.code) raise ConnectionError, "Response is not valid JSON" unless response.parsed_response.is_a?(Hash) return if response.code >= 200 && response.code < 300 diff --git a/lib/feature.rb b/lib/feature.rb index e59cd70f822..749c861d740 100644 --- a/lib/feature.rb +++ b/lib/feature.rb @@ -111,11 +111,11 @@ class Feature end def gate_specified? - %i(user project feature_group).any? { |key| params.key?(key) } + %i(user project group feature_group).any? { |key| params.key?(key) } end def targets - [feature_group, user, project].compact + [feature_group, user, project, group].compact end private @@ -139,5 +139,11 @@ class Feature Project.find_by_full_path(params[:project]) end + + def group + return unless params.key?(:group) + + Group.find_by_full_path(params[:group]) + end end end diff --git a/lib/gitlab/auth/omniauth_identity_linker_base.rb b/lib/gitlab/auth/omniauth_identity_linker_base.rb index 253445570f2..c620fc5d6bd 100644 --- a/lib/gitlab/auth/omniauth_identity_linker_base.rb +++ b/lib/gitlab/auth/omniauth_identity_linker_base.rb @@ -12,7 +12,7 @@ module Gitlab end def link - save if identity.new_record? + save if unlinked? end def changed? @@ -35,6 +35,10 @@ module Gitlab @changed = identity.save end + def unlinked? + identity.new_record? + end + # rubocop: disable CodeReuse/ActiveRecord def identity @identity ||= current_user.identities diff --git a/lib/gitlab/chat.rb b/lib/gitlab/chat.rb new file mode 100644 index 00000000000..23d4fb36b66 --- /dev/null +++ b/lib/gitlab/chat.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module Gitlab + module Chat + # Returns `true` if Chatops is available for the current instance. + def self.available? + ::Feature.enabled?(:chatops, default_enabled: true) + end + end +end diff --git a/lib/gitlab/chat/command.rb b/lib/gitlab/chat/command.rb new file mode 100644 index 00000000000..49b7dcf4bbe --- /dev/null +++ b/lib/gitlab/chat/command.rb @@ -0,0 +1,94 @@ +# frozen_string_literal: true + +module Gitlab + module Chat + # Class for scheduling chat pipelines. + # + # A Command takes care of creating a `Ci::Pipeline` with all the data + # necessary to execute a chat command. This includes data such as the chat + # data (e.g. the response URL) and any environment variables that should be + # exposed to the chat command. + class Command + include Utils::StrongMemoize + + attr_reader :project, :chat_name, :name, :arguments, :response_url, + :channel + + # project - The Project to schedule the command for. + # chat_name - The ChatName belonging to the user that scheduled the + # command. + # name - The name of the chat command to run. + # arguments - The arguments (as a String) to pass to the command. + # channel - The channel the message was sent from. + # response_url - The URL to send the response back to. + def initialize(project:, chat_name:, name:, arguments:, channel:, response_url:) + @project = project + @chat_name = chat_name + @name = name + @arguments = arguments + @channel = channel + @response_url = response_url + end + + # Tries to create a new pipeline. + # + # This method will return a pipeline that _may_ be persisted, or `nil` if + # the pipeline could not be created. + def try_create_pipeline + return unless valid? + + create_pipeline + end + + def create_pipeline + service = ::Ci::CreatePipelineService.new( + project, + chat_name.user, + ref: branch, + sha: commit, + chat_data: { + chat_name_id: chat_name.id, + command: name, + arguments: arguments, + response_url: response_url + } + ) + + service.execute(:chat) do |pipeline| + build_environment_variables(pipeline) + build_chat_data(pipeline) + end + end + + # pipeline - The `Ci::Pipeline` to create the environment variables for. + def build_environment_variables(pipeline) + pipeline.variables.build( + [{ key: 'CHAT_INPUT', value: arguments }, + { key: 'CHAT_CHANNEL', value: channel }] + ) + end + + # pipeline - The `Ci::Pipeline` to create the chat data for. + def build_chat_data(pipeline) + pipeline.build_chat_data( + chat_name_id: chat_name.id, + response_url: response_url + ) + end + + def valid? + branch && commit + end + + def branch + strong_memoize(:branch) { project.default_branch } + end + + def commit + strong_memoize(:commit) do + project.commit(branch)&.id if branch + end + end + end + end +end diff --git a/lib/gitlab/chat/output.rb b/lib/gitlab/chat/output.rb new file mode 100644 index 00000000000..411b1555a7d --- /dev/null +++ b/lib/gitlab/chat/output.rb @@ -0,0 +1,93 @@ +# frozen_string_literal: true + +module Gitlab + module Chat + # Class for gathering and formatting the output of a `Ci::Build`. + class Output + attr_reader :build + + MissingBuildSectionError = Class.new(StandardError) + + # The primary trace section to look for. + PRIMARY_SECTION = 'chat_reply' + + # The backup trace section in case the primary one could not be found. + FALLBACK_SECTION = 'build_script' + + # build - The `Ci::Build` to obtain the output from. + def initialize(build) + @build = build + end + + # Returns a `String` containing the output of the build. + # + # The output _does not_ include the command that was executed. + def to_s + offset, length = read_offset_and_length + + trace.read do |stream| + stream.seek(offset) + + output = stream + .stream + .read(length) + .force_encoding(Encoding.default_external) + + without_executed_command_line(output) + end + end + + # Returns the offset to seek to and the number of bytes to read relative + # to the offset. + def read_offset_and_length + section = find_build_trace_section(PRIMARY_SECTION) || + find_build_trace_section(FALLBACK_SECTION) + + unless section + raise( + MissingBuildSectionError, + "The build_script trace section could not be found for build #{build.id}" + ) + end + + length = section[:byte_end] - section[:byte_start] + + [section[:byte_start], length] + end + + # Removes the line containing the executed command from the build output. + # + # output - A `String` containing the output of a trace section. + def without_executed_command_line(output) + # If `output.split("\n")` produces an empty Array then the slicing that + # follows it will produce a nil. For example: + # + # "\n".split("\n") # => [] + # "\n".split("\n")[1..-1] # => nil + # + # To work around this we only "join" if we're given an Array. + if (converted = output.split("\n")[1..-1]) + converted.join("\n") + else + '' + end + end + + # Returns the trace section for the given name, or `nil` if the section + # could not be found. + # + # name - The name of the trace section to find. + def find_build_trace_section(name) + trace_sections.find { |s| s[:name] == name } + end + + def trace_sections + @trace_sections ||= trace.extract_sections + end + + def trace + @trace ||= build.trace + end + end + end +end diff --git a/lib/gitlab/chat/responder.rb b/lib/gitlab/chat/responder.rb new file mode 100644 index 00000000000..6267fbc20e2 --- /dev/null +++ b/lib/gitlab/chat/responder.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Gitlab + module Chat + module Responder + # Returns an instance of the responder to use for generating chat + # responses. + # + # This method will return `nil` if no formatter is available for the given + # build. + # + # build - A `Ci::Build` that executed a chat command. + def self.responder_for(build) + service = build.pipeline.chat_data&.chat_name&.service + + if (responder = service.try(:chat_responder)) + responder.new(build) + end + end + end + end +end diff --git a/lib/gitlab/chat/responder/base.rb b/lib/gitlab/chat/responder/base.rb new file mode 100644 index 00000000000..f1ad0e36793 --- /dev/null +++ b/lib/gitlab/chat/responder/base.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +module Gitlab + module Chat + module Responder + class Base + attr_reader :build + + # build - The `Ci::Build` that was executed. + def initialize(build) + @build = build + end + + def pipeline + build.pipeline + end + + def project + pipeline.project + end + + def success(*) + raise NotImplementedError, 'You must implement #success(output)' + end + + def failure + raise NotImplementedError, 'You must implement #failure' + end + + def send_response(output) + raise NotImplementedError, 'You must implement #send_response(output)' + end + + def scheduled_output + raise NotImplementedError, 'You must implement #scheduled_output' + end + end + end + end +end diff --git a/lib/gitlab/chat/responder/slack.rb b/lib/gitlab/chat/responder/slack.rb new file mode 100644 index 00000000000..0cf02c92a67 --- /dev/null +++ b/lib/gitlab/chat/responder/slack.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true + +module Gitlab + module Chat + module Responder + class Slack < Responder::Base + SUCCESS_COLOR = '#B3ED8E' + FAILURE_COLOR = '#FF5640' + RESPONSE_TYPE = :in_channel + + # Slack breaks messages apart if they're around 4 KB in size. We use a + # slightly smaller limit here to account for user mentions. + MESSAGE_SIZE_LIMIT = 3.5.kilobytes + + # Sends a response back to Slack + # + # output - The output to send back to Slack, as a Hash. + def send_response(output) + Gitlab::HTTP.post( + pipeline.chat_data.response_url, + { + headers: { Accept: 'application/json' }, + body: output.to_json + } + ) + end + + # Sends the output for a build that completed successfully. + # + # output - The output produced by the chat command. + def success(output) + return if output.empty? + + send_response( + text: message_text(limit_output(output)), + response_type: RESPONSE_TYPE + ) + end + + # Sends the output for a build that failed. + def failure + send_response( + text: message_text("<#{build_url}|Sorry, the build failed!>"), + response_type: RESPONSE_TYPE + ) + end + + # Returns the output to send back after a command has been scheduled. + def scheduled_output + # We return an empty message so that Slack still shows the input + # command, without polluting the channel with standard "The job has + # been scheduled" (or similar) responses. + { text: '' } + end + + private + + def limit_output(output) + if output.bytesize <= MESSAGE_SIZE_LIMIT + output + else + "<#{build_url}|The output is too large to be sent back directly!>" + end + end + + def mention_user + "<@#{pipeline.chat_data.chat_name.chat_id}>" + end + + def message_text(output) + "#{mention_user}: #{output}" + end + + def build_url + ::Gitlab::Routing.url_helpers.project_build_url(project, build) + end + end + end + end +end diff --git a/lib/gitlab/ci/pipeline/chain/build.rb b/lib/gitlab/ci/pipeline/chain/build.rb index 41632211374..164a4634d84 100644 --- a/lib/gitlab/ci/pipeline/chain/build.rb +++ b/lib/gitlab/ci/pipeline/chain/build.rb @@ -12,6 +12,8 @@ module Gitlab ref: @command.ref, sha: @command.sha, before_sha: @command.before_sha, + source_sha: @command.source_sha, + target_sha: @command.target_sha, tag: @command.tag_exists?, trigger_requests: Array(@command.trigger_request), user: @command.current_user, diff --git a/lib/gitlab/ci/pipeline/chain/command.rb b/lib/gitlab/ci/pipeline/chain/command.rb index e62d547d862..7b77e86feae 100644 --- a/lib/gitlab/ci/pipeline/chain/command.rb +++ b/lib/gitlab/ci/pipeline/chain/command.rb @@ -7,10 +7,11 @@ module Gitlab module Chain Command = Struct.new( :source, :project, :current_user, - :origin_ref, :checkout_sha, :after_sha, :before_sha, + :origin_ref, :checkout_sha, :after_sha, :before_sha, :source_sha, :target_sha, :trigger_request, :schedule, :merge_request, :ignore_skip_ci, :save_incompleted, - :seeds_block, :variables_attributes, :push_options + :seeds_block, :variables_attributes, :push_options, + :chat_data ) do include Gitlab::Utils::StrongMemoize diff --git a/lib/gitlab/ci/pipeline/chain/remove_unwanted_chat_jobs.rb b/lib/gitlab/ci/pipeline/chain/remove_unwanted_chat_jobs.rb index 0f687a4ce9b..1e09b417311 100644 --- a/lib/gitlab/ci/pipeline/chain/remove_unwanted_chat_jobs.rb +++ b/lib/gitlab/ci/pipeline/chain/remove_unwanted_chat_jobs.rb @@ -6,7 +6,13 @@ module Gitlab module Chain class RemoveUnwantedChatJobs < Chain::Base def perform! - # to be overriden in EE + return unless pipeline.config_processor && pipeline.chat? + + # When scheduling a chat pipeline we only want to run the build + # that matches the chat command. + pipeline.config_processor.jobs.select! do |name, _| + name.to_s == command.chat_data[:command].to_s + end end def break? diff --git a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml index e369d26f22f..c0d4d4400b3 100644 --- a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml @@ -48,12 +48,15 @@ variables: POSTGRES_PASSWORD: testing-password POSTGRES_ENABLED: "true" POSTGRES_DB: $CI_ENVIRONMENT_SLUG + POSTGRES_VERSION: 9.6.2 - KUBERNETES_VERSION: 1.11.6 - HELM_VERSION: 2.12.2 + KUBERNETES_VERSION: 1.11.7 + HELM_VERSION: 2.12.3 DOCKER_DRIVER: overlay2 + ROLLOUT_RESOURCE_TYPE: deployment + stages: - build - test @@ -71,12 +74,11 @@ stages: build: stage: build - image: docker:stable-git + image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-build-image/master:stable" services: - - docker:stable-dind + - docker:stable-dind script: - - setup_docker - - build + - /build/build.sh only: - branches @@ -488,7 +490,6 @@ rollout 100%: export DATABASE_URL=${DATABASE_URL-$auto_database_url} export CI_APPLICATION_REPOSITORY=$CI_REGISTRY_IMAGE/$CI_COMMIT_REF_SLUG export CI_APPLICATION_TAG=$CI_COMMIT_SHA - export CI_CONTAINER_NAME=ci_job_build_${CI_JOB_ID} export TILLER_NAMESPACE=$KUBE_NAMESPACE # Extract "MAJOR.MINOR" from CI_SERVER_VERSION and generate "MAJOR-MINOR-stable" for Security Products export SP_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/') @@ -698,6 +699,7 @@ rollout 100%: --set postgresql.postgresUser="$POSTGRES_USER" \ --set postgresql.postgresPassword="$POSTGRES_PASSWORD" \ --set postgresql.postgresDatabase="$POSTGRES_DB" \ + --set postgresql.imageTag="$POSTGRES_VERSION" \ --set application.initializeCommand="$DB_INITIALIZE" \ --namespace="$KUBE_NAMESPACE" \ "$name" \ @@ -740,7 +742,7 @@ rollout 100%: chart/ fi - kubectl rollout status -n "$KUBE_NAMESPACE" -w "deployment/$name" + kubectl rollout status -n "$KUBE_NAMESPACE" -w "$ROLLOUT_RESOURCE_TYPE/$name" } function scale() { @@ -826,7 +828,7 @@ rollout 100%: # Function to ensure backwards compatibility with AUTO_DEVOPS_DOMAIN function ensure_kube_ingress_base_domain() { - if [ -z ${KUBE_INGRESS_BASE_DOMAIN+x} ]; then + if [ -z ${KUBE_INGRESS_BASE_DOMAIN+x} ] && [ -n "$AUTO_DEVOPS_DOMAIN" ] ; then export KUBE_INGRESS_BASE_DOMAIN=$AUTO_DEVOPS_DOMAIN fi } @@ -847,50 +849,6 @@ rollout 100%: fi } - function build() { - registry_login - - if [[ -f Dockerfile ]]; then - echo "Building Dockerfile-based application..." - docker build \ - --build-arg HTTP_PROXY="$HTTP_PROXY" \ - --build-arg http_proxy="$http_proxy" \ - --build-arg HTTPS_PROXY="$HTTPS_PROXY" \ - --build-arg https_proxy="$https_proxy" \ - --build-arg FTP_PROXY="$FTP_PROXY" \ - --build-arg ftp_proxy="$ftp_proxy" \ - --build-arg NO_PROXY="$NO_PROXY" \ - --build-arg no_proxy="$no_proxy" \ - -t "$CI_APPLICATION_REPOSITORY:$CI_APPLICATION_TAG" . - else - echo "Building Heroku-based application using gliderlabs/herokuish docker image..." - docker run -i \ - -e BUILDPACK_URL \ - -e HTTP_PROXY \ - -e http_proxy \ - -e HTTPS_PROXY \ - -e https_proxy \ - -e FTP_PROXY \ - -e ftp_proxy \ - -e NO_PROXY \ - -e no_proxy \ - --name="$CI_CONTAINER_NAME" -v "$(pwd):/tmp/app:ro" gliderlabs/herokuish /bin/herokuish buildpack build - docker commit "$CI_CONTAINER_NAME" "$CI_APPLICATION_REPOSITORY:$CI_APPLICATION_TAG" - docker rm "$CI_CONTAINER_NAME" >/dev/null - echo "" - - echo "Configuring $CI_APPLICATION_REPOSITORY:$CI_APPLICATION_TAG docker image..." - docker create --expose 5000 --env PORT=5000 --name="$CI_CONTAINER_NAME" "$CI_APPLICATION_REPOSITORY:$CI_APPLICATION_TAG" /bin/herokuish procfile start web - docker commit "$CI_CONTAINER_NAME" "$CI_APPLICATION_REPOSITORY:$CI_APPLICATION_TAG" - docker rm "$CI_CONTAINER_NAME" >/dev/null - echo "" - fi - - echo "Pushing to GitLab Container Registry..." - docker push "$CI_APPLICATION_REPOSITORY:$CI_APPLICATION_TAG" - echo "" - } - function initialize_tiller() { echo "Checking Tiller..." diff --git a/lib/gitlab/danger/helper.rb b/lib/gitlab/danger/helper.rb index b8428637e19..d3c86fdb629 100644 --- a/lib/gitlab/danger/helper.rb +++ b/lib/gitlab/danger/helper.rb @@ -95,6 +95,7 @@ module Gitlab CATEGORY_LABELS = { docs: "~Documentation", + none: "", qa: "~QA" }.freeze @@ -105,13 +106,13 @@ module Gitlab %r{\A(ee/)?app/(assets|views)/} => :frontend, %r{\A(ee/)?public/} => :frontend, - %r{\A(ee/)?spec/javascripts/} => :frontend, + %r{\A(ee/)?spec/(javascripts|frontend)/} => :frontend, %r{\A(ee/)?vendor/assets/} => :frontend, %r{\A(jest\.config\.js|package\.json|yarn\.lock)\z} => :frontend, %r{\A(ee/)?app/(?!assets|views)[^/]+} => :backend, %r{\A(ee/)?(bin|config|danger|generator_templates|lib|rubocop|scripts)/} => :backend, - %r{\A(ee/)?spec/(?!javascripts)[^/]+} => :backend, + %r{\A(ee/)?spec/(?!javascripts|frontend)[^/]+} => :backend, %r{\A(ee/)?vendor/(?!assets)[^/]+} => :backend, %r{\A(ee/)?vendor/(languages\.yml|licenses\.csv)\z} => :backend, %r{\A(Dangerfile|Gemfile|Gemfile.lock|Procfile|Rakefile|\.gitlab-ci\.yml)\z} => :backend, @@ -120,6 +121,9 @@ module Gitlab %r{\A(ee/)?db/} => :database, %r{\A(ee/)?qa/} => :qa, + # Files that don't fit into any category are marked with :none + %r{\A(ee/)?changelogs/} => :none, + # Fallbacks in case the above patterns miss anything %r{\.rb\z} => :backend, %r{\.(md|txt)\z} => :docs, diff --git a/lib/gitlab/diff/file.rb b/lib/gitlab/diff/file.rb index e410d5a8333..c9d89d56884 100644 --- a/lib/gitlab/diff/file.rb +++ b/lib/gitlab/diff/file.rb @@ -293,6 +293,10 @@ module Gitlab end end + def viewer + rich_viewer || simple_viewer + end + def simple_viewer @simple_viewer ||= simple_viewer_class.new(self) end diff --git a/lib/gitlab/etag_caching/router.rb b/lib/gitlab/etag_caching/router.rb index 08e30214b46..0891f79198d 100644 --- a/lib/gitlab/etag_caching/router.rb +++ b/lib/gitlab/etag_caching/router.rb @@ -52,6 +52,14 @@ module Gitlab Gitlab::EtagCaching::Router::Route.new( %r(^(?!.*(#{RESERVED_WORDS_REGEX})).*/environments\.json\z), 'environments' + ), + Gitlab::EtagCaching::Router::Route.new( + %r(^(?!.*(#{RESERVED_WORDS_REGEX})).*/import/github/realtime_changes\.json\z), + 'realtime_changes_import_github' + ), + Gitlab::EtagCaching::Router::Route.new( + %r(^(?!.*(#{RESERVED_WORDS_REGEX})).*/import/gitea/realtime_changes\.json\z), + 'realtime_changes_import_gitea' ) ].freeze diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index 593a3676519..aea132a3dd9 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -556,6 +556,12 @@ module Gitlab tags.find { |tag| tag.name == name } end + def merge_to_ref(user, source_sha, branch, target_ref, message) + wrapped_gitaly_errors do + gitaly_operation_client.user_merge_to_ref(user, source_sha, branch, target_ref, message) + end + end + def merge(user, source_sha, target_branch, message, &block) wrapped_gitaly_errors do gitaly_operation_client.user_merge_branch(user, source_sha, target_branch, message, &block) diff --git a/lib/gitlab/gitaly_client.rb b/lib/gitlab/gitaly_client.rb index 0ab53f8f706..5aeedb0f50d 100644 --- a/lib/gitlab/gitaly_client.rb +++ b/lib/gitlab/gitaly_client.rb @@ -28,7 +28,7 @@ module Gitlab PEM_REGEX = /\-+BEGIN CERTIFICATE\-+.+?\-+END CERTIFICATE\-+/m SERVER_VERSION_FILE = 'GITALY_SERVER_VERSION' - MAXIMUM_GITALY_CALLS = 35 + MAXIMUM_GITALY_CALLS = 30 CLIENT_NAME = (Sidekiq.server? ? 'gitlab-sidekiq' : 'gitlab-web').freeze MUTEX = Mutex.new diff --git a/lib/gitlab/gitaly_client/operation_service.rb b/lib/gitlab/gitaly_client/operation_service.rb index 22d2d149e65..d172c798da2 100644 --- a/lib/gitlab/gitaly_client/operation_service.rb +++ b/lib/gitlab/gitaly_client/operation_service.rb @@ -100,6 +100,25 @@ module Gitlab end end + def user_merge_to_ref(user, source_sha, branch, target_ref, message) + request = Gitaly::UserMergeToRefRequest.new( + repository: @gitaly_repo, + source_sha: source_sha, + branch: encode_binary(branch), + target_ref: encode_binary(target_ref), + user: Gitlab::Git::User.from_gitlab(user).to_gitaly, + message: message + ) + + response = GitalyClient.call(@repository.storage, :operation_service, :user_merge_to_ref, request) + + if pre_receive_error = response.pre_receive_error.presence + raise Gitlab::Git::PreReceiveError, pre_receive_error + end + + response.commit_id + end + def user_merge_branch(user, source_sha, target_branch, message) request_enum = QueueEnumerator.new response_enum = GitalyClient.call( diff --git a/lib/gitlab/graphql/authorize.rb b/lib/gitlab/graphql/authorize.rb index 5e48bf9043d..f62813db82c 100644 --- a/lib/gitlab/graphql/authorize.rb +++ b/lib/gitlab/graphql/authorize.rb @@ -10,21 +10,6 @@ module Gitlab def self.use(schema_definition) schema_definition.instrument(:field, Instrumentation.new) end - - def required_permissions - # If the `#authorize` call is used on multiple classes, we add the - # permissions specified on a subclass, to the ones that were specified - # on it's superclass. - @required_permissions ||= if self.respond_to?(:superclass) && superclass.respond_to?(:required_permissions) - superclass.required_permissions.dup - else - [] - end - end - - def authorize(*permissions) - required_permissions.concat(permissions) - end end end end diff --git a/lib/gitlab/graphql/authorize/authorize_resource.rb b/lib/gitlab/graphql/authorize/authorize_resource.rb index a56c4f6368d..b367a97105c 100644 --- a/lib/gitlab/graphql/authorize/authorize_resource.rb +++ b/lib/gitlab/graphql/authorize/authorize_resource.rb @@ -6,8 +6,21 @@ module Gitlab module AuthorizeResource extend ActiveSupport::Concern - included do - extend Gitlab::Graphql::Authorize + class_methods do + def required_permissions + # If the `#authorize` call is used on multiple classes, we add the + # permissions specified on a subclass, to the ones that were specified + # on it's superclass. + @required_permissions ||= if self.respond_to?(:superclass) && superclass.respond_to?(:required_permissions) + superclass.required_permissions.dup + else + [] + end + end + + def authorize(*permissions) + required_permissions.concat(permissions) + end end def find_object(*args) diff --git a/lib/gitlab/graphql/authorize/instrumentation.rb b/lib/gitlab/graphql/authorize/instrumentation.rb index d638d2b43ee..593da8471dd 100644 --- a/lib/gitlab/graphql/authorize/instrumentation.rb +++ b/lib/gitlab/graphql/authorize/instrumentation.rb @@ -6,19 +6,15 @@ module Gitlab class Instrumentation # Replace the resolver for the field with one that will only return the # resolved object if the permissions check is successful. - # - # Collections are not supported. Apply permissions checks for those at the - # database level instead, to avoid loading superfluous data from the DB def instrument(_type, field) - field_definition = field.metadata[:type_class] - return field unless field_definition.respond_to?(:required_permissions) - return field if field_definition.required_permissions.empty? + required_permissions = Array.wrap(field.metadata[:authorize]) + return field if required_permissions.empty? old_resolver = field.resolve_proc new_resolver = -> (obj, args, ctx) do resolved_obj = old_resolver.call(obj, args, ctx) - checker = build_checker(ctx[:current_user], field_definition.required_permissions) + checker = build_checker(ctx[:current_user], required_permissions) if resolved_obj.respond_to?(:then) resolved_obj.then(&checker) @@ -35,10 +31,22 @@ module Gitlab private def build_checker(current_user, abilities) - proc do |obj| + lambda do |value| # Load the elements if they weren't loaded by BatchLoader yet - obj = obj.sync if obj.respond_to?(:sync) - obj if abilities.all? { |ability| Ability.allowed?(current_user, ability, obj) } + value = value.sync if value.respond_to?(:sync) + + check = lambda do |object| + abilities.all? do |ability| + Ability.allowed?(current_user, ability, object) + end + end + + case value + when Array + value.select(&check) + else + value if check.call(value) + end end end end diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml index 099677a791c..fa54fc17d95 100644 --- a/lib/gitlab/import_export/import_export.yml +++ b/lib/gitlab/import_export/import_export.yml @@ -28,7 +28,6 @@ project_tree: - notes: :author - releases: - - :author - :links - project_members: - :user @@ -133,7 +132,6 @@ excluded_attributes: - :external_diff - :stored_externally - :external_diff_store - - :st_diffs merge_request_diff_files: - :diff - :external_diff_offset diff --git a/lib/gitlab/import_export/merge_request_parser.rb b/lib/gitlab/import_export/merge_request_parser.rb index 040a70d6775..deb2f59f05f 100644 --- a/lib/gitlab/import_export/merge_request_parser.rb +++ b/lib/gitlab/import_export/merge_request_parser.rb @@ -20,6 +20,17 @@ module Gitlab create_target_branch unless branch_exists?(@merge_request.target_branch) end + # The merge_request_diff associated with the current @merge_request might + # be invalid. Than means, when the @merge_request object is saved, the + # @merge_request.merge_request_diff won't. This can leave the merge request + # in an invalid state, because a merge request must have an associated + # merge request diff. + # In this change, if the associated merge request diff is invalid, we set + # it to nil. This change, in association with the after callback + # :ensure_merge_request_diff in the MergeRequest class, makes that + # when the merge request is going to be created and it doesn't have + # one, a default one will be generated. + @merge_request.merge_request_diff = nil unless @merge_request.merge_request_diff&.valid? @merge_request end diff --git a/lib/gitlab/kubernetes/helm.rb b/lib/gitlab/kubernetes/helm.rb index bbac15c7710..42c4745ff98 100644 --- a/lib/gitlab/kubernetes/helm.rb +++ b/lib/gitlab/kubernetes/helm.rb @@ -3,8 +3,8 @@ module Gitlab module Kubernetes module Helm - HELM_VERSION = '2.12.2'.freeze - KUBECTL_VERSION = '1.11.0'.freeze + HELM_VERSION = '2.12.3'.freeze + KUBECTL_VERSION = '1.11.7'.freeze NAMESPACE = 'gitlab-managed-apps'.freeze SERVICE_ACCOUNT = 'tiller'.freeze CLUSTER_ROLE_BINDING = 'tiller-admin'.freeze diff --git a/lib/gitlab/lfs_token.rb b/lib/gitlab/lfs_token.rb index 26b81847d37..31e6fc9d8c7 100644 --- a/lib/gitlab/lfs_token.rb +++ b/lib/gitlab/lfs_token.rb @@ -30,8 +30,8 @@ module Gitlab end end - def token(expire_time: DEFAULT_EXPIRE_TIME) - HMACToken.new(actor).token(expire_time) + def token + HMACToken.new(actor).token(DEFAULT_EXPIRE_TIME) end def token_valid?(token_to_check) @@ -47,6 +47,15 @@ module Gitlab user? ? :lfs_token : :lfs_deploy_token end + def authentication_payload(repository_http_path) + { + username: actor_name, + lfs_token: token, + repository_http_path: repository_http_path, + expires_in: DEFAULT_EXPIRE_TIME + } + end + private # rubocop:disable Lint/UselessAccessModifier class HMACToken diff --git a/lib/gitlab/project_template.rb b/lib/gitlab/project_template.rb index ef656e5b2ce..29c511524a2 100644 --- a/lib/gitlab/project_template.rb +++ b/lib/gitlab/project_template.rb @@ -28,11 +28,17 @@ module Gitlab ProjectTemplate.new('rails', 'Ruby on Rails', _('Includes an MVC structure, Gemfile, Rakefile, along with many others, to help you get started.'), 'https://gitlab.com/gitlab-org/project-templates/rails', 'illustrations/logos/rails.svg'), ProjectTemplate.new('spring', 'Spring', _('Includes an MVC structure, mvnw and pom.xml to help you get started.'), 'https://gitlab.com/gitlab-org/project-templates/spring', 'illustrations/logos/spring.svg'), ProjectTemplate.new('express', 'NodeJS Express', _('Includes an MVC structure to help you get started.'), 'https://gitlab.com/gitlab-org/project-templates/express', 'illustrations/logos/express.svg'), + ProjectTemplate.new('dotnetcore', '.NET Core', _('A .NET Core console application template, customizable for any .NET Core project'), 'https://gitlab.com/gitlab-org/project-templates/dotnetcore', 'illustrations/logos/dotnet.svg'), ProjectTemplate.new('hugo', 'Pages/Hugo', _('Everything you need to create a GitLab Pages site using Hugo.'), 'https://gitlab.com/pages/hugo'), ProjectTemplate.new('jekyll', 'Pages/Jekyll', _('Everything you need to create a GitLab Pages site using Jekyll.'), 'https://gitlab.com/pages/jekyll'), ProjectTemplate.new('plainhtml', 'Pages/Plain HTML', _('Everything you need to create a GitLab Pages site using plain HTML.'), 'https://gitlab.com/pages/plain-html'), ProjectTemplate.new('gitbook', 'Pages/GitBook', _('Everything you need to create a GitLab Pages site using GitBook.'), 'https://gitlab.com/pages/gitbook'), - ProjectTemplate.new('hexo', 'Pages/Hexo', _('Everything you need to create a GitLab Pages site using Hexo.'), 'https://gitlab.com/pages/hexo') + ProjectTemplate.new('hexo', 'Pages/Hexo', _('Everything you need to create a GitLab Pages site using Hexo.'), 'https://gitlab.com/pages/hexo'), + ProjectTemplate.new('nfhugo', 'Netlify/Hugo', _('A Hugo site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfhugo', 'illustrations/logos/netlify.svg'), + ProjectTemplate.new('nfjekyll', 'Netlify/Jekyll', _('A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfjekyll', 'illustrations/logos/netlify.svg'), + ProjectTemplate.new('nfplainhtml', 'Netlify/Plain HTML', _('A plain HTML site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfplain-html', 'illustrations/logos/netlify.svg'), + ProjectTemplate.new('nfgitbook', 'Netlify/GitBook', _('A GitBook site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfgitbook', 'illustrations/logos/netlify.svg'), + ProjectTemplate.new('nfhexo', 'Netlify/Hexo', _('A Hexo site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfhexo', 'illustrations/logos/netlify.svg') ].freeze class << self diff --git a/lib/gitlab/shell.rb b/lib/gitlab/shell.rb index 1153e69d3de..c7d8dfcd495 100644 --- a/lib/gitlab/shell.rb +++ b/lib/gitlab/shell.rb @@ -280,7 +280,10 @@ module Gitlab # add_namespace("default", "gitlab") # def add_namespace(storage, name) - Gitlab::GitalyClient::NamespaceService.new(storage).add(name) + # https://gitlab.com/gitlab-org/gitlab-ce/issues/58012 + Gitlab::GitalyClient.allow_n_plus_1_calls do + Gitlab::GitalyClient::NamespaceService.new(storage).add(name) + end rescue GRPC::InvalidArgument => e raise ArgumentError, e.message end diff --git a/lib/gitlab/slash_commands/application_help.rb b/lib/gitlab/slash_commands/application_help.rb new file mode 100644 index 00000000000..0ea7554ba64 --- /dev/null +++ b/lib/gitlab/slash_commands/application_help.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Gitlab + module SlashCommands + class ApplicationHelp < BaseCommand + def initialize(params) + @params = params + end + + def execute + Gitlab::SlashCommands::Presenters::Help.new(commands).present(trigger, params[:text]) + end + + private + + def trigger + "#{params[:command]} [project name or alias]" + end + + def commands + Gitlab::SlashCommands::Command.commands + end + end + end +end diff --git a/lib/gitlab/slash_commands/command.rb b/lib/gitlab/slash_commands/command.rb index 474c09b9c4d..7c963fcf38a 100644 --- a/lib/gitlab/slash_commands/command.rb +++ b/lib/gitlab/slash_commands/command.rb @@ -9,7 +9,8 @@ module Gitlab Gitlab::SlashCommands::IssueNew, Gitlab::SlashCommands::IssueSearch, Gitlab::SlashCommands::IssueMove, - Gitlab::SlashCommands::Deploy + Gitlab::SlashCommands::Deploy, + Gitlab::SlashCommands::Run ] end diff --git a/lib/gitlab/slash_commands/presenters/error.rb b/lib/gitlab/slash_commands/presenters/error.rb new file mode 100644 index 00000000000..442f8796338 --- /dev/null +++ b/lib/gitlab/slash_commands/presenters/error.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Gitlab + module SlashCommands + module Presenters + class Error < Presenters::Base + def initialize(message) + @message = message + end + + def message + ephemeral_response(text: @message) + end + end + end + end +end diff --git a/lib/gitlab/slash_commands/presenters/run.rb b/lib/gitlab/slash_commands/presenters/run.rb new file mode 100644 index 00000000000..c4bbc231464 --- /dev/null +++ b/lib/gitlab/slash_commands/presenters/run.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module Gitlab + module SlashCommands + module Presenters + class Run < Presenters::Base + # rubocop: disable CodeReuse/ActiveRecord + def present(pipeline) + build = pipeline.builds.take + + if build && (responder = Chat::Responder.responder_for(build)) + in_channel_response(responder.scheduled_output) + else + unsupported_chat_service + end + end + # rubocop: enable CodeReuse/ActiveRecord + + def unsupported_chat_service + ephemeral_response(text: 'Sorry, this chat service is currently not supported by GitLab ChatOps.') + end + + def failed_to_schedule(command) + ephemeral_response( + text: 'The command could not be scheduled. Make sure that your ' \ + 'project has a .gitlab-ci.yml that defines a job with the ' \ + "name #{command.inspect}" + ) + end + end + end + end +end diff --git a/lib/gitlab/slash_commands/run.rb b/lib/gitlab/slash_commands/run.rb new file mode 100644 index 00000000000..10a545e28ac --- /dev/null +++ b/lib/gitlab/slash_commands/run.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +module Gitlab + module SlashCommands + # Slash command for triggering chatops jobs. + class Run < BaseCommand + def self.match(text) + /\Arun\s+(?<command>\S+)(\s+(?<arguments>.+))?\z/.match(text) + end + + def self.help_message + 'run <command> <arguments>' + end + + def self.available?(project) + Chat.available? && project.builds_enabled? + end + + def self.allowed?(project, user) + can?(user, :create_pipeline, project) + end + + def execute(match) + command = Chat::Command.new( + project: project, + chat_name: chat_name, + name: match[:command], + arguments: match[:arguments], + channel: params[:channel_id], + response_url: params[:response_url] + ) + + presenter = Gitlab::SlashCommands::Presenters::Run.new + pipeline = command.try_create_pipeline + + if pipeline&.persisted? + presenter.present(pipeline) + else + presenter.failed_to_schedule(command.name) + end + end + end + end +end diff --git a/lib/gitlab/tracing.rb b/lib/gitlab/tracing.rb index 0d9b0be1c8e..29517591c51 100644 --- a/lib/gitlab/tracing.rb +++ b/lib/gitlab/tracing.rb @@ -27,10 +27,11 @@ module Gitlab def self.tracing_url return unless tracing_url_enabled? - tracing_url_template % { - correlation_id: Gitlab::CorrelationId.current_id.to_s, - service: Gitlab.process_name - } + # Avoid using `format` since it can throw TypeErrors + # which we want to avoid on unsanitised env var input + tracing_url_template.to_s + .gsub(/\{\{\s*correlation_id\s*\}\}/, Gitlab::CorrelationId.current_id.to_s) + .gsub(/\{\{\s*service\s*\}\}/, Gitlab.process_name) end end end diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb index a65f4a8639c..0101ccc046a 100644 --- a/lib/gitlab/usage_data.rb +++ b/lib/gitlab/usage_data.rb @@ -64,12 +64,12 @@ module Gitlab group_clusters_disabled: count(::Clusters::Cluster.disabled.group_type), clusters_platforms_gke: count(::Clusters::Cluster.gcp_installed.enabled), clusters_platforms_user: count(::Clusters::Cluster.user_provided.enabled), - clusters_applications_helm: count(::Clusters::Applications::Helm.installed), - clusters_applications_ingress: count(::Clusters::Applications::Ingress.installed), - clusters_applications_cert_managers: count(::Clusters::Applications::CertManager.installed), - clusters_applications_prometheus: count(::Clusters::Applications::Prometheus.installed), - clusters_applications_runner: count(::Clusters::Applications::Runner.installed), - clusters_applications_knative: count(::Clusters::Applications::Knative.installed), + clusters_applications_helm: count(::Clusters::Applications::Helm.available), + clusters_applications_ingress: count(::Clusters::Applications::Ingress.available), + clusters_applications_cert_managers: count(::Clusters::Applications::CertManager.available), + clusters_applications_prometheus: count(::Clusters::Applications::Prometheus.available), + clusters_applications_runner: count(::Clusters::Applications::Runner.available), + clusters_applications_knative: count(::Clusters::Applications::Knative.available), in_review_folder: count(::Environment.in_review_folder), groups: count(Group), issues: count(Issue), diff --git a/lib/sentry/client.rb b/lib/sentry/client.rb index 4187014d49e..49ec196b103 100644 --- a/lib/sentry/client.rb +++ b/lib/sentry/client.rb @@ -54,7 +54,7 @@ module Sentry def handle_response(response) unless response.code == 200 - raise Client::Error, "Sentry response error: #{response.code}" + raise Client::Error, "Sentry response status code: #{response.code}" end response.as_json |