Commit 3dcc63fa authored by Heinrich Lee Yu's avatar Heinrich Lee Yu

Merge branch 'cablett-231472-group-labels' into 'master'

Use existing group label (if there is one) when promoting

See merge request gitlab-org/gitlab!45122
parents f8c90100 4e0c913c
...@@ -10,81 +10,79 @@ module Labels ...@@ -10,81 +10,79 @@ module Labels
label.is_a?(ProjectLabel) label.is_a?(ProjectLabel)
Label.transaction do Label.transaction do
new_label = clone_label_to_group_label(label) # use the existing group label if it exists
group_label = find_or_create_group_label(label)
label_ids_for_merge(new_label).find_in_batches(batch_size: BATCH_SIZE) do |batched_ids| label_ids_for_merge(group_label).find_in_batches(batch_size: BATCH_SIZE) do |batched_ids|
update_old_label_relations(new_label, batched_ids) update_old_label_relations(group_label, batched_ids)
destroy_project_labels(batched_ids) destroy_project_labels(batched_ids)
end end
# We skipped validations during creation. Let's run them now, after deleting conflicting labels group_label
raise ActiveRecord::RecordInvalid.new(new_label) unless new_label.valid?
new_label
end end
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
private private
def update_old_label_relations(new_label, old_label_ids) def update_old_label_relations(group_label, old_label_ids)
update_issuables(new_label, old_label_ids) update_issuables(group_label, old_label_ids)
update_resource_label_events(new_label, old_label_ids) update_resource_label_events(group_label, old_label_ids)
update_issue_board_lists(new_label, old_label_ids) update_issue_board_lists(group_label, old_label_ids)
update_priorities(new_label, old_label_ids) update_priorities(group_label, old_label_ids)
subscribe_users(new_label, old_label_ids) subscribe_users(group_label, old_label_ids)
end end
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def subscribe_users(new_label, label_ids) def subscribe_users(group_label, label_ids)
# users can be subscribed to multiple labels that will be merged into the group one # users can be subscribed to multiple labels that will be merged into the group one
# we want to keep only one subscription / user # we want to keep only one subscription / user
ids_to_update = Subscription.where(subscribable_id: label_ids, subscribable_type: 'Label') ids_to_update = Subscription.where(subscribable_id: label_ids, subscribable_type: 'Label')
.group(:user_id) .group(:user_id)
.pluck('MAX(id)') .pluck('MAX(id)')
Subscription.where(id: ids_to_update).update_all(subscribable_id: new_label.id) Subscription.where(id: ids_to_update).update_all(subscribable_id: group_label.id)
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def label_ids_for_merge(new_label) def label_ids_for_merge(group_label)
LabelsFinder LabelsFinder
.new(current_user, title: new_label.title, group_id: project.group.id) .new(current_user, title: group_label.title, group_id: project.group.id)
.execute(skip_authorization: true) .execute(skip_authorization: true)
.where.not(id: new_label) .where.not(id: group_label)
.select(:id) # Can't use pluck() to avoid object-creation because of the batching .select(:id) # Can't use pluck() to avoid object-creation because of the batching
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def update_issuables(new_label, label_ids) def update_issuables(group_label, label_ids)
LabelLink LabelLink
.where(label: label_ids) .where(label: label_ids)
.update_all(label_id: new_label.id) .update_all(label_id: group_label.id)
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def update_resource_label_events(new_label, label_ids) def update_resource_label_events(group_label, label_ids)
ResourceLabelEvent ResourceLabelEvent
.where(label: label_ids) .where(label: label_ids)
.update_all(label_id: new_label.id) .update_all(label_id: group_label.id)
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def update_issue_board_lists(new_label, label_ids) def update_issue_board_lists(group_label, label_ids)
List List
.where(label: label_ids) .where(label: label_ids)
.update_all(label_id: new_label.id) .update_all(label_id: group_label.id)
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def update_priorities(new_label, label_ids) def update_priorities(group_label, label_ids)
LabelPriority LabelPriority
.where(label: label_ids) .where(label: label_ids)
.update_all(label_id: new_label.id) .update_all(label_id: group_label.id)
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
...@@ -92,18 +90,12 @@ module Labels ...@@ -92,18 +90,12 @@ module Labels
def destroy_project_labels(label_ids) def destroy_project_labels(label_ids)
Label.where(id: label_ids).destroy_all # rubocop: disable Cop/DestroyAll Label.where(id: label_ids).destroy_all # rubocop: disable Cop/DestroyAll
end end
# rubocop: enable CodeReuse/ActiveRecord
def clone_label_to_group_label(label) def find_or_create_group_label(label)
params = label.attributes.slice('title', 'description', 'color') params = label.attributes.slice('title', 'description', 'color')
# Since the title of the new label has to be the same as the previous labels new_label = GroupLabel.create_with(params).find_or_initialize_by(group_id: project.group.id, title: label.title)
# and we're merging old labels in batches we'll skip validation to omit 2-step
# merge process and do it in one batch
# We'll be forcing validation at the end of the transaction to ensure everything
# was merged correctly
new_label = GroupLabel.new(params.merge(group: project.group))
new_label.save(validate: false)
new_label.save! unless new_label.persisted?
new_label new_label
end end
end end
......
---
title: Use existing group label when promoting project label
merge_request: 45122
author:
type: changed
...@@ -249,9 +249,12 @@ An older endpoint `PUT /projects/:id/labels` with `name` or `label_id` in the pa ...@@ -249,9 +249,12 @@ An older endpoint `PUT /projects/:id/labels` with `name` or `label_id` in the pa
## Promote a project label to a group label ## Promote a project label to a group label
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/25218) in GitLab 12.3. > - [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/25218) in GitLab 12.3.
> - In [GitLab 13.6 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/231472), promoting a
> project label keeps that label's ID and changes it into a group label. Previously, promoting a
> project label created a new group label with a new ID and deleted the old label.
Promotes a project label to a group label. Promotes a project label to a group label. The label keeps its ID.
```plaintext ```plaintext
PUT /projects/:id/labels/:label_id/promote PUT /projects/:id/labels/:label_id/promote
......
...@@ -95,6 +95,8 @@ If you delete a label, it is permanently deleted. All references to the label ar ...@@ -95,6 +95,8 @@ If you delete a label, it is permanently deleted. All references to the label ar
#### Promote a project label to a group label #### Promote a project label to a group label
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/231472) in GitLab 13.6: promoting a project label keeps that label's ID and changes it into a group label. Previously, promoting a project label created a new group label with a new ID and deleted the old label.
If you previously created a project label and now want to make it available for other If you previously created a project label and now want to make it available for other
projects within the same group, you can promote it to a group label. projects within the same group, you can promote it to a group label.
...@@ -105,6 +107,8 @@ also merged. ...@@ -105,6 +107,8 @@ also merged.
All issues, merge requests, issue board lists, issue board filters, and label subscriptions All issues, merge requests, issue board lists, issue board filters, and label subscriptions
with the old labels are assigned to the new group label. with the old labels are assigned to the new group label.
The new group label has the same ID as the previous project label.
CAUTION: **Caution:** CAUTION: **Caution:**
Promoting a label is a permanent action, and cannot be reversed. Promoting a label is a permanent action, and cannot be reversed.
......
...@@ -63,26 +63,12 @@ RSpec.describe Labels::PromoteService do ...@@ -63,26 +63,12 @@ RSpec.describe Labels::PromoteService do
expect(service.execute(group_label)).to be_falsey expect(service.execute(group_label)).to be_falsey
end end
shared_examples 'promoting a project label to a group label' do
it 'is truthy on success' do it 'is truthy on success' do
expect(service.execute(project_label_1_1)).to be_truthy expect(service.execute(project_label_1_1)).to be_truthy
end end
it 'recreates the label as a group label' do it 'removes all project labels with that title within the group' do
expect { service.execute(project_label_1_1) }
.to change(project_1.labels, :count).by(-1)
.and change(group_1.labels, :count).by(1)
expect(new_label).not_to be_nil
end
it 'copies title, description and color' do
service.execute(project_label_1_1)
expect(new_label.title).to eq(promoted_label_name)
expect(new_label.description).to eq(promoted_description)
expect(new_label.color).to eq(promoted_color)
end
it 'merges labels with the same name in group' do
expect { service.execute(project_label_1_1) }.to change(project_2.labels, :count).by(-1).and \ expect { service.execute(project_label_1_1) }.to change(project_2.labels, :count).by(-1).and \
change(project_3.labels, :count).by(-1) change(project_3.labels, :count).by(-1)
end end
...@@ -182,20 +168,44 @@ RSpec.describe Labels::PromoteService do ...@@ -182,20 +168,44 @@ RSpec.describe Labels::PromoteService do
expect(project_label_1_1.send(association.name).any?).to be_falsey expect(project_label_1_1.send(association.name).any?).to be_falsey
end end
end end
end
context 'with invalid group label' do context 'if there is an existing identical group label' do
before do let!(:existing_group_label) { create(:group_label, group: group_1, title: project_label_1_1.title ) }
allow(service).to receive(:clone_label_to_group_label).and_wrap_original do |m, *args|
label = m.call(*args)
allow(label).to receive(:valid?).and_return(false)
label it 'uses the existing group label' do
expect { service.execute(project_label_1_1) }
.to change(project_1.labels, :count).by(-1)
.and not_change(group_1.labels, :count)
expect(new_label).not_to be_nil
end end
it 'does not create a new group label clone' do
expect { service.execute(project_label_1_1) }.not_to change { GroupLabel.maximum(:id) }
end
it_behaves_like 'promoting a project label to a group label'
end end
it 'raises an exception' do context 'if there is no existing identical group label' do
expect { service.execute(project_label_1_1) }.to raise_error(ActiveRecord::RecordInvalid) let(:existing_group_label) { nil }
it 'recreates the label as a group label' do
expect { service.execute(project_label_1_1) }
.to change(project_1.labels, :count).by(-1)
.and change(group_1.labels, :count).by(1)
expect(new_label).not_to be_nil
end end
it 'copies title, description and color to cloned group label' do
service.execute(project_label_1_1)
expect(new_label.title).to eq(promoted_label_name)
expect(new_label.description).to eq(promoted_description)
expect(new_label.color).to eq(promoted_color)
end
it_behaves_like 'promoting a project label to a group label'
end end
end end
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment