Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2025-04-30 12:12:16 +00:00
parent ce9d6d8a63
commit 5494c7bfaa
47 changed files with 327 additions and 113 deletions

View File

@ -3198,7 +3198,6 @@ RSpec/FeatureCategory:
- 'spec/models/event_collection_spec.rb'
- 'spec/models/external_issue_spec.rb'
- 'spec/models/fork_network_member_spec.rb'
- 'spec/models/fork_network_spec.rb'
- 'spec/models/generic_commit_status_spec.rb'
- 'spec/models/gpg_key_spec.rb'
- 'spec/models/gpg_key_subkey_spec.rb'

View File

@ -55,6 +55,11 @@ export default {
required: false,
default: false,
},
fileByFileSupported: {
type: Boolean,
required: false,
default: true,
},
},
computed: {
expandButtonInfo() {
@ -130,6 +135,7 @@ export default {
:show-whitespace="showWhitespace"
:view-diffs-file-by-file="viewDiffsFileByFile"
:diff-view-type="diffViewType"
:file-by-file-supported="fileByFileSupported"
@updateDiffViewType="$emit('updateDiffViewType', $event)"
@toggleWhitespace="$emit('toggleWhitespace', $event)"
@toggleFileByFile="$emit('toggleFileByFile', $event)"

View File

@ -23,7 +23,13 @@ export default {
},
viewDiffsFileByFile: {
type: Boolean,
required: true,
required: false,
default: false,
},
fileByFileSupported: {
type: Boolean,
required: false,
default: true,
},
},
methods: {
@ -68,6 +74,7 @@ export default {
{{ $options.i18n.whitespace }}
</gl-form-checkbox>
<gl-form-checkbox
v-if="fileByFileSupported"
data-testid="file-by-file"
class="gl-mb-0"
:checked="viewDiffsFileByFile"

View File

@ -39,6 +39,7 @@ const initSettingsApp = (el, pinia) => {
showWhitespace: this.showWhitespace,
diffViewType: this.viewType,
viewDiffsFileByFile: this.singleFileMode,
fileByFileSupported: false,
isLoading: this.isLoading,
addedLines: this.diffsStats?.addedLines,
removedLines: this.diffsStats?.removedLines,

View File

@ -197,7 +197,7 @@ export default {
line-height: 16px;
text-overflow: ellipsis;
white-space: nowrap;
margin-left: calc(var(--level) * 16px);
margin-left: calc(var(--level) * var(--file-row-level-padding, 16px));
}
.file-row-name .file-row-icon {

View File

@ -98,7 +98,7 @@ export default {
<div v-if="isLoading">
<gl-loading-icon inline />
</div>
<div v-else class="gl-mb-3 gl-mt-3 gl-text-subtle">
<div v-else class="gl-my-2 gl-text-subtle">
<work-item-state-badge
v-if="workItemState"
:work-item-state="workItemState"

View File

@ -1032,7 +1032,7 @@ export default {
@click="$emit('close')"
/>
</div>
<div :class="{ 'gl-mt-3': !editMode }">
<div>
<work-item-title
v-if="workItem.title && shouldShowAncestors"
ref="title"

View File

@ -282,6 +282,9 @@ export default {
'!gl-px-3 gl-pb-3 gl-pt-2': !this.hasAllChildItemsHidden,
};
},
shouldShowTreeWidget() {
return this.children.length > 0 || this.canUpdateChildren;
},
},
watch: {
'workItem.id': {
@ -384,6 +387,7 @@ export default {
<template>
<crud-component
v-if="shouldShowTreeWidget"
ref="workItemTree"
:title="s__('WorkItem|Child items')"
:anchor-id="widgetName"

View File

@ -193,6 +193,9 @@ export default {
toggleClosedItemsClasses() {
return { '!gl-px-3 gl-pb-3 gl-pt-2': !this.hasAllLinkedItemsHidden };
},
shouldShowRelationshipsWidget() {
return this.linkedWorkItems.length > 0 || this.canAdminWorkItemLink;
},
},
mounted() {
this.showLabels = getToggleFromLocalStorage(this.showLabelsLocalStorageKey);
@ -333,6 +336,7 @@ export default {
</script>
<template>
<crud-component
v-if="shouldShowRelationshipsWidget"
ref="widget"
:anchor-id="widgetName"
:title="$options.i18n.title"

View File

@ -440,6 +440,7 @@ span.idiff {
}
.mr-tree-list {
--file-row-level-padding: 16px;
display: flex;
flex-direction: column;
min-height: 0;
@ -453,25 +454,22 @@ span.idiff {
}
}
.mr-tree-list:not(.tree-list-blobs) {
.tree-list-parent::before {
content: '';
position: absolute;
z-index: 1;
pointer-events: none;
top: -4px;
left: 0;
width: 100%;
bottom: -4px;
// The virtual scroller has a flat HTML structure so instead of the ::before
// element stretching over multiple rows we instead create a repeating background image
// for the line
background: repeating-linear-gradient(to right, var(--gl-border-color-default), var(--gl-border-color-default) 1px, transparent 1px, transparent 14px);
background-size: calc(var(--level) * 14px) 100%;
background-repeat: no-repeat;
background-position: 14px;
}
.mr-tree-list:not(.tree-list-blobs) .tree-list-parent::before {
content: '';
position: absolute;
z-index: 1;
pointer-events: none;
top: -4px;
left: 14px;
width: calc(var(--level) * var(--file-row-level-padding));
bottom: 4px;
// The virtual scroller has a flat HTML structure so instead of the ::before
// element stretching over multiple rows we instead create a repeating background image
// for the line
background:
linear-gradient(to right, var(--gl-border-color-default) 1px, transparent 1px)
repeat-x
0 / var(--file-row-level-padding) 100%;
}
.blame-table {

View File

@ -36,6 +36,10 @@ module Ci
end
end
event :created do
transition all => :created
end
event :pending do
transition all => :pending
end

View File

@ -1403,7 +1403,10 @@ module Ci
return unless bridge_waiting?
return unless current_user.can?(:update_pipeline, source_bridge.pipeline)
Ci::EnqueueJobService.new(source_bridge, current_user: current_user).execute(&:pending!) # rubocop:disable CodeReuse/ServiceClass
# Before enqueuing the trigger job again, its status must be one of :created, :skipped, :manual, and :scheduled.
# Also, we use `skip_pipeline_processing` to prevent processing the pipeline to avoid redundant process.
source_bridge.created!(current_user, skip_pipeline_processing: true)
Ci::EnqueueJobService.new(source_bridge, current_user: current_user).execute # rubocop:disable CodeReuse/ServiceClass
end
# EE-only

View File

@ -2,7 +2,7 @@
class ForkNetwork < ApplicationRecord
belongs_to :root_project, class_name: 'Project'
belongs_to :organization, class_name: 'Organizations::Organization', optional: true
belongs_to :organization, class_name: 'Organizations::Organization'
has_many :fork_network_members
has_many :projects, through: :fork_network_members

View File

@ -53,7 +53,7 @@ module Auth
})
end
def self.push_pull_nested_repositories_access_token(name)
def self.push_pull_nested_repositories_access_token(name, project:)
name = name.chomp('/')
access_token(
@ -61,11 +61,12 @@ module Auth
name => %w[pull push],
"#{name}/*" => %w[pull]
},
project: project,
use_key_as_project_path: true
)
end
def self.push_pull_move_repositories_access_token(name, new_namespace)
def self.push_pull_move_repositories_access_token(name, new_namespace, project:)
name = name.chomp('/')
access_token(
@ -74,11 +75,12 @@ module Auth
"#{name}/*" => %w[pull],
"#{new_namespace}/*" => %w[push]
},
project: project,
use_key_as_project_path: true
)
end
def self.access_token(names_and_actions, type = 'repository', use_key_as_project_path: false)
def self.access_token(names_and_actions, type = 'repository', use_key_as_project_path: false, project: nil)
registry = Gitlab.config.registry
token = JSONWebToken::RSAToken.new(registry.key)
token.issuer = registry.issuer
@ -90,7 +92,7 @@ module Auth
type: type,
name: name,
actions: actions,
meta: access_metadata(path: name, use_key_as_project_path: use_key_as_project_path, actions: actions)
meta: access_metadata(path: name, use_key_as_project_path: use_key_as_project_path, actions: actions, project: project)
}.compact
end
@ -102,9 +104,8 @@ module Auth
end
def self.access_metadata(project: nil, path: nil, use_key_as_project_path: false, actions: [], user: nil)
return { project_path: path.chomp('/*').downcase } if use_key_as_project_path
return access_metadata_with_key_as_project_path(project:, path:, actions:, user:) if use_key_as_project_path
# If the project is not given, try to infer it from the provided path
if project.nil?
return if path.nil? # If no path is given, return early
return if path == 'import' # Ignore the special 'import' path
@ -124,14 +125,23 @@ module Auth
{
project_path: project&.full_path&.downcase,
project_id: project&.id,
root_namespace_id: project&.root_ancestor&.id,
tag_deny_access_patterns: tag_deny_access_patterns(project, user, actions),
tag_immutable_patterns: tag_immutable_patterns(project, user, actions)
}.compact
root_namespace_id: project&.root_ancestor&.id
}.merge(patterns_metadata(project, user, actions)).compact
end
private
def self.access_metadata_with_key_as_project_path(project:, path:, actions: [], user: nil)
{ project_path: path.chomp('/*').downcase }.merge(patterns_metadata(project, user, actions)).compact
end
def self.patterns_metadata(project, user, actions)
{
tag_deny_access_patterns: tag_deny_access_patterns(project, user, actions),
tag_immutable_patterns: tag_immutable_patterns(project, actions)
}
end
def self.tag_deny_access_patterns(project, user, actions)
return if project.nil? || user.nil?
@ -167,8 +177,8 @@ module Auth
patterns
end
def self.tag_immutable_patterns(project, user, actions)
return if project.nil? || user.nil?
def self.tag_immutable_patterns(project, actions)
return if project.nil?
return unless Feature.enabled?(:container_registry_immutable_tags, project)
return unless (actions & %w[push delete *]).any?

View File

@ -10,14 +10,12 @@ module Ci
@variables = variables
end
def execute(&transition)
transition ||= ->(job) { job.enqueue! }
def execute
Gitlab::OptimisticLocking.retry_lock(job, name: 'ci_enqueue_job') do |job|
job.user = current_user
job.job_variables_attributes = variables if variables
transition.call(job)
job.enqueue!
end
ResetSkippedJobsService.new(job.project, current_user).execute(job)

View File

@ -55,7 +55,8 @@ module Projects
ensure_registry_tags_can_be_handled
result = ContainerRegistry::GitlabApiClient.rename_base_repository_path(
full_path_before, name: project_path)
full_path_before, name: project_path, project: project
)
return if result == :ok

View File

@ -123,7 +123,7 @@ module Projects
end
def raise_error_due_to_tags_if_transfer_dry_run_fails(project)
dry_run = transfer_project_path_in_registry(project.full_path, new_namespace.full_path, dry_run: true)
dry_run = transfer_project_path_in_registry(project.full_path, new_namespace.full_path, project: project, dry_run: true)
return if dry_run == :accepted
raise TransferError, format(s_('TransferProject|Project cannot be transferred because of a container registry error: %{error}'), error: dry_run.to_s.titleize)
@ -155,7 +155,7 @@ module Projects
# Update Container Registry
if project.has_container_registry_tags?
transfer_project_path_in_registry(@old_path, @new_namespace.full_path, dry_run: false)
transfer_project_path_in_registry(@old_path, @new_namespace.full_path, project: project, dry_run: false)
end
update_integrations
@ -181,10 +181,11 @@ module Projects
refresh_permissions
end
def transfer_project_path_in_registry(old_project_path, new_namespace_path, dry_run:)
def transfer_project_path_in_registry(old_project_path, new_namespace_path, project:, dry_run:)
ContainerRegistry::GitlabApiClient.move_repository_to_namespace(
old_project_path,
namespace: new_namespace_path,
project: project,
dry_run: dry_run
)
end

View File

@ -131,7 +131,8 @@ module Projects
end
dry_run = ContainerRegistry::GitlabApiClient.rename_base_repository_path(
project.full_path, name: params[:path], dry_run: true)
project.full_path, name: params[:path], project: project, dry_run: true
)
return if dry_run == :accepted

View File

@ -22,6 +22,7 @@ module WorkItems
created_at: work_item.created_at,
updated_at: work_item.updated_at,
updated_by_id: work_item.updated_by_id,
state_id: work_item.state_id,
closed_at: work_item.closed_at,
closed_by_id: work_item.closed_by_id,
duplicated_to_id: work_item.duplicated_to_id,

View File

@ -5,4 +5,4 @@ feature_category: service_desk
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/174507
milestone: '17.7'
queued_migration_version: 20241203080309
finalized_by: # version of the migration that finalized this BBM
finalized_by: '20250428231756'

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
class AddNotNullToForkNetworksOrganizationId < Gitlab::Database::Migration[2.2]
disable_ddl_transaction!
milestone '18.0'
def up
change_column_null :fork_networks, :organization_id, false
end
def down
change_column_null :fork_networks, :organization_id, true
end
end

View File

@ -0,0 +1,21 @@
# frozen_string_literal: true
class FinalizeHkBackfillIssueEmailsNamespaceId < Gitlab::Database::Migration[2.3]
milestone '18.0'
disable_ddl_transaction!
restrict_gitlab_migration gitlab_schema: :gitlab_main_cell
def up
ensure_batched_background_migration_is_finished(
job_class_name: 'BackfillIssueEmailsNamespaceId',
table_name: :issue_emails,
column_name: :id,
job_arguments: [:namespace_id, :issues, :namespace_id, :issue_id],
finalize: true
)
end
def down; end
end

View File

@ -0,0 +1 @@
9f7be8572dc499518d406e34e9c288c8b0814ffa7e64eb6f4b2ebf00ce2e2f81

View File

@ -0,0 +1 @@
ed715ca79c363968875b2193beb563bc59a8648b15db7937ddf9bff29501a1d3

View File

@ -14589,7 +14589,7 @@ CREATE TABLE fork_networks (
id bigint NOT NULL,
root_project_id bigint,
deleted_root_project_name character varying,
organization_id bigint
organization_id bigint NOT NULL
);
CREATE SEQUENCE fork_networks_id_seq

View File

@ -5059,7 +5059,7 @@ In this example, the job has two steps:
**Additional details**:
- A step can have either a `script` or a `step` key, but not both.
- A `run` configuration cannot be used together with existing [`script`](#script) keyword.
- A `run` configuration cannot be used together with existing [`script`](#script), [`after_script`](#after_script) or [`before_script`](#before_script) keywords.
- Multi-line scripts can be defined using [YAML block scalar syntax](script.md#split-long-commands).
### `script`

View File

@ -54,13 +54,13 @@ module ContainerRegistry
Auth::ContainerRegistryAuthenticationService.pull_nested_repositories_access_token(config[:path])
when :push_pull_nested_repositories_token
return unless config[:path]
return unless [:path, :project].all? { |key| config[key].present? }
Auth::ContainerRegistryAuthenticationService.push_pull_nested_repositories_access_token(config[:path])
Auth::ContainerRegistryAuthenticationService.push_pull_nested_repositories_access_token(config[:path], project: config[:project])
when :push_pull_move_repositories_access_token
return unless config[:path].present? && config[:new_path].present?
return unless [:path, :new_path, :project].all? { |key| config[key].present? }
Auth::ContainerRegistryAuthenticationService.push_pull_move_repositories_access_token(config[:path], config[:new_path])
Auth::ContainerRegistryAuthenticationService.push_pull_move_repositories_access_token(config[:path], config[:new_path], project: config[:project])
end
end
end

View File

@ -57,17 +57,23 @@ module ContainerRegistry
end
end
def self.rename_base_repository_path(path, name:, dry_run: false)
def self.rename_base_repository_path(path, name:, project:, dry_run: false)
raise ArgumentError, 'incomplete parameters given' unless path.present? && name.present?
downcased_path = path.downcase
with_dummy_client(token_config: { type: :push_pull_nested_repositories_token, path: downcased_path }) do |client|
token_config = {
type: :push_pull_nested_repositories_token,
path: downcased_path,
project: project
}
with_dummy_client(token_config:) do |client|
client.rename_base_repository_path(downcased_path, name: name.downcase, dry_run: dry_run)
end
end
def self.move_repository_to_namespace(path, namespace:, dry_run: false)
def self.move_repository_to_namespace(path, namespace:, project:, dry_run: false)
raise ArgumentError, 'incomplete parameters given' unless path.present? && namespace.present?
downcased_path = path.downcase
@ -76,10 +82,11 @@ module ContainerRegistry
token_config = {
type: :push_pull_move_repositories_access_token,
path: downcased_path,
new_path: downcased_namespace
new_path: downcased_namespace,
project: project
}
with_dummy_client(token_config: token_config) do |client|
with_dummy_client(token_config:) do |client|
client.move_repository_to_namespace(downcased_path, namespace: downcased_namespace, dry_run: dry_run)
end
end

View File

@ -21,6 +21,8 @@ module Gitlab
validations do
validates :config, allowed_keys: Gitlab::Ci::Config::Entry::Job.allowed_keys + PROCESSABLE_ALLOWED_KEYS
validates :config, mutually_exclusive_keys: %i[script run]
validates :config, mutually_exclusive_keys: %i[before_script run]
validates :config, mutually_exclusive_keys: %i[after_script run]
validates :script, presence: true, if: -> { config.is_a?(Hash) && !config.key?(:run) }
with_options allow_nil: true do

View File

@ -502,7 +502,7 @@ module QA
def cherry_pick!
click_element('cherry-pick-button', Page::Component::CommitModal)
click_element('submit-commit')
submit_commit
end
def revert_change!
@ -511,7 +511,7 @@ module QA
retry_on_exception(reload: true) do
click_element('revert-button', Page::Component::CommitModal)
end
click_element('submit-commit')
submit_commit
end
def mr_widget_text
@ -553,6 +553,13 @@ module QA
def has_exposed_artifact_with_name?(name)
has_link?(name)
end
private
def submit_commit
# There may be two modals due to https://gitlab.com/gitlab-org/gitlab/-/issues/538079
all_elements('submit-commit', minimum: 1).last.click
end
end
end
end

View File

@ -97,6 +97,7 @@ describe('DiffAppControls', () => {
showWhitespace: false,
diffViewType: 'parallel',
viewDiffsFileByFile: true,
fileByFileSupported: true,
});
});

View File

@ -4,6 +4,7 @@ import SettingsDropdown from '~/diffs/components/settings_dropdown.vue';
const defaultProps = {
diffViewType: 'inline',
showWhitespace: false,
};
describe('Diff settings dropdown component', () => {
@ -86,5 +87,10 @@ describe('Diff settings dropdown component', () => {
expect(wrapper.emitted('toggleFileByFile')).toEqual([[eventValue]]);
},
);
it('can be hidden', () => {
createComponent({ fileByFileSupported: false });
expect(findFileByFileCheckbox().exists()).toBe(false);
});
});
});

View File

@ -23,6 +23,7 @@ jest.mock('~/diffs/components/diff_app_controls.vue', () => ({
'data-added-lines': JSON.stringify(this.addedLines),
'data-removed-lines': JSON.stringify(this.removedLines),
'data-diffs-count': JSON.stringify(this.diffsCount),
'data-file-by-file-supported': JSON.stringify(this.fileByFileSupported),
},
});
},
@ -86,6 +87,7 @@ describe('View settings', () => {
expect(getProp('addedLines')).toBe(1);
expect(getProp('removedLines')).toBe(2);
expect(getProp('diffsCount')).toBe(3);
expect(getProp('fileByFileSupported')).toBe(false);
});
it('triggers collapse all files', () => {

View File

@ -466,6 +466,15 @@ describe('WorkItemTree', () => {
expect(findShowClosedButton().exists()).toBe(true);
});
it('does not render the component if there are no children and user does not have permission to update children', async () => {
await createComponent({
workItemHierarchyTreeHandler: jest.fn().mockResolvedValue(workItemHierarchyTreeEmptyResponse),
canUpdateChildren: false,
});
expect(wrapper.findByTestId('work-item-tree').exists()).toBe(false);
});
describe('when there is show URL parameter', () => {
it('emits `show-modal` event when child work item id is encoded in the URL', async () => {
const encodedWorkItemId = btoa(JSON.stringify({ id: 31 }));

View File

@ -120,6 +120,15 @@ describe('WorkItemRelationships', () => {
expect(findWorkItemRelationshipForm().exists()).toBe(false);
});
it('does not render the component if there are no linked items and user does not have permission to admin work item link', async () => {
await createComponent({
workItemLinkedItemsHandler: jest.fn().mockResolvedValue(workItemEmptyLinkedItemsResponse),
canAdminWorkItemLink: false,
});
expect(wrapper.findByTestId('work-item-relationships').exists()).toBe(false);
});
it.each`
hasBlockedWorkItemsFeature | emptyStateMessage
${true} | ${"Link items together to show that they're related or that one is blocking others."}

View File

@ -9,6 +9,7 @@ RSpec.describe ContainerRegistry::GitlabApiClient, feature_category: :container_
include_context 'container registry client stubs'
let(:path) { 'namespace/path/to/repository' }
let_it_be(:project) { create(:project) }
shared_examples 'returning the correct result based on status code' do
where(:dry_run, :status_code, :expected_result) do
@ -710,7 +711,7 @@ RSpec.describe ContainerRegistry::GitlabApiClient, feature_category: :container_
let(:dry_run) { true }
let(:expected_dry_run) { true }
subject(:request) { described_class.rename_base_repository_path(path, name: name, dry_run: true) }
subject(:request) { described_class.rename_base_repository_path(path, name: name, project: project, dry_run: true) }
context 'when both path and name are present' do
before do
@ -738,7 +739,7 @@ RSpec.describe ContainerRegistry::GitlabApiClient, feature_category: :container_
let(:expected_dry_run) { false }
it 'defaults to false' do
described_class.rename_base_repository_path(path, name: 'newname')
described_class.rename_base_repository_path(path, name: 'newname', project: project)
end
end
end
@ -761,7 +762,7 @@ RSpec.describe ContainerRegistry::GitlabApiClient, feature_category: :container_
let(:expected_dry_run) { true }
let(:namespace) { 'group_a/subgroup_b' }
subject(:request) { described_class.move_repository_to_namespace(path, namespace: namespace, dry_run: dry_run) }
subject(:request) { described_class.move_repository_to_namespace(path, namespace: namespace, project: project, dry_run: dry_run) }
context 'when both path and namespace are present' do
before do
@ -789,7 +790,7 @@ RSpec.describe ContainerRegistry::GitlabApiClient, feature_category: :container_
let(:expected_dry_run) { false }
it 'defaults to false' do
described_class.move_repository_to_namespace(path, namespace: namespace)
described_class.move_repository_to_namespace(path, namespace: namespace, project: project)
end
end
end

View File

@ -41,8 +41,24 @@ RSpec.describe Gitlab::BackgroundMigration::BackfillOrganizationIdOnForkNetworks
}
end
let(:connection) { ActiveRecord::Base.connection }
subject(:perform_migration) { described_class.new(**args).perform }
around do |example|
connection.transaction do
connection.execute(<<~SQL)
ALTER TABLE fork_networks ALTER COLUMN organization_id DROP NOT NULL;
SQL
example.run
connection.execute(<<~SQL)
ALTER TABLE fork_networks ALTER COLUMN organization_id SET NOT NULL;
SQL
end
end
context 'when root project exists' do
let(:fork_network) { fork_networks_table.create!(root_project_id: project.id) }

View File

@ -244,12 +244,27 @@ RSpec.describe Gitlab::Ci::Config::Entry::Job, feature_category: :pipeline_compo
end
end
context 'when script and run are used together' do
let(:config) { { script: 'rspec', run: [{ name: 'step1', step: 'some reference' }] } }
context 'when mutually exclusive keys are used with run' do
using RSpec::Parameterized::TableSyntax
it 'returns error about using script and run' do
expect(entry).not_to be_valid
expect(entry.errors).to include 'job config these keys cannot be used together: script, run'
where(:conflicting_key, :error_message) do
:script | 'job config these keys cannot be used together: script, run'
:before_script | 'job config these keys cannot be used together: before_script, run'
:after_script | 'job config these keys cannot be used together: after_script, run'
end
with_them do
let(:config) do
{
conflicting_key => 'rspec',
run: [{ name: 'step1', step: 'some reference' }]
}
end
it 'returns error about mutually exclusive keys' do
expect(entry).not_to be_valid
expect(entry.errors).to include error_message
end
end
end

View File

@ -163,6 +163,42 @@ RSpec.describe Gitlab::Database::LooseForeignKeys, feature_category: :database d
end
end
end
it 'ensures that async_nullify does not conflict with not-null constraints' do
definitions
.filter { |definition| definition.on_delete == :async_nullify }
.each do |definition|
base_models_for(definition.from_table).each do |model|
nullable =
model.connection
.select_value(<<~SQL, 'NULLABLE', [definition.from_table, definition.column])
SELECT
CASE
WHEN a.attnotnull THEN false -- column is not-null
WHEN c.contype = 'c' AND pg_get_constraintdef(c.oid) LIKE '%IS NOT NULL%' THEN false -- not-null constraint check
WHEN c.contype = 'p' THEN false -- part of primary key constraint
ELSE true
END AS nullable
FROM pg_attribute a
LEFT JOIN pg_constraint c ON a.attrelid = c.conrelid AND a.attnum = ANY(c.conkey)
JOIN pg_class t ON a.attrelid = t.oid
JOIN pg_namespace s ON t.relnamespace = s.oid
WHERE
s.nspname = current_schema()
AND t.relname = $1
AND a.attname = $2
AND a.attnum > 0 -- non-system column
AND NOT a.attisdropped -- non-dropped column
LIMIT 1;
SQL
expect(nullable).to be_truthy, <<~ERROR
Column `#{definition.from_table}.#{definition.column}` is not-nullable,
and this conflicts with `on_delete: async_nullify`.
ERROR
end
end
end
end
end

View File

@ -5523,7 +5523,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep, feature_category:
let(:current_user) { owner }
context 'when the downstream has strategy: depend' do
it 'marks source bridge as pending' do
it 'enqueues the source bridge and marks it as pending' do
expect { reset_bridge }
.to change { bridge.reload.status }
.to('pending')
@ -5606,6 +5606,18 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep, feature_category:
end
end
end
context 'when the source bridge has a resource group' do
before do
bridge.update!(resource_group: create(:ci_resource_group, project: bridge.project))
end
it 'enqueues the source bridge and marks it as waiting_for_resource' do
expect { reset_bridge }
.to change { bridge.reload.status }
.to('waiting_for_resource')
end
end
end
context 'when the current user is not the bridge user' do

View File

@ -2,13 +2,17 @@
require 'spec_helper'
RSpec.describe ForkNetwork do
RSpec.describe ForkNetwork, feature_category: :source_code_management do
include ProjectForksHelper
describe "validations" do
it { is_expected.to belong_to(:organization) }
end
describe '#add_root_as_member' do
it 'adds the root project as a member when creating a new root network' do
project = create(:project)
fork_network = described_class.create!(root_project: project)
fork_network = described_class.create!(root_project: project, organization_id: project.organization_id)
expect(fork_network.projects).to include(project)
end

View File

@ -63,22 +63,6 @@ RSpec.describe Ci::EnqueueJobService, '#execute', feature_category: :continuous_
end
end
context 'when a transition block is supplied' do
let(:bridge) { create(:ci_bridge, :playable, pipeline: pipeline) }
let(:service) do
described_class.new(bridge, current_user: user)
end
subject(:execute) { service.execute(&:pending!) }
it 'calls the transition block instead of enqueue!' do
expect(bridge).to receive(:pending!)
expect(bridge).not_to receive(:enqueue!)
execute
end
end
context 'when the job is manually triggered another user' do
let(:job_variables) do
[{ key: 'third', secret_value: 'third' },
@ -90,19 +74,10 @@ RSpec.describe Ci::EnqueueJobService, '#execute', feature_category: :continuous_
end
it 'assigns the user and variables to the job', :aggregate_failures do
called = false
service.execute do
unless called
called = true
raise ActiveRecord::StaleObjectError
end
build.enqueue!
end
service.execute
build.reload
expect(called).to be true # ensure we actually entered the failure path
expect(build.user).to eq(user)
expect(build.job_variables.map(&:key)).to contain_exactly('third', 'fourth')
end

View File

@ -33,6 +33,7 @@ RSpec.describe Projects::AfterRenameService, feature_category: :groups_and_proje
# call. This makes testing a bit easier.
allow(project).to receive(:gitlab_shell).and_return(gitlab_shell)
stub_container_registry_config(enabled: false)
stub_application_setting(hashed_storage_enabled: true)
end
@ -46,8 +47,6 @@ RSpec.describe Projects::AfterRenameService, feature_category: :groups_and_proje
end
it 'renames a repository' do
stub_container_registry_config(enabled: false)
expect_any_instance_of(SystemHooksService)
.to receive(:execute_hooks_for)
.with(project, :rename)
@ -94,7 +93,7 @@ RSpec.describe Projects::AfterRenameService, feature_category: :groups_and_proje
it 'renames the base repository in the registry' do
expect(ContainerRegistry::GitlabApiClient).to receive(:rename_base_repository_path)
.with(full_path_before_rename, name: path_after_rename).and_return(:ok)
.with(full_path_before_rename, name: path_after_rename, project: project).and_return(:ok)
service_execute
end

View File

@ -460,7 +460,7 @@ RSpec.describe Projects::TransferService, feature_category: :groups_and_projects
context 'when the dry run in the registry succeeds' do
it 'allows the transfer to continue' do
expect(ContainerRegistry::GitlabApiClient).to receive(:move_repository_to_namespace).with(
project.full_path, namespace: target.full_path, dry_run: true)
project.full_path, namespace: target.full_path, project: project, dry_run: true)
expect(execute_transfer).to eq true
end
@ -472,7 +472,7 @@ RSpec.describe Projects::TransferService, feature_category: :groups_and_projects
before do
expect(ContainerRegistry::GitlabApiClient)
.to receive(:move_repository_to_namespace)
.with(project.full_path, namespace: target.full_path, dry_run: true)
.with(project.full_path, namespace: target.full_path, project: project, dry_run: true)
.and_return(dry_run_result)
end

View File

@ -31,6 +31,10 @@ RSpec.describe Projects::UpdateService, feature_category: :groups_and_projects d
let_it_be(:developer) { create(:user) }
let_it_be(:owner) { create(:user) }
before do
stub_container_registry_config(enabled: false)
end
context 'when changing restrict_user_defined_variables' do
using RSpec::Parameterized::TableSyntax
@ -615,7 +619,7 @@ RSpec.describe Projects::UpdateService, feature_category: :groups_and_projects d
end
def stub_rename_base_repository_in_registry(dry_run:, result: nil)
options = { name: new_name }
options = { name: new_name, project: project }
options[:dry_run] = true if dry_run
allow(ContainerRegistry::GitlabApiClient)
@ -625,7 +629,7 @@ RSpec.describe Projects::UpdateService, feature_category: :groups_and_projects d
end
def expect_rename_of_base_repository_in_registry(dry_run:, path: nil)
options = { name: new_name }
options = { name: new_name, project: project }
options[:dry_run] = true if dry_run
expect(ContainerRegistry::GitlabApiClient)

View File

@ -139,7 +139,7 @@ RSpec.describe WorkItems::DataSync::MoveService, feature_category: :team_plannin
author: original_work_item.author,
title: original_work_item.title,
description: original_work_item.description,
state_id: original_work_item.state_id,
state_id: original_work_item.reload.state_id,
created_at: original_work_item.reload.created_at,
updated_by: original_work_item.updated_by,
updated_at: original_work_item.reload.updated_at,
@ -163,6 +163,15 @@ RSpec.describe WorkItems::DataSync::MoveService, feature_category: :team_plannin
it_behaves_like 'cloneable and moveable work item'
context 'when original work item is closed', :freeze_time do
before do
original_work_item.update_columns(state_id: 2)
original_work_item_attrs[:state_id] = 2
end
it_behaves_like 'cloneable and moveable work item'
end
context 'when moving a project level work item to same project' do
let(:target_namespace) { project }

View File

@ -199,6 +199,24 @@ end
RSpec.shared_examples 'a container registry auth service' do
include_context 'container registry auth service context'
let(:push_delete_patterns_meta) { {} }
shared_examples 'returning tag name patterns when tag rules exist' do
context 'when the project has protection rules' do
let(:push_delete_patterns_meta) { { 'tag_immutable_patterns' => %w[immutable1 immutable2] } }
before do
create(:container_registry_protection_tag_rule, project: project, tag_name_pattern: 'mutable')
create(:container_registry_protection_tag_rule, :immutable, project: project, tag_name_pattern: 'immutable1')
create(:container_registry_protection_tag_rule, :immutable, project: project, tag_name_pattern: 'immutable2')
end
it_behaves_like 'having the correct scope'
it_behaves_like 'a valid token'
it_behaves_like 'not a container repository factory'
end
end
describe '.full_access_token' do
let_it_be(:project) { create(:project) }
@ -275,14 +293,15 @@ RSpec.shared_examples 'a container registry auth service' do
describe '.push_pull_nested_repositories_access_token' do
let_it_be(:project) { create(:project) }
let(:name) { project.full_path }
let(:token) { described_class.push_pull_nested_repositories_access_token(name) }
let(:token) { described_class.push_pull_nested_repositories_access_token(name, project:) }
let(:access) do
[
{
'type' => 'repository',
'name' => project.full_path,
'actions' => %w[pull push],
'meta' => { 'project_path' => project.full_path }
'meta' => { 'project_path' => project.full_path }.merge(push_delete_patterns_meta)
},
{
'type' => 'repository',
@ -296,7 +315,7 @@ RSpec.shared_examples 'a container registry auth service' do
subject { { token: token } }
it 'sends override project path as true for the access token' do
expect(described_class).to receive(:access_token).with(anything, use_key_as_project_path: true)
expect(described_class).to receive(:access_token).with(anything, project: project, use_key_as_project_path: true)
subject
end
@ -312,20 +331,23 @@ RSpec.shared_examples 'a container registry auth service' do
it_behaves_like 'a valid token'
it_behaves_like 'not a container repository factory'
end
it_behaves_like 'returning tag name patterns when tag rules exist'
end
describe '.push_pull_move_repositories_access_token' do
let_it_be(:project) { create(:project) }
let_it_be(:group) { create(:group) }
let(:name) { project.full_path }
let(:token) { described_class.push_pull_move_repositories_access_token(name, group.full_path) }
let(:token) { described_class.push_pull_move_repositories_access_token(name, group.full_path, project:) }
let(:access) do
[
{
'type' => 'repository',
'name' => project.full_path,
'actions' => %w[pull push],
'meta' => { 'project_path' => project.full_path }
'meta' => { 'project_path' => project.full_path }.merge(push_delete_patterns_meta)
},
{
'type' => 'repository',
@ -337,7 +359,7 @@ RSpec.shared_examples 'a container registry auth service' do
'type' => 'repository',
'name' => "#{group.full_path}/*",
'actions' => %w[push],
'meta' => { 'project_path' => group.full_path }
'meta' => { 'project_path' => group.full_path }.merge(push_delete_patterns_meta)
}
]
end
@ -355,6 +377,8 @@ RSpec.shared_examples 'a container registry auth service' do
it_behaves_like 'a valid token'
it_behaves_like 'not a container repository factory'
end
it_behaves_like 'returning tag name patterns when tag rules exist'
end
context 'user authorization' do