diff --git a/app/graphql/resolvers/concerns/resolves_merge_requests.rb b/app/graphql/resolvers/concerns/resolves_merge_requests.rb index 54ffba6073ca43fb3cc7f6e59108e292af5a8e25..c748666b6dd6d39c432215d380e26f3f68b83ccc 100644 --- a/app/graphql/resolvers/concerns/resolves_merge_requests.rb +++ b/app/graphql/resolvers/concerns/resolves_merge_requests.rb @@ -63,6 +63,8 @@ def preloads diff_stats_summary: [:metrics], approved_by: [:approved_by_users], merge_after: [:merge_schedule], + mergeable: [:merge_schedule], + detailed_merge_status: [:merge_schedule], milestone: [:milestone], security_auto_fix: [:author], head_pipeline: [:merge_request_diff, { head_pipeline: [:merge_request] }], diff --git a/app/graphql/types/merge_requests/detailed_merge_status_enum.rb b/app/graphql/types/merge_requests/detailed_merge_status_enum.rb index ce934458aec8423d8f5f9f1592ba4e474e264fa9..1dfc21f833444fa580d419e4e7d75c2b708245fa 100644 --- a/app/graphql/types/merge_requests/detailed_merge_status_enum.rb +++ b/app/graphql/types/merge_requests/detailed_merge_status_enum.rb @@ -66,6 +66,9 @@ class DetailedMergeStatusEnum < BaseEnum value 'SECURITY_POLICIES_EVALUATING', value: :security_policy_evaluation, description: 'All security policies must be evaluated.' + value 'MERGE_TIME', + value: :merge_time, + description: 'Merge request may not be merged until after the specified time.' end end end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 699af3ec43cd2528e1a40321b679ab80acc458e6..e3ac518209fc63c80d6e7442d03a960055ddadf2 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -1297,7 +1297,8 @@ def skipped_mergeable_checks(options = {}) skip_locked_paths_check: merge_when_checks_pass_strat, skip_jira_check: merge_when_checks_pass_strat, skip_locked_lfs_files_check: merge_when_checks_pass_strat, - skip_security_policy_check: merge_when_checks_pass_strat + skip_security_policy_check: merge_when_checks_pass_strat, + skip_merge_time_check: merge_when_checks_pass_strat } end @@ -1325,6 +1326,7 @@ def self.mergeable_state_checks # [ ::MergeRequests::Mergeability::CheckOpenStatusService, + ::MergeRequests::Mergeability::CheckMergeTimeService, ::MergeRequests::Mergeability::CheckDraftStatusService, ::MergeRequests::Mergeability::CheckCommitsStatusService, ::MergeRequests::Mergeability::CheckDiscussionsStatusService, diff --git a/app/services/merge_requests/mergeability/check_merge_time_service.rb b/app/services/merge_requests/mergeability/check_merge_time_service.rb new file mode 100644 index 0000000000000000000000000000000000000000..cfdd60044247e56a17464ca228140572b3b337f4 --- /dev/null +++ b/app/services/merge_requests/mergeability/check_merge_time_service.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module MergeRequests + module Mergeability + class CheckMergeTimeService < CheckBaseService + identifier :merge_time + description 'Checks whether the merge is blocked due to a scheduled merge time' + + def execute + merge_after = merge_request.merge_schedule&.merge_after + return inactive if merge_after.nil? + + if merge_after.future? + failure + else + success + end + end + + def skip? + params[:skip_merge_time_check].present? + end + + def cacheable? + false + end + end + end +end diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index a67904baaed96550a755471dd58395b75335a4ec..39f3b40c5f66bdbf40d4d7ea123d7d4e0b96d28a 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -36982,6 +36982,7 @@ Detailed representation of whether a GitLab merge request can be merged. | <a id="detailedmergestatuslocked_lfs_files"></a>`LOCKED_LFS_FILES` | Merge request includes locked LFS files. | | <a id="detailedmergestatuslocked_paths"></a>`LOCKED_PATHS` | Merge request includes locked paths. | | <a id="detailedmergestatusmergeable"></a>`MERGEABLE` | Branch can be merged. | +| <a id="detailedmergestatusmerge_time"></a>`MERGE_TIME` | Merge request may not be merged until after the specified time. | | <a id="detailedmergestatusneed_rebase"></a>`NEED_REBASE` | Merge request needs to be rebased. | | <a id="detailedmergestatusnot_approved"></a>`NOT_APPROVED` | Merge request must be approved before merging. | | <a id="detailedmergestatusnot_open"></a>`NOT_OPEN` | Merge request must be open before merging. | @@ -37806,6 +37807,7 @@ Representation of mergeability check identifier. | <a id="mergeabilitycheckidentifierlocked_lfs_files"></a>`LOCKED_LFS_FILES` | Checks whether the merge request contains locked LFS files that are locked by users other than the merge request author. | | <a id="mergeabilitycheckidentifierlocked_paths"></a>`LOCKED_PATHS` | Checks whether the merge request contains locked paths. | | <a id="mergeabilitycheckidentifiermerge_request_blocked"></a>`MERGE_REQUEST_BLOCKED` | Checks whether the merge request is blocked. | +| <a id="mergeabilitycheckidentifiermerge_time"></a>`MERGE_TIME` | Checks whether the merge is blocked due to a scheduled merge time. | | <a id="mergeabilitycheckidentifierneed_rebase"></a>`NEED_REBASE` | Checks whether the merge request needs to be rebased. | | <a id="mergeabilitycheckidentifiernot_approved"></a>`NOT_APPROVED` | Checks whether the merge request is approved. | | <a id="mergeabilitycheckidentifiernot_open"></a>`NOT_OPEN` | Checks whether the merge request is open. | diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index 515da356e4d182261c07198d47778beea1a37a5c..f83442301180b23d9d6e3baf4ff1da55bd895083 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -909,6 +909,7 @@ Use `detailed_merge_status` instead of `merge_status` to account for all potenti [Require associated Jira issue for merge requests to be merged](../integration/jira/issues.md#require-associated-jira-issue-for-merge-requests-to-be-merged). - `mergeable`: The branch can merge cleanly into the target branch. - `merge_request_blocked`: Blocked by another merge request. + - `merge_time`: May not be merged until after the specified time. - `need_rebase`: The merge request must be rebased. - `not_approved`: Approval is required before merge. - `not_open`: The merge request must be open before merge. diff --git a/ee/app/graphql/resolvers/concerns/ee/resolves_merge_requests.rb b/ee/app/graphql/resolvers/concerns/ee/resolves_merge_requests.rb index 3f8abaa64c53a4859d4feab146ec91e7be077531..2391c1a95a0afbe76e335a81592c1e5b73ca9b04 100644 --- a/ee/app/graphql/resolvers/concerns/ee/resolves_merge_requests.rb +++ b/ee/app/graphql/resolvers/concerns/ee/resolves_merge_requests.rb @@ -8,14 +8,14 @@ module ResolvesMergeRequests def preloads super.tap do |h| - h[:mergeable] = [ + h[:mergeable] += [ *approved_mergeability_check_preloads, *blocked_by_other_mrs_mergeability_check_preloads, *commits_status_mergeability_check_preloads, *security_policy_evaluation_check_preloads ] - h[:detailed_merge_status] = [ + h[:detailed_merge_status] += [ *approved_mergeability_check_preloads, *blocked_by_other_mrs_mergeability_check_preloads, *commits_status_mergeability_check_preloads, diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 2fab1f07887caf431e16ff6a095a8b7676fbfde4..ce5d3f788a6201663016683a70d95ff8e07343ed 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -3945,7 +3945,8 @@ def set_compare(merge_request) is_expected.to include(skip_approved_check: skip_checks, skip_draft_check: skip_checks, skip_blocked_check: skip_checks, skip_discussions_check: skip_checks, skip_external_status_check: skip_checks, skip_requested_changes_check: skip_checks, - skip_jira_check: skip_checks, skip_security_policy_check: skip_checks) + skip_jira_check: skip_checks, skip_security_policy_check: skip_checks, + skip_merge_time_check: skip_checks) end end end diff --git a/spec/services/merge_requests/mergeability/check_merge_time_service_spec.rb b/spec/services/merge_requests/mergeability/check_merge_time_service_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..ba931501a3e082632ae7824f6e836f9b25c5f0ef --- /dev/null +++ b/spec/services/merge_requests/mergeability/check_merge_time_service_spec.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe MergeRequests::Mergeability::CheckMergeTimeService, feature_category: :code_review_workflow do + subject(:check_merge_time) { described_class.new(merge_request: merge_request, params: params) } + + let_it_be(:merge_request) { build(:merge_request) } + + let(:params) { { skip_merge_time_check: skip_check } } + let(:skip_check) { false } + + it_behaves_like 'mergeability check service', :merge_time, + 'Checks whether the merge is blocked due to a scheduled merge time' + + describe '#execute' do + let(:result) { check_merge_time.execute } + + before do + merge_request.merge_schedule = build( + :merge_request_merge_schedule, + merge_request: merge_request, + merge_after: merge_after + ) + end + + context 'when merge_after is not set' do + let(:merge_after) { nil } + + it 'returns a check result with status inactive' do + expect(result.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::INACTIVE_STATUS + expect(result.payload[:identifier]).to eq(:merge_time) + end + end + + context 'when merge_after is in the future' do + let(:merge_after) { 1.minute.from_now } + + it 'returns a check result with status failed' do + expect(result.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::FAILED_STATUS + expect(result.payload[:identifier]).to eq(:merge_time) + end + end + + context 'when merge_after is in the past' do + let(:merge_after) { 1.minute.ago } + + it 'returns a check result with status success' do + expect(result.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::SUCCESS_STATUS + expect(result.payload[:identifier]).to eq(:merge_time) + end + end + end + + describe '#skip?' do + context 'when skip check param is true' do + let(:skip_check) { true } + + it 'returns true' do + expect(check_merge_time.skip?).to eq true + end + end + + context 'when skip check param is false' do + let(:skip_check) { false } + + it 'returns false' do + expect(check_merge_time.skip?).to eq false + end + end + end + + describe '#cacheable?' do + it 'returns false' do + expect(check_merge_time.cacheable?).to eq false + end + end +end