diff options
author | James Lopez <james@jameslopez.es> | 2016-03-14 12:32:46 +0100 |
---|---|---|
committer | James Lopez <james@jameslopez.es> | 2016-03-14 12:32:46 +0100 |
commit | 25e078c114ee973dddc6410145c09cf7cad640d6 (patch) | |
tree | 817edd01ecaa4664ffc4b2778309e748abdb4142 /app/models | |
parent | 37c68fe025b3b4dd4ac3b510cbb32d42cbe6b23b (diff) | |
parent | ba1fcf36197b86049b5e6a0f2da8842b236bcf62 (diff) | |
download | gitlab-ce-25e078c114ee973dddc6410145c09cf7cad640d6.tar.gz |
Merge branches 'feature/project-export' and 'feature/project-import' of gitlab.com:gitlab-org/gitlab-ce into feature/project-import
Diffstat (limited to 'app/models')
-rw-r--r-- | app/models/ability.rb | 5 | ||||
-rw-r--r-- | app/models/blob.rb | 3 | ||||
-rw-r--r-- | app/models/ci/runner.rb | 20 | ||||
-rw-r--r-- | app/models/concerns/issuable.rb | 24 | ||||
-rw-r--r-- | app/models/concerns/milestoneish.rb | 25 | ||||
-rw-r--r-- | app/models/global_label.rb | 7 | ||||
-rw-r--r-- | app/models/global_milestone.rb | 55 | ||||
-rw-r--r-- | app/models/group.rb | 12 | ||||
-rw-r--r-- | app/models/merge_request.rb | 21 | ||||
-rw-r--r-- | app/models/merge_request_diff.rb | 2 | ||||
-rw-r--r-- | app/models/milestone.rb | 45 | ||||
-rw-r--r-- | app/models/namespace.rb | 12 | ||||
-rw-r--r-- | app/models/note.rb | 19 | ||||
-rw-r--r-- | app/models/personal_snippet.rb | 1 | ||||
-rw-r--r-- | app/models/project.rb | 45 | ||||
-rw-r--r-- | app/models/project_services/ci_service.rb | 2 | ||||
-rw-r--r-- | app/models/project_snippet.rb | 3 | ||||
-rw-r--r-- | app/models/repository.rb | 55 | ||||
-rw-r--r-- | app/models/snippet.rb | 31 | ||||
-rw-r--r-- | app/models/user.rb | 23 |
20 files changed, 288 insertions, 122 deletions
diff --git a/app/models/ability.rb b/app/models/ability.rb index f34554d557c..fe9e0aab717 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -9,6 +9,7 @@ class Ability when CommitStatus then commit_status_abilities(user, subject) when Project then project_abilities(user, subject) when Issue then issue_abilities(user, subject) + when ExternalIssue then external_issue_abilities(user, subject) when Note then note_abilities(user, subject) when ProjectSnippet then project_snippet_abilities(user, subject) when PersonalSnippet then personal_snippet_abilities(user, subject) @@ -424,6 +425,10 @@ class Ability end end + def external_issue_abilities(user, subject) + project_abilities(user, subject.project) + end + private def named_abilities(name) diff --git a/app/models/blob.rb b/app/models/blob.rb index 8ee9f3006b2..72e6c5fa3fd 100644 --- a/app/models/blob.rb +++ b/app/models/blob.rb @@ -1,5 +1,8 @@ # Blob is a Rails-specific wrapper around Gitlab::Git::Blob objects class Blob < SimpleDelegator + CACHE_TIME = 60 # Cache raw blobs referred to by a (mutable) ref for 1 minute + CACHE_TIME_IMMUTABLE = 3600 # Cache blobs referred to by an immutable reference for 1 hour + # Wrap a Gitlab::Git::Blob object, or return nil when given nil # # This method prevents the decorated object from evaluating to "truthy" when diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb index e725a6d468c..90349a07594 100644 --- a/app/models/ci/runner.rb +++ b/app/models/ci/runner.rb @@ -23,7 +23,7 @@ module Ci LAST_CONTACT_TIME = 5.minutes.ago AVAILABLE_SCOPES = ['specific', 'shared', 'active', 'paused', 'online'] - + has_many :builds, class_name: 'Ci::Build' has_many :runner_projects, dependent: :destroy, class_name: 'Ci::RunnerProject' has_many :projects, through: :runner_projects, class_name: '::Project', foreign_key: :gl_project_id @@ -46,9 +46,23 @@ module Ci acts_as_taggable + # Searches for runners matching the given query. + # + # This method uses ILIKE on PostgreSQL and LIKE on MySQL. + # + # This method performs a *partial* match on tokens, thus a query for "a" + # will match any runner where the token contains the letter "a". As a result + # you should *not* use this method for non-admin purposes as otherwise users + # might be able to query a list of all runners. + # + # query - The search query as a String + # + # Returns an ActiveRecord::Relation. def self.search(query) - where('LOWER(ci_runners.token) LIKE :query OR LOWER(ci_runners.description) like :query', - query: "%#{query.try(:downcase)}%") + t = arel_table + pattern = "%#{query}%" + + where(t[:token].matches(pattern).or(t[:description].matches(pattern))) end def set_default_values diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 286d6655861..3c42f582937 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -29,12 +29,15 @@ module Issuable scope :assigned, -> { where("assignee_id IS NOT NULL") } scope :unassigned, -> { where("assignee_id IS NULL") } scope :of_projects, ->(ids) { where(project_id: ids) } + scope :of_milestones, ->(ids) { where(milestone_id: ids) } scope :opened, -> { with_state(:opened, :reopened) } scope :only_opened, -> { with_state(:opened) } scope :only_reopened, -> { with_state(:reopened) } scope :closed, -> { with_state(:closed) } scope :order_milestone_due_desc, -> { joins(:milestone).reorder('milestones.due_date DESC, milestones.id DESC') } scope :order_milestone_due_asc, -> { joins(:milestone).reorder('milestones.due_date ASC, milestones.id ASC') } + scope :with_label, ->(title) { joins(:labels).where(labels: { title: title }) } + scope :without_label, -> { joins("LEFT OUTER JOIN label_links ON label_links.target_type = '#{name}' AND label_links.target_id = #{table_name}.id").where(label_links: { id: nil }) } scope :join_project, -> { joins(:project) } scope :references_project, -> { references(:project) } @@ -58,12 +61,29 @@ module Issuable end module ClassMethods + # Searches for records with a matching title. + # + # This method uses ILIKE on PostgreSQL and LIKE on MySQL. + # + # query - The search query as a String + # + # Returns an ActiveRecord::Relation. def search(query) - where("LOWER(title) like :query", query: "%#{query.downcase}%") + where(arel_table[:title].matches("%#{query}%")) end + # Searches for records with a matching title or description. + # + # This method uses ILIKE on PostgreSQL and LIKE on MySQL. + # + # query - The search query as a String + # + # Returns an ActiveRecord::Relation. def full_search(query) - where("LOWER(title) like :query OR LOWER(description) like :query", query: "%#{query.downcase}%") + t = arel_table + pattern = "%#{query}%" + + where(t[:title].matches(pattern).or(t[:description].matches(pattern))) end def sort(method) diff --git a/app/models/concerns/milestoneish.rb b/app/models/concerns/milestoneish.rb new file mode 100644 index 00000000000..d67df7c1d9c --- /dev/null +++ b/app/models/concerns/milestoneish.rb @@ -0,0 +1,25 @@ +module Milestoneish + def closed_items_count + issues.closed.size + merge_requests.closed_and_merged.size + end + + def total_items_count + issues.size + merge_requests.size + end + + def complete? + total_items_count == closed_items_count + end + + def percent_complete + ((closed_items_count * 100) / total_items_count).abs + rescue ZeroDivisionError + 0 + end + + def remaining_days + return 0 if !due_date || expired? + + (due_date - Date.today).to_i + end +end diff --git a/app/models/global_label.rb b/app/models/global_label.rb index 0171f7d54b7..ddd4bad5c21 100644 --- a/app/models/global_label.rb +++ b/app/models/global_label.rb @@ -2,16 +2,19 @@ class GlobalLabel attr_accessor :title, :labels alias_attribute :name, :title + delegate :color, :description, to: :@first_label + def self.build_collection(labels) labels = labels.group_by(&:title) - labels.map do |title, label| - new(title, label) + labels.map do |title, labels| + new(title, labels) end end def initialize(title, labels) @title = title @labels = labels + @first_label = labels.find { |lbl| lbl.description.present? } || labels.first end end diff --git a/app/models/global_milestone.rb b/app/models/global_milestone.rb index 7ee276255a0..97bd79af083 100644 --- a/app/models/global_milestone.rb +++ b/app/models/global_milestone.rb @@ -1,4 +1,6 @@ class GlobalMilestone + include Milestoneish + attr_accessor :title, :milestones alias_attribute :name, :title @@ -28,33 +30,7 @@ class GlobalMilestone end def projects - milestones.map { |milestone| milestone.project } - end - - def issue_count - milestones.map { |milestone| milestone.issues.count }.sum - end - - def merge_requests_count - milestones.map { |milestone| milestone.merge_requests.count }.sum - end - - def open_items_count - milestones.map { |milestone| milestone.open_items_count }.sum - end - - def closed_items_count - milestones.map { |milestone| milestone.closed_items_count }.sum - end - - def total_items_count - milestones.map { |milestone| milestone.total_items_count }.sum - end - - def percent_complete - ((closed_items_count * 100) / total_items_count).abs - rescue ZeroDivisionError - 0 + @projects ||= Project.for_milestones(milestones.map(&:id)) end def state @@ -76,35 +52,20 @@ class GlobalMilestone end def issues - @issues ||= milestones.map(&:issues).flatten.group_by(&:state) + @issues ||= Issue.of_milestones(milestones.map(&:id)).includes(:project) end def merge_requests - @merge_requests ||= milestones.map(&:merge_requests).flatten.group_by(&:state) + @merge_requests ||= MergeRequest.of_milestones(milestones.map(&:id)).includes(:target_project) end def participants @participants ||= milestones.map(&:participants).flatten.compact.uniq end - def opened_issues - issues.values_at("opened", "reopened").compact.flatten - end - - def closed_issues - issues['closed'] - end - - def opened_merge_requests - merge_requests.values_at("opened", "reopened").compact.flatten - end - - def closed_merge_requests - merge_requests.values_at("closed", "merged", "locked").compact.flatten - end - - def complete? - total_items_count == closed_items_count + def labels + @labels ||= GlobalLabel.build_collection(milestones.map(&:labels).flatten) + .sort_by!(&:title) end def due_date diff --git a/app/models/group.rb b/app/models/group.rb index 76042b3e3fd..afbc2922013 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -33,8 +33,18 @@ class Group < Namespace after_destroy :post_destroy_hook class << self + # Searches for groups matching the given query. + # + # This method uses ILIKE on PostgreSQL and LIKE on MySQL. + # + # query - The search query as a String + # + # Returns an ActiveRecord::Relation. def search(query) - where("LOWER(namespaces.name) LIKE :query or LOWER(namespaces.path) LIKE :query", query: "%#{query.downcase}%") + table = Namespace.arel_table + pattern = "%#{query}%" + + where(table[:name].matches(pattern).or(table[:path].matches(pattern))) end def sort(method) diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index d9fbbb85003..4da2d829fa0 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -137,11 +137,8 @@ class MergeRequest < ActiveRecord::Base scope :by_branch, ->(branch_name) { where("(source_branch LIKE :branch) OR (target_branch LIKE :branch)", branch: branch_name) } scope :cared, ->(user) { where('assignee_id = :user OR author_id = :user', user: user.id) } scope :by_milestone, ->(milestone) { where(milestone_id: milestone) } - scope :in_projects, ->(project_ids) { where("source_project_id in (:project_ids) OR target_project_id in (:project_ids)", project_ids: project_ids) } scope :of_projects, ->(ids) { where(target_project_id: ids) } - scope :opened, -> { with_states(:opened, :reopened) } scope :merged, -> { with_state(:merged) } - scope :closed, -> { with_state(:closed) } scope :closed_and_merged, -> { with_states(:closed, :merged) } scope :join_project, -> { joins(:target_project) } @@ -165,6 +162,24 @@ class MergeRequest < ActiveRecord::Base super("merge_requests", /(?<merge_request>\d+)/) end + # Returns all the merge requests from an ActiveRecord:Relation. + # + # This method uses a UNION as it usually operates on the result of + # ProjectsFinder#execute. PostgreSQL in particular doesn't always like queries + # using multiple sub-queries especially when combined with an OR statement. + # UNIONs on the other hand perform much better in these cases. + # + # relation - An ActiveRecord::Relation that returns a list of Projects. + # + # Returns an ActiveRecord::Relation. + def self.in_projects(relation) + source = where(source_project_id: relation).select(:id) + target = where(target_project_id: relation).select(:id) + union = Gitlab::SQL::Union.new([source, target]) + + where("merge_requests.id IN (#{union.to_sql})") + end + def to_reference(from_project = nil) reference = "#{self.class.reference_prefix}#{iid}" diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index df08d3a6dfb..33884118595 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -17,7 +17,7 @@ class MergeRequestDiff < ActiveRecord::Base include Sortable # Prevent store of diff if commits amount more then 500 - COMMITS_SAFE_SIZE = 500 + COMMITS_SAFE_SIZE = 100 belongs_to :merge_request diff --git a/app/models/milestone.rb b/app/models/milestone.rb index 7dc2f909b2f..374590ba0c5 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -19,17 +19,19 @@ class Milestone < ActiveRecord::Base MilestoneStruct = Struct.new(:title, :name, :id) None = MilestoneStruct.new('No Milestone', 'No Milestone', 0) Any = MilestoneStruct.new('Any Milestone', '', -1) + Upcoming = MilestoneStruct.new('Upcoming', '#upcoming', -2) include InternalId include Sortable include Referable include StripAttribute + include Milestoneish belongs_to :project has_many :issues has_many :labels, -> { distinct.reorder('labels.title') }, through: :issues has_many :merge_requests - has_many :participants, through: :issues, source: :assignee + has_many :participants, -> { distinct.reorder('users.name') }, through: :issues, source: :assignee scope :active, -> { with_state(:active) } scope :closed, -> { with_state(:closed) } @@ -57,9 +59,18 @@ class Milestone < ActiveRecord::Base alias_attribute :name, :title class << self + # Searches for milestones matching the given query. + # + # This method uses ILIKE on PostgreSQL and LIKE on MySQL. + # + # query - The search query as a String + # + # Returns an ActiveRecord::Relation. def search(query) - query = "%#{query}%" - where("title like ? or description like ?", query, query) + t = arel_table + pattern = "%#{query}%" + + where(t[:title].matches(pattern).or(t[:description].matches(pattern))) end end @@ -71,6 +82,10 @@ class Milestone < ActiveRecord::Base super("milestones", /(?<milestone>\d+)/) end + def self.upcoming + self.where('due_date > ?', Time.now).order(due_date: :asc).first + end + def to_reference(from_project = nil) escaped_title = self.title.gsub("]", "\\]") @@ -92,30 +107,6 @@ class Milestone < ActiveRecord::Base end end - def open_items_count - self.issues.opened.count + self.merge_requests.opened.count - end - - def closed_items_count - self.issues.closed.count + self.merge_requests.closed_and_merged.count - end - - def total_items_count - self.issues.count + self.merge_requests.count - end - - def percent_complete - ((closed_items_count * 100) / total_items_count).abs - rescue ZeroDivisionError - 0 - end - - def remaining_days - return 0 if !due_date || expired? - - (due_date - Date.today).to_i - end - def expires_at if due_date if due_date.past? diff --git a/app/models/namespace.rb b/app/models/namespace.rb index bdb33f37495..55842df1e2d 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -52,8 +52,18 @@ class Namespace < ActiveRecord::Base find_by("lower(path) = :path OR lower(name) = :path", path: path.downcase) end + # Searches for namespaces matching the given query. + # + # This method uses ILIKE on PostgreSQL and LIKE on MySQL. + # + # query - The search query as a String + # + # Returns an ActiveRecord::Relation def search(query) - where("name LIKE :query OR path LIKE :query", query: "%#{query}%") + t = arel_table + pattern = "%#{query}%" + + where(t[:name].matches(pattern).or(t[:path].matches(pattern))) end def clean_path(path) diff --git a/app/models/note.rb b/app/models/note.rb index 3b20d5d22b6..2e084b5c80c 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -44,6 +44,7 @@ class Note < ActiveRecord::Base delegate :name, :email, to: :author, prefix: true before_validation :set_award! + before_validation :clear_blank_line_code! validates :note, :project, presence: true validates :note, uniqueness: { scope: [:author, :noteable_type, :noteable_id] }, if: ->(n) { n.is_award } @@ -63,7 +64,7 @@ class Note < ActiveRecord::Base scope :nonawards, ->{ where(is_award: false) } scope :for_commit_id, ->(commit_id) { where(noteable_type: "Commit", commit_id: commit_id) } scope :inline, ->{ where("line_code IS NOT NULL") } - scope :not_inline, ->{ where(line_code: [nil, '']) } + scope :not_inline, ->{ where(line_code: nil) } scope :system, ->{ where(system: true) } scope :user, ->{ where(system: false) } scope :common, ->{ where(noteable_type: ["", nil]) } @@ -105,8 +106,18 @@ class Note < ActiveRecord::Base [:discussion, type.try(:underscore), id, line_code].join("-").to_sym end + # Searches for notes matching the given query. + # + # This method uses ILIKE on PostgreSQL and LIKE on MySQL. + # + # query - The search query as a String. + # + # Returns an ActiveRecord::Relation. def search(query) - where("LOWER(note) like :query", query: "%#{query.downcase}%") + table = arel_table + pattern = "%#{query}%" + + where(table[:note].matches(pattern)) end def grouped_awards @@ -365,6 +376,10 @@ class Note < ActiveRecord::Base private + def clear_blank_line_code! + self.line_code = nil if self.line_code.blank? + end + def awards_supported? (for_issue? || for_merge_request?) && !for_diff_line? end diff --git a/app/models/personal_snippet.rb b/app/models/personal_snippet.rb index 9cee3b70cb3..452f3913eef 100644 --- a/app/models/personal_snippet.rb +++ b/app/models/personal_snippet.rb @@ -10,7 +10,6 @@ # created_at :datetime # updated_at :datetime # file_name :string(255) -# expires_at :datetime # type :string(255) # visibility_level :integer default(0), not null # diff --git a/app/models/project.rb b/app/models/project.rb index 148eab692ff..1f18ad78164 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -151,6 +151,7 @@ class Project < ActiveRecord::Base has_many :releases, dependent: :destroy has_many :lfs_objects_projects, dependent: :destroy has_many :lfs_objects, through: :lfs_objects_projects + has_many :todos, dependent: :destroy has_one :import_data, dependent: :destroy, class_name: "ProjectImportData" @@ -215,6 +216,7 @@ class Project < ActiveRecord::Base scope :public_only, -> { where(visibility_level: Project::PUBLIC) } scope :public_and_internal_only, -> { where(visibility_level: Project.public_and_internal_levels) } scope :non_archived, -> { where(archived: false) } + scope :for_milestones, ->(ids) { joins(:milestones).where('milestones.id' => ids).distinct } state_machine :import_status, initial: :none do event :import_start do @@ -264,13 +266,31 @@ class Project < ActiveRecord::Base joins(:issues, :notes, :merge_requests).order('issues.created_at, notes.created_at, merge_requests.created_at DESC') end + # Searches for a list of projects based on the query given in `query`. + # + # On PostgreSQL this method uses "ILIKE" to perform a case-insensitive + # search. On MySQL a regular "LIKE" is used as it's already + # case-insensitive. + # + # query - The search query as a String. def search(query) - joins(:namespace). - where('LOWER(projects.name) LIKE :query OR - LOWER(projects.path) LIKE :query OR - LOWER(namespaces.name) LIKE :query OR - LOWER(projects.description) LIKE :query', - query: "%#{query.try(:downcase)}%") + ptable = arel_table + ntable = Namespace.arel_table + pattern = "%#{query}%" + + projects = select(:id).where( + ptable[:path].matches(pattern). + or(ptable[:name].matches(pattern)). + or(ptable[:description].matches(pattern)) + ) + + namespaces = select(:id). + joins(:namespace). + where(ntable[:name].matches(pattern)) + + union = Gitlab::SQL::Union.new([projects, namespaces]) + + where("projects.id IN (#{union.to_sql})") end def search_by_visibility(level) @@ -278,7 +298,10 @@ class Project < ActiveRecord::Base end def search_by_title(query) - non_archived.where('LOWER(projects.name) LIKE :query', query: "%#{query.downcase}%") + pattern = "%#{query}%" + table = Project.arel_table + + non_archived.where(table[:name].matches(pattern)) end def find_with_namespace(id) @@ -526,11 +549,11 @@ class Project < ActiveRecord::Base end def ci_services - services.select { |service| service.category == :ci } + services.where(category: :ci) end def ci_service - @ci_service ||= ci_services.find(&:activated?) + @ci_service ||= ci_services.reorder(nil).find_by(active: true) end def jira_tracker? @@ -907,13 +930,13 @@ class Project < ActiveRecord::Base end def valid_runners_token? token - self.runners_token && self.runners_token == token + self.runners_token && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.runners_token) end # TODO (ayufan): For now we use runners_token (backward compatibility) # In 8.4 every build will have its own individual token valid for time of build def valid_build_token? token - self.builds_enabled? && self.runners_token && self.runners_token == token + self.builds_enabled? && self.runners_token && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.runners_token) end def build_coverage_enabled? diff --git a/app/models/project_services/ci_service.rb b/app/models/project_services/ci_service.rb index e10b5529b42..d9f0849d147 100644 --- a/app/models/project_services/ci_service.rb +++ b/app/models/project_services/ci_service.rb @@ -26,7 +26,7 @@ class CiService < Service default_value_for :category, 'ci' def valid_token?(token) - self.respond_to?(:token) && self.token.present? && self.token == token + self.respond_to?(:token) && self.token.present? && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.token) end def supported_events diff --git a/app/models/project_snippet.rb b/app/models/project_snippet.rb index 9e2c1b0e18e..1f7d85a5f3d 100644 --- a/app/models/project_snippet.rb +++ b/app/models/project_snippet.rb @@ -10,7 +10,6 @@ # created_at :datetime # updated_at :datetime # file_name :string(255) -# expires_at :datetime # type :string(255) # visibility_level :integer default(0), not null # @@ -23,6 +22,4 @@ class ProjectSnippet < Snippet # Scopes scope :fresh, -> { order("created_at DESC") } - scope :non_expired, -> { where(["expires_at IS NULL OR expires_at > ?", Time.current]) } - scope :expired, -> { where(["expires_at IS NOT NULL AND expires_at < ?", Time.current]) } end diff --git a/app/models/repository.rb b/app/models/repository.rb index c135ab61f6a..6441cd87e87 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -133,18 +133,18 @@ class Repository rugged.branches.create(branch_name, target) end - expire_branches_cache + after_create_branch find_branch(branch_name) end def add_tag(tag_name, ref, message = nil) - expire_tags_cache + before_push_tag gitlab_shell.add_tag(path_with_namespace, tag_name, ref, message) end def rm_branch(user, branch_name) - expire_branches_cache + before_remove_branch branch = find_branch(branch_name) oldrev = branch.try(:target) @@ -155,12 +155,12 @@ class Repository rugged.branches.delete(branch_name) end - expire_branches_cache + after_remove_branch true end def rm_tag(tag_name) - expire_tags_cache + before_remove_tag gitlab_shell.rm_tag(path_with_namespace, tag_name) end @@ -183,6 +183,14 @@ class Repository end end + def branch_count + @branch_count ||= cache.fetch(:branch_count) { raw_repository.branch_count } + end + + def tag_count + @tag_count ||= cache.fetch(:tag_count) { raw_repository.rugged.tags.count } + end + # Return repo size in megabytes # Cached in redis def size @@ -278,6 +286,16 @@ class Repository @has_visible_content = nil end + def expire_branch_count_cache + cache.expire(:branch_count) + @branch_count = nil + end + + def expire_tag_count_cache + cache.expire(:tag_count) + @tag_count = nil + end + def rebuild_cache cache_keys.each do |key| cache.expire(key) @@ -313,9 +331,17 @@ class Repository expire_root_ref_cache end - # Runs code before creating a new tag. - def before_create_tag + # Runs code before pushing (= creating or removing) a tag. + def before_push_tag expire_cache + expire_tags_cache + expire_tag_count_cache + end + + # Runs code before removing a tag. + def before_remove_tag + expire_tags_cache + expire_tag_count_cache end # Runs code after a repository has been forked/imported. @@ -330,12 +356,21 @@ class Repository # Runs code after a new branch has been created. def after_create_branch + expire_branches_cache expire_has_visible_content_cache + expire_branch_count_cache + end + + # Runs code before removing an existing branch. + def before_remove_branch + expire_branches_cache end # Runs code after an existing branch has been removed. def after_remove_branch expire_has_visible_content_cache + expire_branch_count_cache + expire_branches_cache end def method_missing(m, *args, &block) @@ -812,6 +847,12 @@ class Repository raw_repository.ls_files(actual_ref) end + def main_language + unless empty? + Linguist::Repository.new(rugged, rugged.head.target_id).language + end + end + private def cache diff --git a/app/models/snippet.rb b/app/models/snippet.rb index f876be7a4c8..b9e835a4486 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -10,7 +10,6 @@ # created_at :datetime # updated_at :datetime # file_name :string(255) -# expires_at :datetime # type :string(255) # visibility_level :integer default(0), not null # @@ -46,8 +45,6 @@ class Snippet < ActiveRecord::Base scope :are_public, -> { where(visibility_level: Snippet::PUBLIC) } scope :public_and_internal, -> { where(visibility_level: [Snippet::PUBLIC, Snippet::INTERNAL]) } scope :fresh, -> { order("created_at DESC") } - scope :expired, -> { where(["expires_at IS NOT NULL AND expires_at < ?", Time.current]) } - scope :non_expired, -> { where(["expires_at IS NULL OR expires_at > ?", Time.current]) } participant :author, :notes @@ -111,21 +108,37 @@ class Snippet < ActiveRecord::Base nil end - def expired? - expires_at && expires_at < Time.current - end - def visibility_level_field visibility_level end class << self + # Searches for snippets with a matching title or file name. + # + # This method uses ILIKE on PostgreSQL and LIKE on MySQL. + # + # query - The search query as a String. + # + # Returns an ActiveRecord::Relation. def search(query) - where('(title LIKE :query OR file_name LIKE :query)', query: "%#{query}%") + t = arel_table + pattern = "%#{query}%" + + where(t[:title].matches(pattern).or(t[:file_name].matches(pattern))) end + # Searches for snippets with matching content. + # + # This method uses ILIKE on PostgreSQL and LIKE on MySQL. + # + # query - The search query as a String. + # + # Returns an ActiveRecord::Relation. def search_code(query) - where('(content LIKE :query)', query: "%#{query}%") + table = Snippet.arel_table + pattern = "%#{query}%" + + where(table[:content].matches(pattern)) end def accessible_to(user) diff --git a/app/models/user.rb b/app/models/user.rb index 3098d49d58a..043bc825ade 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -286,8 +286,22 @@ class User < ActiveRecord::Base end end + # Searches users matching the given query. + # + # This method uses ILIKE on PostgreSQL and LIKE on MySQL. + # + # query - The search query as a String + # + # Returns an ActiveRecord::Relation. def search(query) - where("lower(name) LIKE :query OR lower(email) LIKE :query OR lower(username) LIKE :query", query: "%#{query.downcase}%") + table = arel_table + pattern = "%#{query}%" + + where( + table[:name].matches(pattern). + or(table[:email].matches(pattern)). + or(table[:username].matches(pattern)) + ) end def by_login(login) @@ -612,6 +626,13 @@ class User < ActiveRecord::Base end end + def try_obtain_ldap_lease + # After obtaining this lease LDAP checks will be blocked for 600 seconds + # (10 minutes) for this user. + lease = Gitlab::ExclusiveLease.new("user_ldap_check:#{id}", timeout: 600) + lease.try_obtain + end + def solo_owned_groups @solo_owned_groups ||= owned_groups.select do |group| group.owners == [self] |