summaryrefslogtreecommitdiff
path: root/app/models/concerns/group_descendant.rb
blob: ed14b73ac1ba05d41575085c090634bfd9998b9c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
# frozen_string_literal: true

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?
      parent = child.parent

      exception = ArgumentError.new <<~MSG
        Parent was not preloaded for child when rendering group hierarchy.
        This error is not user facing, but causes a +1 query.
      MSG
      extras = {
        parent: parent.inspect,
        child: child.inspect,
        preloaded: preloaded.map(&:full_path)
      }
      issue_url = 'https://gitlab.com/gitlab-org/gitlab-ce/issues/49404'

      Gitlab::Sentry.track_exception(exception, issue_url: issue_url, extra: extras)
    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