Update from merge request

This commit is contained in:
root
2025-07-22 16:00:03 +00:00
parent a20f9746be
commit c07c07add1
20 changed files with 223 additions and 59 deletions

View File

@ -219,7 +219,7 @@
{"name":"gitaly","version":"18.2.0","platform":"ruby","checksum":"229010b9e8a9e8de213591989795df17a3bdcc01957903bd2a2f1474bbff5578"},
{"name":"gitlab","version":"4.19.0","platform":"ruby","checksum":"3f645e3e195dbc24f0834fbf83e8ccfb2056d8e9712b01a640aad418a6949679"},
{"name":"gitlab-chronic","version":"0.10.6","platform":"ruby","checksum":"a244d11a1396d2aac6ae9b2f326adf1605ec1ad20c29f06e8b672047d415a9ac"},
{"name":"gitlab-cloud-connector","version":"1.22.0","platform":"ruby","checksum":"5c9cffd0a24b7004fa7a16d0a5ef378c7192ceb11b38b8147f18c268720b2a86"},
{"name":"gitlab-cloud-connector","version":"1.23.0","platform":"ruby","checksum":"8d0765432340a03a65bce48e16ebea8062841b0fc9001b84aedbce70a468385d"},
{"name":"gitlab-crystalball","version":"1.1.1","platform":"ruby","checksum":"0464a113b0809e0e9fa7c0100bb6634fe38465af95aa04efa49541d64250b8ed"},
{"name":"gitlab-dangerfiles","version":"4.9.2","platform":"ruby","checksum":"d5c050f685d8720f6e70191a7d1216854d860dbdea5b455f87abe7542e005798"},
{"name":"gitlab-experiment","version":"0.9.1","platform":"ruby","checksum":"f230ee742154805a755d5f2539dc44d93cdff08c5bbbb7656018d61f93d01f48"},

View File

@ -732,9 +732,9 @@ GEM
terminal-table (>= 1.5.1)
gitlab-chronic (0.10.6)
numerizer (~> 0.2)
gitlab-cloud-connector (1.22.0)
gitlab-cloud-connector (1.23.0)
activesupport (~> 7.0)
jwt (~> 2.9.3)
jwt (~> 2.9)
gitlab-crystalball (1.1.1)
git (< 4)
ostruct (< 1)

View File

@ -219,7 +219,7 @@
{"name":"gitaly","version":"18.2.0","platform":"ruby","checksum":"229010b9e8a9e8de213591989795df17a3bdcc01957903bd2a2f1474bbff5578"},
{"name":"gitlab","version":"4.19.0","platform":"ruby","checksum":"3f645e3e195dbc24f0834fbf83e8ccfb2056d8e9712b01a640aad418a6949679"},
{"name":"gitlab-chronic","version":"0.10.6","platform":"ruby","checksum":"a244d11a1396d2aac6ae9b2f326adf1605ec1ad20c29f06e8b672047d415a9ac"},
{"name":"gitlab-cloud-connector","version":"1.22.0","platform":"ruby","checksum":"5c9cffd0a24b7004fa7a16d0a5ef378c7192ceb11b38b8147f18c268720b2a86"},
{"name":"gitlab-cloud-connector","version":"1.23.0","platform":"ruby","checksum":"8d0765432340a03a65bce48e16ebea8062841b0fc9001b84aedbce70a468385d"},
{"name":"gitlab-crystalball","version":"1.1.1","platform":"ruby","checksum":"0464a113b0809e0e9fa7c0100bb6634fe38465af95aa04efa49541d64250b8ed"},
{"name":"gitlab-dangerfiles","version":"4.9.2","platform":"ruby","checksum":"d5c050f685d8720f6e70191a7d1216854d860dbdea5b455f87abe7542e005798"},
{"name":"gitlab-experiment","version":"0.9.1","platform":"ruby","checksum":"f230ee742154805a755d5f2539dc44d93cdff08c5bbbb7656018d61f93d01f48"},

View File

@ -726,9 +726,9 @@ GEM
terminal-table (>= 1.5.1)
gitlab-chronic (0.10.6)
numerizer (~> 0.2)
gitlab-cloud-connector (1.22.0)
gitlab-cloud-connector (1.23.0)
activesupport (~> 7.0)
jwt (~> 2.9.3)
jwt (~> 2.9)
gitlab-crystalball (1.1.1)
git (< 4)
ostruct (< 1)

View File

