Commit 0ab86d46 authored by Sean McGivern's avatar Sean McGivern

Explain approvals logic better in MR model

parent 871381cd
...@@ -560,11 +560,36 @@ class MergeRequest < ActiveRecord::Base ...@@ -560,11 +560,36 @@ class MergeRequest < ActiveRecord::Base
locked_at.nil? || locked_at < (Time.now - 1.day) locked_at.nil? || locked_at < (Time.now - 1.day)
end end
def requires_approve?
approvals_required.nonzero?
end
def approved?
approvals_left < 1
end
# Number of approvals remaining (excluding existing approvals) before the MR is
# considered approved. If there are fewer potential approvers than approvals left,
# choose the lower so the MR doesn't get 'stuck' in a state where it can't be approved.
#
def approvals_left def approvals_left
[approvals_required - approvals.count, potential_approvers].min [approvals_required - approvals.count, number_of_potential_approvers].min
end end
def potential_approvers def approvals_required
approvals_before_merge || target_project.approvals_before_merge
end
# An MR can potentially be approved by:
# - anyone in the approvers list
# - any other project member with developer access or higher (if there are no approvers
# left)
#
# It cannot be approved by:
# - a user who has already approved the MR
# - the MR author
#
def number_of_potential_approvers
has_access = ['access_level > ?', Member::REPORTER] has_access = ['access_level > ?', Member::REPORTER]
wheres = [ wheres = [
"id IN (#{overall_approvers.select(:user_id).to_sql})", "id IN (#{overall_approvers.select(:user_id).to_sql})",
...@@ -575,21 +600,18 @@ class MergeRequest < ActiveRecord::Base ...@@ -575,21 +600,18 @@ class MergeRequest < ActiveRecord::Base
wheres << "id IN (#{project.group.members.where(has_access).select(:user_id).to_sql})" wheres << "id IN (#{project.group.members.where(has_access).select(:user_id).to_sql})"
end end
User.count_by_sql("SELECT COUNT(*) FROM users WHERE (#{wheres.join(' OR ')}) AND id NOT IN (#{approvals.select(:user_id).to_sql}) AND id != #{author.id}") User.where("(#{wheres.join(' OR ')}) AND id NOT IN (#{approvals.select(:user_id).to_sql}) AND id != #{author.id}").count
end end
# Users in the list of approvers who have not already approved this MR.
#
def approvers_left def approvers_left
User.where(id: overall_approvers.select(:user_id)).where.not(id: approvals.select(:user_id)) User.where(id: overall_approvers.select(:user_id)).where.not(id: approvals.select(:user_id))
end end
def approvals_required # The list of approvers from either this MR (if they've been set on the MR) or the
approvals_before_merge || target_project.approvals_before_merge # target project. Excludes the author by default.
end #
def requires_approve?
approvals_required.nonzero?
end
# Before a merge request has been created, author will be nil, so pass the current user # Before a merge request has been created, author will be nil, so pass the current user
# on the MR create page. # on the MR create page.
# #
...@@ -600,30 +622,33 @@ class MergeRequest < ActiveRecord::Base ...@@ -600,30 +622,33 @@ class MergeRequest < ActiveRecord::Base
exclude_user ? approvers_relation.where.not(user_id: exclude_user.id) : approvers_relation exclude_user ? approvers_relation.where.not(user_id: exclude_user.id) : approvers_relation
end end
def approved?
approvals_left < 1
end
def approved_by?(user)
approved_by_users.include?(user)
end
def approved_by_users
approvals.map(&:user)
end
def can_approve?(user) def can_approve?(user)
return true if approvers_left.include?(user) return true if approvers_left.include?(user)
return false if user == author return false if user == author
return false unless user.can?(:update_merge_request, self) return false unless user.can?(:update_merge_request, self)
any_approver_allowed? && !approved_by?(user) any_approver_allowed? && approvals.where(user: user).empty?
end end
# Once there are fewer approvers left in the list than approvals required, allow other
# project members to approve the MR.
#
def any_approver_allowed? def any_approver_allowed?
approvals_left > approvers_left.count approvals_left > approvers_left.count
end end
def approved_by_users
approvals.map(&:user)
end
def approver_ids=(value)
value.split(",").map(&:strip).each do |user_id|
next if author && user_id == author.id
approvers.find_or_initialize_by(user_id: user_id, target_id: id)
end
end
def has_ci? def has_ci?
source_project.ci_service && commits.any? source_project.ci_service && commits.any?
end end
...@@ -656,14 +681,6 @@ class MergeRequest < ActiveRecord::Base ...@@ -656,14 +681,6 @@ class MergeRequest < ActiveRecord::Base
end end
end end
def approver_ids=(value)
value.split(",").map(&:strip).each do |user_id|
next if author && user_id == author.id
approvers.find_or_initialize_by(user_id: user_id, target_id: id)
end
end
def state_icon_name def state_icon_name
if merged? if merged?
"check" "check"
......
...@@ -264,7 +264,7 @@ describe MergeRequest, models: true do ...@@ -264,7 +264,7 @@ describe MergeRequest, models: true do
end end
end end
describe "#potential_approvers" do describe "#number_of_potential_approvers" do
let(:project) { create(:empty_project) } let(:project) { create(:empty_project) }
let(:author) { create(:user) } let(:author) { create(:user) }
let(:merge_request) { create(:merge_request, source_project: project, author: author) } let(:merge_request) { create(:merge_request, source_project: project, author: author) }
...@@ -272,7 +272,7 @@ describe MergeRequest, models: true do ...@@ -272,7 +272,7 @@ describe MergeRequest, models: true do
it "includes approvers set on the MR" do it "includes approvers set on the MR" do
expect do expect do
create(:approver, user: create(:user), target: merge_request) create(:approver, user: create(:user), target: merge_request)
end.to change { merge_request.potential_approvers }.by(1) end.to change { merge_request.number_of_potential_approvers }.by(1)
end end
it "includes project members with developer access and up" do it "includes project members with developer access and up" do
...@@ -281,7 +281,7 @@ describe MergeRequest, models: true do ...@@ -281,7 +281,7 @@ describe MergeRequest, models: true do
project.team << [create(:user), :reporter] project.team << [create(:user), :reporter]
project.team << [create(:user), :developer] project.team << [create(:user), :developer]
project.team << [create(:user), :master] project.team << [create(:user), :master]
end.to change { merge_request.potential_approvers }.by(2) end.to change { merge_request.number_of_potential_approvers }.by(2)
end end
it "excludes users who have already approved the MR" do it "excludes users who have already approved the MR" do
...@@ -289,14 +289,14 @@ describe MergeRequest, models: true do ...@@ -289,14 +289,14 @@ describe MergeRequest, models: true do
approver = create(:user) approver = create(:user)
create(:approver, user: approver, target: merge_request) create(:approver, user: approver, target: merge_request)
create(:approval, user: approver, merge_request: merge_request) create(:approval, user: approver, merge_request: merge_request)
end.not_to change { merge_request.potential_approvers } end.not_to change { merge_request.number_of_potential_approvers }
end end
it "excludes the MR author" do it "excludes the MR author" do
expect do expect do
create(:approver, user: create(:user), target: merge_request) create(:approver, user: create(:user), target: merge_request)
create(:approver, user: author, target: merge_request) create(:approver, user: author, target: merge_request)
end.to change { merge_request.potential_approvers }.by(1) end.to change { merge_request.number_of_potential_approvers }.by(1)
end end
context "when the project is part of a group" do context "when the project is part of a group" do
...@@ -309,7 +309,7 @@ describe MergeRequest, models: true do ...@@ -309,7 +309,7 @@ describe MergeRequest, models: true do
group.add_reporter(create(:user)) group.add_reporter(create(:user))
group.add_developer(create(:user)) group.add_developer(create(:user))
group.add_master(create(:user)) group.add_master(create(:user))
end.to change { merge_request.potential_approvers }.by(2) end.to change { merge_request.number_of_potential_approvers }.by(2)
end end
end end
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment