diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index 9a1c2a4c9e1ab02d746c8b2391f5af72baa460b0..086bb38ce9a63c4e2aee59249c428e23f4052899 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -217,7 +217,8 @@ def visible_attributes :user_oauth_applications, :version_check_enabled, :web_ide_clientside_preview_enabled, - :diff_max_patch_bytes + :diff_max_patch_bytes, + :commit_email_hostname ] end diff --git a/app/helpers/avatars_helper.rb b/app/helpers/avatars_helper.rb index 7fc4c1a023f8b15adb8f0016204ccd5197105163..5906ddabee4095740aaf95adc8e59edfff1057b2 100644 --- a/app/helpers/avatars_helper.rb +++ b/app/helpers/avatars_helper.rb @@ -22,7 +22,7 @@ def avatar_icon_for(user = nil, email = nil, size = nil, scale = 2, only_path: t end def avatar_icon_for_email(email = nil, size = nil, scale = 2, only_path: true) - user = User.find_by_any_email(email.try(:downcase)) + user = User.find_by_any_email(email) if user avatar_icon_for_user(user, size, scale, only_path: only_path) else diff --git a/app/helpers/profiles_helper.rb b/app/helpers/profiles_helper.rb index 55674e37a34e8b5b91545677e348ccff30b54c04..42f9a1213e9582fea296d5a233efe282939870dd 100644 --- a/app/helpers/profiles_helper.rb +++ b/app/helpers/profiles_helper.rb @@ -1,6 +1,20 @@ # frozen_string_literal: true module ProfilesHelper + def commit_email_select_options(user) + private_email = user.private_commit_email + verified_emails = user.verified_emails - [private_email] + + [ + [s_("Profiles|Use a private email - %{email}").html_safe % { email: private_email }, Gitlab::PrivateCommitEmail::TOKEN], + verified_emails + ] + end + + def selected_commit_email(user) + user.read_attribute(:commit_email) || user.commit_email + end + def attribute_provider_label(attribute) user_synced_attributes_metadata = current_user.user_synced_attributes_metadata if user_synced_attributes_metadata&.synced?(attribute) diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 704310f53f055089750c1856b79ec2483daa9d15..207ffae873ab26a406bb6188ee3577f98ea532e2 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -187,6 +187,8 @@ class ApplicationSetting < ActiveRecord::Base validates :user_default_internal_regex, js_regex: true, allow_nil: true + validates :commit_email_hostname, format: { with: /\A[^@]+\z/ } + validates :archive_builds_in_seconds, allow_nil: true, numericality: { only_integer: true, greater_than_or_equal_to: 1.day.seconds } @@ -299,10 +301,15 @@ def self.defaults user_default_internal_regex: nil, user_show_add_ssh_key_message: true, usage_stats_set_by_user_id: nil, - diff_max_patch_bytes: Gitlab::Git::Diff::DEFAULT_MAX_PATCH_BYTES + diff_max_patch_bytes: Gitlab::Git::Diff::DEFAULT_MAX_PATCH_BYTES, + commit_email_hostname: default_commit_email_hostname } end + def self.default_commit_email_hostname + "users.noreply.#{Gitlab.config.gitlab.host}" + end + def self.create_from_defaults create(defaults) end @@ -358,6 +365,10 @@ def repository_storages Array(read_attribute(:repository_storages)) end + def commit_email_hostname + super.presence || self.class.default_commit_email_hostname + end + def default_project_visibility=(level) super(Gitlab::VisibilityLevel.level_value(level)) end diff --git a/app/models/commit.rb b/app/models/commit.rb index a61ed03cf357e6bad7487b1cbd41b30a5fa436dc..9dd0cbacd9ebd94dc7c1d4b3a470417e6df38d5b 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -260,7 +260,7 @@ def author request_cache(:author) { author_email.downcase } def committer - @committer ||= User.find_by_any_email(committer_email.downcase) + @committer ||= User.find_by_any_email(committer_email) end def parents diff --git a/app/models/user.rb b/app/models/user.rb index 039a3854edb73d5fed15dd93c623a1c711b395c1..a400058e87e23b2e5ec35a9ecdc2b6cb2dd093af 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -347,7 +347,11 @@ def for_github_id(id) # Find a User by their primary email or any associated secondary email def find_by_any_email(email, confirmed: false) - by_any_email(email, confirmed: confirmed).take + return unless email + + downcased = email.downcase + + find_by_private_commit_email(downcased) || by_any_email(downcased, confirmed: confirmed).take end # Returns a relation containing all the users for the given Email address @@ -361,6 +365,12 @@ def by_any_email(email, confirmed: false) from_union([users, emails]) end + def find_by_private_commit_email(email) + user_id = Gitlab::PrivateCommitEmail.user_id_for_email(email) + + find_by(id: user_id) + end + def filter(filter_name) case filter_name when 'admins' @@ -633,6 +643,10 @@ def owns_commit_email def commit_email return self.email unless has_attribute?(:commit_email) + if super == Gitlab::PrivateCommitEmail::TOKEN + return private_commit_email + end + # The commit email is the same as the primary email if undefined super.presence || self.email end @@ -645,6 +659,10 @@ def commit_email_changed? has_attribute?(:commit_email) && super end + def private_commit_email + Gitlab::PrivateCommitEmail.for_user(self) + end + # see if the new email is already a verified secondary email def check_for_verified_email skip_reconfirmation! if emails.confirmed.where(email: self.email).any? @@ -1020,13 +1038,21 @@ def all_emails def verified_emails verified_emails = [] verified_emails << email if primary_email_verified? + verified_emails << private_commit_email verified_emails.concat(emails.confirmed.pluck(:email)) verified_emails end def verified_email?(check_email) downcased = check_email.downcase - email == downcased ? primary_email_verified? : emails.confirmed.where(email: downcased).exists? + + if email == downcased + primary_email_verified? + else + user_id = Gitlab::PrivateCommitEmail.user_id_for_email(downcased) + + user_id == id || emails.confirmed.where(email: downcased).exists? + end end def hook_attrs diff --git a/app/views/admin/application_settings/_email.html.haml b/app/views/admin/application_settings/_email.html.haml index 86339e6121558dd4b3aec2dc0f405da3767ddd35..60a6be731ea769f45688fce8fff5ff9fdb7c9798 100644 --- a/app/views/admin/application_settings/_email.html.haml +++ b/app/views/admin/application_settings/_email.html.haml @@ -20,5 +20,11 @@ By default GitLab sends emails in HTML and plain text formats so mail clients can choose what format to use. Disable this option if you only want to send emails in plain text format. + .form-group + = f.label :commit_email_hostname, _('Custom hostname (for private commit emails)'), class: 'label-bold' + = f.text_field :commit_email_hostname, class: 'form-control' + .form-text.text-muted + - commit_email_hostname_docs_link = link_to _('Learn more'), help_page_path('user/admin_area/settings/email', anchor: 'custom-private-commit-email-hostname'), target: '_blank' + = _("This setting will update the hostname that is used to generate private commit emails. %{learn_more}").html_safe % { learn_more: commit_email_hostname_docs_link } = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml index ea215e3e71856d8c5222ec544debf1c83c5c6db4..2603c558c0f73e7cde749c9535171396c4c960c6 100644 --- a/app/views/profiles/show.html.haml +++ b/app/views/profiles/show.html.haml @@ -91,8 +91,9 @@ = f.select :public_email, options_for_select(@user.all_emails, selected: @user.public_email), { help: s_("Profiles|This email will be displayed on your public profile."), include_blank: s_("Profiles|Do not show on profile") }, control_class: 'select2' - = f.select :commit_email, options_for_select(@user.verified_emails, selected: @user.commit_email), - { help: 'This email will be used for web based operations, such as edits and merges.' }, + - commit_email_docs_link = link_to s_('Profiles|Learn more'), help_page_path('user/profile/index', anchor: 'commit-email', target: '_blank') + = f.select :commit_email, options_for_select(commit_email_select_options(@user), selected: selected_commit_email(@user)), + { help: s_("Profiles|This email will be used for web based operations, such as edits and merges. %{learn_more}").html_safe % { learn_more: commit_email_docs_link } }, control_class: 'select2' = f.select :preferred_language, Gitlab::I18n::AVAILABLE_LANGUAGES.map { |value, label| [label, value] }, { help: s_("Profiles|This feature is experimental and translations are not complete yet.") }, diff --git a/changelogs/unreleased/43521-keep-personal-emails-private.yml b/changelogs/unreleased/43521-keep-personal-emails-private.yml new file mode 100644 index 0000000000000000000000000000000000000000..0f0bede64823a5a1e8aa848b7d157c4687047108 --- /dev/null +++ b/changelogs/unreleased/43521-keep-personal-emails-private.yml @@ -0,0 +1,5 @@ +--- +title: Adds option to override commit email with a noreply private email +merge_request: 22560 +author: +type: added diff --git a/db/migrate/20181025115728_add_private_commit_email_hostname_to_application_settings.rb b/db/migrate/20181025115728_add_private_commit_email_hostname_to_application_settings.rb new file mode 100644 index 0000000000000000000000000000000000000000..89ddaf2ae2b34774e107ae4d9754472dc29f364a --- /dev/null +++ b/db/migrate/20181025115728_add_private_commit_email_hostname_to_application_settings.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class AddPrivateCommitEmailHostnameToApplicationSettings < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def change + add_column(:application_settings, :commit_email_hostname, :string, null: true) + end +end diff --git a/db/schema.rb b/db/schema.rb index cfbfd7ad375362519f32e709f9c87f1f6e10d78b..4695d923b799e2cebcc7c40b3ba7b8668c09eda6 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -166,6 +166,7 @@ t.integer "receive_max_input_size" t.integer "diff_max_patch_bytes", default: 102400, null: false t.integer "archive_builds_in_seconds" + t.string "commit_email_hostname" end create_table "audit_events", force: :cascade do |t| diff --git a/doc/development/utilities.md b/doc/development/utilities.md index 0d074a3ef057c0b16a46ca760cbbaac62f6281e1..e5466ae8914f4e4c1e15fe28278bd2f47b663c0b 100644 --- a/doc/development/utilities.md +++ b/doc/development/utilities.md @@ -171,8 +171,8 @@ class Commit extend Gitlab::Cache::RequestCache def author - User.find_by_any_email(author_email.downcase) + User.find_by_any_email(author_email) end - request_cache(:author) { author_email.downcase } + request_cache(:author) { author_email } end ``` diff --git a/doc/user/admin_area/settings/email.md b/doc/user/admin_area/settings/email.md index 7c9e5bf882ee5ea88e1294ea288fe591c143f0d8..50c318a49699e26c41d93b781ec75e2aaac7c966 100644 --- a/doc/user/admin_area/settings/email.md +++ b/doc/user/admin_area/settings/email.md @@ -3,3 +3,20 @@ ## Custom logo The logo in the header of some emails can be customized, see the [logo customization section](../../../customization/branded_page_and_email_header.md). + +## Custom hostname for private commit emails + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22560) in GitLab 11.5. + +This configuration option sets the email hostname for [private commit emails](../../profile/index.md#private-commit-email), +and it's, by default, set to `users.noreply.YOUR_CONFIGURED_HOSTNAME`. + +In order to change this option: + +1. Go to **Admin area > Settings** (`/admin/application_settings`). +1. Under the **Email** section, change the **Custom hostname (for private commit emails)** field. +1. Hit **Save** for the changes to take effect. + +NOTE: **Note**: Once the hostname gets configured, every private commit email using the previous hostname, will not get +recognized by GitLab. This can directly conflict with certain [Push rules](https://docs.gitlab.com/ee/push_rules/push_rules.html) such as +`Check whether author is a GitLab user` and `Check whether committer is the current authenticated user`. diff --git a/doc/user/profile/index.md b/doc/user/profile/index.md index ab62762f3436dedadc76802320321c51a60da48d..da7c30b6b39e36f7a731bde426c8d405f2409a87 100644 --- a/doc/user/profile/index.md +++ b/doc/user/profile/index.md @@ -31,6 +31,7 @@ From there, you can: - Update your personal information - Set a [custom status](#current-status) for your profile +- Manage your [commit email](#commit-email) for your profile - Manage [2FA](account/two_factor_authentication.md) - Change your username and [delete your account](account/delete_account.md) - Manage applications that can @@ -132,6 +133,45 @@ They may however contain emoji codes such as `I'm on vacation :palm_tree:`. You can also set your current status [using the API](../../api/users.md#user-status). +## Commit email + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/21598) in GitLab 11.4. + +A commit email, is the email that will be displayed in every Git-related action done through the +GitLab interface. + +You are able to select from the list of your own verified emails which email you want to use as the commit email. + +To change it: + +1. Open the user menu in the top-right corner of the navigation bar. +1. Hit **Commit email** selection box. +1. Select any of the verified emails. +1. Hit **Update profile settings**. + +### Private commit email + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22560) in GitLab 11.5. + +GitLab provides the user with an automatically generated private commit email option, +which allows the user to not make their email information public. + +To enable this option: + +1. Open the user menu in the top-right corner of the navigation bar. +1. Hit **Commit email** selection box. +1. Select **Use a private email** option. +1. Hit **Update profile settings**. + +Once this option is enabled, every Git-related action will be performed using the private commit email. + +In order to stay fully annonymous, you can also copy this private commit email +and configure it on your local machine using the following command: + +``` +git config --global user.email "YOUR_PRIVATE_COMMIT_EMAIL" +``` + ## Troubleshooting ### Why do I keep getting signed out? diff --git a/lib/gitlab/private_commit_email.rb b/lib/gitlab/private_commit_email.rb new file mode 100644 index 0000000000000000000000000000000000000000..bade2248ccd72c9f91402f32ded0c4e3fc11487d --- /dev/null +++ b/lib/gitlab/private_commit_email.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module Gitlab + module PrivateCommitEmail + TOKEN = "_private".freeze + + class << self + def regex + hostname_regexp = Regexp.escape(Gitlab::CurrentSettings.current_application_settings.commit_email_hostname) + + /\A(?([0-9]+))\-([^@]+)@#{hostname_regexp}\z/ + end + + def user_id_for_email(email) + match = email&.match(regex) + return unless match + + match[:id].to_i + end + + def for_user(user) + hostname = Gitlab::CurrentSettings.current_application_settings.commit_email_hostname + + "#{user.id}-#{user.username}@#{hostname}" + end + end + end +end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 45fc072900a4a8938068bff4cc45efb747a944c0..7d15d6a11fd79500ab9a8fe33c39a6e7d87b4f2d 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -2094,6 +2094,9 @@ msgstr "" msgid "Custom CI config path" msgstr "" +msgid "Custom hostname (for private commit emails)" +msgstr "" + msgid "Custom notification events" msgstr "" @@ -4701,6 +4704,9 @@ msgstr "" msgid "Profiles|Invalid username" msgstr "" +msgid "Profiles|Learn more" +msgstr "" + msgid "Profiles|Made a private contribution" msgstr "" @@ -4743,6 +4749,9 @@ msgstr "" msgid "Profiles|This email will be displayed on your public profile." msgstr "" +msgid "Profiles|This email will be used for web based operations, such as edits and merges. %{learn_more}" +msgstr "" + msgid "Profiles|This emoji and message will appear on your profile and throughout the interface." msgstr "" @@ -4767,6 +4776,9 @@ msgstr "" msgid "Profiles|Upload new avatar" msgstr "" +msgid "Profiles|Use a private email - %{email}" +msgstr "" + msgid "Profiles|Username change failed - %{message}" msgstr "" @@ -6296,6 +6308,9 @@ msgstr "" msgid "This setting can be overridden in each project." msgstr "" +msgid "This setting will update the hostname that is used to generate private commit emails. %{learn_more}" +msgstr "" + msgid "This source diff could not be displayed because it is too large." msgstr "" diff --git a/spec/helpers/profiles_helper_spec.rb b/spec/helpers/profiles_helper_spec.rb index c1d0614c79e7fdc8947742040a7aad3776151cf8..9a2372de69f4e53c27386056b797c313948a587c 100644 --- a/spec/helpers/profiles_helper_spec.rb +++ b/spec/helpers/profiles_helper_spec.rb @@ -1,6 +1,35 @@ require 'rails_helper' describe ProfilesHelper do + describe '#commit_email_select_options' do + it 'returns an array with private commit email along with all the verified emails' do + user = create(:user) + private_email = user.private_commit_email + + verified_emails = user.verified_emails - [private_email] + emails = [ + ["Use a private email - #{private_email}", Gitlab::PrivateCommitEmail::TOKEN], + verified_emails + ] + + expect(helper.commit_email_select_options(user)).to match_array(emails) + end + end + + describe '#selected_commit_email' do + let(:user) { create(:user) } + + it 'returns main email when commit email attribute is nil' do + expect(helper.selected_commit_email(user)).to eq(user.email) + end + + it 'returns DB stored commit_email' do + user.update(commit_email: Gitlab::PrivateCommitEmail::TOKEN) + + expect(helper.selected_commit_email(user)).to eq(Gitlab::PrivateCommitEmail::TOKEN) + end + end + describe '#email_provider_label' do it "returns nil for users without external email" do user = create(:user) diff --git a/spec/lib/gitlab/private_commit_email_spec.rb b/spec/lib/gitlab/private_commit_email_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..bc86cd3842a2f68437e8a47a7fb3e2b4db788292 --- /dev/null +++ b/spec/lib/gitlab/private_commit_email_spec.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::PrivateCommitEmail do + let(:hostname) { Gitlab::CurrentSettings.current_application_settings.commit_email_hostname } + + context '.regex' do + subject { described_class.regex } + + it { is_expected.to match("1-foo@#{hostname}") } + it { is_expected.not_to match("1-foo@#{hostname}.foo") } + it { is_expected.not_to match('1-foo@users.noreply.gitlab.com') } + it { is_expected.not_to match('foo-1@users.noreply.gitlab.com') } + it { is_expected.not_to match('foobar@gitlab.com') } + 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) + end + + it 'returns nil on invalid commit email' do + email = "#{id}-foo@users.noreply.bar.com" + + expect(described_class.user_id_for_email(email)).to be_nil + end + end + + context '.for_user' do + it 'returns email in the format id-username@hostname' do + user = create(:user) + + expect(described_class.for_user(user)).to eq("#{user.id}-#{user.username}@#{hostname}") + end + end +end diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index 95ae7bd21ab632cd4089558a4f04a54114be7db1..96aa9a82b718dc1811a3f9781e7f3a7fbb41e33e 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -25,6 +25,9 @@ it { is_expected.to allow_value(https).for(:after_sign_out_path) } it { is_expected.not_to allow_value(ftp).for(:after_sign_out_path) } + it { is_expected.to allow_value("dev.gitlab.com").for(:commit_email_hostname) } + it { is_expected.not_to allow_value("@dev.gitlab").for(:commit_email_hostname) } + describe 'default_artifacts_expire_in' do it 'sets an error if it cannot parse' do setting.update(default_artifacts_expire_in: 'a') @@ -107,6 +110,14 @@ def expect_invalid it { expect(setting.repository_storages).to eq(['default']) } end + context '#commit_email_hostname' do + it 'returns configured gitlab hostname if commit_email_hostname is not defined' do + setting.update(commit_email_hostname: nil) + + expect(setting.commit_email_hostname).to eq("users.noreply.#{Gitlab.config.gitlab.host}") + end + end + context 'auto_devops_domain setting' do context 'when auto_devops_enabled? is true' do before do diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 4e7c8523e65089959f288282da3e09d301a9e509..0ac5bd666aefcda27b8610fbc1fa50b318800856 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -183,6 +183,12 @@ expect(found_user.commit_email).to eq(user.email) end + it 'returns the private commit email when commit_email has _private' do + user.update_column(:commit_email, Gitlab::PrivateCommitEmail::TOKEN) + + expect(user.commit_email).to eq(user.private_commit_email) + end + it 'can be set to a confirmed email' do confirmed = create(:email, :confirmed, user: user) user.commit_email = confirmed.email @@ -333,6 +339,40 @@ expect(user).to be_valid end end + + context 'set_commit_email' do + it 'keeps commit email when private commit email is being used' do + user = create(:user, commit_email: Gitlab::PrivateCommitEmail::TOKEN) + + expect(user.read_attribute(:commit_email)).to eq(Gitlab::PrivateCommitEmail::TOKEN) + end + + it 'keeps the commit email when nil' do + user = create(:user, commit_email: nil) + + expect(user.read_attribute(:commit_email)).to be_nil + end + + it 'reverts to nil when email is not verified' do + user = create(:user, commit_email: "foo@bar.com") + + expect(user.read_attribute(:commit_email)).to be_nil + end + end + + context 'owns_commit_email' do + it 'accepts private commit email' do + user = build(:user, commit_email: Gitlab::PrivateCommitEmail::TOKEN) + + expect(user).to be_valid + end + + it 'accepts nil commit email' do + user = build(:user, commit_email: nil) + + expect(user).to be_valid + end + end end end @@ -1075,6 +1115,14 @@ end describe '.find_by_any_email' do + it 'finds user through private commit email' do + user = create(:user) + private_email = user.private_commit_email + + expect(described_class.find_by_any_email(private_email)).to eq(user) + expect(described_class.find_by_any_email(private_email, confirmed: true)).to eq(user) + end + it 'finds by primary email' do user = create(:user, email: 'foo@example.com') @@ -1082,6 +1130,13 @@ expect(described_class.find_by_any_email(user.email, confirmed: true)).to eq user end + it 'finds by uppercased email' do + user = create(:user, email: 'foo@example.com') + + expect(described_class.find_by_any_email(user.email.upcase)).to eq user + expect(described_class.find_by_any_email(user.email.upcase, confirmed: true)).to eq user + end + it 'finds by secondary email' do email = create(:email, email: 'foo@example.com') user = email.user @@ -1457,7 +1512,7 @@ email_confirmed = create :email, user: user, confirmed_at: Time.now create :email, user: user - expect(user.verified_emails).to match_array([user.email, email_confirmed.email]) + expect(user.verified_emails).to match_array([user.email, user.private_commit_email, email_confirmed.email]) end end @@ -1473,6 +1528,10 @@ expect(user.verified_email?(email_confirmed.email.titlecase)).to be_truthy end + it 'returns true when user is found through private commit email' do + expect(user.verified_email?(user.private_commit_email)).to be_truthy + end + it 'returns false when the email is not verified/confirmed' do email_unconfirmed = create :email, user: user user.reload @@ -1668,6 +1727,24 @@ end end + describe '.find_by_private_commit_email' do + context 'with email' do + set(:user) { create(:user) } + + it 'returns user through private commit email' do + expect(described_class.find_by_private_commit_email(user.private_commit_email)).to eq(user) + end + + it 'returns nil when email other than private_commit_email is used' do + expect(described_class.find_by_private_commit_email(user.email)).to be_nil + end + end + + it 'returns nil when email is nil' do + expect(described_class.find_by_private_commit_email(nil)).to be_nil + end + end + describe '#sort_by_attribute' do before do described_class.delete_all