From 01be4d0e450c5ac038b542b60337162feb2e53ab Mon Sep 17 00:00:00 2001 From: Tetiana Zavediuk <tzave@softserveinc.com> Date: Thu, 13 Oct 2022 08:22:02 +0000 Subject: [PATCH] Map 'Require pull request before merging' GitHub rule with access_levels Select Developers + Maintainers in the Allowed to merge list, and select 'No one' in the 'Allowed to push list' in GitLab protected branch settings when importing a GitHub project to GitLab if the "Require a pull request before merging" GitHub protected rule is enabled. Details: https://gitlab.com/gitlab-org/gitlab/-/issues/370951 Changelog: added --- .../protected_branch/merge_access_level.rb | 2 + .../protected_branch/push_access_level.rb | 2 + doc/user/project/import/github.md | 2 +- .../importer/protected_branch_importer.rb | 73 ++++++++++- .../representation/protected_branch.rb | 8 +- .../protected_branch_importer_spec.rb | 115 +++++++++++++++++- .../protected_branches_importer_spec.rb | 6 + .../representation/protected_branch_spec.rb | 13 +- 8 files changed, 210 insertions(+), 11 deletions(-) diff --git a/app/models/protected_branch/merge_access_level.rb b/app/models/protected_branch/merge_access_level.rb index de240e4031630..df75c557717a9 100644 --- a/app/models/protected_branch/merge_access_level.rb +++ b/app/models/protected_branch/merge_access_level.rb @@ -2,4 +2,6 @@ class ProtectedBranch::MergeAccessLevel < ApplicationRecord include ProtectedBranchAccess + # default value for the access_level column + GITLAB_DEFAULT_ACCESS_LEVEL = Gitlab::Access::MAINTAINER end diff --git a/app/models/protected_branch/push_access_level.rb b/app/models/protected_branch/push_access_level.rb index 5248834a2f229..6076fab20b7c7 100644 --- a/app/models/protected_branch/push_access_level.rb +++ b/app/models/protected_branch/push_access_level.rb @@ -2,6 +2,8 @@ class ProtectedBranch::PushAccessLevel < ApplicationRecord include ProtectedBranchAccess + # default value for the access_level column + GITLAB_DEFAULT_ACCESS_LEVEL = Gitlab::Access::MAINTAINER belongs_to :deploy_key diff --git a/doc/user/project/import/github.md b/doc/user/project/import/github.md index 0e556de515f88..0fc49798a1511 100644 --- a/doc/user/project/import/github.md +++ b/doc/user/project/import/github.md @@ -204,7 +204,7 @@ defaults to the default project visibility. Supported GitHub branch protection rules are mapped to GitLab branch protection rules or project-wide GitLab settings when they are imported: - GitHub rule **Require conversation resolution before merging** for the project's default branch is mapped to the [**All threads must be resolved** GitLab setting](../../discussions/index.md#prevent-merge-unless-all-threads-are-resolved). [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/371110) in GitLab 15.5. -- Support for GitHub rule **Require a pull request before merging** is proposed in issue [370951](https://gitlab.com/gitlab-org/gitlab/-/issues/370951). +- GitHub rule **Require a pull request before merging** is mapped to the **No one** option in the **Allowed to push** list of the branch protection rule. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/370951) in GitLab 15.5. - GitHub rule **Require signed commits** for the project's default branch is mapped to the **Reject unsigned commits** GitLab setting. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/370949) in GitLab 15.5. - Support for GitHub rule **Require status checks to pass before merging** was proposed in issue [370948](https://gitlab.com/gitlab-org/gitlab/-/issues/370948). However, this rule cannot be translated during project import into GitLab due to technical difficulties. You can still create [status checks](../merge_requests/status_checks.md) in GitLab yourself. diff --git a/lib/gitlab/github_import/importer/protected_branch_importer.rb b/lib/gitlab/github_import/importer/protected_branch_importer.rb index ac80f40c8bc7e..21075e21e1def 100644 --- a/lib/gitlab/github_import/importer/protected_branch_importer.rb +++ b/lib/gitlab/github_import/importer/protected_branch_importer.rb @@ -6,6 +6,10 @@ module Importer class ProtectedBranchImporter attr_reader :protected_branch, :project, :client + # By default on GitHub, both developers and maintainers can merge + # a PR into the protected branch + GITHUB_DEFAULT_MERGE_ACCESS_LEVEL = Gitlab::Access::DEVELOPER + # protected_branch - An instance of # `Gitlab::GithubImport::Representation::ProtectedBranch`. # project - An instance of `Project` @@ -31,8 +35,8 @@ def execute def params { name: protected_branch.id, - push_access_levels_attributes: [{ access_level: Gitlab::Access::MAINTAINER }], - merge_access_levels_attributes: [{ access_level: Gitlab::Access::MAINTAINER }], + push_access_levels_attributes: [{ access_level: push_access_level }], + merge_access_levels_attributes: [{ access_level: merge_access_level }], allow_force_push: allow_force_push? } end @@ -68,6 +72,71 @@ def update_project_push_rule push_rule.update!(reject_unsigned_commits: true) project.project_setting.update!(push_rule_id: push_rule.id) end + + def push_access_level + if protected_branch.required_pull_request_reviews + Gitlab::Access::NO_ACCESS + else + gitlab_access_level_for(:push) + end + end + + # Gets the strictest merge_access_level between GitHub and GitLab + def merge_access_level + gitlab_access = gitlab_access_level_for(:merge) + + return gitlab_access if gitlab_access == Gitlab::Access::NO_ACCESS + + [gitlab_access, GITHUB_DEFAULT_MERGE_ACCESS_LEVEL].max + end + + # action - :push/:merge + def gitlab_access_level_for(action) + if default_branch? + action == :push ? default_branch_push_access_level : default_branch_merge_access_level + elsif protected_on_gitlab? + non_default_branch_access_level_for(action) + else + gitlab_default_access_level_for(action) + end + end + + def default_branch_push_access_level + if default_branch_protection.developer_can_push? + Gitlab::Access::DEVELOPER + else + gitlab_default_access_level_for(:push) + end + end + + def default_branch_merge_access_level + if default_branch_protection.developer_can_merge? + Gitlab::Access::DEVELOPER + else + gitlab_default_access_level_for(:merge) + end + end + + def default_branch_protection + Gitlab::Access::BranchProtection.new(project.namespace.default_branch_protection) + end + + def protected_on_gitlab? + ProtectedBranch.protected?(project, protected_branch.id) + end + + def non_default_branch_access_level_for(action) + access_level = ProtectedBranch.access_levels_for_ref(protected_branch.id, action: action) + .find(&:role?)&.access_level + + access_level || gitlab_default_access_level_for(action) + end + + def gitlab_default_access_level_for(action) + return ProtectedBranch::PushAccessLevel::GITLAB_DEFAULT_ACCESS_LEVEL if action == :push + + ProtectedBranch::MergeAccessLevel::GITLAB_DEFAULT_ACCESS_LEVEL + end end end end diff --git a/lib/gitlab/github_import/representation/protected_branch.rb b/lib/gitlab/github_import/representation/protected_branch.rb index 4795e67e01152..07a607ae70dfa 100644 --- a/lib/gitlab/github_import/representation/protected_branch.rb +++ b/lib/gitlab/github_import/representation/protected_branch.rb @@ -9,12 +9,13 @@ class ProtectedBranch attr_reader :attributes - expose_attribute :id, :allow_force_pushes, :required_conversation_resolution, :required_signatures + expose_attribute :id, :allow_force_pushes, :required_conversation_resolution, :required_signatures, + :required_pull_request_reviews # Builds a Branch Protection info from a GitHub API response. # Resource structure details: # https://docs.github.com/en/rest/branches/branch-protection#get-branch-protection - # branch_protection - An instance of `Sawyer::Resource` containing the protection details. + # branch_protection - An instance of `Hash` containing the protection details. def self.from_api_response(branch_protection, _additional_object_data = {}) branch_name = branch_protection[:url].match(%r{/branches/(\S{1,255})/protection$})[1] @@ -22,7 +23,8 @@ def self.from_api_response(branch_protection, _additional_object_data = {}) id: branch_name, allow_force_pushes: branch_protection.dig(:allow_force_pushes, :enabled), required_conversation_resolution: branch_protection.dig(:required_conversation_resolution, :enabled), - required_signatures: branch_protection.dig(:required_signatures, :enabled) + required_signatures: branch_protection.dig(:required_signatures, :enabled), + required_pull_request_reviews: branch_protection[:required_pull_request_reviews].present? } new(hash) diff --git a/spec/lib/gitlab/github_import/importer/protected_branch_importer_spec.rb b/spec/lib/gitlab/github_import/importer/protected_branch_importer_spec.rb index 4529c2675ff24..027b2ac422e33 100644 --- a/spec/lib/gitlab/github_import/importer/protected_branch_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/protected_branch_importer_spec.rb @@ -9,12 +9,17 @@ let(:allow_force_pushes_on_github) { true } let(:required_conversation_resolution) { false } let(:required_signatures) { false } + let(:required_pull_request_reviews) { false } + let(:expected_push_access_level) { Gitlab::Access::MAINTAINER } + let(:expected_merge_access_level) { Gitlab::Access::MAINTAINER } + let(:expected_allow_force_push) { true } let(:github_protected_branch) do Gitlab::GithubImport::Representation::ProtectedBranch.new( id: branch_name, allow_force_pushes: allow_force_pushes_on_github, required_conversation_resolution: required_conversation_resolution, - required_signatures: required_signatures + required_signatures: required_signatures, + required_pull_request_reviews: required_pull_request_reviews ) end @@ -28,8 +33,8 @@ let(:expected_ruleset) do { name: 'protection', - push_access_levels_attributes: [{ access_level: Gitlab::Access::MAINTAINER }], - merge_access_levels_attributes: [{ access_level: Gitlab::Access::MAINTAINER }], + push_access_levels_attributes: [{ access_level: expected_push_access_level }], + merge_access_levels_attributes: [{ access_level: expected_merge_access_level }], allow_force_push: expected_allow_force_push } end @@ -165,7 +170,7 @@ end end - context "when branch is not default" do + context 'when branch is not default' do context 'when required_conversation_resolution rule is enabled' do let(:required_conversation_resolution) { true } @@ -190,5 +195,107 @@ it_behaves_like 'does not change project attributes' end end + + context 'when required_pull_request_reviews rule is enabled on GitHub' do + let(:required_pull_request_reviews) { true } + let(:expected_push_access_level) { Gitlab::Access::NO_ACCESS } + let(:expected_merge_access_level) { Gitlab::Access::MAINTAINER } + + it_behaves_like 'create branch protection by the strictest ruleset' + end + + context 'when required_pull_request_reviews rule is disabled on GitHub' do + let(:required_pull_request_reviews) { false } + + context 'when branch is default' do + before do + allow(project).to receive(:default_branch).and_return(branch_name) + end + + context 'when default branch protection = Gitlab::Access::PROTECTION_DEV_CAN_PUSH' do + before do + stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_PUSH) + end + + let(:expected_push_access_level) { Gitlab::Access::DEVELOPER } + let(:expected_merge_access_level) { Gitlab::Access::MAINTAINER } + + it_behaves_like 'create branch protection by the strictest ruleset' + end + + context 'when default branch protection = Gitlab::Access::PROTECTION_DEV_CAN_MERGE' do + before do + stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_MERGE) + end + + let(:expected_push_access_level) { Gitlab::Access::MAINTAINER } + let(:expected_merge_access_level) { Gitlab::Access::DEVELOPER } + + it_behaves_like 'create branch protection by the strictest ruleset' + end + end + + context 'when branch is protected on GitLab' do + let(:protected_branch) do + create( + :protected_branch, + project: project, + name: 'protect*', + allow_force_push: true + ) + end + + let(:push_access_level) { protected_branch.push_access_levels.first } + let(:merge_access_level) { protected_branch.merge_access_levels.first } + + context 'when there is branch protection rule for the role' do + context 'when No one can merge' do + before do + merge_access_level.update_column(:access_level, Gitlab::Access::NO_ACCESS) + end + + let(:expected_push_access_level) { push_access_level.access_level } + let(:expected_merge_access_level) { Gitlab::Access::NO_ACCESS } + + it_behaves_like 'create branch protection by the strictest ruleset' + end + + context 'when Maintainers and Developers can merge' do + before do + merge_access_level.update_column(:access_level, Gitlab::Access::DEVELOPER) + end + + let(:gitlab_push_access_level) { push_access_level.access_level } + let(:gitlab_merge_access_level) { merge_access_level.access_level } + let(:expected_push_access_level) { gitlab_push_access_level } + let(:expected_merge_access_level) { [gitlab_merge_access_level, github_default_merge_access_level].max } + let(:github_default_merge_access_level) do + Gitlab::GithubImport::Importer::ProtectedBranchImporter::GITHUB_DEFAULT_MERGE_ACCESS_LEVEL + end + + it_behaves_like 'create branch protection by the strictest ruleset' + end + end + + context 'when there is no branch protection rule for the role' do + before do + push_access_level.update_column(:user_id, project.owner.id) + merge_access_level.update_column(:user_id, project.owner.id) + end + + let(:expected_push_access_level) { ProtectedBranch::PushAccessLevel::GITLAB_DEFAULT_ACCESS_LEVEL } + let(:expected_merge_access_level) { Gitlab::Access::MAINTAINER } + + it_behaves_like 'create branch protection by the strictest ruleset' + end + end + + context 'when branch is neither default nor protected on GitLab' do + let(:expected_push_access_level) { ProtectedBranch::PushAccessLevel::GITLAB_DEFAULT_ACCESS_LEVEL } + let(:expected_merge_access_level) { ProtectedBranch::MergeAccessLevel::GITLAB_DEFAULT_ACCESS_LEVEL } + + it_behaves_like 'create branch protection by the strictest ruleset' + end + end end end diff --git a/spec/lib/gitlab/github_import/importer/protected_branches_importer_spec.rb b/spec/lib/gitlab/github_import/importer/protected_branches_importer_spec.rb index 4e9208be98541..a0ced456391e8 100644 --- a/spec/lib/gitlab/github_import/importer/protected_branches_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/protected_branches_importer_spec.rb @@ -23,11 +23,13 @@ let(:github_protection_rule) do response = Struct.new(:name, :url, :required_signatures, :enforce_admins, :required_linear_history, :allow_force_pushes, :allow_deletion, :block_creations, :required_conversation_resolution, + :required_pull_request_reviews, keyword_init: true ) required_signatures = Struct.new(:url, :enabled, keyword_init: true) enforce_admins = Struct.new(:url, :enabled, keyword_init: true) allow_option = Struct.new(:enabled, keyword_init: true) + required_pull_request_reviews = Struct.new(:url, :dismissal_restrictions, keyword_init: true) response.new( name: 'main', url: 'https://example.com/branches/main/protection', @@ -53,6 +55,10 @@ ), required_conversation_resolution: allow_option.new( enabled: false + ), + required_pull_request_reviews: required_pull_request_reviews.new( + url: 'https://example.com/branches/main/protection/required_pull_request_reviews', + dismissal_restrictions: {} ) ) end diff --git a/spec/lib/gitlab/github_import/representation/protected_branch_spec.rb b/spec/lib/gitlab/github_import/representation/protected_branch_spec.rb index 21fd3b2e44a93..30b29659eee08 100644 --- a/spec/lib/gitlab/github_import/representation/protected_branch_spec.rb +++ b/spec/lib/gitlab/github_import/representation/protected_branch_spec.rb @@ -20,6 +20,10 @@ it 'includes the protected branch required_conversation_resolution attribute' do expect(protected_branch.required_conversation_resolution).to eq true end + + it 'includes the protected branch required_pull_request_reviews' do + expect(protected_branch.required_pull_request_reviews).to eq true + end end end @@ -27,9 +31,11 @@ let(:response) do response = Struct.new( :url, :allow_force_pushes, :required_conversation_resolution, :required_signatures, + :required_pull_request_reviews, keyword_init: true ) enabled_setting = Struct.new(:enabled, keyword_init: true) + required_pull_request_reviews = Struct.new(:url, :dismissal_restrictions, keyword_init: true) response.new( url: 'https://example.com/branches/main/protection', allow_force_pushes: enabled_setting.new( @@ -40,6 +46,10 @@ ), required_signatures: enabled_setting.new( enabled: true + ), + required_pull_request_reviews: required_pull_request_reviews.new( + url: 'https://example.com/branches/main/protection/required_pull_request_reviews', + dismissal_restrictions: {} ) ) end @@ -56,7 +66,8 @@ 'id' => 'main', 'allow_force_pushes' => true, 'required_conversation_resolution' => true, - 'required_signatures' => true + 'required_signatures' => true, + 'required_pull_request_reviews' => true } end -- GitLab