Merge branch '54046-fix-by-any-email-for-private-commit-emails' into 'master'
Match users better by their private commit email Closes #54046 See merge request gitlab-org/gitlab-ce!23080
This commit is contained in:
commit
af1ed0e2bb
6 changed files with 117 additions and 42 deletions
|
@ -230,24 +230,13 @@ class Commit
|
|||
|
||||
def lazy_author
|
||||
BatchLoader.for(author_email.downcase).batch do |emails, loader|
|
||||
# A Hash that maps user Emails to the corresponding User objects. The
|
||||
# Emails at this point are the _primary_ Emails of the Users.
|
||||
users_for_emails = User
|
||||
.by_any_email(emails)
|
||||
.each_with_object({}) { |user, hash| hash[user.email] = user }
|
||||
users = User.by_any_email(emails).includes(:emails)
|
||||
|
||||
users_for_ids = users_for_emails
|
||||
.values
|
||||
.each_with_object({}) { |user, hash| hash[user.id] = user }
|
||||
emails.each do |email|
|
||||
user = users.find { |u| u.any_email?(email) }
|
||||
|
||||
# Some commits may have used an alternative Email address. In this case we
|
||||
# need to query the "emails" table to map those addresses to User objects.
|
||||
Email
|
||||
.where(email: emails - users_for_emails.keys)
|
||||
.pluck(:email, :user_id)
|
||||
.each { |(email, id)| users_for_emails[email] = users_for_ids[id] }
|
||||
|
||||
users_for_emails.each { |email, user| loader.call(email, user) }
|
||||
loader.call(email, user)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -349,20 +349,28 @@ class User < ActiveRecord::Base
|
|||
def find_by_any_email(email, confirmed: false)
|
||||
return unless email
|
||||
|
||||
downcased = email.downcase
|
||||
|
||||
find_by_private_commit_email(downcased) || by_any_email(downcased, confirmed: confirmed).take
|
||||
by_any_email(email, confirmed: confirmed).take
|
||||
end
|
||||
|
||||
# Returns a relation containing all the users for the given Email address
|
||||
def by_any_email(email, confirmed: false)
|
||||
users = where(email: email)
|
||||
users = users.confirmed if confirmed
|
||||
# Returns a relation containing all the users for the given email addresses
|
||||
#
|
||||
# @param emails [String, Array<String>] email addresses to check
|
||||
# @param confirmed [Boolean] Only return users where the email is confirmed
|
||||
def by_any_email(emails, confirmed: false)
|
||||
emails = Array(emails).map(&:downcase)
|
||||
|
||||
emails = joins(:emails).where(emails: { email: email })
|
||||
emails = emails.confirmed if confirmed
|
||||
from_users = where(email: emails)
|
||||
from_users = from_users.confirmed if confirmed
|
||||
|
||||
from_union([users, emails])
|
||||
from_emails = joins(:emails).where(emails: { email: emails })
|
||||
from_emails = from_emails.confirmed if confirmed
|
||||
|
||||
items = [from_users, from_emails]
|
||||
|
||||
user_ids = Gitlab::PrivateCommitEmail.user_ids_for_emails(emails)
|
||||
items << where(id: user_ids) if user_ids.present?
|
||||
|
||||
from_union(items)
|
||||
end
|
||||
|
||||
def find_by_private_commit_email(email)
|
||||
|
@ -1031,6 +1039,7 @@ class User < ActiveRecord::Base
|
|||
def all_emails
|
||||
all_emails = []
|
||||
all_emails << email unless temp_oauth_email?
|
||||
all_emails << private_commit_email
|
||||
all_emails.concat(emails.map(&:email))
|
||||
all_emails
|
||||
end
|
||||
|
@ -1043,16 +1052,24 @@ class User < ActiveRecord::Base
|
|||
verified_emails
|
||||
end
|
||||
|
||||
def any_email?(check_email)
|
||||
downcased = check_email.downcase
|
||||
|
||||
# handle the outdated private commit email case
|
||||
return true if persisted? &&
|
||||
id == Gitlab::PrivateCommitEmail.user_id_for_email(downcased)
|
||||
|
||||
all_emails.include?(check_email.downcase)
|
||||
end
|
||||
|
||||
def verified_email?(check_email)
|
||||
downcased = check_email.downcase
|
||||
|
||||
if email == downcased
|
||||
primary_email_verified?
|
||||
else
|
||||
user_id = Gitlab::PrivateCommitEmail.user_id_for_email(downcased)
|
||||
# handle the outdated private commit email case
|
||||
return true if persisted? &&
|
||||
id == Gitlab::PrivateCommitEmail.user_id_for_email(downcased)
|
||||
|
||||
user_id == id || emails.confirmed.where(email: downcased).exists?
|
||||
end
|
||||
verified_emails.include?(check_email.downcase)
|
||||
end
|
||||
|
||||
def hook_attrs
|
||||
|
|
|
@ -18,6 +18,10 @@ module Gitlab
|
|||
match[:id].to_i
|
||||
end
|
||||
|
||||
def user_ids_for_emails(emails)
|
||||
emails.map { |email| user_id_for_email(email) }.compact.uniq
|
||||
end
|
||||
|
||||
def for_user(user)
|
||||
hostname = Gitlab::CurrentSettings.current_application_settings.commit_email_hostname
|
||||
|
||||
|
|
|
@ -4,6 +4,9 @@ require 'spec_helper'
|
|||
|
||||
describe Gitlab::PrivateCommitEmail do
|
||||
let(:hostname) { Gitlab::CurrentSettings.current_application_settings.commit_email_hostname }
|
||||
let(:id) { 1 }
|
||||
let(:valid_email) { "#{id}-foo@#{hostname}" }
|
||||
let(:invalid_email) { "#{id}-foo@users.noreply.bar.com" }
|
||||
|
||||
context '.regex' do
|
||||
subject { described_class.regex }
|
||||
|
@ -16,18 +19,25 @@ describe Gitlab::PrivateCommitEmail do
|
|||
end
|
||||
|
||||
context '.user_id_for_email' do
|
||||
let(:id) { 1 }
|
||||
|
||||
it 'parses user id from email' do
|
||||
email = "#{id}-foo@#{hostname}"
|
||||
|
||||
expect(described_class.user_id_for_email(email)).to eq(id)
|
||||
expect(described_class.user_id_for_email(valid_email)).to eq(id)
|
||||
end
|
||||
|
||||
it 'returns nil on invalid commit email' do
|
||||
email = "#{id}-foo@users.noreply.bar.com"
|
||||
expect(described_class.user_id_for_email(invalid_email)).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
expect(described_class.user_id_for_email(email)).to be_nil
|
||||
context '.user_ids_for_email' do
|
||||
it 'returns deduplicated user IDs for each valid email' do
|
||||
result = described_class.user_ids_for_emails([valid_email, valid_email, invalid_email])
|
||||
|
||||
expect(result).to eq([id])
|
||||
end
|
||||
|
||||
it 'returns an empty array with no valid emails' do
|
||||
result = described_class.user_ids_for_emails([invalid_email])
|
||||
expect(result).to eq([])
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -72,6 +72,7 @@ describe Commit do
|
|||
context 'using eager loading' do
|
||||
let!(:alice) { create(:user, email: 'alice@example.com') }
|
||||
let!(:bob) { create(:user, email: 'hunter2@example.com') }
|
||||
let!(:jeff) { create(:user) }
|
||||
|
||||
let(:alice_commit) do
|
||||
described_class.new(RepoHelpers.sample_commit, project).tap do |c|
|
||||
|
@ -93,7 +94,14 @@ describe Commit do
|
|||
end
|
||||
end
|
||||
|
||||
let!(:commits) { [alice_commit, bob_commit, eve_commit] }
|
||||
let(:jeff_commit) do
|
||||
# The commit for Jeff uses his private commit email
|
||||
described_class.new(RepoHelpers.sample_commit, project).tap do |c|
|
||||
c.author_email = jeff.private_commit_email
|
||||
end
|
||||
end
|
||||
|
||||
let!(:commits) { [alice_commit, bob_commit, eve_commit, jeff_commit] }
|
||||
|
||||
before do
|
||||
create(:email, user: bob, email: 'bob@example.com')
|
||||
|
@ -125,6 +133,20 @@ describe Commit do
|
|||
expect(bob_commit.author).to eq(bob)
|
||||
end
|
||||
|
||||
it "preloads the authors for Commits using a User's private commit Email" do
|
||||
commits.each(&:lazy_author)
|
||||
|
||||
expect(jeff_commit.author).to eq(jeff)
|
||||
end
|
||||
|
||||
it "preloads the authors for Commits using a User's outdated private commit Email" do
|
||||
jeff.update!(username: 'new-username')
|
||||
|
||||
commits.each(&:lazy_author)
|
||||
|
||||
expect(jeff_commit.author).to eq(jeff)
|
||||
end
|
||||
|
||||
it 'sets the author to Nil if an author could not be found for a Commit' do
|
||||
commits.each(&:lazy_author)
|
||||
|
||||
|
|
|
@ -1174,6 +1174,22 @@ describe User do
|
|||
|
||||
expect(described_class.by_any_email(user.email, confirmed: true)).to eq([user])
|
||||
end
|
||||
|
||||
it 'finds user through a private commit email' do
|
||||
user = create(:user)
|
||||
private_email = user.private_commit_email
|
||||
|
||||
expect(described_class.by_any_email(private_email)).to eq([user])
|
||||
expect(described_class.by_any_email(private_email, confirmed: true)).to eq([user])
|
||||
end
|
||||
|
||||
it 'finds user through a private commit email in an array' do
|
||||
user = create(:user)
|
||||
private_email = user.private_commit_email
|
||||
|
||||
expect(described_class.by_any_email([private_email])).to eq([user])
|
||||
expect(described_class.by_any_email([private_email], confirmed: true)).to eq([user])
|
||||
end
|
||||
end
|
||||
|
||||
describe '.search' do
|
||||
|
@ -1501,7 +1517,12 @@ describe User do
|
|||
email_unconfirmed = create :email, user: user
|
||||
user.reload
|
||||
|
||||
expect(user.all_emails).to match_array([user.email, email_unconfirmed.email, email_confirmed.email])
|
||||
expect(user.all_emails).to contain_exactly(
|
||||
user.email,
|
||||
user.private_commit_email,
|
||||
email_unconfirmed.email,
|
||||
email_confirmed.email
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -1512,7 +1533,11 @@ describe User do
|
|||
email_confirmed = create :email, user: user, confirmed_at: Time.now
|
||||
create :email, user: user
|
||||
|
||||
expect(user.verified_emails).to match_array([user.email, user.private_commit_email, email_confirmed.email])
|
||||
expect(user.verified_emails).to contain_exactly(
|
||||
user.email,
|
||||
user.private_commit_email,
|
||||
email_confirmed.email
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -1532,6 +1557,14 @@ describe User do
|
|||
expect(user.verified_email?(user.private_commit_email)).to be_truthy
|
||||
end
|
||||
|
||||
it 'returns true for an outdated private commit email' do
|
||||
old_email = user.private_commit_email
|
||||
|
||||
user.update!(username: 'changed-username')
|
||||
|
||||
expect(user.verified_email?(old_email)).to be_truthy
|
||||
end
|
||||
|
||||
it 'returns false when the email is not verified/confirmed' do
|
||||
email_unconfirmed = create :email, user: user
|
||||
user.reload
|
||||
|
|
Loading…
Reference in a new issue