diff options
Diffstat (limited to 'app/models')
-rw-r--r-- | app/models/concerns/group_descendant.rb | 56 | ||||
-rw-r--r-- | app/models/concerns/loaded_in_group_list.rb | 72 | ||||
-rw-r--r-- | app/models/group.rb | 2 | ||||
-rw-r--r-- | app/models/namespace.rb | 7 | ||||
-rw-r--r-- | app/models/project.rb | 14 |
5 files changed, 147 insertions, 4 deletions
diff --git a/app/models/concerns/group_descendant.rb b/app/models/concerns/group_descendant.rb new file mode 100644 index 00000000000..01957da0bf3 --- /dev/null +++ b/app/models/concerns/group_descendant.rb @@ -0,0 +1,56 @@ +module GroupDescendant + # Returns the hierarchy of a project or group in the from of a hash upto a + # given top. + # + # > project.hierarchy + # => { parent_group => { child_group => project } } + def hierarchy(hierarchy_top = nil, preloaded = nil) + preloaded ||= ancestors_upto(hierarchy_top) + expand_hierarchy_for_child(self, self, hierarchy_top, preloaded) + end + + # Merges all hierarchies of the given groups or projects into an array of + # hashes. All ancestors need to be loaded into the given `descendants` to avoid + # queries down the line. + # + # > GroupDescendant.merge_hierarchy([project, child_group, child_group2, parent]) + # => { parent => [{ child_group => project}, child_group2] } + def self.build_hierarchy(descendants, hierarchy_top = nil) + descendants = Array.wrap(descendants).uniq + return [] if descendants.empty? + + unless descendants.all? { |hierarchy| hierarchy.is_a?(GroupDescendant) } + raise ArgumentError.new('element is not a hierarchy') + end + + all_hierarchies = descendants.map do |descendant| + descendant.hierarchy(hierarchy_top, descendants) + end + + Gitlab::Utils::MergeHash.merge(all_hierarchies) + end + + private + + def expand_hierarchy_for_child(child, hierarchy, hierarchy_top, preloaded) + parent = hierarchy_top if hierarchy_top && child.parent_id == hierarchy_top.id + parent ||= preloaded.detect { |possible_parent| possible_parent.is_a?(Group) && possible_parent.id == child.parent_id } + + if parent.nil? && !child.parent_id.nil? + raise ArgumentError.new('parent was not preloaded') + end + + if parent.nil? && hierarchy_top.present? + raise ArgumentError.new('specified top is not part of the tree') + end + + if parent && parent != hierarchy_top + expand_hierarchy_for_child(parent, + { parent => hierarchy }, + hierarchy_top, + preloaded) + else + hierarchy + end + end +end diff --git a/app/models/concerns/loaded_in_group_list.rb b/app/models/concerns/loaded_in_group_list.rb new file mode 100644 index 00000000000..dcb3b2b5ff3 --- /dev/null +++ b/app/models/concerns/loaded_in_group_list.rb @@ -0,0 +1,72 @@ +module LoadedInGroupList + extend ActiveSupport::Concern + + module ClassMethods + def with_counts(archived:) + selects_including_counts = [ + 'namespaces.*', + "(#{project_count_sql(archived).to_sql}) AS preloaded_project_count", + "(#{member_count_sql.to_sql}) AS preloaded_member_count", + "(#{subgroup_count_sql.to_sql}) AS preloaded_subgroup_count" + ] + + select(selects_including_counts) + end + + def with_selects_for_list(archived: nil) + with_route.with_counts(archived: archived) + end + + private + + def project_count_sql(archived = nil) + projects = Project.arel_table + namespaces = Namespace.arel_table + + base_count = projects.project(Arel.star.count.as('preloaded_project_count')) + .where(projects[:namespace_id].eq(namespaces[:id])) + if archived == 'only' + base_count.where(projects[:archived].eq(true)) + elsif Gitlab::Utils.to_boolean(archived) + base_count + else + base_count.where(projects[:archived].not_eq(true)) + end + end + + def subgroup_count_sql + namespaces = Namespace.arel_table + children = namespaces.alias('children') + + namespaces.project(Arel.star.count.as('preloaded_subgroup_count')) + .from(children) + .where(children[:parent_id].eq(namespaces[:id])) + end + + def member_count_sql + members = Member.arel_table + namespaces = Namespace.arel_table + + members.project(Arel.star.count.as('preloaded_member_count')) + .where(members[:source_type].eq(Namespace.name)) + .where(members[:source_id].eq(namespaces[:id])) + .where(members[:requested_at].eq(nil)) + end + end + + def children_count + @children_count ||= project_count + subgroup_count + end + + def project_count + @project_count ||= try(:preloaded_project_count) || projects.non_archived.count + end + + def subgroup_count + @subgroup_count ||= try(:preloaded_subgroup_count) || children.count + end + + def member_count + @member_count ||= try(:preloaded_member_count) || users.count + end +end diff --git a/app/models/group.rb b/app/models/group.rb index e746e4a12c9..07fb62bb249 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -6,6 +6,8 @@ class Group < Namespace include Avatarable include Referable include SelectForProjectAuthorization + include LoadedInGroupList + include GroupDescendant has_many :group_members, -> { where(requested_at: nil) }, dependent: :destroy, as: :source # rubocop:disable Cop/ActiveRecordDependent alias_method :members, :group_members diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 4672881e220..0601a61a926 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -162,6 +162,13 @@ class Namespace < ActiveRecord::Base .base_and_ancestors end + # returns all ancestors upto but excluding the the given namespace + # when no namespace is given, all ancestors upto the top are returned + def ancestors_upto(top = nil) + Gitlab::GroupHierarchy.new(self.class.where(id: id)) + .ancestors(upto: top) + end + def self_and_ancestors return self.class.where(id: id) unless parent_id diff --git a/app/models/project.rb b/app/models/project.rb index 57e91ab3b88..6e1cf9e31ee 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -17,6 +17,7 @@ class Project < ActiveRecord::Base include ProjectFeaturesCompatibility include SelectForProjectAuthorization include Routable + include GroupDescendant extend Gitlab::ConfigHelper extend Gitlab::CurrentSettings @@ -81,6 +82,8 @@ class Project < ActiveRecord::Base belongs_to :creator, class_name: 'User' belongs_to :group, -> { where(type: 'Group') }, foreign_key: 'namespace_id' belongs_to :namespace + alias_method :parent, :namespace + alias_attribute :parent_id, :namespace_id has_one :last_event, -> {order 'events.created_at DESC'}, class_name: 'Event' has_many :boards, before_add: :validate_board_limit @@ -479,6 +482,13 @@ class Project < ActiveRecord::Base end end + # returns all ancestor-groups upto but excluding the given namespace + # when no namespace is given, all ancestors upto the top are returned + def ancestors_upto(top = nil) + Gitlab::GroupHierarchy.new(Group.where(id: namespace_id)) + .base_and_ancestors(upto: top) + end + def lfs_enabled? return namespace.lfs_enabled? if self[:lfs_enabled].nil? @@ -1549,10 +1559,6 @@ class Project < ActiveRecord::Base map.public_path_for_source_path(path) end - def parent - namespace - end - def parent_changed? namespace_id_changed? end |