diff options
author | Jan Provaznik <jprovaznik@gitlab.com> | 2018-07-02 13:06:38 +0200 |
---|---|---|
committer | Jan Provaznik <jprovaznik@gitlab.com> | 2018-07-03 21:21:36 +0200 |
commit | e33c95cf960e6e572dae17e17c4a94fbd5bd4d51 (patch) | |
tree | c821334c23595cb9e5a2f8bb84bbfb648c38ca98 | |
parent | acc6e3a0fdb8f710d77b8faa2a39823f7e1c7e5d (diff) | |
download | gitlab-ce-e33c95cf960e6e572dae17e17c4a94fbd5bd4d51.tar.gz |
Migration which fixes cross-project label references
5 files changed, 256 insertions, 1 deletions
diff --git a/changelogs/unreleased/jprovazn-label-links-update.yml b/changelogs/unreleased/jprovazn-label-links-update.yml new file mode 100644 index 00000000000..75fb46ede6b --- /dev/null +++ b/changelogs/unreleased/jprovazn-label-links-update.yml @@ -0,0 +1,5 @@ +--- +title: Fix cross-project label references. +merge_request: +author: +type: fixed diff --git a/db/post_migrate/20180702120647_enqueue_fix_cross_project_label_links.rb b/db/post_migrate/20180702120647_enqueue_fix_cross_project_label_links.rb new file mode 100644 index 00000000000..59aa41adede --- /dev/null +++ b/db/post_migrate/20180702120647_enqueue_fix_cross_project_label_links.rb @@ -0,0 +1,30 @@ +class EnqueueFixCrossProjectLabelLinks < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + BATCH_SIZE = 100 + MIGRATION = 'FixCrossProjectLabelLinks' + DELAY_INTERVAL = 5.minutes + + disable_ddl_transaction! + + class Label < ActiveRecord::Base + self.table_name = 'labels' + end + + class Namespace < ActiveRecord::Base + self.table_name = 'namespaces' + + include ::EachBatch + + default_scope { where(type: 'Group', id: Label.where(type: 'GroupLabel').select('distinct group_id')) } + end + + def up + queue_background_migration_jobs_by_range_at_intervals(Namespace, MIGRATION, DELAY_INTERVAL, batch_size: BATCH_SIZE) + end + + def down + # noop + end +end diff --git a/db/schema.rb b/db/schema.rb index 384a1ec6d37..ea355921bcf 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20180629191052) do +ActiveRecord::Schema.define(version: 20180702120647) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" diff --git a/lib/gitlab/background_migration/fix_cross_project_label_links.rb b/lib/gitlab/background_migration/fix_cross_project_label_links.rb new file mode 100644 index 00000000000..c15a062bc7d --- /dev/null +++ b/lib/gitlab/background_migration/fix_cross_project_label_links.rb @@ -0,0 +1,138 @@ +# 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.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.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: 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).pluck(:id) + + check_issues(project_ids) + check_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 check_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)') + .pluck('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 check_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)') + .pluck('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 avaiable_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 + + # puts "#{label.label_link_id};#{resource.class.name.demodulize};project:#{project_id};id:#{resource.id};label:#{label.title}: #{label.label_id} -> #{matching_label.id}" + LabelLink.find(label.label_link_id).update!(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 diff --git a/spec/lib/gitlab/background_migration/fix_cross_project_label_links_spec.rb b/spec/lib/gitlab/background_migration/fix_cross_project_label_links_spec.rb new file mode 100644 index 00000000000..0ff2b874a6e --- /dev/null +++ b/spec/lib/gitlab/background_migration/fix_cross_project_label_links_spec.rb @@ -0,0 +1,82 @@ +require 'spec_helper' + +describe Gitlab::BackgroundMigration::FixCrossProjectLabelLinks, :migration, schema: 20180702120647 do + let(:namespaces_table) { table(:namespaces) } + let(:projects_table) { table(:projects) } + let(:issues_table) { table(:issues) } + let(:merge_requests_table) { table(:merge_requests) } + let(:labels_table) { table(:labels) } + let(:label_links_table) { table(:label_links) } + + let!(:group1) { namespaces_table.create(id: 10, type: 'Group', name: 'group1', path: 'group1') } + let!(:group2) { namespaces_table.create(id: 20, type: 'Group', name: 'group2', path: 'group2') } + + let!(:project1) { projects_table.create(id: 1, name: 'project1', path: 'group1/project1', namespace_id: 10) } + let!(:project2) { projects_table.create(id: 3, name: 'project2', path: 'group1/project2', namespace_id: 20) } + + let!(:label1) { labels_table.create(id: 1, title: 'bug', color: 'red', group_id: 10, type: 'GroupLabel') } + let!(:label2) { labels_table.create(id: 2, title: 'bug', color: 'red', group_id: 20, type: 'GroupLabel') } + + let(:merge_request) do + merge_requests_table.create(target_project_id: project.id, + target_branch: 'master', + source_project_id: project.id, + source_branch: 'mr name', + title: 'mr name') + end + + it 'updates cross-project label links which exist in the local project or group' do + issues_table.create(id: 1, title: 'issue1', project_id: 1) + link = label_links_table.create(label_id: 2, target_type: 'Issue', target_id: 1) + + subject.perform(1, 100) + + expect(link.reload.label_id).to eq(1) + end + + it 'ignores cross-project label links if label color is different' do + labels_table.create(id: 3, title: 'bug', color: 'green', group_id: 20, type: 'GroupLabel') + issues_table.create(id: 1, title: 'issue1', project_id: 1) + link = label_links_table.create(label_id: 3, target_type: 'Issue', target_id: 1) + + subject.perform(1, 100) + + expect(link.reload.label_id).to eq(3) + end + + it 'ignores cross-project label links if label name is different' do + labels_table.create(id: 3, title: 'bug1', color: 'red', group_id: 20, type: 'GroupLabel') + issues_table.create(id: 1, title: 'issue1', project_id: 1) + link = label_links_table.create(label_id: 3, target_type: 'Issue', target_id: 1) + + subject.perform(1, 100) + + expect(link.reload.label_id).to eq(3) + end + + context 'with nested group' do + before do + namespaces_table.create(id: 11, type: 'Group', name: 'subgroup1', path: 'group1/subgroup1', parent_id: 10) + projects_table.create(id: 2, name: 'subproject1', path: 'group1/subgroup1/subproject1', namespace_id: 11) + issues_table.create(id: 1, title: 'issue1', project_id: 2) + end + + it 'ignores label links referencing ancestor group labels', :nested_groups do + labels_table.create(id: 4, title: 'bug', color: 'red', project_id: 2, type: 'ProjectLabel') + label_links_table.create(label_id: 4, target_type: 'Issue', target_id: 1) + link = label_links_table.create(label_id: 1, target_type: 'Issue', target_id: 1) + + subject.perform(1, 100) + + expect(link.reload.label_id).to eq(1) + end + + it 'checks also issues and MRs in subgroups', :nested_groups do + link = label_links_table.create(label_id: 2, target_type: 'Issue', target_id: 1) + + subject.perform(1, 100) + + expect(link.reload.label_id).to eq(1) + end + end +end |