diff options
Diffstat (limited to 'app/models')
-rw-r--r-- | app/models/ci/build.rb | 7 | ||||
-rw-r--r-- | app/models/ci/pipeline.rb | 5 | ||||
-rw-r--r-- | app/models/ci/runner.rb | 10 | ||||
-rw-r--r-- | app/models/ci/trigger_request.rb | 4 | ||||
-rw-r--r-- | app/models/commit.rb | 2 | ||||
-rw-r--r-- | app/models/commit_status.rb | 13 | ||||
-rw-r--r-- | app/models/concerns/issuable.rb | 11 | ||||
-rw-r--r-- | app/models/concerns/resolvable_discussion.rb | 1 | ||||
-rw-r--r-- | app/models/concerns/resolvable_note.rb | 28 | ||||
-rw-r--r-- | app/models/gpg_key.rb | 23 | ||||
-rw-r--r-- | app/models/gpg_signature.rb | 14 | ||||
-rw-r--r-- | app/models/group.rb | 1 | ||||
-rw-r--r-- | app/models/member.rb | 62 | ||||
-rw-r--r-- | app/models/merge_request.rb | 15 | ||||
-rw-r--r-- | app/models/project.rb | 3 | ||||
-rw-r--r-- | app/models/repository.rb | 126 | ||||
-rw-r--r-- | app/models/user.rb | 13 |
17 files changed, 195 insertions, 143 deletions
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 8adaafe6439..28c16d4037f 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -3,6 +3,7 @@ module Ci include TokenAuthenticatable include AfterCommitQueue include Presentable + include Importable belongs_to :runner belongs_to :trigger_request @@ -26,6 +27,7 @@ module Ci validates :coverage, numericality: true, allow_blank: true validates :ref, presence: true + validates :protected, inclusion: { in: [true, false], unless: :importing? }, on: :create scope :unstarted, ->() { where(runner_id: nil) } scope :ignore_failures, ->() { where(allow_failure: false) } @@ -34,6 +36,7 @@ module Ci scope :with_expired_artifacts, ->() { with_artifacts.where('artifacts_expire_at < ?', Time.now) } scope :last_month, ->() { where('created_at > ?', Date.today - 1.month) } scope :manual_actions, ->() { where(when: :manual, status: COMPLETED_STATUSES + [:manual]) } + scope :ref_protected, -> { where(protected: true) } mount_uploader :artifacts_file, ArtifactUploader mount_uploader :artifacts_metadata, ArtifactUploader @@ -448,6 +451,10 @@ module Ci trace end + def serializable_hash(options = {}) + super(options).merge(when: read_attribute(:when)) + end + private def update_artifacts_size diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 90fce6576a5..e72843d085e 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -36,6 +36,7 @@ module Ci validates :sha, presence: { unless: :importing? } validates :ref, presence: { unless: :importing? } validates :status, presence: { unless: :importing? } + validates :protected, inclusion: { in: [true, false], unless: :importing? }, on: :create validate :valid_commit_sha, unless: :importing? after_initialize :set_config_source, if: :new_record? @@ -311,6 +312,10 @@ module Ci @stage_seeds ||= config_processor.stage_seeds(self) end + def has_kubernetes_active? + project.kubernetes_service&.active? + end + def has_stage_seeds? stage_seeds.any? end diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb index 906a76ec560..b1798084787 100644 --- a/app/models/ci/runner.rb +++ b/app/models/ci/runner.rb @@ -5,7 +5,7 @@ module Ci RUNNER_QUEUE_EXPIRY_TIME = 60.minutes ONLINE_CONTACT_TIMEOUT = 1.hour AVAILABLE_SCOPES = %w[specific shared active paused online].freeze - FORM_EDITABLE = %i[description tag_list active run_untagged locked].freeze + FORM_EDITABLE = %i[description tag_list active run_untagged locked access_level].freeze has_many :builds has_many :runner_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent @@ -35,11 +35,17 @@ module Ci end validate :tag_constraints + validates :access_level, presence: true acts_as_taggable after_destroy :cleanup_runner_queue + enum access_level: { + not_protected: 0, + ref_protected: 1 + } + # Searches for runners matching the given query. # # This method uses ILIKE on PostgreSQL and LIKE on MySQL. @@ -106,6 +112,8 @@ module Ci end def can_pick?(build) + return false if self.ref_protected? && !build.protected? + assignable_for?(build.project) && accepting_tags?(build) end diff --git a/app/models/ci/trigger_request.rb b/app/models/ci/trigger_request.rb index c58ce5c3717..2c860598281 100644 --- a/app/models/ci/trigger_request.rb +++ b/app/models/ci/trigger_request.rb @@ -6,6 +6,10 @@ module Ci belongs_to :pipeline, foreign_key: :commit_id has_many :builds + # We switched to Ci::PipelineVariable from Ci::TriggerRequest.variables. + # Ci::TriggerRequest doesn't save variables anymore. + validates :variables, absence: true + serialize :variables # rubocop:disable Cop/ActiveRecordSerialize def user_variables diff --git a/app/models/commit.rb b/app/models/commit.rb index c943365016f..ba3845df867 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -405,6 +405,6 @@ class Commit end def gpg_commit - @gpg_commit ||= Gitlab::Gpg::Commit.for_commit(self) + @gpg_commit ||= Gitlab::Gpg::Commit.new(self) end end diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 842c6e5cb50..f3888528940 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -38,6 +38,14 @@ class CommitStatus < ActiveRecord::Base scope :retried_ordered, -> { retried.ordered.includes(project: :namespace) } scope :after_stage, -> (index) { where('stage_idx > ?', index) } + enum failure_reason: { + unknown_failure: nil, + script_failure: 1, + api_failure: 2, + stuck_or_timeout_failure: 3, + runner_system_failure: 4 + } + state_machine :status do event :process do transition [:skipped, :manual] => :created @@ -79,6 +87,11 @@ class CommitStatus < ActiveRecord::Base commit_status.finished_at = Time.now end + before_transition any => :failed do |commit_status, transition| + failure_reason = transition.args.first + commit_status.failure_reason = failure_reason + end + after_transition do |commit_status, transition| next if transition.loopback? diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 3731b7c8577..681c3241dbb 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -6,6 +6,7 @@ # module Issuable extend ActiveSupport::Concern + include Gitlab::SQL::Pattern include CacheMarkdownField include Participable include Mentionable @@ -122,7 +123,9 @@ module Issuable # # Returns an ActiveRecord::Relation. def search(query) - where(arel_table[:title].matches("%#{query}%")) + title = to_fuzzy_arel(:title, query) + + where(title) end # Searches for records with a matching title or description. @@ -133,10 +136,10 @@ module Issuable # # Returns an ActiveRecord::Relation. def full_search(query) - t = arel_table - pattern = "%#{query}%" + title = to_fuzzy_arel(:title, query) + description = to_fuzzy_arel(:description, query) - where(t[:title].matches(pattern).or(t[:description].matches(pattern))) + where(title&.or(description)) end def sort(method, excluded_labels: []) diff --git a/app/models/concerns/resolvable_discussion.rb b/app/models/concerns/resolvable_discussion.rb index dd979e7bb17..f006a271327 100644 --- a/app/models/concerns/resolvable_discussion.rb +++ b/app/models/concerns/resolvable_discussion.rb @@ -24,6 +24,7 @@ module ResolvableDiscussion delegate :resolved_at, :resolved_by, + :resolved_by_push?, to: :last_resolved_note, allow_nil: true diff --git a/app/models/concerns/resolvable_note.rb b/app/models/concerns/resolvable_note.rb index 05eb6f86704..668c5a079e3 100644 --- a/app/models/concerns/resolvable_note.rb +++ b/app/models/concerns/resolvable_note.rb @@ -51,22 +51,34 @@ module ResolvableNote end # If you update this method remember to also update `.resolve!` - def resolve!(current_user) - return unless resolvable? - return if resolved? + def resolve_without_save(current_user, resolved_by_push: false) + return false unless resolvable? + return false if resolved? self.resolved_at = Time.now self.resolved_by = current_user - save! + self.resolved_by_push = resolved_by_push + + true end # If you update this method remember to also update `.unresolve!` - def unresolve! - return unless resolvable? - return unless resolved? + def unresolve_without_save + return false unless resolvable? + return false unless resolved? self.resolved_at = nil self.resolved_by = nil - save! + + true + end + + def resolve!(current_user, resolved_by_push: false) + resolve_without_save(current_user, resolved_by_push: resolved_by_push) && + save! + end + + def unresolve! + unresolve_without_save && save! end end diff --git a/app/models/gpg_key.rb b/app/models/gpg_key.rb index 3df60ddc950..1633acd4fa9 100644 --- a/app/models/gpg_key.rb +++ b/app/models/gpg_key.rb @@ -56,7 +56,7 @@ class GpgKey < ActiveRecord::Base def verified_user_infos user_infos.select do |user_info| - user_info[:email] == user.email + user.verified_email?(user_info[:email]) end end @@ -64,13 +64,17 @@ class GpgKey < ActiveRecord::Base user_infos.map do |user_info| [ user_info[:email], - user_info[:email] == user.email + user.verified_email?(user_info[:email]) ] end.to_h end def verified? - emails_with_verified_status.any? { |_email, verified| verified } + emails_with_verified_status.values.any? + end + + def verified_and_belongs_to_email?(email) + emails_with_verified_status.fetch(email, false) end def update_invalid_gpg_signatures @@ -78,11 +82,14 @@ class GpgKey < ActiveRecord::Base end def revoke - GpgSignature.where(gpg_key: self, valid_signature: true).update_all( - gpg_key_id: nil, - valid_signature: false, - updated_at: Time.zone.now - ) + GpgSignature + .where(gpg_key: self) + .where.not(verification_status: GpgSignature.verification_statuses[:unknown_key]) + .update_all( + gpg_key_id: nil, + verification_status: GpgSignature.verification_statuses[:unknown_key], + updated_at: Time.zone.now + ) destroy end diff --git a/app/models/gpg_signature.rb b/app/models/gpg_signature.rb index 50fb35c77ec..454c90d5fc4 100644 --- a/app/models/gpg_signature.rb +++ b/app/models/gpg_signature.rb @@ -1,9 +1,21 @@ class GpgSignature < ActiveRecord::Base include ShaAttribute + include IgnorableColumn + + ignore_column :valid_signature sha_attribute :commit_sha sha_attribute :gpg_key_primary_keyid + enum verification_status: { + unverified: 0, + verified: 1, + same_user_different_email: 2, + other_user: 3, + unverified_key: 4, + unknown_key: 5 + } + belongs_to :project belongs_to :gpg_key @@ -20,6 +32,6 @@ class GpgSignature < ActiveRecord::Base end def gpg_commit - Gitlab::Gpg::Commit.new(project, commit_sha) + Gitlab::Gpg::Commit.new(commit) end end diff --git a/app/models/group.rb b/app/models/group.rb index 190b27cf66b..e746e4a12c9 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -16,6 +16,7 @@ class Group < Namespace source: :user has_many :requesters, -> { where.not(requested_at: nil) }, dependent: :destroy, as: :source, class_name: 'GroupMember' # rubocop:disable Cop/ActiveRecordDependent + has_many :members_and_requesters, as: :source, class_name: 'GroupMember' has_many :milestones has_many :project_group_links, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent diff --git a/app/models/member.rb b/app/models/member.rb index ee2cb13697b..cbbd58f2eaf 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -126,20 +126,11 @@ class Member < ActiveRecord::Base find_by(invite_token: invite_token) end - def add_user(source, user, access_level, current_user: nil, expires_at: nil) - user = retrieve_user(user) + def add_user(source, user, access_level, existing_members: nil, current_user: nil, expires_at: nil) + # `user` can be either a User object, User ID or an email to be invited + member = retrieve_member(source, user, existing_members) access_level = retrieve_access_level(access_level) - # `user` can be either a User object or an email to be invited - member = - if user.is_a?(User) - source.members.find_by(user_id: user.id) || - source.requesters.find_by(user_id: user.id) || - source.members.build(user_id: user.id) - else - source.members.build(invite_email: user) - end - return member unless can_update_member?(current_user, member) member.attributes = { @@ -165,17 +156,15 @@ class Member < ActiveRecord::Base def add_users(source, users, access_level, current_user: nil, expires_at: nil) return [] unless users.present? - # Collect all user ids into separate array - # so we can use single sql query to get user objects - user_ids = users.select { |user| user =~ /\A\d+\Z/ } - users = users - user_ids + User.where(id: user_ids) + emails, users, existing_members = parse_users_list(source, users) self.transaction do - users.map do |user| + (emails + users).map! do |user| add_user( source, user, access_level, + existing_members: existing_members, current_user: current_user, expires_at: expires_at ) @@ -189,6 +178,31 @@ class Member < ActiveRecord::Base private + def parse_users_list(source, list) + emails, user_ids, users = [], [], [] + existing_members = {} + + list.each do |item| + case item + when User + users << item + when Integer + user_ids << item + when /\A\d+\Z/ + user_ids << item.to_i + when Devise.email_regexp + emails << item + end + end + + if user_ids.present? + users.concat(User.where(id: user_ids)) + existing_members = source.members_and_requesters.where(user_id: user_ids).index_by(&:user_id) + end + + [emails, users, existing_members] + end + # This method is used to find users that have been entered into the "Add members" field. # These can be the User objects directly, their IDs, their emails, or new emails to be invited. def retrieve_user(user) @@ -197,6 +211,20 @@ class Member < ActiveRecord::Base User.find_by(id: user) || User.find_by(email: user) || user end + def retrieve_member(source, user, existing_members) + user = retrieve_user(user) + + if user.is_a?(User) + if existing_members + existing_members[user.id] || source.members.build(user_id: user.id) + else + source.members_and_requesters.find_or_initialize_by(user_id: user.id) + end + else + source.members.build(invite_email: user) + end + end + def retrieve_access_level(access_level) access_levels.fetch(access_level) { access_level.to_i } end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 7a817eedec2..b82f49d7073 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -918,6 +918,12 @@ class MergeRequest < ActiveRecord::Base active_diff_discussions.each do |discussion| service.execute(discussion) end + + if project.resolve_outdated_diff_discussions? + MergeRequests::ResolvedDiscussionNotificationService + .new(project, current_user) + .execute(self) + end end def keep_around_commit @@ -957,13 +963,6 @@ class MergeRequest < ActiveRecord::Base private def write_ref - target_project.repository.with_repo_branch_commit( - source_project.repository, source_branch) do |commit| - if commit - target_project.repository.write_ref(ref_path, commit.sha) - else - raise Rugged::ReferenceError, 'source repository is empty' - end - end + target_project.repository.fetch_source_branch(source_project.repository, source_branch, ref_path) end end diff --git a/app/models/project.rb b/app/models/project.rb index 1354c988a47..9c1a2a15d2a 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -37,6 +37,7 @@ class Project < ActiveRecord::Base default_value_for :archived, false default_value_for :visibility_level, gitlab_config_features.visibility_level + default_value_for :resolve_outdated_diff_discussions, false default_value_for :container_registry_enabled, gitlab_config_features.container_registry default_value_for(:repository_storage) { current_application_settings.pick_repository_storage } default_value_for(:shared_runners_enabled) { current_application_settings.shared_runners_enabled } @@ -68,7 +69,6 @@ class Project < ActiveRecord::Base acts_as_taggable - attr_accessor :new_default_branch attr_accessor :old_path_with_namespace attr_accessor :template_name attr_writer :pipeline_status @@ -145,6 +145,7 @@ class Project < ActiveRecord::Base has_many :requesters, -> { where.not(requested_at: nil) }, as: :source, class_name: 'ProjectMember', dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent + has_many :members_and_requesters, as: :source, class_name: 'ProjectMember' has_many :deploy_keys_projects has_many :deploy_keys, through: :deploy_keys_projects diff --git a/app/models/repository.rb b/app/models/repository.rb index 5474c8eeb68..035f85a0b46 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -20,7 +20,6 @@ class Repository delegate :ref_name_for_sha, to: :raw_repository - CommitError = Class.new(StandardError) CreateTreeError = Class.new(StandardError) # Methods that cache data from the Git repository. @@ -95,19 +94,6 @@ class Repository "#<#{self.class.name}:#{@disk_path}>" end - # - # Git repository can contains some hidden refs like: - # /refs/notes/* - # /refs/git-as-svn/* - # /refs/pulls/* - # This refs by default not visible in project page and not cloned to client side. - # - # This method return true if repository contains some content visible in project page. - # - def has_visible_content? - branch_count > 0 - end - def commit(ref = 'HEAD') return nil unless exists? @@ -180,32 +166,25 @@ class Repository end def add_branch(user, branch_name, ref) - newrev = commit(ref).try(:sha) - - return false unless newrev - - GitOperationService.new(user, self).add_branch(branch_name, newrev) + branch = raw_repository.add_branch(branch_name, committer: user, target: ref) after_create_branch - find_branch(branch_name) + + branch + rescue Gitlab::Git::Repository::InvalidRef + false end def add_tag(user, tag_name, target, message = nil) - newrev = commit(target).try(:id) - options = { message: message, tagger: user_to_committer(user) } if message - - return false unless newrev - - GitOperationService.new(user, self).add_tag(tag_name, newrev, options) - - find_tag(tag_name) + raw_repository.add_tag(tag_name, committer: user, target: target, message: message) + rescue Gitlab::Git::Repository::InvalidRef + false end def rm_branch(user, branch_name) before_remove_branch - branch = find_branch(branch_name) - GitOperationService.new(user, self).rm_branch(branch) + raw_repository.rm_branch(branch_name, committer: user) after_remove_branch true @@ -213,9 +192,8 @@ class Repository def rm_tag(user, tag_name) before_remove_tag - tag = find_tag(tag_name) - GitOperationService.new(user, self).rm_tag(tag) + raw_repository.rm_tag(tag_name, committer: user) after_remove_tag true @@ -784,16 +762,30 @@ class Repository multi_action(**options) end + def with_branch(user, *args) + result = Gitlab::Git::OperationService.new(user, raw_repository).with_branch(*args) do |start_commit| + yield start_commit + end + + newrev, should_run_after_create, should_run_after_create_branch = result + + after_create if should_run_after_create + after_create_branch if should_run_after_create_branch + + newrev + end + # rubocop:disable Metrics/ParameterLists def multi_action( user:, branch_name:, message:, actions:, author_email: nil, author_name: nil, start_branch_name: nil, start_project: project) - GitOperationService.new(user, self).with_branch( + with_branch( + user, branch_name, start_branch_name: start_branch_name, - start_project: start_project) do |start_commit| + start_repository: start_project.repository.raw_repository) do |start_commit| index = Gitlab::Git::Index.new(raw_repository) @@ -846,7 +838,8 @@ class Repository end def merge(user, source, merge_request, options = {}) - GitOperationService.new(user, self).with_branch( + with_branch( + user, merge_request.target_branch) do |start_commit| our_commit = start_commit.sha their_commit = source @@ -866,17 +859,18 @@ class Repository merge_request.update(in_progress_merge_commit_sha: commit_id) commit_id end - rescue Repository::CommitError # when merge_index.conflicts? + rescue Gitlab::Git::CommitError # when merge_index.conflicts? false end def revert( user, commit, branch_name, start_branch_name: nil, start_project: project) - GitOperationService.new(user, self).with_branch( + with_branch( + user, branch_name, start_branch_name: start_branch_name, - start_project: start_project) do |start_commit| + start_repository: start_project.repository.raw_repository) do |start_commit| revert_tree_id = check_revert_content(commit, start_commit.sha) unless revert_tree_id @@ -896,10 +890,11 @@ class Repository def cherry_pick( user, commit, branch_name, start_branch_name: nil, start_project: project) - GitOperationService.new(user, self).with_branch( + with_branch( + user, branch_name, start_branch_name: start_branch_name, - start_project: start_project) do |start_commit| + start_repository: start_project.repository.raw_repository) do |start_commit| cherry_pick_tree_id = check_cherry_pick_content(commit, start_commit.sha) unless cherry_pick_tree_id @@ -921,7 +916,7 @@ class Repository end def resolve_conflicts(user, branch_name, params) - GitOperationService.new(user, self).with_branch(branch_name) do + with_branch(user, branch_name) do committer = user_to_committer(user) create_commit(params.merge(author: committer, committer: committer)) @@ -1011,25 +1006,6 @@ class Repository run_git(args).first.lines.map(&:strip) end - def with_repo_branch_commit(start_repository, start_branch_name) - return yield nil if start_repository.empty_repo? - - if start_repository == self - yield commit(start_branch_name) - else - sha = start_repository.commit(start_branch_name).sha - - if branch_commit = commit(sha) - yield branch_commit - else - with_repo_tmp_commit( - start_repository, start_branch_name, sha) do |tmp_commit| - yield tmp_commit - end - end - end - end - def add_remote(name, url) raw_repository.remote_add(name, url) rescue Rugged::ConfigError @@ -1047,14 +1023,12 @@ class Repository gitlab_shell.fetch_remote(raw_repository, remote, forced: forced, no_tags: no_tags) end - def fetch_ref(source_path, source_ref, target_ref) - args = %W(fetch --no-tags -f #{source_path} #{source_ref}:#{target_ref}) - message, status = run_git(args) - - # Make sure ref was created, and raise Rugged::ReferenceError when not - raise Rugged::ReferenceError, message if status != 0 + def fetch_source_branch(source_repository, source_branch, local_ref) + raw_repository.fetch_source_branch(source_repository.raw_repository, source_branch, local_ref) + end - target_ref + def compare_source_branch(target_branch_name, source_repository, source_branch_name, straight:) + raw_repository.compare_source_branch(target_branch_name, source_repository.raw_repository, source_branch_name, straight: straight) end def create_ref(ref, ref_path) @@ -1135,12 +1109,6 @@ class Repository private - def run_git(args) - circuit_breaker.perform do - Gitlab::Popen.popen([Gitlab.config.git.bin_path, *args], path_to_repo) - end - end - def blob_data_at(sha, path) blob = blob_at(sha, path) return unless blob @@ -1236,16 +1204,4 @@ class Repository .commits_by_message(query, revision: ref, path: path, limit: limit, offset: offset) .map { |c| commit(c) } end - - def with_repo_tmp_commit(start_repository, start_branch_name, sha) - tmp_ref = fetch_ref( - start_repository.path_to_repo, - "#{Gitlab::Git::BRANCH_REF_PREFIX}#{start_branch_name}", - "refs/tmp/#{SecureRandom.hex}/head" - ) - - yield commit(sha) - ensure - delete_refs(tmp_ref) if tmp_ref - end end diff --git a/app/models/user.rb b/app/models/user.rb index 68ec93a3ec5..c5b5f09722f 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -644,11 +644,6 @@ class User < ActiveRecord::Base @personal_projects_count ||= personal_projects.count end - def projects_limit_percent - return 100 if projects_limit.zero? - (personal_projects.count.to_f / projects_limit) * 100 - end - def recent_push(project_ids = nil) # Get push events not earlier than 2 hours ago events = recent_events.code_push.where("created_at > ?", Time.now - 2.hours) @@ -666,10 +661,6 @@ class User < ActiveRecord::Base end end - def projects_sorted_by_activity - authorized_projects.sorted_by_activity - end - def several_namespaces? owned_groups.any? || masters_groups.any? end @@ -1050,6 +1041,10 @@ class User < ActiveRecord::Base ensure_rss_token! end + def verified_email?(email) + self.email == email + end + protected # override, from Devise::Validatable |