Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
ce9d6d8a63
commit
5494c7bfaa
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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)"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -36,6 +36,10 @@ module Ci
|
|||
end
|
||||
end
|
||||
|
||||
event :created do
|
||||
transition all => :created
|
||||
end
|
||||
|
||||
event :pending do
|
||||
transition all => :pending
|
||||
end
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1 @@
|
|||
9f7be8572dc499518d406e34e9c288c8b0814ffa7e64eb6f4b2ebf00ce2e2f81
|
||||
|
|
@ -0,0 +1 @@
|
|||
ed715ca79c363968875b2193beb563bc59a8648b15db7937ddf9bff29501a1d3
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -97,6 +97,7 @@ describe('DiffAppControls', () => {
|
|||
showWhitespace: false,
|
||||
diffViewType: 'parallel',
|
||||
viewDiffsFileByFile: true,
|
||||
fileByFileSupported: true,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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 }));
|
||||
|
|
|
|||
|
|
@ -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."}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue