diff --git a/app/models/compare.rb b/app/models/compare.rb index b2d46ada831b7db4a65c2a6ed5407299747afbc4..f1ed84ab5a52395f54b7785bfa9cdb16a154e5a2 100644 --- a/app/models/compare.rb +++ b/app/models/compare.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require 'set' + class Compare include Gitlab::Utils::StrongMemoize @@ -77,4 +79,13 @@ def diff_refs head_sha: head_commit_sha ) end + + def modified_paths + paths = Set.new + diffs.diff_files.each do |diff| + paths.add diff.old_path + paths.add diff.new_path + end + paths.to_a + end end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index be8f8bd9e6e31a35b00b9a8cd690132fdc4c6005..727778c4e32bf12177618bb7ae9c5a09fdcc0cb3 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -411,6 +411,18 @@ def diff_size merge_request_diff&.real_size || diffs.real_size end + def modified_paths(past_merge_request_diff: nil) + diffs = if past_merge_request_diff + past_merge_request_diff + elsif compare + compare + else + self.merge_request_diff + end + + diffs.modified_paths + end + def diff_base_commit if persisted? merge_request_diff.base_commit diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index bb6ff8921dffcb431f91bc6b518e2e6eab1ded47..74583af1a294eb66527256295ed61892f8560204 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -6,6 +6,7 @@ class MergeRequestDiff < ActiveRecord::Base include ManualInverseAssociation include IgnorableColumn include EachBatch + include Gitlab::Utils::StrongMemoize # Don't display more than 100 commits at once COMMITS_SAFE_SIZE = 100 @@ -234,6 +235,12 @@ def compare_with(sha) end # rubocop: enable CodeReuse/ServiceClass + def modified_paths + strong_memoize(:modified_paths) do + merge_request_diff_files.pluck(:new_path, :old_path).flatten.uniq + end + end + private def create_merge_request_diff_files(diffs) diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb index 14bdb8de1a21d1273ab2728139c44ef09800b462..c7753d52f217cbdef8a8520890713f6eb3b227fd 100644 --- a/app/services/merge_requests/refresh_service.rb +++ b/app/services/merge_requests/refresh_service.rb @@ -2,18 +2,18 @@ module MergeRequests class RefreshService < MergeRequests::BaseService + attr_reader :push + def execute(oldrev, newrev, ref) - push = Gitlab::Git::Push.new(@project, oldrev, newrev, ref) - return true unless push.branch_push? + @push = Gitlab::Git::Push.new(@project, oldrev, newrev, ref) + return true unless @push.branch_push? - refresh_merge_requests!(push) + refresh_merge_requests! end private - def refresh_merge_requests!(push) - @push = push - + def refresh_merge_requests! Gitlab::GitalyClient.allow_n_plus_1_calls(&method(:find_new_commits)) # Be sure to close outstanding MRs before reloading them to avoid generating an # empty diff during a manual merge diff --git a/config/routes/project.rb b/config/routes/project.rb index 7209b0132d4079b0577587e4889a799e82a86240..0261c62a035de17ce4fc7d63556a159e347021ac 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -156,6 +156,7 @@ ## EE-specific resources :approvers, only: :destroy + delete 'approvers', to: 'approvers#destroy_via_user_id', as: :approver_via_user_id resources :approver_groups, only: :destroy ## EE-specific diff --git a/doc/user/project/merge_requests/merge_request_approvals.md b/doc/user/project/merge_requests/merge_request_approvals.md index 4e12e3f0184d9d134c95fb701bf80e9b9fec66bb..2623adc27b5fd4610c13da5e976695f752145ab9 100644 --- a/doc/user/project/merge_requests/merge_request_approvals.md +++ b/doc/user/project/merge_requests/merge_request_approvals.md @@ -19,7 +19,6 @@ To edit the merge request approvals: 1. Navigate to your project's **Settings > General** and expand the **Merge requests settings** -1. Tick the "Merge requests approvals" checkbox 1. Search for users or groups that will be [eligible to approve](#eligible-approvers) merge requests and click the **Add** button to add them as approvers 1. Set the minimum number of required approvals under the "Approvals required" @@ -36,36 +35,31 @@ suitable to your workflow: [overridden per merge request](#overriding-the-merge-request-approvals-default-settings) - Choose whether [approvals will be reset with new pushed commits](#resetting-approvals-on-push) -NOTE: **Note:** -If the approvers are changed via the project's settings after a merge request -is created, the merge request retains the previous approvers, but you can always -change them by [editing the merge request](#overriding-the-merge-request-approvals-default-settings). - ## Eligible approvers -An individual user is an eligible approver if they are a member of the given project, -a member of the project's immediate parent group, or a member of a group that has share access -to the project via a [share](../members/share_project_with_groups.md). +The following can approve merge requests: + +- Users being added as approvers at project or merge request level. +- [Code owners](../code_owners.md) related to the merge request ([introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/7933) in [GitLab Starter](https://about.gitlab.com/pricing/) 11.5). -A group is also an eligible approver. [In the future](https://gitlab.com/gitlab-org/gitlab-ee/issues/2048), +An individual user can be added as an approver for a project if they are a member of: + +- The project. +- The project's immediate parent group. +- A group that has access to the project via a [share](../members/share_project_with_groups.md). + +A group can also be added as an approver. [In the future](https://gitlab.com/gitlab-org/gitlab-ee/issues/2048), group approvers will be restricted. If a user is added as an individual approver and is also part of a group approver, then that user is just counted once. The merge request author does not count as an eligible approver, unless [self-approval] is explicitly enabled on the project settings. -Let's say that `m` is the number of required approvals, and `Ω` is the set of -explicit approvers. Depending on their number, there are different cases: - -- If `m <= count(Ω)`, then only those explicit approvers can approve the merge request. -- If `m > count(Ω)` , then all the explicit approvers _and_ the members of the given - project with Developer role or higher are eligible approvers of the merge - request. +### Implicit approvers -NOTE: **Note:** -If the approvals settings are [overridden](#overriding-the-merge-request-approvals-default-settings) -for the particular merge request, then the set of explicit approvers is the -union of the default approvers and the extra approvers set in the merge request. +If the number of required approvals is greater than the number of approvers, +other users will become implicit approvers to fill the gap. +Those implicit approvers include members of the given project with Developer role or higher. ## Adding or removing an approval @@ -124,6 +118,12 @@ First, you have to enable this option in the project's settings: 1. Click **Save changes** +NOTE: **Note:** +If approver overriding is enabled +and the project level approvers are changed after a merge request is created, +the merge request retains the previous approvers. +However, the approvers can be changed by [editing the merge request](#overriding-the-merge-request-approvals-default-settings). + --- The default approval settings can now be overridden when creating a @@ -200,9 +200,3 @@ request itself. It belongs to the target branch's project. ## Approver suggestions Approvers are suggested for merge requests based on the previous authors of the files affected by the merge request. - -### CODEOWNERS file - -> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/7437>) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.4. - -If the [CODEOWNERS](../code_owners.md) file is present in the target branch, more precise suggestions are provided based on its rules. diff --git a/ee/app/controllers/projects/approvers_controller.rb b/ee/app/controllers/projects/approvers_controller.rb index 826b434c9a35751ca358f03582db15a3761d5c12..11e6d9140646e224a02de52df5f961d003ea9f1a 100644 --- a/ee/app/controllers/projects/approvers_controller.rb +++ b/ee/app/controllers/projects/approvers_controller.rb @@ -1,12 +1,19 @@ class Projects::ApproversController < Projects::ApplicationController before_action :authorize_for_subject! + # @deprecated def destroy subject.approvers.find(params[:id]).destroy redirect_back_or_default(default: { action: 'index' }) end + def destroy_via_user_id + subject.approvers.find_by_user_id(params[:user_id]).destroy + + redirect_back_or_default(default: { action: 'index' }) + end + private def authorize_for_subject! diff --git a/ee/app/models/approver.rb b/ee/app/models/approver.rb index 77a53e700a3a7357f4f19f00b0fea78f423a054a..2c2575fa2c053cba45f9bc589553bdb3846a9eaf 100644 --- a/ee/app/models/approver.rb +++ b/ee/app/models/approver.rb @@ -3,4 +3,8 @@ class Approver < ActiveRecord::Base belongs_to :user validates :user, presence: true + + def find_by_user_id(user_id) + find_by(user_id: user_id) + end end diff --git a/ee/app/models/concerns/visible_approvable.rb b/ee/app/models/concerns/visible_approvable.rb index 9eef34d9d53c42cef53a5dfc414b58653efe3371..f86576a3092ba4838a42efb993e47b679980e120 100644 --- a/ee/app/models/concerns/visible_approvable.rb +++ b/ee/app/models/concerns/visible_approvable.rb @@ -23,14 +23,23 @@ def approvers_left # Before a merge request has been created, author will be nil, so pass the current user # on the MR create page. # + # @return [Array] def overall_approvers - approvers_relation = approvers_overwritten? ? approvers : target_project.approvers + if approvers_overwritten? + code_owners = [] # already persisted into database, no need to recompute + approvers_relation = approvers + else + code_owners = self.code_owners.dup + approvers_relation = target_project.approvers + end if author && !authors_can_approve? approvers_relation = approvers_relation.where.not(user_id: author.id) end - approvers_relation.includes(:user) + results = code_owners.concat(approvers_relation.includes(:user).map(&:user)) + results.uniq! + results end def overall_approver_groups @@ -41,8 +50,8 @@ def all_approvers_including_groups strong_memoize(:all_approvers_including_groups) do approvers = [] - # Approvers from direct assignment - approvers << approvers_from_users + # Approvers not sourced from group level + approvers << overall_approvers approvers << approvers_from_groups @@ -50,10 +59,6 @@ def all_approvers_including_groups end end - def approvers_from_users - overall_approvers.map(&:user) - end - def approvers_from_groups group_approvers = overall_approver_groups.flat_map(&:users) group_approvers.delete(author) unless authors_can_approve? diff --git a/ee/app/models/ee/merge_request.rb b/ee/app/models/ee/merge_request.rb index 9fd1e7ca1dddf0ed0eec459a4d88e1c46a1f5a33..64baed70cea2f9dc1ffaeabc5169299d113fedfb 100644 --- a/ee/app/models/ee/merge_request.rb +++ b/ee/app/models/ee/merge_request.rb @@ -50,5 +50,11 @@ def validate_approvals_before_merge def participant_approvers approval_needed? ? approvers_left : [] end + + def code_owners + strong_memoize(:code_owners) do + ::Gitlab::CodeOwners.for_merge_request(self).freeze + end + end end end diff --git a/ee/app/services/ee/merge_requests/refresh_service.rb b/ee/app/services/ee/merge_requests/refresh_service.rb index efaa526c0bc31143613cc780bba7c5b47d0e1427..64fd76a44bcccba190522f50099a5a5f6bc230b6 100644 --- a/ee/app/services/ee/merge_requests/refresh_service.rb +++ b/ee/app/services/ee/merge_requests/refresh_service.rb @@ -6,8 +6,10 @@ module RefreshService private override :refresh_merge_requests! - def refresh_merge_requests!(push) - super && reset_approvals_for_merge_requests(push.ref, push.newrev) + def refresh_merge_requests! + update_approvers do + super && reset_approvals_for_merge_requests(push.ref, push.newrev) + end end # Note: Closed merge requests also need approvals reset. @@ -25,6 +27,53 @@ def reset_approvals_for_merge_requests(ref, newrev) end end end + + # @return [Hash] Diffs prior to code push, mapped from merge request id + def fetch_latest_merge_request_diffs + merge_requests = merge_requests_for_source_branch + ActiveRecord::Associations::Preloader.new.preload(merge_requests, :latest_merge_request_diff) # rubocop: disable CodeReuse/ActiveRecord + merge_requests.map(&:latest_merge_request_diff) + end + + def update_approvers + return yield unless project.feature_available?(:code_owners) + + previous_diffs = fetch_latest_merge_request_diffs + + results = yield + + merge_requests = merge_requests_for_source_branch + ActiveRecord::Associations::Preloader.new.preload(merge_requests, :latest_merge_request_diff) # rubocop: disable CodeReuse/ActiveRecord) + + merge_requests.each do |merge_request| + previous_diff = previous_diffs.find { |diff| diff.merge_request == merge_request } + previous_code_owners = ::Gitlab::CodeOwners.for_merge_request(merge_request, merge_request_diff: previous_diff) + new_code_owners = merge_request.code_owners - previous_code_owners + + create_approvers(merge_request, new_code_owners) + end + + results + end + + def create_approvers(merge_request, users) + return if users.empty? + + if merge_request.approvers_overwritten? + rows = users.map do |user| + { + target_id: merge_request.id, + target_type: merge_request.class.name, + user_id: user.id + } + end + + ::Gitlab::Database.bulk_insert(Approver.table_name, rows) + end + + todo_service.add_merge_request_approvers(merge_request, users) + notification_service.add_merge_request_approvers(merge_request, users, current_user) + end end end end diff --git a/ee/app/services/ee/merge_requests/update_service.rb b/ee/app/services/ee/merge_requests/update_service.rb index 810b86bbadedb6e633b3d19fd446640a6599219a..360997aa0e4507d23f57e965ec6d5827e4d0f448 100644 --- a/ee/app/services/ee/merge_requests/update_service.rb +++ b/ee/app/services/ee/merge_requests/update_service.rb @@ -8,11 +8,11 @@ module UpdateService override :execute def execute(merge_request) should_remove_old_approvers = params.delete(:remove_old_approvers) - old_approvers = merge_request.overall_approvers.to_a + old_approvers = merge_request.overall_approvers merge_request = super(merge_request) - new_approvers = merge_request.overall_approvers.to_a - old_approvers + new_approvers = merge_request.overall_approvers - old_approvers if new_approvers.any? todo_service.add_merge_request_approvers(merge_request, new_approvers) diff --git a/ee/app/services/ee/notification_service.rb b/ee/app/services/ee/notification_service.rb index 242f2b7a454dd43ca9e4e56d5ebb63ed3c82c3bc..87e42659aa991e1a8cc93ca327be935de302e09a 100644 --- a/ee/app/services/ee/notification_service.rb +++ b/ee/app/services/ee/notification_service.rb @@ -70,9 +70,7 @@ def prometheus_alerts_fired(project, alerts) def add_mr_approvers_email(merge_request, approvers, current_user) approvers.each do |approver| - recipient = approver.user - - mailer.add_merge_request_approver_email(recipient.id, merge_request.id, current_user.id).deliver_later + mailer.add_merge_request_approver_email(approver.id, merge_request.id, current_user.id).deliver_later end end diff --git a/ee/app/services/ee/todo_service.rb b/ee/app/services/ee/todo_service.rb index 16c4aaddc861438d24690fc382d6b8ea9877092b..0ada74ff65a97f93bf42b11313f4176378584fae 100644 --- a/ee/app/services/ee/todo_service.rb +++ b/ee/app/services/ee/todo_service.rb @@ -42,7 +42,7 @@ def attributes_for_target(target) def create_approval_required_todos(merge_request, approvers, author) attributes = attributes_for_todo(merge_request.project, merge_request, author, ::Todo::APPROVAL_REQUIRED) - create_todos(approvers.map(&:user), attributes) + create_todos(approvers, attributes) end end end diff --git a/ee/app/views/shared/issuable/_approvals.html.haml b/ee/app/views/shared/issuable/_approvals.html.haml index 56e65b0df5cbde0b1922f25d6a3d06ea4794b406..fbce3f902edf103500909bdb66d8e95a452b41cb 100644 --- a/ee/app/views/shared/issuable/_approvals.html.haml +++ b/ee/app/views/shared/issuable/_approvals.html.haml @@ -42,16 +42,16 @@ - unsaved_approvers = !presenter.approvers_overwritten? - item_classes = unsaved_approvers ? ['unsaved-approvers'] : [] - presenter.overall_approvers.each do |approver| - %li{ id: dom_id(approver.user), class: item_classes + ['approver'] } - = link_to approver.user.name, approver.user + %li{ id: dom_id(approver), class: item_classes + ['approver'] } + = link_to approver.name, approver - if can_update_approvers .float-right - if unsaved_approvers - = link_to "#", data: { confirm: "Are you sure you want to remove approver #{approver.user.name}"}, class: "btn-sm btn btn-remove", title: 'Remove approver' do + = link_to "#", data: { confirm: "Are you sure you want to remove approver #{approver.name}"}, class: "btn-sm btn btn-remove", title: 'Remove approver' do = icon("sign-out") Remove - else - = link_to project_merge_request_approver_path(@project, issuable, approver), data: { confirm: "Are you sure you want to remove approver #{approver.user.name}"}, method: :delete, class: "btn-sm btn btn-remove", title: 'Remove approver' do + = link_to project_merge_request_approver_via_user_id_path(@project, issuable, user_id: approver.id), data: { confirm: "Are you sure you want to remove approver #{approver.name}"}, method: :delete, class: "btn-sm btn btn-remove", title: 'Remove approver' do = icon("sign-out") Remove - presenter.overall_approver_groups.each do |approver_group| diff --git a/ee/changelogs/unreleased/1012-assign-code-owner-as-approver.yml b/ee/changelogs/unreleased/1012-assign-code-owner-as-approver.yml new file mode 100644 index 0000000000000000000000000000000000000000..a892ee7d39515959286f5f813766ec161e2deb11 --- /dev/null +++ b/ee/changelogs/unreleased/1012-assign-code-owner-as-approver.yml @@ -0,0 +1,5 @@ +--- +title: Assign code owner as approver +merge_request: 7933 +author: +type: added diff --git a/ee/lib/gitlab/code_owners.rb b/ee/lib/gitlab/code_owners.rb index 1cd535bb403394d88295bc9b3455a622f2dbd87a..8e8d4136344038980e84b242e2bd073269451237 100644 --- a/ee/lib/gitlab/code_owners.rb +++ b/ee/lib/gitlab/code_owners.rb @@ -12,5 +12,20 @@ def self.for_blob(blob) User.none end end + + # @param merge_request [MergeRequest] + # @param merge_request_diff [MergeRequestDiff] + # Find code owners at a particular MergeRequestDiff. + # Assumed to be the most recent one if not provided. + def self.for_merge_request(merge_request, merge_request_diff: nil) + return [] if merge_request.source_project.nil? || merge_request.source_branch.nil? + return [] unless merge_request.target_project.feature_available?(:code_owners) + + Loader.new( + merge_request.target_project, + merge_request.target_branch, + merge_request.modified_paths(past_merge_request_diff: merge_request_diff) + ).members.where_not_in(merge_request.author).to_a + end end end diff --git a/ee/spec/controllers/projects/merge_requests_controller_spec.rb b/ee/spec/controllers/projects/merge_requests_controller_spec.rb index 074b03d8bfc0e6274772f8a610caa8d9a3e66263..bdeef07a916f7e9e3485f299dda9b99e3e48d229 100644 --- a/ee/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/ee/spec/controllers/projects/merge_requests_controller_spec.rb @@ -339,7 +339,7 @@ def expect_rebase_worker_for(user) end context 'with a forked project' do - let(:forked_project) { fork_project(project, fork_owner) } + let(:forked_project) { fork_project(project, fork_owner, repository: true) } let(:fork_owner) { create(:user) } before do diff --git a/ee/spec/lib/gitlab/code_owners_spec.rb b/ee/spec/lib/gitlab/code_owners_spec.rb index 49d86effe0b082c9d338607b6cad7eb1ab3708bf..675961e786adc3ab8b27d02ccb742cc8f7a6a7bd 100644 --- a/ee/spec/lib/gitlab/code_owners_spec.rb +++ b/ee/spec/lib/gitlab/code_owners_spec.rb @@ -7,18 +7,22 @@ let!(:code_owner) { create(:user, username: 'owner-1') } let(:project) { create(:project, :repository) } - let(:blob) do - project.repository.blob_at(TestEnv::BRANCH_SHA['with-codeowners'], 'docs/CODEOWNERS') - end let(:codeowner_content) { "docs/CODEOWNERS @owner-1" } let(:codeowner_blob) { fake_blob(path: 'CODEOWNERS', data: codeowner_content) } + let(:codeowner_blob_ref) { fake_blob(path: 'CODEOWNERS', data: codeowner_content) } before do project.add_developer(code_owner) - allow(project.repository).to receive(:code_owners_blob).and_return(codeowner_blob) + allow(project.repository).to receive(:code_owners_blob) + .with(ref: codeowner_lookup_ref) + .and_return(codeowner_blob) end describe '.for_blob' do + let(:branch) { TestEnv::BRANCH_SHA['with-codeowners'] } + let(:blob) { project.repository.blob_at(branch, 'docs/CODEOWNERS') } + let(:codeowner_lookup_ref) { branch } + context 'when the feature is available' do before do stub_licensed_features(code_owners: true) @@ -39,4 +43,59 @@ end end end + + describe '.for_merge_request' do + let(:codeowner_lookup_ref) { merge_request.target_branch } + let(:merge_request) do + build( + :merge_request, + source_project: project, + source_branch: 'feature', + target_project: project, + target_branch: 'with-codeowners' + ) + end + + context 'when the feature is available' do + before do + stub_licensed_features(code_owners: true) + end + + it 'returns owners for merge request' do + expect(merge_request).to receive(:modified_paths).with(past_merge_request_diff: nil).and_return(['docs/CODEOWNERS']) + + expect(described_class.for_merge_request(merge_request)).to eq([code_owner]) + end + + context 'when owner is merge request author' do + let(:merge_request) { build(:merge_request, target_project: project, author: code_owner) } + + it 'excludes author' do + expect(merge_request).to receive(:modified_paths).with(past_merge_request_diff: nil).and_return(['docs/CODEOWNERS']) + + expect(described_class.for_merge_request(merge_request)).to eq([]) + end + end + + context 'when merge_request_diff is specified' do + let(:merge_request_diff) { double(:merge_request_diff) } + + it 'returns owners at the specified ref' do + expect(merge_request).to receive(:modified_paths).with(past_merge_request_diff: merge_request_diff).and_return(['docs/CODEOWNERS']) + + expect(described_class.for_merge_request(merge_request, merge_request_diff: merge_request_diff)).to eq([code_owner]) + end + end + end + + context 'when the feature is not available' do + before do + stub_licensed_features(code_owners: false) + end + + it 'returns no users' do + expect(described_class.for_merge_request(merge_request)).to eq([]) + end + end + end end diff --git a/ee/spec/models/merge_request_spec.rb b/ee/spec/models/merge_request_spec.rb index 103bf5a9874aa48bce4b14352c3ef8867edcba04..8bbd78996f9a6dd5fd5ff4358c97b0dcbfd0d4e5 100644 --- a/ee/spec/models/merge_request_spec.rb +++ b/ee/spec/models/merge_request_spec.rb @@ -14,6 +14,18 @@ it { is_expected.to have_many(:approved_by_users) } end + describe '#code_owners' do + subject(:merge_request) { build(:merge_request) } + let(:owners) { [double(:owner)] } + + it 'returns code owners, frozen' do + allow(::Gitlab::CodeOwners).to receive(:for_merge_request).with(subject).and_return(owners) + + expect(subject.code_owners).to eq(owners) + expect(subject.code_owners).to be_frozen + end + end + describe '#approvals_before_merge' do where(:license_value, :db_value, :expected) do true | 5 | 5 diff --git a/ee/spec/models/visible_approvable_spec.rb b/ee/spec/models/visible_approvable_spec.rb index 3437117c32b4e1ce23df2ba90a758468e2f03a42..87e66c73a6ba997c67c3397f347251d03bc9dc4e 100644 --- a/ee/spec/models/visible_approvable_spec.rb +++ b/ee/spec/models/visible_approvable_spec.rb @@ -39,20 +39,20 @@ subject { resource.overall_approvers } it 'returns a list of all the project approvers' do - is_expected.to match_array(project_approver) + is_expected.to match_array(project_approver.user) end context 'when author is approver' do let!(:author_approver) { create(:approver, target: project, user: resource.author) } it 'excludes author if authors cannot approve' do - is_expected.not_to include(author_approver) + is_expected.not_to include(author_approver.user) end it 'includes author if authors are able to approve' do project.update(merge_requests_author_approval: true) - is_expected.to include(author_approver) + is_expected.to include(author_approver.user) end end @@ -60,7 +60,7 @@ let!(:approver) { create(:approver, target: resource) } it 'returns the list of all the merge request user approvers' do - is_expected.to match_array(approver) + is_expected.to match_array(approver.user) end end end @@ -95,7 +95,7 @@ subject { resource.all_approvers_including_groups } it 'only queries once' do - expect(resource).to receive(:approvers_from_users).and_call_original.once + expect(resource).to receive(:overall_approvers).and_call_original.once 3.times { subject } end diff --git a/ee/spec/services/ee/merge_requests/refresh_service_spec.rb b/ee/spec/services/ee/merge_requests/refresh_service_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..a6e075c99b1423c4bb9f5cef6255a45041759f88 --- /dev/null +++ b/ee/spec/services/ee/merge_requests/refresh_service_spec.rb @@ -0,0 +1,159 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe MergeRequests::RefreshService do + include ProjectForksHelper + + let(:group) { create(:group) } + let(:project) { create(:project, :repository, namespace: group, approvals_before_merge: 1, reset_approvals_on_push: true) } + let(:forked_project) { fork_project(project, fork_user, repository: true) } + + let(:fork_user) { create(:user) } + + let(:source_branch) { 'between-create-delete-modify-move' } + let(:merge_request) do + create(:merge_request, + source_project: project, + source_branch: source_branch, + target_branch: 'master', + target_project: project) + end + let(:another_merge_request) do + create(:merge_request, + source_project: project, + source_branch: source_branch, + target_branch: 'test', + target_project: project) + end + let(:forked_merge_request) do + create(:merge_request, + source_project: forked_project, + source_branch: source_branch, + target_branch: 'master', + target_project: project) + end + let(:oldrev) { TestEnv::BRANCH_SHA[source_branch] } + let(:newrev) { TestEnv::BRANCH_SHA['after-create-delete-modify-move'] } # Pretend source_branch is now updated + + subject { service.execute(oldrev, newrev, "refs/heads/#{source_branch}") } + + describe '#execute' do + context '#update_approvers' do + let(:owner) { create(:user) } + let(:current_user) { merge_request.author } + let(:service) { described_class.new(project, current_user) } + let(:enable_code_owner) { true } + let(:todo_service) { double(:todo_service) } + let(:notification_service) { double(:notification_service) } + + before do + stub_licensed_features(code_owners: enable_code_owner) + + allow(service).to receive(:mark_pending_todos_done) + allow(service).to receive(:notify_about_push) + allow(service).to receive(:execute_hooks) + allow(service).to receive(:todo_service).and_return(todo_service) + allow(service).to receive(:notification_service).and_return(notification_service) + + group.add_master(fork_user) + + merge_request + another_merge_request + forked_merge_request + end + + context 'when code owners disabled' do + let(:enable_code_owner) { false } + + it 'does nothing' do + expect(::Gitlab::CodeOwners).not_to receive(:for_merge_request) + + subject + end + end + + context 'when code owners enabled' do + let(:old_owners) { [] } + let(:new_owners) { [] } + let(:relevant_merge_requests) { [merge_request, another_merge_request] } + + before do + relevant_merge_requests.each do |merge_request| + expect(::Gitlab::CodeOwners).to receive(:for_merge_request).with(merge_request).and_return(new_owners) + expect(::Gitlab::CodeOwners).to receive(:for_merge_request).with(merge_request, merge_request_diff: anything).and_wrap_original do |m, *args| + expect(args.last[:merge_request_diff]).to eq(merge_request.merge_request_diffs.order(id: :desc).offset(1).first) + + old_owners + end + end + + [forked_merge_request].each do |merge_request| + expect(::Gitlab::CodeOwners).not_to receive(:for_merge_request).with(merge_request, anything) + end + end + + shared_examples 'notification and todo' do + it 'does nothing if owners do not change' do + expect(service.todo_service).not_to receive(:add_merge_request_approvers) + expect(service.notification_service).not_to receive(:add_merge_request_approvers) + + subject + end + + context 'when new owners are being added' do + let(:new_owners) { [owner] } + + it 'notifies new owner' do + relevant_merge_requests.each do |merge_request| + expect(todo_service).to receive(:add_merge_request_approvers).with(merge_request, [owner]) + expect(notification_service).to receive(:add_merge_request_approvers).with(merge_request, [owner], current_user) + end + + subject + end + end + + context 'when old owners are being removed' do + let(:old_owners) { [owner] } + + it 'does nothing' do + expect(service.todo_service).not_to receive(:add_merge_request_approvers) + expect(service.notification_service).not_to receive(:add_merge_request_approvers) + + subject + end + end + end + + context 'merge request has overwritten approvers' do + include_examples 'notification and todo' + end + + context 'merge request has default approvers' do + let(:existing_approver) { create(:user) } + + before do + create(:approver, target: merge_request, user: existing_approver) + end + + include_examples 'notification and todo' + + context 'when new owners are being added' do + let(:new_owners) { [owner] } + + it 'creates Approver' do + allow(service.todo_service).to receive(:add_merge_request_approvers) + allow(service.notification_service).to receive(:add_merge_request_approvers) + + subject + + expect(merge_request.approvers.first.user).to eq(existing_approver) + expect(merge_request.approvers.last.user).to eq(owner) + end + end + end + end + end + end +end diff --git a/ee/spec/services/quick_actions/interpret_service_spec.rb b/ee/spec/services/quick_actions/interpret_service_spec.rb index c35c92f27c76b7858de5eff77e8030618586831a..a83fdaa304ad0b78e4d73fdc3957a2ae95bb9e86 100644 --- a/ee/spec/services/quick_actions/interpret_service_spec.rb +++ b/ee/spec/services/quick_actions/interpret_service_spec.rb @@ -6,7 +6,7 @@ let(:user2) { create(:user) } let(:user3) { create(:user) } let(:group) { create(:group) } - let(:project) { create(:project, :public, group: group) } + let(:project) { create(:project, :repository, :public, group: group) } let(:issue) { create(:issue, project: project) } let(:service) { described_class.new(project, current_user) } diff --git a/ee/spec/views/shared/issuable/_approvals.html.haml_spec.rb b/ee/spec/views/shared/issuable/_approvals.html.haml_spec.rb index 058c5d35d20d8ed3a5e158e53df30f2edf412fc8..ed0ce35f17e9fb74c7a9add5705a61905f752b20 100644 --- a/ee/spec/views/shared/issuable/_approvals.html.haml_spec.rb +++ b/ee/spec/views/shared/issuable/_approvals.html.haml_spec.rb @@ -2,7 +2,7 @@ describe 'shared/issuable/_approvals.html.haml' do let(:user) { create(:user) } - let(:project) { build(:project) } + let(:project) { create(:project, :repository) } let(:merge_request) { create(:merge_request, source_project: project, target_project: project) } let(:presenter) { merge_request.present(current_user: user) } let(:approver_presenter) { double(any?: false, show_code_owner_tips?: true) } diff --git a/spec/controllers/projects/milestones_controller_spec.rb b/spec/controllers/projects/milestones_controller_spec.rb index 3190f1ce9d43aaae0f7efa9d51c619656f221322..ccd4fc4db3a4ab5e8da4aa030c6765df2087719d 100644 --- a/spec/controllers/projects/milestones_controller_spec.rb +++ b/spec/controllers/projects/milestones_controller_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Projects::MilestonesController do - let(:project) { create(:project) } + let(:project) { create(:project, :repository) } let(:user) { create(:user) } let(:milestone) { create(:milestone, project: project) } let(:issue) { create(:issue, project: project, milestone: milestone) } diff --git a/spec/factories/merge_request_diff_files.rb b/spec/factories/merge_request_diff_files.rb new file mode 100644 index 0000000000000000000000000000000000000000..469a7a0ac8d8e058f170f5c969e4b6b5f4ba0854 --- /dev/null +++ b/spec/factories/merge_request_diff_files.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :merge_request_diff_file do + association :merge_request_diff + + relative_order 0 + new_file true + renamed_file false + deleted_file false + too_large false + a_mode 0 + b_mode 100644 + new_path 'foo' + old_path 'foo' + diff '' + binary false + + trait :new_file do + relative_order 0 + new_file true + renamed_file false + deleted_file false + too_large false + a_mode 0 + b_mode 100644 + new_path 'foo' + old_path 'foo' + diff '' + binary false + end + + trait :renamed_file do + relative_order 662 + new_file false + renamed_file true + deleted_file false + too_large false + a_mode 100644 + b_mode 100644 + new_path 'bar' + old_path 'baz' + diff '' + binary false + end + end +end diff --git a/spec/factories/merge_request_diffs.rb b/spec/factories/merge_request_diffs.rb new file mode 100644 index 0000000000000000000000000000000000000000..e7b5118953811d4f7d2a6f00691cd10a20ed87bb --- /dev/null +++ b/spec/factories/merge_request_diffs.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :merge_request_diff do + association :merge_request + state :collected + commits_count 1 + + base_commit_sha { Digest::SHA1.hexdigest(SecureRandom.hex) } + head_commit_sha { Digest::SHA1.hexdigest(SecureRandom.hex) } + start_commit_sha { Digest::SHA1.hexdigest(SecureRandom.hex) } + end +end diff --git a/spec/models/compare_spec.rb b/spec/models/compare_spec.rb index 8e88bb81162f9f0b946bdc60e5984b306c810963..0bc3ee014e6b4e1731925ebd131bbd02f12ab709 100644 --- a/spec/models/compare_spec.rb +++ b/spec/models/compare_spec.rb @@ -92,4 +92,33 @@ expect(subject.diff_refs.head_sha).to eq(head_commit.id) end end + + describe '#modified_paths' do + context 'changes are present' do + let(:raw_compare) do + Gitlab::Git::Compare.new( + project.repository.raw_repository, 'before-create-delete-modify-move', 'after-create-delete-modify-move' + ) + end + + it 'returns affected file paths, without duplication' do + expect(subject.modified_paths).to contain_exactly(*%w{ + foo/for_move.txt + foo/bar/for_move.txt + foo/for_create.txt + foo/for_delete.txt + foo/for_edit.txt + }) + end + end + + context 'changes are absent' do + let(:start_commit) { sample_commit } + let(:head_commit) { sample_commit } + + it 'returns empty array' do + expect(subject.modified_paths).to eq([]) + end + end + end end diff --git a/spec/models/merge_request_diff_spec.rb b/spec/models/merge_request_diff_spec.rb index 47e8f04e728dfa0cc66093406ff9b7629b91585c..cbe60b3a4a58cbbd458c07074ebe4f7fe87fd06d 100644 --- a/spec/models/merge_request_diff_spec.rb +++ b/spec/models/merge_request_diff_spec.rb @@ -232,4 +232,17 @@ expect(commits.map(&:sha)).to match_array(commit_shas) end end + + describe '#modified_paths' do + subject do + diff = create(:merge_request_diff) + create(:merge_request_diff_file, :new_file, merge_request_diff: diff) + create(:merge_request_diff_file, :renamed_file, merge_request_diff: diff) + diff + end + + it 'returns affected file paths' do + expect(subject.modified_paths).to eq(%w{foo bar baz}) + end + end end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 1099e5db70066fb987e04ce7a09ebee1e11a83db..b19bc8cae8594e6373287b048060544dbc533d9a 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -632,6 +632,44 @@ def set_compare(merge_request) end end + describe '#modified_paths' do + let(:paths) { double(:paths) } + subject(:merge_request) { build(:merge_request) } + + before do + expect(diff).to receive(:modified_paths).and_return(paths) + end + + context 'when past_merge_request_diff is specified' do + let(:another_diff) { double(:merge_request_diff) } + let(:diff) { another_diff } + + it 'returns affected file paths from specified past_merge_request_diff' do + expect(merge_request.modified_paths(past_merge_request_diff: another_diff)).to eq(paths) + end + end + + context 'when compare is present' do + let(:compare) { double(:compare) } + let(:diff) { compare } + + it 'returns affected file paths from compare' do + merge_request.compare = compare + + expect(merge_request.modified_paths).to eq(paths) + end + end + + context 'when no arguments provided' do + let(:diff) { merge_request.merge_request_diff } + subject(:merge_request) { create(:merge_request, source_branch: 'feature', target_branch: 'master') } + + it 'returns affected file paths for merge_request_diff' do + expect(merge_request.modified_paths).to eq(paths) + end + end + end + describe "#related_notes" do let!(:merge_request) { create(:merge_request) } @@ -2142,7 +2180,7 @@ def set_compare(merge_request) end end - let(:project) { create(:project) } + let(:project) { create(:project, :repository) } let(:merge_request) { create(:merge_request, source_project: project, author: author) } let(:author) { create(:user) } let(:approver) { create(:user) } diff --git a/spec/services/issuable/bulk_update_service_spec.rb b/spec/services/issuable/bulk_update_service_spec.rb index 53c85f73cde2ab152da26f3999053fe270d6d12a..f0b0f7956cef307741de38e98bfce333ad0625b5 100644 --- a/spec/services/issuable/bulk_update_service_spec.rb +++ b/spec/services/issuable/bulk_update_service_spec.rb @@ -2,7 +2,7 @@ describe Issuable::BulkUpdateService do let(:user) { create(:user) } - let(:project) { create(:project, namespace: user.namespace) } + let(:project) { create(:project, :repository, namespace: user.namespace) } def bulk_update(issuables, extra_params = {}) bulk_update_params = extra_params diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb index ef4ae6406d28d04b806abaca0089954244542cbf..a6044201196dd4454653b12c993076ee53d0b843 100644 --- a/spec/services/merge_requests/update_service_spec.rb +++ b/spec/services/merge_requests/update_service_spec.rb @@ -685,8 +685,8 @@ def update_merge_request(opts) end context 'setting `allow_collaboration`' do - let(:target_project) { create(:project, :public) } - let(:source_project) { fork_project(target_project) } + let(:target_project) { create(:project, :repository, :public) } + let(:source_project) { fork_project(target_project, nil, repository: true) } let(:user) { create(:user) } let(:merge_request) do create(:merge_request, diff --git a/spec/services/milestones/destroy_service_spec.rb b/spec/services/milestones/destroy_service_spec.rb index 8680e428517ba634a947b5923d3c1b224708ca18..9d2be30c636c3cdeeab1200df5b569aafe52da7f 100644 --- a/spec/services/milestones/destroy_service_spec.rb +++ b/spec/services/milestones/destroy_service_spec.rb @@ -2,7 +2,7 @@ describe Milestones::DestroyService do let(:user) { create(:user) } - let(:project) { create(:project) } + let(:project) { create(:project, :repository) } let(:milestone) { create(:milestone, title: 'Milestone v1.0', project: project) } before do diff --git a/spec/services/notes/quick_actions_service_spec.rb b/spec/services/notes/quick_actions_service_spec.rb index ae98784b931fdbe88e1ef33b9faf2b04bacab39f..fbd808a3efc55c10e30cd715bf74599fdf7c1e29 100644 --- a/spec/services/notes/quick_actions_service_spec.rb +++ b/spec/services/notes/quick_actions_service_spec.rb @@ -2,7 +2,7 @@ describe Notes::QuickActionsService do shared_context 'note on noteable' do - let(:project) { create(:project) } + let(:project) { create(:project, :repository) } let(:maintainer) { create(:user).tap { |u| project.add_maintainer(u) } } let(:assignee) { create(:user) } diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb index bedcc65f1df59c3e017a77a03100be56d96f68cc..4e74c4a268ff63ca761fd3dd317a1adbb5a41e03 100644 --- a/spec/services/todo_service_spec.rb +++ b/spec/services/todo_service_spec.rb @@ -10,7 +10,7 @@ let(:john_doe) { create(:user) } let(:skipped) { create(:user) } let(:skip_users) { [skipped] } - let(:project) { create(:project) } + let(:project) { create(:project, :repository) } let(:mentions) { 'FYI: ' + [author, assignee, john_doe, member, guest, non_member, admin, skipped].map(&:to_reference).join(' ') } let(:directly_addressed) { [author, assignee, john_doe, member, guest, non_member, admin, skipped].map(&:to_reference).join(' ') } let(:directly_addressed_and_mentioned) { member.to_reference + ", what do you think? cc: " + [guest, admin, skipped].map(&:to_reference).join(' ') } diff --git a/spec/support/helpers/test_env.rb b/spec/support/helpers/test_env.rb index 71d72ff27e94080f0036800bdd3535ea3a40b02a..80b96f20e3f22210c3dc06d03afb5e38e52795ba 100644 --- a/spec/support/helpers/test_env.rb +++ b/spec/support/helpers/test_env.rb @@ -55,6 +55,9 @@ module TestEnv 'update-gitlab-shell-v-6-0-1' => '2f61d70', 'update-gitlab-shell-v-6-0-3' => 'de78448', '2-mb-file' => 'bf12d25', + 'before-create-delete-modify-move' => '845009f', + 'between-create-delete-modify-move' => '3f5f443', + 'after-create-delete-modify-move' => 'ba3faa7', 'with-codeowners' => '219560e' }.freeze