summaryrefslogtreecommitdiff
path: root/app/finders/issuables/label_filter.rb
blob: 4e9c964e51c8af6cfff6f27962ee945f71999fb2 (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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
# frozen_string_literal: true

module Issuables
  class LabelFilter < BaseFilter
    include Gitlab::Utils::StrongMemoize
    extend Gitlab::Cache::RequestCache

    def initialize(project:, group:, **kwargs)
      @project = project
      @group = group

      super(**kwargs)
    end

    def filter(issuables)
      filtered = by_label(issuables)
      by_negated_label(filtered)
    end

    def label_names_excluded_from_priority_sort
      label_names_from_params
    end

    private

    # rubocop: disable CodeReuse/ActiveRecord
    def by_label(issuables)
      return issuables unless label_names_from_params.present?

      target_model = issuables.base_class

      if filter_by_no_label?
        issuables.where(label_link_query(target_model).arel.exists.not)
      elsif filter_by_any_label?
        issuables.where(label_link_query(target_model).arel.exists)
      else
        issuables_with_selected_labels(issuables, label_names_from_params)
      end
    end
    # rubocop: enable CodeReuse/ActiveRecord

    def by_negated_label(issuables)
      return issuables unless label_names_from_not_params.present?

      issuables_without_selected_labels(issuables, label_names_from_not_params)
    end

    def filter_by_no_label?
      label_names_from_params.map(&:downcase).include?(FILTER_NONE)
    end

    def filter_by_any_label?
      label_names_from_params.map(&:downcase).include?(FILTER_ANY)
    end

    # rubocop: disable CodeReuse/ActiveRecord
    def issuables_with_selected_labels(issuables, label_names)
      target_model = issuables.base_class

      if root_namespace
        all_label_ids = find_label_ids(label_names)
        # Found less labels in the DB than we were searching for. Return nothing.
        return issuables.none if all_label_ids.size != label_names.size

        all_label_ids.each do |label_ids|
          issuables = issuables.where(label_link_query(target_model, label_ids: label_ids).arel.exists)
        end
      else
        label_names.each do |label_name|
          issuables = issuables.where(label_link_query(target_model, label_names: label_name).arel.exists)
        end
      end

      issuables
    end
    # rubocop: enable CodeReuse/ActiveRecord

    # rubocop: disable CodeReuse/ActiveRecord
    def issuables_without_selected_labels(issuables, label_names)
      target_model = issuables.base_class

      if root_namespace
        label_ids = find_label_ids(label_names).flatten(1)

        return issuables if label_ids.empty?

        issuables.where(label_link_query(target_model, label_ids: label_ids).arel.exists.not)
      else
        issuables.where(label_link_query(target_model, label_names: label_names).arel.exists.not)
      end
    end
    # rubocop: enable CodeReuse/ActiveRecord

    def find_label_ids(label_names)
      find_label_ids_uncached(label_names)
    end
    # Avoid repeating label queries times when the finder is instantiated multiple times during the request.
    request_cache(:find_label_ids) { root_namespace.id }

    # This returns an array of label IDs per label name. It is possible for a label name
    # to have multiple IDs because we allow labels with the same name if they are on a different
    # project or group.
    #
    # For example, if we pass in `['bug', 'feature']`, this will return something like:
    # `[ [1, 2], [3] ]`
    #
    # rubocop: disable CodeReuse/ActiveRecord
    def find_label_ids_uncached(label_names)
      return [] if label_names.empty?

      group_labels = group_labels_for_root_namespace.where(title: label_names)
      project_labels = project_labels_for_root_namespace.where(title: label_names)

      Label
        .from_union([group_labels, project_labels], remove_duplicates: false)
        .reorder(nil)
        .pluck(:title, :id)
        .group_by(&:first)
        .values
        .map { |labels| labels.map(&:last) }
    end
    # rubocop: enable CodeReuse/ActiveRecord

    # rubocop: disable CodeReuse/ActiveRecord
    def group_labels_for_root_namespace
      Label.where(project_id: nil).where(group_id: root_namespace.self_and_descendant_ids)
    end
    # rubocop: enable CodeReuse/ActiveRecord

    # rubocop: disable CodeReuse/ActiveRecord
    def project_labels_for_root_namespace
      Label.where(group_id: nil).where(project_id: Project.select(:id).where(namespace_id: root_namespace.self_and_descendant_ids))
    end
    # rubocop: enable CodeReuse/ActiveRecord

    # rubocop: disable CodeReuse/ActiveRecord
    def label_link_query(target_model, label_ids: nil, label_names: nil)
      relation = LabelLink.by_target_for_exists_query(target_model.name, target_model.arel_table['id'], label_ids)
      relation = relation.joins(:label).where(labels: { name: label_names }) if label_names

      relation
    end
    # rubocop: enable CodeReuse/ActiveRecord

    def label_names_from_params
      return if params[:label_name].blank?

      strong_memoize(:label_names_from_params) do
        split_label_names(params[:label_name])
      end
    end

    def label_names_from_not_params
      return if not_params.blank? || not_params[:label_name].blank?

      strong_memoize(:label_names_from_not_params) do
        split_label_names(not_params[:label_name])
      end
    end

    def split_label_names(label_name_param)
      label_name_param.is_a?(String) ? label_name_param.split(',') : label_name_param
    end

    def root_namespace
      strong_memoize(:root_namespace) do
        (@project || @group)&.root_ancestor
      end
    end
  end
end

Issuables::LabelFilter.prepend_mod