diff options
Diffstat (limited to 'app/services')
30 files changed, 545 insertions, 1026 deletions
diff --git a/app/services/after_branch_delete_service.rb b/app/services/after_branch_delete_service.rb index e7eb74d3e7d..ece9fbbef43 100644 --- a/app/services/after_branch_delete_service.rb +++ b/app/services/after_branch_delete_service.rb @@ -1,9 +1,6 @@ # frozen_string_literal: true -## -# Branch can be deleted either by DeleteBranchService -# or by GitPushService. -# +# Branch can be deleted either by DeleteBranchService or by Git::BranchPushService. class AfterBranchDeleteService < BaseService attr_reader :branch_name diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb index e95ba09c006..707caee482c 100644 --- a/app/services/auth/container_registry_authentication_service.rb +++ b/app/services/auth/container_registry_authentication_service.rb @@ -116,7 +116,7 @@ module Auth build_can_pull?(requested_project) || user_can_pull?(requested_project) || deploy_token_can_pull?(requested_project) when 'push' build_can_push?(requested_project) || user_can_push?(requested_project) - when '*' + when '*', 'delete' user_can_admin?(requested_project) else false diff --git a/app/services/ci/destroy_pipeline_service.rb b/app/services/ci/destroy_pipeline_service.rb index 5c4a34043c1..2292ec42b16 100644 --- a/app/services/ci/destroy_pipeline_service.rb +++ b/app/services/ci/destroy_pipeline_service.rb @@ -6,6 +6,8 @@ module Ci raise Gitlab::Access::AccessDeniedError unless can?(current_user, :destroy_pipeline, pipeline) pipeline.destroy! + + Gitlab::Cache::Ci::ProjectPipelineStatus.new(pipeline.project).delete_from_cache end end end diff --git a/app/services/ci/prepare_build_service.rb b/app/services/ci/prepare_build_service.rb new file mode 100644 index 00000000000..32f11438b79 --- /dev/null +++ b/app/services/ci/prepare_build_service.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Ci + class PrepareBuildService + attr_reader :build + + def initialize(build) + @build = build + end + + def execute + prerequisites.each(&:complete!) + + unless build.enqueue + build.drop!(:unmet_prerequisites) + end + end + + private + + def prerequisites + build.prerequisites + end + end +end diff --git a/app/services/clusters/applications/base_service.rb b/app/services/clusters/applications/base_service.rb index cbd1cf03ae1..14a45437287 100644 --- a/app/services/clusters/applications/base_service.rb +++ b/app/services/clusters/applications/base_service.rb @@ -41,7 +41,7 @@ module Clusters raise NotImplementedError end - def builders + def builder raise NotImplementedError end @@ -50,11 +50,27 @@ module Clusters end def instantiate_application - builder.call(@cluster) || raise(InvalidApplicationError, "invalid application: #{application_name}") + raise_invalid_application_error if invalid_application? + + builder || raise(InvalidApplicationError, "invalid application: #{application_name}") end - def builder - builders[application_name] || raise(InvalidApplicationError, "invalid application: #{application_name}") + def raise_invalid_application_error + raise(InvalidApplicationError, "invalid application: #{application_name}") + end + + def invalid_application? + unknown_application? || (!cluster.project_type? && project_only_application?) + end + + def unknown_application? + Clusters::Cluster::APPLICATIONS.keys.exclude?(application_name) + end + + # These applications will need extra configuration to enable them to work + # with groups of projects + def project_only_application? + Clusters::Cluster::PROJECT_ONLY_APPLICATIONS.include?(application_name) end def application_name diff --git a/app/services/clusters/applications/create_service.rb b/app/services/clusters/applications/create_service.rb index bd7c31bb981..ae36da7b3dd 100644 --- a/app/services/clusters/applications/create_service.rb +++ b/app/services/clusters/applications/create_service.rb @@ -9,25 +9,9 @@ module Clusters application.updateable? ? ClusterUpgradeAppWorker : ClusterInstallAppWorker end - def builders - { - "helm" => -> (cluster) { cluster.application_helm || cluster.build_application_helm }, - "ingress" => -> (cluster) { cluster.application_ingress || cluster.build_application_ingress }, - "cert_manager" => -> (cluster) { cluster.application_cert_manager || cluster.build_application_cert_manager } - }.tap do |hash| - hash.merge!(project_builders) if cluster.project_type? - end - end - - # These applications will need extra configuration to enable them to work - # with groups of projects - def project_builders - { - "prometheus" => -> (cluster) { cluster.application_prometheus || cluster.build_application_prometheus }, - "runner" => -> (cluster) { cluster.application_runner || cluster.build_application_runner }, - "jupyter" => -> (cluster) { cluster.application_jupyter || cluster.build_application_jupyter }, - "knative" => -> (cluster) { cluster.application_knative || cluster.build_application_knative } - } + def builder + cluster.method("application_#{application_name}").call || + cluster.method("build_application_#{application_name}").call end end end diff --git a/app/services/clusters/applications/update_service.rb b/app/services/clusters/applications/update_service.rb index a9d4e609992..5071c31839c 100644 --- a/app/services/clusters/applications/update_service.rb +++ b/app/services/clusters/applications/update_service.rb @@ -9,25 +9,8 @@ module Clusters ClusterPatchAppWorker end - def builders - { - "helm" => -> (cluster) { cluster.application_helm }, - "ingress" => -> (cluster) { cluster.application_ingress }, - "cert_manager" => -> (cluster) { cluster.application_cert_manager } - }.tap do |hash| - hash.merge!(project_builders) if cluster.project_type? - end - end - - # These applications will need extra configuration to enable them to work - # with groups of projects - def project_builders - { - "prometheus" => -> (cluster) { cluster.application_prometheus }, - "runner" => -> (cluster) { cluster.application_runner }, - "jupyter" => -> (cluster) { cluster.application_jupyter }, - "knative" => -> (cluster) { cluster.application_knative } - } + def builder + cluster.method("application_#{application_name}").call end end end diff --git a/app/services/concerns/suggestible.rb b/app/services/concerns/suggestible.rb new file mode 100644 index 00000000000..0b9822b1909 --- /dev/null +++ b/app/services/concerns/suggestible.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module Suggestible + extend ActiveSupport::Concern + + # This translates into limiting suggestion changes to `suggestion:-100+100`. + MAX_LINES_CONTEXT = 100.freeze + + def fetch_from_content + diff_file.new_blob_lines_between(from_line, to_line).join + end + + def from_line + real_above = [lines_above, MAX_LINES_CONTEXT].min + [target_line - real_above, 1].max + end + + def to_line + real_below = [lines_below, MAX_LINES_CONTEXT].min + target_line + real_below + end + + def diff_file + raise NotImplementedError + end + + def target_line + raise NotImplementedError + end +end diff --git a/app/services/concerns/users/participable_service.rb b/app/services/concerns/users/participable_service.rb index 6713b6617ae..a3cc6014fd3 100644 --- a/app/services/concerns/users/participable_service.rb +++ b/app/services/concerns/users/participable_service.rb @@ -28,19 +28,35 @@ module Users end def groups - current_user.authorized_groups.sort_by(&:path).map do |group| - group_as_hash(group) + group_counts = GroupMember + .in_groups(current_user.authorized_groups) + .non_request + .count_users_by_group_id + + current_user.authorized_groups.with_route.sort_by(&:path).map do |group| + group_as_hash(group, group_counts) end end private def user_as_hash(user) - { type: user.class.name, username: user.username, name: user.name, avatar_url: user.avatar_url } + { + type: user.class.name, + username: user.username, + name: user.name, + avatar_url: user.avatar_url + } end - def group_as_hash(group) - { type: group.class.name, username: group.full_path, name: group.full_name, avatar_url: group.avatar_url, count: group.users.count } + def group_as_hash(group, group_counts) + { + type: group.class.name, + username: group.full_path, + name: group.full_name, + avatar_url: group.avatar_url, + count: group_counts.fetch(group.id, 0) + } end end end diff --git a/app/services/error_tracking/list_issues_service.rb b/app/services/error_tracking/list_issues_service.rb index a6c6bec9598..86ab21fa865 100644 --- a/app/services/error_tracking/list_issues_service.rb +++ b/app/services/error_tracking/list_issues_service.rb @@ -18,7 +18,7 @@ module ErrorTracking end if result[:error].present? - return error(result[:error], :bad_request) + return error(result[:error], http_status_from_error_type(result[:error_type])) end success(issues: result[:issues]) @@ -30,6 +30,15 @@ module ErrorTracking private + def http_status_from_error_type(error_type) + case error_type + when ErrorTracking::ProjectErrorTrackingSetting::SENTRY_API_ERROR_TYPE_MISSING_KEYS + :internal_server_error + else + :bad_request + end + end + def project_error_tracking_setting project.error_tracking_setting end diff --git a/app/services/git/branch_push_service.rb b/app/services/git/branch_push_service.rb new file mode 100644 index 00000000000..b55aeb5f2b9 --- /dev/null +++ b/app/services/git/branch_push_service.rb @@ -0,0 +1,244 @@ +# frozen_string_literal: true + +module Git + class BranchPushService < BaseService + attr_accessor :push_data, :push_commits + include Gitlab::Access + include Gitlab::Utils::StrongMemoize + + # The N most recent commits to process in a single push payload. + PROCESS_COMMIT_LIMIT = 100 + + # This method will be called after each git update + # and only if the provided user and project are present in GitLab. + # + # All callbacks for post receive action should be placed here. + # + # Next, this method: + # 1. Creates the push event + # 2. Updates merge requests + # 3. Recognizes cross-references from commit messages + # 4. Executes the project's webhooks + # 5. Executes the project's services + # 6. Checks if the project's main language has changed + # + def execute + update_commits + execute_related_hooks + perform_housekeeping + + update_remote_mirrors + update_caches + + update_signatures + end + + def update_commits + project.repository.after_create if project.empty_repo? + project.repository.after_push_commit(branch_name) + + if push_remove_branch? + project.repository.after_remove_branch + @push_commits = [] + elsif push_to_new_branch? + project.repository.after_create_branch + + # Re-find the pushed commits. + if default_branch? + # Initial push to the default branch. Take the full history of that branch as "newly pushed". + process_default_branch + else + # Use the pushed commits that aren't reachable by the default branch + # as a heuristic. This may include more commits than are actually pushed, but + # that shouldn't matter because we check for existing cross-references later. + @push_commits = project.repository.commits_between(project.default_branch, params[:newrev]) + + # don't process commits for the initial push to the default branch + process_commit_messages + end + elsif push_to_existing_branch? + # Collect data for this git push + @push_commits = project.repository.commits_between(params[:oldrev], params[:newrev]) + + process_commit_messages + + # Update the bare repositories info/attributes file using the contents of the default branches + # .gitattributes file + update_gitattributes if default_branch? + end + end + + def update_gitattributes + project.repository.copy_gitattributes(params[:ref]) + end + + def update_caches + if default_branch? + if push_to_new_branch? + # If this is the initial push into the default branch, the file type caches + # will already be reset as a result of `Project#change_head`. + types = [] + else + paths = Set.new + + last_pushed_commits.each do |commit| + commit.raw_deltas.each do |diff| + paths << diff.new_path + end + end + + types = Gitlab::FileDetector.types_in_paths(paths.to_a) + end + + DetectRepositoryLanguagesWorker.perform_async(@project.id, current_user.id) + else + types = [] + end + + ProjectCacheWorker.perform_async(project.id, types, [:commit_count, :repository_size]) + end + + # rubocop: disable CodeReuse/ActiveRecord + def update_signatures + commit_shas = last_pushed_commits.map(&:sha) + + return if commit_shas.empty? + + shas_with_cached_signatures = GpgSignature.where(commit_sha: commit_shas).pluck(:commit_sha) + commit_shas -= shas_with_cached_signatures + + return if commit_shas.empty? + + commit_shas = Gitlab::Git::Commit.shas_with_signatures(project.repository, commit_shas) + + CreateGpgSignatureWorker.perform_async(commit_shas, project.id) + end + # rubocop: enable CodeReuse/ActiveRecord + + # Schedules processing of commit messages. + def process_commit_messages + default = default_branch? + + last_pushed_commits.each do |commit| + if commit.matches_cross_reference_regex? + ProcessCommitWorker + .perform_async(project.id, current_user.id, commit.to_hash, default) + end + end + end + + def update_remote_mirrors + return unless project.has_remote_mirror? + + project.mark_stuck_remote_mirrors_as_failed! + project.update_remote_mirrors + end + + def execute_related_hooks + # Update merge requests that may be affected by this push. A new branch + # could cause the last commit of a merge request to change. + # + UpdateMergeRequestsWorker + .perform_async(project.id, current_user.id, params[:oldrev], params[:newrev], params[:ref]) + + EventCreateService.new.push(project, current_user, build_push_data) + Ci::CreatePipelineService.new(project, current_user, build_push_data).execute(:push, pipeline_options) + + project.execute_hooks(build_push_data.dup, :push_hooks) + project.execute_services(build_push_data.dup, :push_hooks) + + if push_remove_branch? + AfterBranchDeleteService + .new(project, current_user) + .execute(branch_name) + end + end + + def perform_housekeeping + housekeeping = Projects::HousekeepingService.new(project) + housekeeping.increment! + housekeeping.execute if housekeeping.needed? + rescue Projects::HousekeepingService::LeaseTaken + end + + def process_default_branch + offset = [push_commits_count_for_ref - PROCESS_COMMIT_LIMIT, 0].max + @push_commits = project.repository.commits(params[:newrev], offset: offset, limit: PROCESS_COMMIT_LIMIT) + + project.after_create_default_branch + end + + def build_push_data + @push_data ||= Gitlab::DataBuilder::Push.build( + project, + current_user, + params[:oldrev], + params[:newrev], + params[:ref], + @push_commits, + commits_count: commits_count, + push_options: params[:push_options] || [] + ) + end + + def push_to_existing_branch? + # Return if this is not a push to a branch (e.g. new commits) + branch_ref? && !Gitlab::Git.blank_ref?(params[:oldrev]) + end + + def push_to_new_branch? + strong_memoize(:push_to_new_branch) do + branch_ref? && Gitlab::Git.blank_ref?(params[:oldrev]) + end + end + + def push_remove_branch? + strong_memoize(:push_remove_branch) do + branch_ref? && Gitlab::Git.blank_ref?(params[:newrev]) + end + end + + def default_branch? + branch_ref? && + (branch_name == project.default_branch || project.default_branch.nil?) + end + + def commit_user(commit) + commit.author || current_user + end + + def branch_name + strong_memoize(:branch_name) do + Gitlab::Git.ref_name(params[:ref]) + end + end + + def branch_ref? + strong_memoize(:branch_ref) do + Gitlab::Git.branch_ref?(params[:ref]) + end + end + + def commits_count + return push_commits_count_for_ref if default_branch? && push_to_new_branch? + + Array(@push_commits).size + end + + def push_commits_count_for_ref + strong_memoize(:push_commits_count_for_ref) do + project.repository.commit_count_for_ref(params[:ref]) + end + end + + def last_pushed_commits + @last_pushed_commits ||= @push_commits.last(PROCESS_COMMIT_LIMIT) + end + + private + + def pipeline_options + {} # to be overridden in EE + end + end +end diff --git a/app/services/git/tag_push_service.rb b/app/services/git/tag_push_service.rb new file mode 100644 index 00000000000..318dfd4f886 --- /dev/null +++ b/app/services/git/tag_push_service.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +module Git + class TagPushService < BaseService + attr_accessor :push_data + + def execute + project.repository.after_create if project.empty_repo? + project.repository.before_push_tag + + @push_data = build_push_data + + EventCreateService.new.push(project, current_user, push_data) + Ci::CreatePipelineService.new(project, current_user, push_data).execute(:push, pipeline_options) + + SystemHooksService.new.execute_hooks(build_system_push_data, :tag_push_hooks) + project.execute_hooks(push_data.dup, :tag_push_hooks) + project.execute_services(push_data.dup, :tag_push_hooks) + + ProjectCacheWorker.perform_async(project.id, [], [:commit_count, :repository_size]) + + true + end + + private + + def build_push_data + commits = [] + message = nil + + unless Gitlab::Git.blank_ref?(params[:newrev]) + tag_name = Gitlab::Git.ref_name(params[:ref]) + tag = project.repository.find_tag(tag_name) + + if tag && tag.target == params[:newrev] + commit = project.commit(tag.dereferenced_target) + commits = [commit].compact + message = tag.message + end + end + + Gitlab::DataBuilder::Push.build( + project, + current_user, + params[:oldrev], + params[:newrev], + params[:ref], + commits, + message, + push_options: params[:push_options] || []) + end + + def build_system_push_data + Gitlab::DataBuilder::Push.build( + project, + current_user, + params[:oldrev], + params[:newrev], + params[:ref], + [], + '') + end + + def pipeline_options + {} # to be overridden in EE + end + end +end diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb deleted file mode 100644 index f387c749a21..00000000000 --- a/app/services/git_push_service.rb +++ /dev/null @@ -1,240 +0,0 @@ -# frozen_string_literal: true - -class GitPushService < BaseService - attr_accessor :push_data, :push_commits - include Gitlab::Access - include Gitlab::Utils::StrongMemoize - - # The N most recent commits to process in a single push payload. - PROCESS_COMMIT_LIMIT = 100 - - # This method will be called after each git update - # and only if the provided user and project are present in GitLab. - # - # All callbacks for post receive action should be placed here. - # - # Next, this method: - # 1. Creates the push event - # 2. Updates merge requests - # 3. Recognizes cross-references from commit messages - # 4. Executes the project's webhooks - # 5. Executes the project's services - # 6. Checks if the project's main language has changed - # - def execute - project.repository.after_create if project.empty_repo? - project.repository.after_push_commit(branch_name) - - if push_remove_branch? - project.repository.after_remove_branch - @push_commits = [] - elsif push_to_new_branch? - project.repository.after_create_branch - - # Re-find the pushed commits. - if default_branch? - # Initial push to the default branch. Take the full history of that branch as "newly pushed". - process_default_branch - else - # Use the pushed commits that aren't reachable by the default branch - # as a heuristic. This may include more commits than are actually pushed, but - # that shouldn't matter because we check for existing cross-references later. - @push_commits = project.repository.commits_between(project.default_branch, params[:newrev]) - - # don't process commits for the initial push to the default branch - process_commit_messages - end - elsif push_to_existing_branch? - # Collect data for this git push - @push_commits = project.repository.commits_between(params[:oldrev], params[:newrev]) - - process_commit_messages - - # Update the bare repositories info/attributes file using the contents of the default branches - # .gitattributes file - update_gitattributes if default_branch? - end - - execute_related_hooks - perform_housekeeping - - update_remote_mirrors - update_caches - - update_signatures - end - - def update_gitattributes - project.repository.copy_gitattributes(params[:ref]) - end - - def update_caches - if default_branch? - if push_to_new_branch? - # If this is the initial push into the default branch, the file type caches - # will already be reset as a result of `Project#change_head`. - types = [] - else - paths = Set.new - - last_pushed_commits.each do |commit| - commit.raw_deltas.each do |diff| - paths << diff.new_path - end - end - - types = Gitlab::FileDetector.types_in_paths(paths.to_a) - end - - DetectRepositoryLanguagesWorker.perform_async(@project.id, current_user.id) - else - types = [] - end - - ProjectCacheWorker.perform_async(project.id, types, [:commit_count, :repository_size]) - end - - # rubocop: disable CodeReuse/ActiveRecord - def update_signatures - commit_shas = last_pushed_commits.map(&:sha) - - return if commit_shas.empty? - - shas_with_cached_signatures = GpgSignature.where(commit_sha: commit_shas).pluck(:commit_sha) - commit_shas -= shas_with_cached_signatures - - return if commit_shas.empty? - - commit_shas = Gitlab::Git::Commit.shas_with_signatures(project.repository, commit_shas) - - CreateGpgSignatureWorker.perform_async(commit_shas, project.id) - end - # rubocop: enable CodeReuse/ActiveRecord - - # Schedules processing of commit messages. - def process_commit_messages - default = default_branch? - - last_pushed_commits.each do |commit| - if commit.matches_cross_reference_regex? - ProcessCommitWorker - .perform_async(project.id, current_user.id, commit.to_hash, default) - end - end - end - - protected - - def update_remote_mirrors - return unless project.has_remote_mirror? - - project.mark_stuck_remote_mirrors_as_failed! - project.update_remote_mirrors - end - - def execute_related_hooks - # Update merge requests that may be affected by this push. A new branch - # could cause the last commit of a merge request to change. - # - UpdateMergeRequestsWorker - .perform_async(project.id, current_user.id, params[:oldrev], params[:newrev], params[:ref]) - - EventCreateService.new.push(project, current_user, build_push_data) - Ci::CreatePipelineService.new(project, current_user, build_push_data).execute(:push, pipeline_options) - - project.execute_hooks(build_push_data.dup, :push_hooks) - project.execute_services(build_push_data.dup, :push_hooks) - - if push_remove_branch? - AfterBranchDeleteService - .new(project, current_user) - .execute(branch_name) - end - end - - def perform_housekeeping - housekeeping = Projects::HousekeepingService.new(project) - housekeeping.increment! - housekeeping.execute if housekeeping.needed? - rescue Projects::HousekeepingService::LeaseTaken - end - - def process_default_branch - offset = [push_commits_count_for_ref - PROCESS_COMMIT_LIMIT, 0].max - @push_commits = project.repository.commits(params[:newrev], offset: offset, limit: PROCESS_COMMIT_LIMIT) - - project.after_create_default_branch - end - - def build_push_data - @push_data ||= Gitlab::DataBuilder::Push.build( - project, - current_user, - params[:oldrev], - params[:newrev], - params[:ref], - @push_commits, - commits_count: commits_count, - push_options: params[:push_options] || []) - end - - def push_to_existing_branch? - # Return if this is not a push to a branch (e.g. new commits) - branch_ref? && !Gitlab::Git.blank_ref?(params[:oldrev]) - end - - def push_to_new_branch? - strong_memoize(:push_to_new_branch) do - branch_ref? && Gitlab::Git.blank_ref?(params[:oldrev]) - end - end - - def push_remove_branch? - strong_memoize(:push_remove_branch) do - branch_ref? && Gitlab::Git.blank_ref?(params[:newrev]) - end - end - - def default_branch? - branch_ref? && - (branch_name == project.default_branch || project.default_branch.nil?) - end - - def commit_user(commit) - commit.author || current_user - end - - def branch_name - strong_memoize(:branch_name) do - Gitlab::Git.ref_name(params[:ref]) - end - end - - def branch_ref? - strong_memoize(:branch_ref) do - Gitlab::Git.branch_ref?(params[:ref]) - end - end - - def commits_count - return push_commits_count_for_ref if default_branch? && push_to_new_branch? - - Array(@push_commits).size - end - - def push_commits_count_for_ref - strong_memoize(:push_commits_count_for_ref) do - project.repository.commit_count_for_ref(params[:ref]) - end - end - - def last_pushed_commits - @last_pushed_commits ||= @push_commits.last(PROCESS_COMMIT_LIMIT) - end - - private - - def pipeline_options - {} # to be overridden in EE - end -end diff --git a/app/services/git_tag_push_service.rb b/app/services/git_tag_push_service.rb deleted file mode 100644 index e39b3603c6c..00000000000 --- a/app/services/git_tag_push_service.rb +++ /dev/null @@ -1,66 +0,0 @@ -# frozen_string_literal: true - -class GitTagPushService < BaseService - attr_accessor :push_data - - def execute - project.repository.after_create if project.empty_repo? - project.repository.before_push_tag - - @push_data = build_push_data - - EventCreateService.new.push(project, current_user, push_data) - Ci::CreatePipelineService.new(project, current_user, push_data).execute(:push, pipeline_options) - - SystemHooksService.new.execute_hooks(build_system_push_data, :tag_push_hooks) - project.execute_hooks(push_data.dup, :tag_push_hooks) - project.execute_services(push_data.dup, :tag_push_hooks) - - ProjectCacheWorker.perform_async(project.id, [], [:commit_count, :repository_size]) - - true - end - - private - - def build_push_data - commits = [] - message = nil - - unless Gitlab::Git.blank_ref?(params[:newrev]) - tag_name = Gitlab::Git.ref_name(params[:ref]) - tag = project.repository.find_tag(tag_name) - - if tag && tag.target == params[:newrev] - commit = project.commit(tag.dereferenced_target) - commits = [commit].compact - message = tag.message - end - end - - Gitlab::DataBuilder::Push.build( - project, - current_user, - params[:oldrev], - params[:newrev], - params[:ref], - commits, - message, - push_options: params[:push_options] || []) - end - - def build_system_push_data - Gitlab::DataBuilder::Push.build( - project, - current_user, - params[:oldrev], - params[:newrev], - params[:ref], - [], - '') - end - - def pipeline_options - {} # to be overridden in EE - end -end diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index f35ad2a9d8b..95bd83bb03c 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -76,13 +76,11 @@ class IssuableBaseService < BaseService find_or_create_label_ids end - # rubocop: disable CodeReuse/ActiveRecord def filter_labels_in_param(key) return if params[key].to_a.empty? - params[key] = available_labels.where(id: params[key]).pluck(:id) + params[key] = available_labels.id_in(params[key]).pluck_primary_key end - # rubocop: enable CodeReuse/ActiveRecord def find_or_create_label_ids labels = params.delete(:labels) @@ -115,7 +113,7 @@ class IssuableBaseService < BaseService new_label_ids -= remove_label_ids if remove_label_ids end - new_label_ids + new_label_ids.uniq end def available_labels diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb index 11ede5223e5..3e208241da5 100644 --- a/app/services/merge_requests/base_service.rb +++ b/app/services/merge_requests/base_service.rb @@ -54,7 +54,7 @@ module MergeRequests merge_request, merge_request.project, current_user, merge_request.assignee) end - def create_merge_request_pipeline(merge_request, user) + def create_pipeline_for(merge_request, user) return unless Feature.enabled?(:ci_merge_request_pipeline, merge_request.source_project, default_enabled: true) @@ -65,12 +65,24 @@ module MergeRequests return if merge_request.merge_request_pipeline_exists? return if merge_request.has_no_commits? - Ci::CreatePipelineService - .new(merge_request.source_project, user, ref: merge_request.source_branch) - .execute(:merge_request_event, - ignore_skip_ci: true, - save_on_errors: false, - merge_request: merge_request) + create_detached_merge_request_pipeline(merge_request, user) + end + + def create_detached_merge_request_pipeline(merge_request, user) + if can_use_merge_request_ref?(merge_request) + Ci::CreatePipelineService.new(merge_request.source_project, user, + ref: merge_request.ref_path) + .execute(:merge_request_event, merge_request: merge_request) + else + Ci::CreatePipelineService.new(merge_request.source_project, user, + ref: merge_request.source_branch) + .execute(:merge_request_event, merge_request: merge_request) + end + end + + def can_use_merge_request_ref?(merge_request) + Feature.enabled?(:ci_use_merge_request_ref, project, default_enabled: true) && + !merge_request.for_fork? end # Returns all origin and fork merge requests from `@project` satisfying passed arguments. diff --git a/app/services/merge_requests/create_service.rb b/app/services/merge_requests/create_service.rb index 02c2388c05c..06e46595b95 100644 --- a/app/services/merge_requests/create_service.rb +++ b/app/services/merge_requests/create_service.rb @@ -25,7 +25,7 @@ module MergeRequests def after_create(issuable) todo_service.new_merge_request(issuable, current_user) issuable.cache_merge_request_closes_issues!(current_user) - create_merge_request_pipeline(issuable, current_user) + create_pipeline_for(issuable, current_user) issuable.update_head_pipeline super diff --git a/app/services/merge_requests/delete_non_latest_diffs_service.rb b/app/services/merge_requests/delete_non_latest_diffs_service.rb index d5929446122..bdb7ec8a7c2 100644 --- a/app/services/merge_requests/delete_non_latest_diffs_service.rb +++ b/app/services/merge_requests/delete_non_latest_diffs_service.rb @@ -8,15 +8,13 @@ module MergeRequests @merge_request = merge_request end - # rubocop: disable CodeReuse/ActiveRecord def execute diffs = @merge_request.non_latest_diffs.with_files diffs.each_batch(of: BATCH_SIZE) do |relation, index| - ids = relation.pluck(:id).map { |id| [id] } + ids = relation.pluck_primary_key.map { |id| [id] } DeleteDiffFilesWorker.bulk_perform_in(index * 5.minutes, ids) end end - # rubocop: enable CodeReuse/ActiveRecord end end diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb index 8241e408ce5..d8a78001b79 100644 --- a/app/services/merge_requests/merge_service.rb +++ b/app/services/merge_requests/merge_service.rb @@ -76,8 +76,8 @@ module MergeRequests def try_merge repository.merge(current_user, source, merge_request, commit_message) rescue Gitlab::Git::PreReceiveError => e - handle_merge_error(log_message: e.message) - raise_error('Something went wrong during merge pre-receive hook') + raise MergeError, + "Something went wrong during merge pre-receive hook. #{e.message}".strip rescue => e handle_merge_error(log_message: e.message) raise_error('Something went wrong during merge') diff --git a/app/services/merge_requests/migrate_external_diffs_service.rb b/app/services/merge_requests/migrate_external_diffs_service.rb new file mode 100644 index 00000000000..16050244637 --- /dev/null +++ b/app/services/merge_requests/migrate_external_diffs_service.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module MergeRequests + class MigrateExternalDiffsService < ::BaseService + MAX_JOBS = 1000.freeze + + attr_reader :diff + + def self.enqueue! + ids = MergeRequestDiff.ids_for_external_storage_migration(limit: MAX_JOBS) + + MigrateExternalDiffsWorker.bulk_perform_async(ids.map { |id| [id] }) + end + + def initialize(merge_request_diff) + @diff = merge_request_diff + end + + def execute + diff.migrate_files_to_external_storage! + end + end +end diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb index f712b8863cd..51d27673787 100644 --- a/app/services/merge_requests/refresh_service.rb +++ b/app/services/merge_requests/refresh_service.rb @@ -20,6 +20,7 @@ module MergeRequests close_upon_missing_source_branch_ref post_merge_manually_merged reload_merge_requests + outdate_suggestions reset_merge_when_pipeline_succeeds mark_pending_todos_done cache_merge_requests_closing_issues @@ -106,7 +107,7 @@ module MergeRequests end merge_request.mark_as_unchecked - create_merge_request_pipeline(merge_request, current_user) + create_pipeline_for(merge_request, current_user) UpdateHeadPipelineForMergeRequestWorker.perform_async(merge_request.id) end @@ -125,6 +126,14 @@ module MergeRequests merge_request.source_branch == @push.branch_name end + def outdate_suggestions + outdate_service = Suggestions::OutdateService.new + + merge_requests_for_source_branch.each do |merge_request| + outdate_service.execute(merge_request) + end + end + def reset_merge_when_pipeline_succeeds merge_requests_for_source_branch.each(&:reset_merge_when_pipeline_succeeds) end diff --git a/app/services/milestones/promote_service.rb b/app/services/milestones/promote_service.rb index cbe5996e8ca..f6c04a7bae2 100644 --- a/app/services/milestones/promote_service.rb +++ b/app/services/milestones/promote_service.rb @@ -26,17 +26,15 @@ module Milestones private - # rubocop: disable CodeReuse/ActiveRecord def milestone_ids_for_merge(group_milestone) # Pluck need to be used here instead of select so the array of ids # is persistent after old milestones gets deleted. @milestone_ids_for_merge ||= begin search_params = { title: group_milestone.title, project_ids: group_project_ids, state: 'all' } milestones = MilestonesFinder.new(search_params).execute - milestones.pluck(:id) + milestones.pluck_primary_key end end - # rubocop: enable CodeReuse/ActiveRecord def move_children_to_group_milestone(group_milestone) milestone_ids_for_merge(group_milestone).in_groups_of(100, false) do |milestone_ids| diff --git a/app/services/quick_actions/interpret_service.rb b/app/services/quick_actions/interpret_service.rb index 131efb8925e..f463e08ee7e 100644 --- a/app/services/quick_actions/interpret_service.rb +++ b/app/services/quick_actions/interpret_service.rb @@ -4,21 +4,24 @@ module QuickActions class InterpretService < BaseService include Gitlab::Utils::StrongMemoize include Gitlab::QuickActions::Dsl + include Gitlab::QuickActions::IssueActions + include Gitlab::QuickActions::IssueAndMergeRequestActions + include Gitlab::QuickActions::IssuableActions + include Gitlab::QuickActions::MergeRequestActions + include Gitlab::QuickActions::CommitActions + include Gitlab::QuickActions::CommonActions - attr_reader :issuable + attr_reader :quick_action_target # Counts how many commands have been executed. # Used to display relevant feedback on UI when a note # with only commands has been processed. attr_accessor :commands_executed_count - SHRUG = '¯\\_(ツ)_/¯'.freeze - TABLEFLIP = '(╯°□°)╯︵ ┻━┻'.freeze - - # Takes an issuable and returns an array of all the available commands + # Takes an quick_action_target and returns an array of all the available commands # represented with .to_h - def available_commands(issuable) - @issuable = issuable + def available_commands(quick_action_target) + @quick_action_target = quick_action_target self.class.command_definitions.map do |definition| next unless definition.available?(self) @@ -29,10 +32,10 @@ module QuickActions # Takes a text and interprets the commands that are extracted from it. # Returns the content without commands, and hash of changes to be applied to a record. - def execute(content, issuable, only: nil) + def execute(content, quick_action_target, only: nil) return [content, {}] unless current_user.can?(:use_quick_actions) - @issuable = issuable + @quick_action_target = quick_action_target @updates = {} content, commands = extractor.extract_commands(content, only: only) @@ -43,10 +46,10 @@ module QuickActions # Takes a text and interprets the commands that are extracted from it. # Returns the content without commands, and array of changes explained. - def explain(content, issuable) + def explain(content, quick_action_target) return [content, []] unless current_user.can?(:use_quick_actions) - @issuable = issuable + @quick_action_target = quick_action_target content, commands = extractor.extract_commands(content) commands = explain_commands(commands) @@ -59,598 +62,6 @@ module QuickActions Gitlab::QuickActions::Extractor.new(self.class.command_definitions) end - desc do - "Close this #{issuable.to_ability_name.humanize(capitalize: false)}" - end - explanation do - "Closes this #{issuable.to_ability_name.humanize(capitalize: false)}." - end - condition do - issuable.is_a?(Issuable) && - issuable.persisted? && - issuable.open? && - current_user.can?(:"update_#{issuable.to_ability_name}", issuable) - end - command :close do - @updates[:state_event] = 'close' - end - - desc do - "Reopen this #{issuable.to_ability_name.humanize(capitalize: false)}" - end - explanation do - "Reopens this #{issuable.to_ability_name.humanize(capitalize: false)}." - end - condition do - issuable.is_a?(Issuable) && - issuable.persisted? && - issuable.closed? && - current_user.can?(:"update_#{issuable.to_ability_name}", issuable) - end - command :reopen do - @updates[:state_event] = 'reopen' - end - - desc 'Merge (when the pipeline succeeds)' - explanation 'Merges this merge request when the pipeline succeeds.' - condition do - last_diff_sha = params && params[:merge_request_diff_head_sha] - issuable.is_a?(MergeRequest) && - issuable.persisted? && - issuable.mergeable_with_quick_action?(current_user, autocomplete_precheck: !last_diff_sha, last_diff_sha: last_diff_sha) - end - command :merge do - @updates[:merge] = params[:merge_request_diff_head_sha] - end - - desc 'Change title' - explanation do |title_param| - "Changes the title to \"#{title_param}\"." - end - params '<New title>' - condition do - issuable.persisted? && - current_user.can?(:"update_#{issuable.to_ability_name}", issuable) - end - command :title do |title_param| - @updates[:title] = title_param - end - - desc 'Assign' - # rubocop: disable CodeReuse/ActiveRecord - explanation do |users| - users = issuable.allows_multiple_assignees? ? users : users.take(1) - "Assigns #{users.map(&:to_reference).to_sentence}." - end - # rubocop: enable CodeReuse/ActiveRecord - params do - issuable.allows_multiple_assignees? ? '@user1 @user2' : '@user' - end - condition do - current_user.can?(:"admin_#{issuable.to_ability_name}", project) - end - parse_params do |assignee_param| - extract_users(assignee_param) - end - command :assign do |users| - next if users.empty? - - if issuable.allows_multiple_assignees? - @updates[:assignee_ids] ||= issuable.assignees.map(&:id) - @updates[:assignee_ids] += users.map(&:id) - else - @updates[:assignee_ids] = [users.first.id] - end - end - - desc do - if issuable.allows_multiple_assignees? - 'Remove all or specific assignee(s)' - else - 'Remove assignee' - end - end - explanation do |users = nil| - assignees = issuable.assignees - assignees &= users if users.present? && issuable.allows_multiple_assignees? - "Removes #{'assignee'.pluralize(assignees.size)} #{assignees.map(&:to_reference).to_sentence}." - end - params do - issuable.allows_multiple_assignees? ? '@user1 @user2' : '' - end - condition do - issuable.is_a?(Issuable) && - issuable.persisted? && - issuable.assignees.any? && - current_user.can?(:"admin_#{issuable.to_ability_name}", project) - end - parse_params do |unassign_param| - # When multiple users are assigned, all will be unassigned if multiple assignees are no longer allowed - extract_users(unassign_param) if issuable.allows_multiple_assignees? - end - command :unassign do |users = nil| - if issuable.allows_multiple_assignees? && users&.any? - @updates[:assignee_ids] ||= issuable.assignees.map(&:id) - @updates[:assignee_ids] -= users.map(&:id) - else - @updates[:assignee_ids] = [] - end - end - - desc 'Set milestone' - explanation do |milestone| - "Sets the milestone to #{milestone.to_reference}." if milestone - end - params '%"milestone"' - condition do - current_user.can?(:"admin_#{issuable.to_ability_name}", project) && - find_milestones(project, state: 'active').any? - end - parse_params do |milestone_param| - extract_references(milestone_param, :milestone).first || - find_milestones(project, title: milestone_param.strip).first - end - command :milestone do |milestone| - @updates[:milestone_id] = milestone.id if milestone - end - - desc 'Remove milestone' - explanation do - "Removes #{issuable.milestone.to_reference(format: :name)} milestone." - end - condition do - issuable.is_a?(Issuable) && - issuable.persisted? && - issuable.milestone_id? && - current_user.can?(:"admin_#{issuable.to_ability_name}", project) - end - command :remove_milestone do - @updates[:milestone_id] = nil - end - - desc 'Add label(s)' - explanation do |labels_param| - labels = find_label_references(labels_param) - - "Adds #{labels.join(' ')} #{'label'.pluralize(labels.count)}." if labels.any? - end - params '~label1 ~"label 2"' - condition do - parent && - current_user.can?(:"admin_#{issuable.to_ability_name}", parent) && - find_labels.any? - end - command :label do |labels_param| - label_ids = find_label_ids(labels_param) - - if label_ids.any? - @updates[:add_label_ids] ||= [] - @updates[:add_label_ids] += label_ids - - @updates[:add_label_ids].uniq! - end - end - - desc 'Remove all or specific label(s)' - explanation do |labels_param = nil| - if labels_param.present? - labels = find_label_references(labels_param) - "Removes #{labels.join(' ')} #{'label'.pluralize(labels.count)}." if labels.any? - else - 'Removes all labels.' - end - end - params '~label1 ~"label 2"' - condition do - issuable.is_a?(Issuable) && - issuable.persisted? && - issuable.labels.any? && - current_user.can?(:"admin_#{issuable.to_ability_name}", parent) - end - command :unlabel do |labels_param = nil| - if labels_param.present? - label_ids = find_label_ids(labels_param) - - if label_ids.any? - @updates[:remove_label_ids] ||= [] - @updates[:remove_label_ids] += label_ids - - @updates[:remove_label_ids].uniq! - end - else - @updates[:label_ids] = [] - end - end - - desc 'Replace all label(s)' - explanation do |labels_param| - labels = find_label_references(labels_param) - "Replaces all labels with #{labels.join(' ')} #{'label'.pluralize(labels.count)}." if labels.any? - end - params '~label1 ~"label 2"' - condition do - issuable.is_a?(Issuable) && - issuable.persisted? && - issuable.labels.any? && - current_user.can?(:"admin_#{issuable.to_ability_name}", project) - end - command :relabel do |labels_param| - label_ids = find_label_ids(labels_param) - - if label_ids.any? - @updates[:label_ids] ||= [] - @updates[:label_ids] += label_ids - - @updates[:label_ids].uniq! - end - end - - desc 'Copy labels and milestone from other issue or merge request' - explanation do |source_issuable| - "Copy labels and milestone from #{source_issuable.to_reference}." - end - params '#issue | !merge_request' - condition do - [MergeRequest, Issue].include?(issuable.class) && - current_user.can?(:"update_#{issuable.to_ability_name}", issuable) - end - parse_params do |issuable_param| - extract_references(issuable_param, :issue).first || - extract_references(issuable_param, :merge_request).first - end - command :copy_metadata do |source_issuable| - if source_issuable.present? && source_issuable.project.id == issuable.project.id - @updates[:add_label_ids] = source_issuable.labels.map(&:id) - @updates[:milestone_id] = source_issuable.milestone.id if source_issuable.milestone - end - end - - desc 'Add a todo' - explanation 'Adds a todo.' - condition do - issuable.is_a?(Issuable) && - issuable.persisted? && - !TodoService.new.todo_exist?(issuable, current_user) - end - command :todo do - @updates[:todo_event] = 'add' - end - - desc 'Mark todo as done' - explanation 'Marks todo as done.' - condition do - issuable.persisted? && - TodoService.new.todo_exist?(issuable, current_user) - end - command :done do - @updates[:todo_event] = 'done' - end - - desc 'Subscribe' - explanation do - "Subscribes to this #{issuable.to_ability_name.humanize(capitalize: false)}." - end - condition do - issuable.is_a?(Issuable) && - issuable.persisted? && - !issuable.subscribed?(current_user, project) - end - command :subscribe do - @updates[:subscription_event] = 'subscribe' - end - - desc 'Unsubscribe' - explanation do - "Unsubscribes from this #{issuable.to_ability_name.humanize(capitalize: false)}." - end - condition do - issuable.is_a?(Issuable) && - issuable.persisted? && - issuable.subscribed?(current_user, project) - end - command :unsubscribe do - @updates[:subscription_event] = 'unsubscribe' - end - - desc 'Set due date' - explanation do |due_date| - "Sets the due date to #{due_date.to_s(:medium)}." if due_date - end - params '<in 2 days | this Friday | December 31st>' - condition do - issuable.respond_to?(:due_date) && - current_user.can?(:"admin_#{issuable.to_ability_name}", project) - end - parse_params do |due_date_param| - Chronic.parse(due_date_param).try(:to_date) - end - command :due do |due_date| - @updates[:due_date] = due_date if due_date - end - - desc 'Remove due date' - explanation 'Removes the due date.' - condition do - issuable.persisted? && - issuable.respond_to?(:due_date) && - issuable.due_date? && - current_user.can?(:"admin_#{issuable.to_ability_name}", project) - end - command :remove_due_date do - @updates[:due_date] = nil - end - - desc 'Toggle the Work In Progress status' - explanation do - verb = issuable.work_in_progress? ? 'Unmarks' : 'Marks' - noun = issuable.to_ability_name.humanize(capitalize: false) - "#{verb} this #{noun} as Work In Progress." - end - condition do - issuable.respond_to?(:work_in_progress?) && - # Allow it to mark as WIP on MR creation page _or_ through MR notes. - (issuable.new_record? || current_user.can?(:"update_#{issuable.to_ability_name}", issuable)) - end - command :wip do - @updates[:wip_event] = issuable.work_in_progress? ? 'unwip' : 'wip' - end - - desc 'Toggle emoji award' - explanation do |name| - "Toggles :#{name}: emoji award." if name - end - params ':emoji:' - condition do - issuable.is_a?(Issuable) && - issuable.persisted? - end - parse_params do |emoji_param| - match = emoji_param.match(Banzai::Filter::EmojiFilter.emoji_pattern) - match[1] if match - end - command :award do |name| - if name && issuable.user_can_award?(current_user) - @updates[:emoji_award] = name - end - end - - desc 'Set time estimate' - explanation do |time_estimate| - time_estimate = Gitlab::TimeTrackingFormatter.output(time_estimate) - - "Sets time estimate to #{time_estimate}." if time_estimate - end - params '<1w 3d 2h 14m>' - condition do - current_user.can?(:"admin_#{issuable.to_ability_name}", project) - end - parse_params do |raw_duration| - Gitlab::TimeTrackingFormatter.parse(raw_duration) - end - command :estimate do |time_estimate| - if time_estimate - @updates[:time_estimate] = time_estimate - end - end - - desc 'Add or subtract spent time' - explanation do |time_spent, time_spent_date| - if time_spent - if time_spent > 0 - verb = 'Adds' - value = time_spent - else - verb = 'Subtracts' - value = -time_spent - end - - "#{verb} #{Gitlab::TimeTrackingFormatter.output(value)} spent time." - end - end - params '<time(1h30m | -1h30m)> <date(YYYY-MM-DD)>' - condition do - issuable.is_a?(TimeTrackable) && - current_user.can?(:"admin_#{issuable.to_ability_name}", issuable) - end - parse_params do |raw_time_date| - Gitlab::QuickActions::SpendTimeAndDateSeparator.new(raw_time_date).execute - end - command :spend do |time_spent, time_spent_date| - if time_spent - @updates[:spend_time] = { - duration: time_spent, - user_id: current_user.id, - spent_at: time_spent_date - } - end - end - - desc 'Remove time estimate' - explanation 'Removes time estimate.' - condition do - issuable.persisted? && - current_user.can?(:"admin_#{issuable.to_ability_name}", project) - end - command :remove_estimate do - @updates[:time_estimate] = 0 - end - - desc 'Remove spent time' - explanation 'Removes spent time.' - condition do - issuable.persisted? && - current_user.can?(:"admin_#{issuable.to_ability_name}", project) - end - command :remove_time_spent do - @updates[:spend_time] = { duration: :reset, user_id: current_user.id } - end - - desc "Append the comment with #{SHRUG}" - params '<Comment>' - substitution :shrug do |comment| - "#{comment} #{SHRUG}" - end - - desc "Append the comment with #{TABLEFLIP}" - params '<Comment>' - substitution :tableflip do |comment| - "#{comment} #{TABLEFLIP}" - end - - desc "Lock the discussion" - explanation "Locks the discussion" - condition do - [MergeRequest, Issue].include?(issuable.class) && - issuable.persisted? && - !issuable.discussion_locked? && - current_user.can?(:"admin_#{issuable.to_ability_name}", issuable) - end - command :lock do - @updates[:discussion_locked] = true - end - - desc "Unlock the discussion" - explanation "Unlocks the discussion" - condition do - [MergeRequest, Issue].include?(issuable.class) && - issuable.persisted? && - issuable.discussion_locked? && - current_user.can?(:"admin_#{issuable.to_ability_name}", issuable) - end - command :unlock do - @updates[:discussion_locked] = false - end - - # This is a dummy command, so that it appears in the autocomplete commands - desc 'CC' - params '@user' - command :cc - - desc 'Set target branch' - explanation do |branch_name| - "Sets target branch to #{branch_name}." - end - params '<Local branch name>' - condition do - issuable.respond_to?(:target_branch) && - (current_user.can?(:"update_#{issuable.to_ability_name}", issuable) || - issuable.new_record?) - end - parse_params do |target_branch_param| - target_branch_param.strip - end - command :target_branch do |branch_name| - @updates[:target_branch] = branch_name if project.repository.branch_exists?(branch_name) - end - - desc 'Move issue from one column of the board to another' - explanation do |target_list_name| - label = find_label_references(target_list_name).first - "Moves issue to #{label} column in the board." if label - end - params '~"Target column"' - condition do - issuable.is_a?(Issue) && - current_user.can?(:"update_#{issuable.to_ability_name}", issuable) && - issuable.project.boards.count == 1 - end - # rubocop: disable CodeReuse/ActiveRecord - command :board_move do |target_list_name| - label_ids = find_label_ids(target_list_name) - - if label_ids.size == 1 - label_id = label_ids.first - - # Ensure this label corresponds to a list on the board - next unless Label.on_project_boards(issuable.project_id).where(id: label_id).exists? - - @updates[:remove_label_ids] = - issuable.labels.on_project_boards(issuable.project_id).where.not(id: label_id).pluck(:id) - @updates[:add_label_ids] = [label_id] - end - end - # rubocop: enable CodeReuse/ActiveRecord - - desc 'Mark this issue as a duplicate of another issue' - explanation do |duplicate_reference| - "Marks this issue as a duplicate of #{duplicate_reference}." - end - params '#issue' - condition do - issuable.is_a?(Issue) && - issuable.persisted? && - current_user.can?(:"update_#{issuable.to_ability_name}", issuable) - end - command :duplicate do |duplicate_param| - canonical_issue = extract_references(duplicate_param, :issue).first - - if canonical_issue.present? - @updates[:canonical_issue_id] = canonical_issue.id - end - end - - desc 'Move this issue to another project.' - explanation do |path_to_project| - "Moves this issue to #{path_to_project}." - end - params 'path/to/project' - condition do - issuable.is_a?(Issue) && - issuable.persisted? && - current_user.can?(:"admin_#{issuable.to_ability_name}", project) - end - command :move do |target_project_path| - target_project = Project.find_by_full_path(target_project_path) - - if target_project.present? - @updates[:target_project] = target_project - end - end - - desc 'Make issue confidential.' - explanation do - 'Makes this issue confidential' - end - condition do - issuable.is_a?(Issue) && current_user.can?(:"admin_#{issuable.to_ability_name}", issuable) - end - command :confidential do - @updates[:confidential] = true - end - - desc 'Tag this commit.' - explanation do |tag_name, message| - with_message = %{ with "#{message}"} if message.present? - "Tags this commit to #{tag_name}#{with_message}." - end - params 'v1.2.3 <message>' - parse_params do |tag_name_and_message| - tag_name_and_message.split(' ', 2) - end - condition do - issuable.is_a?(Commit) && current_user.can?(:push_code, project) - end - command :tag do |tag_name, message| - @updates[:tag_name] = tag_name - @updates[:tag_message] = message - end - - desc 'Create a merge request.' - explanation do |branch_name = nil| - branch_text = branch_name ? "branch '#{branch_name}'" : 'a branch' - "Creates #{branch_text} and a merge request to resolve this issue" - end - params "<branch name>" - condition do - issuable.is_a?(Issue) && current_user.can?(:create_merge_request_in, project) && current_user.can?(:push_code, project) - end - command :create_merge_request do |branch_name = nil| - @updates[:create_merge_request] = { - branch_name: branch_name, - issue_iid: issuable.iid - } - end - # rubocop: disable CodeReuse/ActiveRecord def extract_users(params) return [] if params.nil? @@ -680,7 +91,7 @@ module QuickActions def group strong_memoize(:group) do - issuable.group if issuable.respond_to?(:group) + quick_action_target.group if quick_action_target.respond_to?(:group) end end diff --git a/app/services/releases/destroy_service.rb b/app/services/releases/destroy_service.rb index 8c2bc3b4e6e..f9f6101abdd 100644 --- a/app/services/releases/destroy_service.rb +++ b/app/services/releases/destroy_service.rb @@ -5,7 +5,6 @@ module Releases include Releases::Concerns def execute - return error('Tag does not exist', 404) unless existing_tag return error('Release does not exist', 404) unless release return error('Access Denied', 403) unless allowed? diff --git a/app/services/search/global_service.rb b/app/services/search/global_service.rb index d6af26d949d..f711839e389 100644 --- a/app/services/search/global_service.rb +++ b/app/services/search/global_service.rb @@ -23,7 +23,8 @@ module Search def allowed_scopes strong_memoize(:allowed_scopes) do - %w[issues merge_requests milestones] + allowed_scopes = %w[issues merge_requests milestones] + allowed_scopes << 'users' if Feature.enabled?(:users_search, default_enabled: true) end end diff --git a/app/services/search/group_service.rb b/app/services/search/group_service.rb index 34803d005e3..6f3b5f00b86 100644 --- a/app/services/search/group_service.rb +++ b/app/services/search/group_service.rb @@ -11,6 +11,12 @@ module Search @group = group end + def execute + Gitlab::GroupSearchResults.new( + current_user, projects, group, params[:search], default_project_filter: default_project_filter + ) + end + def projects return Project.none unless group return @projects if defined? @projects diff --git a/app/services/search/project_service.rb b/app/services/search/project_service.rb index f223c8be103..32d5cd7ddb2 100644 --- a/app/services/search/project_service.rb +++ b/app/services/search/project_service.rb @@ -16,7 +16,12 @@ module Search end def scope - @scope ||= %w[notes issues merge_requests milestones wiki_blobs commits].delete(params[:scope]) { 'blobs' } + @scope ||= begin + allowed_scopes = %w[notes issues merge_requests milestones wiki_blobs commits] + allowed_scopes << 'users' if Feature.enabled?(:users_search, default_enabled: true) + + allowed_scopes.delete(params[:scope]) { 'blobs' } + end end end end diff --git a/app/services/suggestions/apply_service.rb b/app/services/suggestions/apply_service.rb index f778c5aa5f5..8ba50e22b09 100644 --- a/app/services/suggestions/apply_service.rb +++ b/app/services/suggestions/apply_service.rb @@ -7,7 +7,7 @@ module Suggestions end def execute(suggestion) - unless suggestion.appliable? + unless suggestion.appliable?(cached: false) return error('Suggestion is not appliable') end @@ -15,7 +15,7 @@ module Suggestions return error('The file has been changed') end - diff_file = suggestion.note.latest_diff_file + diff_file = suggestion.diff_file unless diff_file return error('The file was not found') diff --git a/app/services/suggestions/create_service.rb b/app/services/suggestions/create_service.rb index c7ac2452c53..1d3338c1b45 100644 --- a/app/services/suggestions/create_service.rb +++ b/app/services/suggestions/create_service.rb @@ -9,52 +9,24 @@ module Suggestions def execute return unless @note.supports_suggestion? - diff_file = @note.latest_diff_file - - return unless diff_file - - suggestions = Banzai::SuggestionsParser.parse(@note.note) - - # For single line suggestion we're only looking forward to - # change the line receiving the comment. Though, in - # https://gitlab.com/gitlab-org/gitlab-ce/issues/53310 - # we'll introduce a ```suggestion:L<x>-<y>, so this will - # slightly change. - comment_line = @note.position.new_line + suggestions = Gitlab::Diff::SuggestionsParser.parse(@note.note, + project: @note.project, + position: @note.position) rows = suggestions.map.with_index do |suggestion, index| - from_content = changing_lines(diff_file, comment_line, comment_line) + creation_params = + suggestion.to_hash.slice(:from_content, + :to_content, + :lines_above, + :lines_below) - # The parsed suggestion doesn't have information about the correct - # ending characters (we may have a line break, or not), so we take - # this information from the last line being changed (last - # characters). - endline_chars = line_break_chars(from_content.lines.last) - to_content = "#{suggestion}#{endline_chars}" - - { - note_id: @note.id, - from_content: from_content, - to_content: to_content, - relative_order: index - } + creation_params.merge!(note_id: @note.id, relative_order: index) end rows.in_groups_of(100, false) do |rows| Gitlab::Database.bulk_insert('suggestions', rows) end end - - private - - def changing_lines(diff_file, from_line, to_line) - diff_file.new_blob_lines_between(from_line, to_line).join - end - - def line_break_chars(line) - match = /\r\n|\r|\n/.match(line) - match[0] if match - end end end diff --git a/app/services/suggestions/outdate_service.rb b/app/services/suggestions/outdate_service.rb new file mode 100644 index 00000000000..a33aac9f6b5 --- /dev/null +++ b/app/services/suggestions/outdate_service.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Suggestions + class OutdateService + def execute(merge_request) + # rubocop: disable CodeReuse/ActiveRecord + suggestions = merge_request.suggestions.active.includes(:note) + + suggestions.find_in_batches(batch_size: 100) do |group| + outdatable_suggestion_ids = group.select do |suggestion| + suggestion.outdated?(cached: false) + end.map(&:id) + + Suggestion.where(id: outdatable_suggestion_ids).update_all(outdated: true) + end + # rubocop: enable CodeReuse/ActiveRecord + end + end +end |