@ -1,16 +1,18 @@
<script>
import { GlTooltipDirective, GlFormCheckbox, GlLink } from '@gitlab/ui';
import HelpIcon from '~/vue_shared/components/help_icon/help_icon.vue';
import HelpPopover from '~/vue_shared/components/help_popover.vue';
import SafeHtml from '~/vue_shared/directives/safe_html';
import { SQUASH_BEFORE_MERGE } from '../../i18n';
export default {
components: {
GlFormCheckbox,
GlLink,
HelpIcon,
HelpPopover,
},
directives: {
GlTooltip: GlTooltipDirective,
SafeHtml,
},
i18n: {
...SQUASH_BEFORE_MERGE,
@ -35,6 +37,9 @@ export default {
tooltipTitle() {
return this.isDisabled ? this.$options.i18n.tooltipTitle : null;
},
popoverOptions() {
return this.$options.i18n.popoverOptions;
},
},
};
</script>
@ -53,15 +58,26 @@ export default {
>
{{ $options.i18n.checkboxLabel }}
</gl-form-checkbox>
<gl-link
<help-popover
v-if="helpPath"
v-gl-tooltip
:href="helpPath"
:title="$options.i18n.helpLabel"
class="gl-leading-1"
target="_blank"
class="gl-flex gl-items-start"
:options="popoverOptions"
:aria-label="$options.i18n.helpLabel"
>
<help-icon :aria-label="$options.i18n.helpLabel" />
</gl-link>
<template v-if="popoverOptions.content">
<p
v-if="popoverOptions.content.text"
v-safe-html="popoverOptions.content.text"
class="gl-mb-0"
></p>
<gl-link
v-if="popoverOptions.content.learnMorePath"
:href="popoverOptions.content.learnMorePath"
target="_blank"
class="gl-text-sm"
>{{ $options.i18n.learnMore }}</gl-link
>
</template>
</help-popover>
</div>
</template>

View File

@ -1,4 +1,5 @@
import { __, s__ } from '~/locale';
import { helpPagePath } from '~/helpers/help_page_helper';
export const MR_WIDGET_PREPARING_ASYNCHRONOUSLY = s__(
'mrWidget|Your merge request is almost ready!',
@ -18,6 +19,16 @@ export const SQUASH_BEFORE_MERGE = {
tooltipTitle: __('Required in this project.'),
checkboxLabel: __('Squash commits'),
helpLabel: __('What is squashing?'),
popoverOptions: {
title: __('What is squashing?'),
content: {
text: __(
'Squashing combines multiple commits into a single commit on merge. This keeps your repository history clean and makes it easier to revert changes.',
),
learnMorePath: helpPagePath('user/project/merge_requests/squash_and_merge'),
},
},
learnMore: __('Learn more'),
};
export const I18N_SHA_MISMATCH = {

View File

@ -13,10 +13,8 @@ module WorkItems
end
def execute
if cleanup_data_source_work_item_data?
cleanup_work_item_widgets_data
cleanup_work_item_non_widgets_data
end
cleanup_work_item_non_widgets_data
cleanup_work_item_widgets_data
cleanup_work_item
end
@ -32,6 +30,10 @@ module WorkItems
sync_data_callback_class = widget.class.sync_data_callback_class
next if sync_data_callback_class.nil?
unless cleanup_data_source_work_item_data? || sync_data_callback_class.class.name.demodulize == 'Hierarchy'
next
end
data_handler = sync_data_callback_class.new(
work_item: work_item,
target_work_item: nil,
@ -46,6 +48,7 @@ module WorkItems
WorkItem.non_widgets.filter_map do |association_name|
sync_callback_class = WorkItem.sync_callback_class(association_name)
next if sync_callback_class.nil?
next unless cleanup_data_source_work_item_data?
data_handler = sync_callback_class.new(
work_item: work_item,

View File

@ -22,6 +22,10 @@ module WorkItems
# Has to be implemented in the specific widget class or it can be an empty implementation if it does not need to
# cleanup any data on the original work item
def post_move_cleanup; end
def cleanup_data_source_work_item_data?
Feature.enabled?(:cleanup_data_source_work_item_data, work_item.resource_parent)
end
end
end
end

View File

@ -18,13 +18,15 @@ module WorkItems
end
def post_move_cleanup
return unless cleanup_data_source_work_item_data?
# Cleanup children linked to moved item when that is an issue because we are currently creating those
# child items in the destination namespace anyway. If we decide to relink child items for Issue WIT
# then we should not be deleting them here.
work_item.child_links.each { |child_link| child_link.work_item.destroy! } if work_item.work_item_type.issue?
# cleanup parent link
work_item.parent_link&.destroy!
remove_parent_link_from_work_item
end
private
@ -81,6 +83,14 @@ module WorkItems
).execute
end
end
def remove_parent_link_from_work_item
return unless work_item.parent_link
::WorkItems::ParentLinks::DestroyService
.new(work_item.parent_link, current_user, skip_policy_check: true)
.execute
end
end
end
end

View File

@ -5,7 +5,7 @@ module WorkItems
class DestroyService < IssuableLinks::DestroyService
extend ::Gitlab::Utils::Override
attr_reader :link, :current_user, :parent, :child
attr_reader :link, :current_user, :parent, :child, :params
def initialize(link, user, params = {})
@link = link
@ -34,6 +34,9 @@ module WorkItems
end
def permission_to_remove_relation?
# we can skip policy check if we check policy in the service that calls this
return true if skip_policy_check?
can?(current_user, :admin_parent_link, child) && can?(current_user, :admin_parent_link, parent)
end
@ -43,6 +46,10 @@ module WorkItems
GraphqlTriggers.work_item_updated(@link.work_item_parent)
end
def skip_policy_check?
params.fetch(:skip_policy_check, false)
end
end
end
end

View File

@ -1,5 +1,14 @@
- merge_request = local_assigns.fetch(:issuable)
- popover_title = s_("What is squashing?")
- popover_content = capture do
%p.gl-mb-0
= s_("Squashing combines multiple commits into a single commit on merge. This keeps your repository history clean and makes it easier to revert changes.")
= link_to s_("Learn more"), help_page_path('user/project/merge_requests/squash_and_merge.md'), target: '_blank', rel: 'noopener noreferrer'
- popover_data = { container: 'body', toggle: 'popover', placement: "top", html: 'true', triggers: 'hover focus', title: popover_title, content: popover_content }
- return if !merge_request.is_a?(MergeRequest) || merge_request.closed_or_merged_without_fork?
.form-group.row.gl-mb-7
@ -28,4 +37,4 @@
= render Pajamas::CheckboxTagComponent.new(name: 'merge_request[squash]', checked: merge_request_squash_option?(merge_request), value: '1', checkbox_options: { class: 'js-form-update' }) do |c|
- c.with_label do
= _("Squash commits when merge request is accepted.")
= link_to sprite_icon('question-o'), help_page_path('user/project/merge_requests/squash_and_merge.md'), target: '_blank', rel: 'noopener noreferrer'
= render Pajamas::ButtonComponent.new(variant: :link, icon: 'question-o', button_options: { class: '-gl-mt-1', data: popover_data })

View File

@ -60909,6 +60909,9 @@ msgstr ""
msgid "SquashTmIntegration|Update Squash TM requirements when GitLab issues are modified."
msgstr ""
msgid "Squashing combines multiple commits into a single commit on merge. This keeps your repository history clean and makes it easier to revert changes."
msgstr ""
msgid "Stack trace"
msgstr ""

View File

@ -1,5 +1,6 @@
import { GlFormCheckbox, GlLink } from '@gitlab/ui';
import { GlFormCheckbox } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import HelpPopover from '~/vue_shared/components/help_popover.vue';
import SquashBeforeMerge from '~/vue_merge_request_widget/components/states/squash_before_merge.vue';
import { SQUASH_BEFORE_MERGE } from '~/vue_merge_request_widget/i18n';
@ -64,37 +65,48 @@ describe('Squash before merge component', () => {
});
});
describe('about link', () => {
describe('help popover', () => {
it('is not rendered if no help path is passed', () => {
createComponent({
value: false,
});
const aboutLink = wrapper.findComponent(GlLink);
const helpPopover = wrapper.findComponent(HelpPopover);
expect(aboutLink.exists()).toBe(false);
expect(helpPopover.exists()).toBe(false);
});
it('is rendered if help path is passed', () => {
it('is rendered if help path is passed', () => {
createComponent({
value: false,
helpPath: 'test-path',
});
const aboutLink = wrapper.findComponent(GlLink);
const helpPopover = wrapper.findComponent(HelpPopover);
expect(aboutLink.exists()).toBe(true);
expect(helpPopover.exists()).toBe(true);
});
it('should have a correct help path if passed', () => {
it('should have correct popover options', () => {
createComponent({
value: false,
helpPath: 'test-path',
});
const aboutLink = wrapper.findComponent(GlLink);
const helpPopover = wrapper.findComponent(HelpPopover);
expect(aboutLink.attributes('href')).toEqual('test-path');
expect(helpPopover.props('options')).toEqual(SQUASH_BEFORE_MERGE.popoverOptions);
});
it('should have correct aria-label', () => {
createComponent({
value: false,
helpPath: 'test-path',
});
const helpPopover = wrapper.findComponent(HelpPopover);
expect(helpPopover.props('ariaLabel')).toBe(SQUASH_BEFORE_MERGE.helpLabel);
});
});
});

View File

@ -129,12 +129,32 @@ RSpec.describe WorkItems::DataSync::Widgets::Hierarchy, feature_category: :team_
describe '#post_move_cleanup' do
let_it_be(:parent_link) { create(:parent_link, work_item: work_item, work_item_parent: parent) }
it "clears the parent for the original work_item" do
expect { callback.post_move_cleanup }.to change { work_item.reload.work_item_parent }.from(parent).to(nil)
context 'when cleanup_data_source_work_item_data feature is enabled' do
before do
stub_feature_flags(cleanup_data_source_work_item_data: true)
end
it "clears the parent for the original work_item" do
expect { callback.post_move_cleanup }.to change { work_item.reload.work_item_parent }.from(parent).to(nil)
end
it "deletes a work_item_parent_link record" do
expect { callback.post_move_cleanup }.to change { WorkItems::ParentLink.count }.by(-1)
end
end
it "deletes a work_item_parent_link record" do
expect { callback.post_move_cleanup }.to change { WorkItems::ParentLink.count }.by(-1)
context 'when cleanup_data_source_work_item_data feature is disabled' do
before do
stub_feature_flags(cleanup_data_source_work_item_data: false)
end
it "does not clear the parent for the original work_item" do
expect { callback.post_move_cleanup }.not_to change { work_item.reload.work_item_parent }
end
it "does not clear a work_item_parent_link record" do
expect { callback.post_move_cleanup }.not_to change { WorkItems::ParentLink.count }
end
end
end
end

View File

@ -8,7 +8,7 @@ RSpec.describe WorkItems::ParentLinks::DestroyService, feature_category: :team_p
let_it_be(:project) { create(:project) }
let_it_be(:work_item) { create(:work_item, project: project) }
let_it_be(:task) { create(:work_item, :task, project: project) }
let_it_be(:parent_link) { create(:parent_link, work_item: task, work_item_parent: work_item) }
let!(:parent_link) { create(:parent_link, work_item: task, work_item_parent: work_item) }
let(:parent_link_class) { WorkItems::ParentLink }
@ -88,6 +88,16 @@ RSpec.describe WorkItems::ParentLinks::DestroyService, feature_category: :team_p
.and not_change(WorkItems::ResourceLinkEvent, :count)
expect(SystemNoteService).not_to receive(:unrelate_work_item)
end
context 'when skip_policy_check is true' do
it 'removes relation' do
expect(SystemNoteService).to receive(:unrelate_work_item)
expect { described_class.new(parent_link, user, skip_policy_check: true).execute }
.to change { WorkItems::ParentLink.count }.by(-1)
.and change { WorkItems::ResourceLinkEvent.count }.by(1)
end
end
end
end
end

View File

@ -662,6 +662,23 @@ RSpec.shared_examples 'work items iteration' do
end
RSpec.shared_examples 'work items time tracking' do
def add_estimate(estimate)
click_button 'estimate'
within_testid 'set-time-estimate-modal' do
fill_in 'Estimate', with: estimate
click_button 'Save'
end
end
def add_time_entry(time, summary = '')
click_button 'Add time entry'
within_testid 'create-timelog-modal' do
fill_in 'Time spent', with: time
fill_in 'Summary', with: summary
click_button 'Save'
end
end
it 'passes axe automated accessibility testing for estimate and time spent modals', :aggregate_failures do
click_button 'estimate'
@ -676,11 +693,7 @@ RSpec.shared_examples 'work items time tracking' do
end
it 'adds and removes an estimate', :aggregate_failures do
click_button 'estimate'
within_testid 'set-time-estimate-modal' do
fill_in 'Estimate', with: '5d'
click_button 'Save'
end
add_estimate('5d')
expect(page).to have_text 'Estimate 5d'
expect(page).to have_button '5d'
@ -697,21 +710,9 @@ RSpec.shared_examples 'work items time tracking' do
end
it 'adds and deletes time entries and view report', :aggregate_failures do
click_button 'Add time entry'
add_time_entry('1d', 'First summary')
within_testid 'create-timelog-modal' do
fill_in 'Time spent', with: '1d'
fill_in 'Summary', with: 'First summary'
click_button 'Save'
end
click_button 'Add time entry'
within_testid 'create-timelog-modal' do
fill_in 'Time spent', with: '2d'
fill_in 'Summary', with: 'Second summary'
click_button 'Save'
end
add_time_entry('2d', 'Second summary')
expect(page).to have_text 'Spent 3d'
expect(page).to have_button '3d'
@ -734,6 +735,52 @@ RSpec.shared_examples 'work items time tracking' do
expect(page).to have_text 'Spent 1d'
expect(page).to have_button '1d'
end
it 'checks for progess bar with both time entries and estimate', :aggregate_failures do
add_estimate('5d')
expect(page).to have_text 'Estimate 5d'
expect(page).to have_button '5d'
expect(page).not_to have_button 'estimate'
add_time_entry('1d')
expect(page).to have_text 'Spent 1d'
expect(page).to have_button '1d'
within_testid 'time-tracking-body' do
expect(page).to have_selector('[role="progressbar"][aria-valuenow="20"]')
end
end
it 'using quick actions', :aggregate_failures do
add_estimate('5d')
expect(page).to have_text 'Estimate 5d'
expect(page).to have_button '5d'
expect(page).not_to have_button 'estimate'
add_time_entry('1d')
expect(page).to have_text 'Spent 1d'
expect(page).to have_button '1d'
fill_in _('Add a reply'), with: '/estimate 4d'
click_button "Comment"
fill_in _('Add a reply'), with: '/spend 1d'
click_button "Comment"
expect(page).to have_text 'Estimate 4d'
expect(page).to have_button '4d'
expect(page).to have_text 'Spent 2d'
expect(page).to have_button '2d'
within_testid 'time-tracking-body' do
expect(page).to have_selector('[role="progressbar"][aria-valuenow="50"]')
end
end
end
RSpec.shared_examples 'work items crm contacts' do

View File

@ -73,11 +73,12 @@ RSpec.describe ProcessCommitWorker, feature_category: :source_code_management do
end
context 'when commit is not a merge request merge commit' do
context 'when commit has work_item reference' do
context 'when commit has work_item reference', :clean_gitlab_redis_cache do
let(:work_item) { create(:work_item, :task, project: project) }
let(:work_item_url) { Gitlab::UrlBuilder.build(work_item) }
before do
# markdown cache from CacheMarkdownField needs to be cleared otherwise cached references are used
allow(commit).to receive_messages(
safe_message: "Ref #{work_item_url}",
author: author

View File

@ -36,7 +36,6 @@ func Handler(rails *api.API) http.Handler {
fail.Request(w, r, fmt.Errorf("failed to execute a Duo Workflow: %v", err))
return
}
defer func() { _ = wf.CloseSend() }()
runner := &runner{
rails: rails,
@ -45,6 +44,7 @@ func Handler(rails *api.API) http.Handler {
conn: conn,
wf: wf,
}
defer func() { _ = runner.threadSafeCloseSend() }()
if err := runner.Execute(r.Context()); err != nil {
log.WithRequest(r).WithError(err).Error()

View File

@ -32,6 +32,7 @@ type websocketConn interface {
type workflowStream interface {
Send(*pb.ClientEvent) error
Recv() (*pb.Action, error)
CloseSend() error
}
type runner struct {
@ -137,3 +138,9 @@ func (r *runner) threadSafeSend(event *pb.ClientEvent) error {
defer r.sendMu.Unlock()
return r.wf.Send(event)
}
func (r *runner) threadSafeCloseSend() error {
r.sendMu.Lock()
defer r.sendMu.Unlock()
return r.wf.CloseSend()
}

View File

@ -84,6 +84,10 @@ func (m *mockWorkflowStream) Recv() (*pb.Action, error) {
return action, nil
}
func (m *mockWorkflowStream) CloseSend() error {
return nil
}
func TestRunner_Execute(t *testing.T) {
tests := []struct {
name string