Match users better by their private commit email

Private commit emails were introduced in !22560, but some parts of
GitLab were not updated to take account of them. This commit adds
support in places that were missed.
This commit is contained in:
Nick Thomas 2018-11-14 18:42:36 +00:00
parent 3eb366722e
commit 59c4fb4ecb
No known key found for this signature in database
GPG Key ID: 2A313A47AFADACE9
6 changed files with 117 additions and 42 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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