diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index 4667e48f233dfb84edc09b40f787184f21d3059e..ba4a3292ce846887f4ffc6a3aad10fd73bf96517 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -160,6 +160,7 @@ Supported attributes: }, "merge_when_pipeline_succeeds": true, "merge_status": "can_be_merged", + "detailed_merge_status": "not_open", "sha": "8888888888888888888888888888888888888888", "merge_commit_sha": null, "squash_commit_sha": null, @@ -360,6 +361,7 @@ Supported attributes: }, "merge_when_pipeline_succeeds": true, "merge_status": "can_be_merged", + "detailed_merge_status": "not_open", "sha": "8888888888888888888888888888888888888888", "merge_commit_sha": null, "squash_commit_sha": null, @@ -547,6 +549,7 @@ Supported attributes: }, "merge_when_pipeline_succeeds": true, "merge_status": "can_be_merged", + "detailed_merge_status": "not_open", "sha": "8888888888888888888888888888888888888888", "merge_commit_sha": null, "squash_commit_sha": null, @@ -655,6 +658,7 @@ Supported attributes: "milestone": null, "merge_when_pipeline_succeeds": false, "merge_status": "can_be_merged", + "detailed_merge_status": "can_be_merged", "sha": "e82eb4a098e32c796079ca3915e07487fc4db24c", "merge_commit_sha": null, "squash_commit_sha": null, @@ -766,12 +770,6 @@ the `approvals_before_merge` parameter: ### Single merge request response notes -- The `merge_status` field may hold one of the following values: - - `unchecked`: This merge request has not yet been checked. - - `checking`: This merge request is currently being checked to see if it can be merged. - - `can_be_merged`: This merge request can be merged without conflict. - - `cannot_be_merged`: There are merge conflicts between the source and target branches. - - `cannot_be_merged_recheck`: Currently unchecked. Before the current changes, there were conflicts. - The `diff_refs` in the response correspond to the latest diff version of the merge request. - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/29984) in GitLab 12.8, the mergeability (`merge_status`) of each merge request is checked asynchronously when a request is made to this endpoint. Poll this API endpoint @@ -787,6 +785,31 @@ the `approvals_before_merge` parameter: - `pipeline` is an old parameter and should not be used. Use `head_pipeline` instead, as it is faster and returns more information. +### Merge status + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/101724) in GitLab 15.6. + +- The `merge_status` field may hold one of the following values: + - `unchecked`: This merge request has not yet been checked. + - `checking`: This merge request is currently being checked to see if it can be merged. + - `can_be_merged`: This merge request can be merged without conflict. + - `cannot_be_merged`: There are merge conflicts between the source and target branches. + - `cannot_be_merged_recheck`: Currently unchecked. Before the current changes, there were conflicts. +- The `detailed_merge_status` field may hold one of the following values: + - `blocked_status`: Merge request is blocked by another merge request. + - `broken_status`: Can not merge the source into the target branch, potential conflict. + - `checking`: currently checking for mergeability. + - `ci_must_pass`: Pipeline must succeed before merging. + - `ci_still_running`: Pipeline is still running. + - `discussions_not_resolved`: Discussions must be resolved before merging. + - `draft_status`: Merge request must not be draft before merging. + - `external_status_checks`: Status checks must pass. + - `mergeable`: branch can be merged. + - `not_approved`: Merge request must be approved before merging. + - `not_open`: merge request must be open before merging. + - `policies_denied`: There are denied policies for the merge request. + - `unchecked`: merge status has not been checked. + ## Get single MR participants Get a list of merge request participants. @@ -994,6 +1017,7 @@ Supported attributes: }, "merge_when_pipeline_succeeds": true, "merge_status": "can_be_merged", + "detailed_merge_status": "can_be_merged", "subscribed" : true, "sha": "8888888888888888888888888888888888888888", "merge_commit_sha": null, @@ -1211,6 +1235,7 @@ If `approvals_before_merge` is not provided, it inherits the value from the targ }, "merge_when_pipeline_succeeds": true, "merge_status": "can_be_merged", + "detailed_merge_status": "not_open", "merge_error": null, "sha": "8888888888888888888888888888888888888888", "merge_commit_sha": null, @@ -1392,6 +1417,7 @@ Must include at least one non-required attribute from above. }, "merge_when_pipeline_succeeds": true, "merge_status": "can_be_merged", + "detailed_merge_status": "not_open", "merge_error": null, "sha": "8888888888888888888888888888888888888888", "merge_commit_sha": null, @@ -1580,6 +1606,7 @@ Supported attributes: }, "merge_when_pipeline_succeeds": true, "merge_status": "can_be_merged", + "detailed_merge_status": "not_open", "merge_error": null, "sha": "8888888888888888888888888888888888888888", "merge_commit_sha": null, @@ -1792,6 +1819,7 @@ Supported attributes: }, "merge_when_pipeline_succeeds": false, "merge_status": "can_be_merged", + "detailed_merge_status": "not_open", "merge_error": null, "sha": "8888888888888888888888888888888888888888", "merge_commit_sha": null, @@ -2124,6 +2152,7 @@ Example response: }, "merge_when_pipeline_succeeds": true, "merge_status": "can_be_merged", + "detailed_merge_status": "not_open", "sha": "8888888888888888888888888888888888888888", "merge_commit_sha": null, "squash_commit_sha": null, @@ -2294,6 +2323,7 @@ Example response: }, "merge_when_pipeline_succeeds": true, "merge_status": "can_be_merged", + "detailed_merge_status": "not_open", "sha": "8888888888888888888888888888888888888888", "merge_commit_sha": null, "squash_commit_sha": null, @@ -2479,6 +2509,7 @@ Example response: }, "merge_when_pipeline_succeeds": false, "merge_status": "unchecked", + "detailed_merge_status": "not_open", "subscribed": true, "sha": "8888888888888888888888888888888888888888", "merge_commit_sha": null, diff --git a/ee/app/models/ee/merge_request.rb b/ee/app/models/ee/merge_request.rb index 4b2c7920005118e28ca3112e39a3cf3944ca90d4..5d57445570569972dc19a5d07c2e6847b51ce470 100644 --- a/ee/app/models/ee/merge_request.rb +++ b/ee/app/models/ee/merge_request.rb @@ -95,7 +95,8 @@ def merge_requests_disable_committers_approval? class_methods do # This is an ActiveRecord scope in CE def with_api_entity_associations - super.preload(:blocking_merge_requests, target_project: [group: :saml_provider]) + super.preload(:blocking_merge_requests, :approval_rules, + target_project: [:regular_or_any_approver_approval_rules, group: :saml_provider]) end def sort_by_attribute(method, *args, **kwargs) diff --git a/ee/app/models/ee/project.rb b/ee/app/models/ee/project.rb index edd90fb829465f7e93f42f8f53d6b5b391538a27..b3308c5d1d21648e11703e0be361dc038bd8a69b 100644 --- a/ee/app/models/ee/project.rb +++ b/ee/app/models/ee/project.rb @@ -16,6 +16,16 @@ module Project GIT_LFS_DOWNLOAD_OPERATION = 'download' ISSUE_BATCH_SIZE = 500 + module FilterByBranch + def applicable_to_branch(branch) + includes(:protected_branches).select { |rule| rule.applies_to_branch?(branch) } + end + + def inapplicable_to_branch(branch) + includes(:protected_branches).reject { |rule| rule.applies_to_branch?(branch) } + end + end + prepended do include Elastic::ProjectsSearch include EachBatch @@ -53,15 +63,9 @@ module Project has_many :approvers, as: :target, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :approver_users, through: :approvers, source: :user has_many :approver_groups, as: :target, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent - has_many :approval_rules, class_name: 'ApprovalProjectRule' do - def applicable_to_branch(branch) - includes(:protected_branches).select { |rule| rule.applies_to_branch?(branch) } - end - - def inapplicable_to_branch(branch) - includes(:protected_branches).reject { |rule| rule.applies_to_branch?(branch) } - end - end + has_many :approval_rules, class_name: 'ApprovalProjectRule', extend: FilterByBranch + # NOTE: This was added to avoid N+1 queries when we load list of MergeRequests + has_many :regular_or_any_approver_approval_rules, -> { regular_or_any_approver.order(rule_type: :desc, id: :asc) }, class_name: 'ApprovalProjectRule', extend: FilterByBranch has_many :external_status_checks, class_name: 'MergeRequests::ExternalStatusCheck' has_many :approval_merge_request_rules, through: :merge_requests, source: :approval_rules has_many :audit_events, as: :entity @@ -1015,7 +1019,7 @@ def open_source_license_granted? def user_defined_rules strong_memoize(:user_defined_rules) do # Loading the relation in order to memoize it loaded - approval_rules.regular_or_any_approver.order(rule_type: :desc, id: :asc).load + regular_or_any_approver_approval_rules.load end end diff --git a/lib/api/entities/merge_request_basic.rb b/lib/api/entities/merge_request_basic.rb index 55d58166590ac4a9e6680ab41fbb6dfac9d397c3..27f6e6ade062e2d4b429f974415a72fac33d0e2a 100644 --- a/lib/api/entities/merge_request_basic.rb +++ b/lib/api/entities/merge_request_basic.rb @@ -58,6 +58,7 @@ class MergeRequestBasic < IssuableEntity merge_request.check_mergeability(async: true) unless options[:skip_merge_status_recheck] merge_request.public_merge_status end + expose :detailed_merge_status expose :diff_head_sha, as: :sha expose :merge_commit_sha expose :squash_commit_sha @@ -93,6 +94,12 @@ class MergeRequestBasic < IssuableEntity expose :task_completion_status expose :cannot_be_merged?, as: :has_conflicts expose :mergeable_discussions_state?, as: :blocking_discussions_resolved + + private + + def detailed_merge_status + ::MergeRequests::Mergeability::DetailedMergeStatusService.new(merge_request: object).execute + end end end end diff --git a/spec/lib/api/entities/merge_request_basic_spec.rb b/spec/lib/api/entities/merge_request_basic_spec.rb index 40f259b86e2e3bb75a989228d4eceb7dbcd433a5..bb0e25d2613211b8629a6912e5f7b8ad20a348f7 100644 --- a/spec/lib/api/entities/merge_request_basic_spec.rb +++ b/spec/lib/api/entities/merge_request_basic_spec.rb @@ -18,12 +18,16 @@ def present(obj) subject { entity.as_json } - it 'includes basic fields' do - is_expected.to include( - draft: merge_request.draft?, - work_in_progress: merge_request.draft?, - merge_user: nil - ) + it 'includes expected fields' do + expected_fields = %i[ + merged_by merge_user merged_at closed_by closed_at target_branch user_notes_count upvotes downvotes + author assignees assignee reviewers source_project_id target_project_id labels draft work_in_progress + milestone merge_when_pipeline_succeeds merge_status detailed_merge_status sha merge_commit_sha + squash_commit_sha discussion_locked should_remove_source_branch force_remove_source_branch + reference references web_url time_stats squash task_completion_status has_conflicts blocking_discussions_resolved + ] + + is_expected.to include(*expected_fields) end context "with :with_api_entity_associations scope" do diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 7a0d25960490b0cb824bac9f4bd6a48f7ebbd54b..c11927150e439d4f503761f489fca9767b0f736b 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -540,6 +540,7 @@ project: - jenkins_integration - index_status - feature_usage +- regular_or_any_approver_approval_rules - approval_rules - approval_merge_request_rules - approval_merge_request_rule_sources diff --git a/spec/requests/api/todos_spec.rb b/spec/requests/api/todos_spec.rb index 0fcb6412a2d31c17fe4dc9b0c4fff82a701f14fd..7a626ee4d2974e572bf9ecacbf38f01bdbd55164 100644 --- a/spec/requests/api/todos_spec.rb +++ b/spec/requests/api/todos_spec.rb @@ -231,7 +231,7 @@ create(:on_commit_todo, project: new_todo.project, author: author_1, user: john_doe, target: merge_request_3) create(:todo, project: new_todo.project, author: author_2, user: john_doe, target: merge_request_3) - expect { get api('/todos', john_doe) }.not_to exceed_query_limit(control1).with_threshold(4) + expect { get api('/todos', john_doe) }.not_to exceed_query_limit(control1).with_threshold(5) control2 = ActiveRecord::QueryRecorder.new { get api('/todos', john_doe) } create_issue_todo_for(john_doe)