Commit f4d8114c authored by charlie ablett's avatar charlie ablett

Merge branch...

Merge branch '351293-large-pushes-to-merge-requests-can-cause-emails-not-to-be-delivered' into 'master'

Limit the number of commits in push merge request emails

See merge request gitlab-org/gitlab!82801
parents 90ff7dcf 931bad56
...@@ -14,10 +14,17 @@ module Emails ...@@ -14,10 +14,17 @@ module Emails
mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, reason)) mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, reason))
end end
def push_to_merge_request_email(recipient_id, merge_request_id, updated_by_user_id, reason = nil, new_commits: [], existing_commits: []) # existing_commits - an array containing the first and last commits
def push_to_merge_request_email(recipient_id, merge_request_id, updated_by_user_id, reason = nil, new_commits: [], total_new_commits_count: nil, existing_commits: [], total_existing_commits_count: nil)
setup_merge_request_mail(merge_request_id, recipient_id) setup_merge_request_mail(merge_request_id, recipient_id)
@new_commits = new_commits @new_commits = new_commits
@total_new_commits_count = total_new_commits_count || @new_commits.length
@total_stripped_new_commits_count = @total_new_commits_count - @new_commits.length
@existing_commits = existing_commits @existing_commits = existing_commits
@total_existing_commits_count = total_existing_commits_count || @existing_commits.length
@updated_by_user = User.find(updated_by_user_id) @updated_by_user = User.find(updated_by_user_id)
mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, reason)) mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, reason))
......
...@@ -208,13 +208,30 @@ class NotificationService ...@@ -208,13 +208,30 @@ class NotificationService
new_resource_email(merge_request, current_user, :new_merge_request_email) new_resource_email(merge_request, current_user, :new_merge_request_email)
end end
NEW_COMMIT_EMAIL_DISPLAY_LIMIT = 20
def push_to_merge_request(merge_request, current_user, new_commits: [], existing_commits: []) def push_to_merge_request(merge_request, current_user, new_commits: [], existing_commits: [])
new_commits = new_commits.map { |c| { short_id: c.short_id, title: c.title } } total_new_commits_count = new_commits.count
existing_commits = existing_commits.map { |c| { short_id: c.short_id, title: c.title } } truncated_new_commits = new_commits.first(NEW_COMMIT_EMAIL_DISPLAY_LIMIT).map do |commit|
{ short_id: commit.short_id, title: commit.title }
end
# We don't need the list of all existing commits. We need the first, the
# last, and the total number of existing commits only.
total_existing_commits_count = existing_commits.count
existing_commits = [existing_commits.first, existing_commits.last] if total_existing_commits_count > 2
existing_commits = existing_commits.map do |commit|
{ short_id: commit.short_id, title: commit.title }
end
recipients = NotificationRecipients::BuildService.build_recipients(merge_request, current_user, action: "push_to") recipients = NotificationRecipients::BuildService.build_recipients(merge_request, current_user, action: "push_to")
recipients.each do |recipient| recipients.each do |recipient|
mailer.send(:push_to_merge_request_email, recipient.user.id, merge_request.id, current_user.id, recipient.reason, new_commits: new_commits, existing_commits: existing_commits).deliver_later mailer.send(
:push_to_merge_request_email,
recipient.user.id, merge_request.id, current_user.id, recipient.reason,
new_commits: truncated_new_commits, total_new_commits_count: total_new_commits_count,
existing_commits: existing_commits, total_existing_commits_count: total_existing_commits_count
).deliver_later
end end
end end
......
...@@ -3,24 +3,25 @@ ...@@ -3,24 +3,25 @@
pushed new commits to merge request pushed new commits to merge request
= merge_request_reference_link(@merge_request) = merge_request_reference_link(@merge_request)
- if @existing_commits.any? - if @total_existing_commits_count > 0
- count = @existing_commits.size
%ul %ul
%li %li
- if count == 1 - if @total_existing_commits_count == 1
- commit_id = @existing_commits.first[:short_id] - commit_id = @existing_commits.first[:short_id]
= link_to(commit_id, project_commit_url(@merge_request.target_project, commit_id)) = link_to(commit_id, project_commit_url(@merge_request.target_project, commit_id))
- else - else
= link_to(project_compare_url(@merge_request.target_project, from: @existing_commits.first[:short_id], to: @existing_commits.last[:short_id])) do = link_to(project_compare_url(@merge_request.target_project, from: @existing_commits.first[:short_id], to: @existing_commits.last[:short_id])) do
#{@existing_commits.first[:short_id]}...#{@existing_commits.last[:short_id]} #{@existing_commits.first[:short_id]}...#{@existing_commits.last[:short_id]}
= precede ' - ' do = precede ' - ' do
- commits_text = "#{count} commit".pluralize(count) - commits_text = "#{@total_existing_commits_count} commit".pluralize(@total_existing_commits_count)
#{commits_text} from branch `#{@merge_request.target_branch}` #{commits_text} from branch `#{@merge_request.target_branch}`
- if @new_commits.any? - if @total_new_commits_count > 0
%ul %ul
- @new_commits.each do |commit| - @new_commits.each do |commit|
%li %li
= link_to(commit[:short_id], project_commit_url(@merge_request.target_project, commit[:short_id])) = link_to(commit[:short_id], project_commit_url(@merge_request.target_project, commit[:short_id]))
= precede ' - ' do = precede ' - ' do
#{commit[:title]} #{commit[:title]}
- if @total_stripped_new_commits_count > 0
%li And #{@total_stripped_new_commits_count} more
...@@ -317,6 +317,9 @@ RSpec.describe Notify do ...@@ -317,6 +317,9 @@ RSpec.describe Notify do
end end
context 'for merge requests' do context 'for merge requests' do
let(:push_user) { create(:user) }
let(:commit_limit) { NotificationService::NEW_COMMIT_EMAIL_DISPLAY_LIMIT }
describe 'that are new' do describe 'that are new' do
subject { described_class.new_merge_request_email(merge_request.assignee_ids.first, merge_request.id) } subject { described_class.new_merge_request_email(merge_request.assignee_ids.first, merge_request.id) }
...@@ -457,12 +460,6 @@ RSpec.describe Notify do ...@@ -457,12 +460,6 @@ RSpec.describe Notify do
end end
shared_examples 'a push to an existing merge request' do shared_examples 'a push to an existing merge request' do
let(:push_user) { create(:user) }
subject do
described_class.push_to_merge_request_email(recipient.id, merge_request.id, push_user.id, new_commits: merge_request.commits, existing_commits: existing_commits)
end
it_behaves_like 'a multiple recipients email' it_behaves_like 'a multiple recipients email'
it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
let(:model) { merge_request } let(:model) { merge_request }
...@@ -487,16 +484,136 @@ RSpec.describe Notify do ...@@ -487,16 +484,136 @@ RSpec.describe Notify do
end end
end end
describe 'that have new commits' do shared_examples 'shows the compare url between first and last commits' do |count|
let(:existing_commits) { [] } it 'shows the compare url between first and last commits' do
commit_id_1 = existing_commits.first[:short_id]
commit_id_2 = existing_commits.last[:short_id]
is_expected.to have_link("#{commit_id_1}...#{commit_id_2}", href: project_compare_url(project, from: commit_id_1, to: commit_id_2))
is_expected.to have_body_text("#{count} commits from branch `#{merge_request.target_branch}`")
end
end
shared_examples 'shows new commit urls' do |count|
it 'shows new commit urls' do
displayed_new_commits.each do |commit|
is_expected.to have_link(commit[:short_id], href: project_commit_url(project, commit[:short_id]))
is_expected.to have_body_text(commit[:title])
end
end
it 'does not show hidden new commit urls' do
hidden_new_commits.each do |commit|
is_expected.not_to have_link(commit[:short_id], href: project_commit_url(project, commit[:short_id]))
is_expected.not_to have_body_text(commit[:title])
end
end
end
describe 'that have no new commits' do
subject do
described_class.push_to_merge_request_email(recipient.id, merge_request.id, push_user.id, new_commits: [], total_new_commits_count: 0, existing_commits: [], total_existing_commits_count: 0)
end
it_behaves_like 'a push to an existing merge request'
end
describe 'that have fewer than the commit truncation limit' do
let(:new_commits) { merge_request.commits }
let(:displayed_new_commits) { new_commits }
let(:hidden_new_commits) { [] }
subject do
described_class.push_to_merge_request_email(
recipient.id, merge_request.id, push_user.id,
new_commits: new_commits, total_new_commits_count: new_commits.length,
existing_commits: [], total_existing_commits_count: 0
)
end
it_behaves_like 'a push to an existing merge request'
it_behaves_like 'shows new commit urls'
end
describe 'that have more than the commit truncation limit' do
let(:new_commits) do
Array.new(commit_limit + 10) do |i|
{
short_id: SecureRandom.hex(4),
title: "This is commit #{i}"
}
end
end
let(:displayed_new_commits) { new_commits.first(commit_limit) }
let(:hidden_new_commits) { new_commits.last(10) }
subject do
described_class.push_to_merge_request_email(
recipient.id, merge_request.id, push_user.id,
new_commits: displayed_new_commits, total_new_commits_count: commit_limit + 10,
existing_commits: [], total_existing_commits_count: 0
)
end
it_behaves_like 'a push to an existing merge request' it_behaves_like 'a push to an existing merge request'
it_behaves_like 'shows new commit urls'
it 'shows "and more" message' do
is_expected.to have_body_text("And 10 more")
end
end end
describe 'that have new commits on top of an existing one' do describe 'that have new commits on top of an existing one' do
let(:existing_commits) { [merge_request.commits.first] } let(:existing_commits) { [merge_request.commits.first] }
subject do
described_class.push_to_merge_request_email(
recipient.id, merge_request.id, push_user.id,
new_commits: merge_request.commits, total_new_commits_count: merge_request.commits.length,
existing_commits: existing_commits, total_existing_commits_count: existing_commits.length
)
end
it_behaves_like 'a push to an existing merge request'
it 'shows the existing commit' do
commit_id = existing_commits.first.short_id
is_expected.to have_link(commit_id, href: project_commit_url(project, commit_id))
is_expected.to have_body_text("1 commit from branch `#{merge_request.target_branch}`")
end
end
describe 'that have new commits on top of two existing ones' do
let(:existing_commits) { [merge_request.commits.first, merge_request.commits.second] }
subject do
described_class.push_to_merge_request_email(
recipient.id, merge_request.id, push_user.id,
new_commits: merge_request.commits, total_new_commits_count: merge_request.commits.length,
existing_commits: existing_commits, total_existing_commits_count: existing_commits.length
)
end
it_behaves_like 'a push to an existing merge request'
it_behaves_like 'shows the compare url between first and last commits', 2
end
describe 'that have new commits on top of more than two existing ones' do
let(:existing_commits) do
[merge_request.commits.first] + [double(:commit)] * 3 + [merge_request.commits.second]
end
subject do
described_class.push_to_merge_request_email(
recipient.id, merge_request.id, push_user.id,
new_commits: merge_request.commits, total_new_commits_count: merge_request.commits.length,
existing_commits: existing_commits, total_existing_commits_count: existing_commits.length
)
end
it_behaves_like 'a push to an existing merge request' it_behaves_like 'a push to an existing merge request'
it_behaves_like 'shows the compare url between first and last commits', 5
end end
end end
......
...@@ -2101,6 +2101,70 @@ RSpec.describe NotificationService, :mailer do ...@@ -2101,6 +2101,70 @@ RSpec.describe NotificationService, :mailer do
should_not_email(@u_lazy_participant) should_not_email(@u_lazy_participant)
end end
describe 'triggers push_to_merge_request_email with corresponding email' do
let_it_be(:merge_request) { create(:merge_request, author: author, source_project: project) }
def mock_commits(length)
Array.new(length) { |i| double(:commit, short_id: SecureRandom.hex(4), title: "This is commit #{i}") }
end
def commit_to_hash(commit)
{ short_id: commit.short_id, title: commit.title }
end
let(:existing_commits) { mock_commits(50) }
let(:expected_existing_commits) { [commit_to_hash(existing_commits.first), commit_to_hash(existing_commits.last)] }
before do
allow(::Notify).to receive(:push_to_merge_request_email).and_call_original
end
where(:number_of_new_commits, :number_of_new_commits_displayed) do
limit = described_class::NEW_COMMIT_EMAIL_DISPLAY_LIMIT
[
[0, 0],
[limit - 2, limit - 2],
[limit - 1, limit - 1],
[limit, limit],
[limit + 1, limit],
[limit + 2, limit]
]
end
with_them do
let(:new_commits) { mock_commits(number_of_new_commits) }
let(:expected_new_commits) { new_commits.first(number_of_new_commits_displayed).map(&method(:commit_to_hash)) }
it 'triggers the corresponding mailer method with list of stripped commits' do
notification.push_to_merge_request(
merge_request, merge_request.author,
new_commits: new_commits, existing_commits: existing_commits
)
expect(Notify).to have_received(:push_to_merge_request_email).at_least(:once).with(
@subscriber.id, merge_request.id, merge_request.author.id, "subscribed",
new_commits: expected_new_commits, total_new_commits_count: number_of_new_commits,
existing_commits: expected_existing_commits, total_existing_commits_count: 50
)
end
end
context 'there is only one existing commit' do
let(:new_commits) { mock_commits(10) }
let(:expected_new_commits) { new_commits.map(&method(:commit_to_hash)) }
it 'triggers corresponding mailer method with only one existing commit' do
notification.push_to_merge_request(merge_request, merge_request.author, new_commits: new_commits, existing_commits: existing_commits.first(1))
expect(Notify).to have_received(:push_to_merge_request_email).at_least(:once).with(
@subscriber.id, merge_request.id, merge_request.author.id, "subscribed",
new_commits: expected_new_commits, total_new_commits_count: 10,
existing_commits: expected_existing_commits.first(1), total_existing_commits_count: 1
)
end
end
end
it_behaves_like 'participating notifications' do it_behaves_like 'participating notifications' do
let(:participant) { create(:user, username: 'user-participant') } let(:participant) { create(:user, username: 'user-participant') }
let(:issuable) { merge_request } let(:issuable) { merge_request }
......
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