summaryrefslogtreecommitdiff
path: root/lib/gitlab/background_migration/fix_cross_project_label_links.rb
blob: 20a98c8e141f276f9d82326766df0a614881da08 (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
# frozen_string_literal: true
# rubocop:disable Style/Documentation

module Gitlab
  module BackgroundMigration
    class FixCrossProjectLabelLinks
      GROUP_NESTED_LEVEL = 10.freeze

      class Project < ActiveRecord::Base
        self.table_name = 'projects'
      end

      class Label < ActiveRecord::Base
        self.inheritance_column = :_type_disabled
        self.table_name = 'labels'
      end

      class LabelLink < ActiveRecord::Base
        self.table_name = 'label_links'
      end

      class Issue < ActiveRecord::Base
        self.table_name = 'issues'
      end

      class MergeRequest < ActiveRecord::Base
        self.table_name = 'merge_requests'
      end

      class Namespace < ActiveRecord::Base
        self.inheritance_column = :_type_disabled
        self.table_name = 'namespaces'

        def self.groups_with_descendants_ids(start_id, stop_id)
          # To isolate migration code, we avoid usage of
          # Gitlab::GroupHierarchy#base_and_descendants which already
          # does this job better
          ids = Namespace.where(type: 'Group', id: Label.where(type: 'GroupLabel').select('distinct group_id')).where(id: start_id..stop_id).pluck(:id)
          group_ids = ids

          GROUP_NESTED_LEVEL.times do
            ids = Namespace.where(type: 'Group', parent_id: ids).pluck(:id)
            break if ids.empty?

            group_ids += ids
          end

          group_ids.uniq
        end
      end

      def perform(start_id, stop_id)
        group_ids = Namespace.groups_with_descendants_ids(start_id, stop_id)
        project_ids = Project.where(namespace_id: group_ids).select(:id)

        fix_issues(project_ids)
        fix_merge_requests(project_ids)
      end

      private

      # select IDs of issues which reference a label which is:
      # a) a project label of a different project, or
      # b) a group label of a different group than issue's project group
      def fix_issues(project_ids)
        issue_ids = Label
          .joins('INNER JOIN label_links ON label_links.label_id = labels.id AND label_links.target_type = \'Issue\'
                  INNER JOIN issues ON issues.id = label_links.target_id
                  INNER JOIN projects ON projects.id = issues.project_id')
          .where('issues.project_id in (?)', project_ids)
          .where('(labels.project_id is not null and labels.project_id != issues.project_id) '\
                 'or (labels.group_id is not null and labels.group_id != projects.namespace_id)')
          .select('distinct issues.id')

        Issue.where(id: issue_ids).find_each { |issue| check_resource_labels(issue, issue.project_id) }
      end

      # select IDs of MRs which reference a label which is:
      # a) a project label of a different project, or
      # b) a group label of a different group than MR's project group
      def fix_merge_requests(project_ids)
        mr_ids = Label
          .joins('INNER JOIN label_links ON label_links.label_id = labels.id AND label_links.target_type = \'MergeRequest\'
                  INNER JOIN merge_requests ON merge_requests.id = label_links.target_id
                  INNER JOIN projects ON projects.id = merge_requests.target_project_id')
          .where('merge_requests.target_project_id in (?)', project_ids)
          .where('(labels.project_id is not null and labels.project_id != merge_requests.target_project_id) '\
                 'or (labels.group_id is not null and labels.group_id != projects.namespace_id)')
          .select('distinct merge_requests.id')

        MergeRequest.where(id: mr_ids).find_each { |merge_request| check_resource_labels(merge_request, merge_request.target_project_id) }
      end

      def check_resource_labels(resource, project_id)
        local_labels = available_labels(project_id)

        # get all label links for the given resource (issue/MR)
        # which reference a label not included in available_labels
        # (other than its project labels and labels of ancestor groups)
        cross_labels = LabelLink
          .select('label_id, labels.title as title, labels.color as color, label_links.id as label_link_id')
          .joins('INNER JOIN labels ON labels.id = label_links.label_id')
          .where(target_type: resource.class.name.demodulize, target_id: resource.id)
          .where('labels.id not in (?)', local_labels.select(:id))

        cross_labels.each do |label|
          matching_label = local_labels.find {|l| l.title == label.title && l.color == label.color}

          next unless matching_label

          Rails.logger.info "#{resource.class.name.demodulize} #{resource.id}: replacing #{label.label_id} with #{matching_label.id}" # rubocop:disable Gitlab/RailsLogger
          LabelLink.update(label.label_link_id, label_id: matching_label.id)
        end
      end

      # get all labels available for the project (including
      # group labels of ancestor groups)
      def available_labels(project_id)
        @labels ||= {}
        @labels[project_id] ||= Label
          .where("(type = 'GroupLabel' and group_id in (?)) or (type = 'ProjectLabel' and id = ?)",
                 project_group_ids(project_id),
                 project_id)
      end

      def project_group_ids(project_id)
        ids = [Project.find(project_id).namespace_id]

        GROUP_NESTED_LEVEL.times do
          group = Namespace.find(ids.last)
          break unless group.parent_id

          ids << group.parent_id
        end

        ids
      end
    end
  end
end