Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2024-02-05 15:12:13 +00:00
parent 0e18e8e966
commit de828d08e2
35 changed files with 542 additions and 135 deletions

View File

@ -224,7 +224,6 @@
- ".gitlab/ci/**/*.yml"
- "lib/gitlab/ci/templates/**/*.yml"
- "data/deprecations/**/*.yml"
- "data/removals/**/*.yml"
- "data/whats_new/**/*.yml"
.lint-metrics-yaml-patterns: &lint-metrics-yaml-patterns
@ -256,22 +255,12 @@
- '{yarn.lock,*/yarn.lock,*/*/yarn.lock}'
.python-patterns: &python-patterns
- '{requirements.txt,*/requirements.txt,*/*/requirements.txt}'
- '{requirements.pip,*/requirements.pip,*/*/requirements.pip}'
- '{Pipfile,*/Pipfile,*/*/Pipfile}'
- '{requires.txt,*/requires.txt,*/*/requires.txt}'
- '{setup.py,*/setup.py,*/*/setup.py}'
.dependency-patterns: &dependency-patterns
- '{Gemfile.lock,*/Gemfile.lock,*/*/Gemfile.lock}'
- '{composer.lock,*/composer.lock,*/*/composer.lock}'
- '{gems.locked,*/gems.locked,*/*/gems.locked}'
- '{go.sum,*/go.sum,*/*/go.sum}'
- '{npm-shrinkwrap.json,*/npm-shrinkwrap.json,*/*/npm-shrinkwrap.json}'
- '{package-lock.json,*/package-lock.json,*/*/package-lock.json}'
- '{yarn.lock,*/yarn.lock,*/*/yarn.lock}'
- '{packages.lock.json,*/packages.lock.json,*/*/packages.lock.json}'
- '{conan.lock,*/conan.lock,*/*/conan.lock}'
.frontend-dependency-patterns: &frontend-dependency-patterns
- "{package.json,yarn.lock}"
@ -344,7 +333,7 @@
- "config.ru"
# List explicitly all the app/ dirs that are backend (i.e. all except app/assets).
- "{,ee/,jh/}{app/channels,app/components,app/controllers,app/finders,app/graphql,app/helpers,app/mailers,app/models,app/policies,app/presenters,app/serializers,app/services,app/uploaders,app/validators,app/views,app/workers}/**/*"
- "{,ee/,jh/}{bin,config,db,gems,generator_templates,lib}/**/*"
- "{,ee/,jh/}{bin,config,db,elastic,gems,generator_templates,lib}/**/*"
- "{,ee/,jh/}spec/**/*"
# CI changes
- ".gitlab-ci.yml"
@ -360,7 +349,7 @@
- "GITLAB_ELASTICSEARCH_INDEXER_VERSION"
# List explicitly all the app/ dirs that are backend (i.e. all except app/assets).
- "{,ee/,jh/}{app/channels,app/components,app/controllers,app/finders,app/graphql,app/helpers,app/mailers,app/models,app/policies,app/presenters,app/serializers,app/services,app/uploaders,app/validators,app/views,app/workers}/**/*"
- "{,ee/,jh/}{bin,config,db,generator_templates,lib}/**/*"
- "{,ee/,jh/}{bin,config,db,elastic,gems,generator_templates,lib}/**/*"
- "{,ee/,jh/}spec/**/*"
# Redis patterns + feature flags
@ -370,7 +359,6 @@
- "{,ee/,jh/}{,spec/}lib/{,ee/,jh/}gitlab/usage_data_counters/{hll_redis_counter,redis_counter}{,_spec}.rb"
- "{,ee/,jh/}{,spec/}lib/{,ee/,jh/}gitlab/usage/metrics/instrumentations/redis{_metric,hll_metric}{,_spec}.rb"
- "{,ee/,jh/}{,spec/}lib/{,ee/,jh/}gitlab/usage/metrics/aggregates/sources/redis_hll{,_spec}.rb"
- "{,ee/,jh/}{,spec/}lib/{,ee/,jh/}gitlab/patch/action_cable_redis_listener{,_spec}.rb"
- "{,ee/,jh/}{,spec/}lib/{,ee/,jh/}gitlab/merge_requests/mergeability/redis_interface{,_spec}.rb"
- "{,ee/,jh/}{,spec/}lib/{,ee/,jh/}gitlab/markdown_cache/redis/*.rb"
- "{,ee/,jh/}{,spec/}lib/{,ee/,jh/}gitlab/redis/**/*.rb"
@ -383,9 +371,9 @@
# AI patterns:
.ai-patterns: &ai-patterns
- "{,ee/,jh/}lib/gitlab/llm/**/*"
- "{,ee/,jh/}{,spec/}lib/gitlab/llm/**/*"
- "{,ee/,jh/}lib/gitlab/duo/**/*"
- "{ee/,jh/}lib/gitlab/llm/**/*"
- "{ee/,jh/}{,spec/}lib/gitlab/llm/**/*"
- "{ee/,jh/}lib/gitlab/duo/**/*"
# DB patterns + .ci-patterns
.db-patterns: &db-patterns
@ -453,8 +441,10 @@
- "Rakefile"
- "tests.yml"
- "config.ru"
- "{,ee/,jh/}{app,bin,config,db,generator_templates,gems,haml_lint,lib,locale,public,scripts,storybook,symbol,vendor}/**/*"
- "doc/api/graphql/reference/*" # Files in this folder are auto-generated
- "{,ee/,jh/}{app,bin,config,db,elastic,generator_templates,gems,haml_lint,lib,locale,public,scripts,storybook,symbol,vendor}/**/*"
# Auto-generated files
- "doc/api/graphql/reference/*"
- "doc/administration/audit_event_streaming/audit_event_types.md"
# CI changes
- ".gitlab-ci.yml"
- ".gitlab/ci/**/*"
@ -477,8 +467,10 @@
- "Rakefile"
- "tests.yml"
- "config.ru"
- "{,ee/,jh/}{app,bin,config,db,generator_templates,gems,haml_lint,lib,locale,public,scripts,storybook,symbol,vendor}/**/*"
- "doc/api/graphql/reference/*" # Files in this folder are auto-generated
- "{,ee/,jh/}{app,bin,config,db,elastic,generator_templates,gems,haml_lint,lib,locale,public,scripts,storybook,symbol,vendor}/**/*"
# Auto-generated files
- "doc/api/graphql/reference/*"
- "doc/administration/audit_event_streaming/audit_event_types.md"
# CI changes
- ".gitlab-ci.yml"
- ".gitlab/ci/**/*"
@ -508,8 +500,10 @@
- "Rakefile"
- "tests.yml"
- "config.ru"
- "{,ee/,jh/}{app,bin,config,db,generator_templates,gems,haml_lint,lib,locale,public,scripts,storybook,symbol,vendor}/**/*"
- "doc/api/graphql/reference/*" # Files in this folder are auto-generated
- "{,ee/,jh/}{app,bin,config,db,elastic,generator_templates,gems,haml_lint,lib,locale,public,scripts,storybook,symbol,vendor}/**/*"
# Auto-generated files
- "doc/api/graphql/reference/*"
- "doc/administration/audit_event_streaming/audit_event_types.md"
# CI changes
- ".gitlab-ci.yml"
- ".gitlab/ci/**/*"
@ -536,7 +530,7 @@
- "Rakefile"
- "tests.yml"
- "config.ru"
- "{,ee/,jh/}{app,bin,config,db,generator_templates,gems,haml_lint,lib,locale,public,scripts,storybook,symbol,vendor}/**/*"
- "{,ee/,jh/}{app,bin,config,db,elastic,generator_templates,gems,haml_lint,lib,locale,public,scripts,storybook,symbol,vendor}/**/*"
# Auto-generated files
- "doc/api/graphql/reference/*"
- "doc/administration/audit_event_streaming/audit_event_types.md"
@ -575,8 +569,10 @@
- "Rakefile"
- "tests.yml"
- "config.ru"
- "{,ee/,jh/}{app,bin,config,db,generator_templates,gems,haml_lint,lib,locale,public,scripts,storybook,symbol,vendor}/**/*"
- "doc/api/graphql/reference/*" # Files in this folder are auto-generated
- "{,ee/,jh/}{app,bin,config,db,elastic,generator_templates,gems,haml_lint,lib,locale,public,scripts,storybook,symbol,vendor}/**/*"
# Auto-generated files
- "doc/api/graphql/reference/*"
- "doc/administration/audit_event_streaming/audit_event_types.md"
# CI changes
- ".gitlab-ci.yml"
- ".gitlab/ci/**/*"
@ -926,7 +922,7 @@
variables:
ARCH: amd64,arm64
- <<: *if-ruby-branch
- !reference [".releases:rules:canonical-dot-com-gitlab-stable-branch-only-setup-test-env-patterns", rules]
- !reference [".releases:rules:canonical-dot-com-gitlab-stable-branch-only-setup-test-env", rules]
.build-images:rules:build-qa-image-as-if-foss:
rules:
@ -2501,7 +2497,7 @@
when: never
- if: '$CI_SERVER_HOST == "gitlab.com" && $CI_PROJECT_PATH == "gitlab-org/gitlab" && $CI_COMMIT_REF_NAME =~ /^[\d-]+-stable-ee$/'
.releases:rules:canonical-dot-com-gitlab-stable-branch-only-setup-test-env-patterns:
.releases:rules:canonical-dot-com-gitlab-stable-branch-only-setup-test-env:
rules:
- if: '$CI_COMMIT_MESSAGE =~ /\[merge-train skip\]/'
when: never
@ -3237,7 +3233,7 @@
when: never
- <<: *if-merge-request-labels-pipeline-expedite
when: never
- !reference [".releases:rules:canonical-dot-com-gitlab-stable-branch-only-setup-test-env-patterns", rules]
- !reference [".releases:rules:canonical-dot-com-gitlab-stable-branch-only-setup-test-env", rules]
###################
# Benchmark rules #

View File

@ -1101,7 +1101,6 @@ RSpec/FeatureCategory:
- 'ee/spec/models/ee/project_member_spec.rb'
- 'ee/spec/models/project_repository_state_spec.rb'
- 'ee/spec/models/project_security_setting_spec.rb'
- 'ee/spec/models/protected_branch/required_code_owners_section_spec.rb'
- 'ee/spec/models/protected_environment_spec.rb'
- 'ee/spec/models/protected_environments/approval_rule_spec.rb'
- 'ee/spec/models/protected_environments/deploy_access_level_spec.rb'

View File

@ -160,7 +160,7 @@ export default {
>
{{ __('Show latest version') }}
</gl-button>
<div v-if="hasChanges" class="inline-parallel-buttons d-none d-md-flex ml-auto">
<div v-if="hasChanges" class="inline-parallel-buttons d-none gl-md-display-flex! ml-auto">
<diff-stats
:diff-files-count-text="diffFilesCountText"
:added-lines="addedLines"

View File

@ -89,6 +89,7 @@
"MergeRequest"
],
"OrchestrationPolicy": [
"ApprovalPolicy",
"ScanExecutionPolicy",
"ScanResultPolicy"
],

View File

@ -58,7 +58,7 @@ export default {
return {
'gl-display-none': true,
'gl-display-flex': this.tagCount === 1,
'd-md-flex': this.tagCount > 1,
'gl-md-display-flex!': this.tagCount > 1,
'gl-mr-2': index !== this.tagsToRender.length - 1,
'gl-ml-3': !this.hideLabel && index === 0,
};

View File

@ -1,12 +1,12 @@
export const viewers = {
csv: () => import('./csv_viewer.vue'),
download: () => import('./download_viewer.vue'),
download: () => import('jh_else_ce/repository/components/blob_viewers/download_viewer.vue'),
image: () => import('./image_viewer.vue'),
video: () => import('./video_viewer.vue'),
empty: () => import('./empty_viewer.vue'),
text: () => import('~/vue_shared/components/source_viewer/source_viewer.vue'),
pdf: () => import('./pdf_viewer.vue'),
lfs: () => import('./lfs_viewer.vue'),
pdf: () => import('jh_else_ce/repository/components/blob_viewers/pdf_viewer.vue'),
lfs: () => import('jh_else_ce/repository/components/blob_viewers/lfs_viewer.vue'),
audio: () => import('./audio_viewer.vue'),
svg: () => import('./image_viewer.vue'),
sketch: () => import('./sketch_viewer.vue'),

View File

@ -155,7 +155,7 @@ export default {
<div>
<div class="gl-display-flex gl-align-items-center gl-gap-3">
<!-- hide header when editing, since we then have a form label. Keep it reachable for screenreader nav -->
<h3 :class="{ 'gl-sr-only': isEditing }" class="gl-mb-0! gl-heading-scale-5">
<h3 :class="{ 'gl-sr-only': isEditing }" class="gl-mb-0! gl-heading-5">
{{ dropdownLabel }}
</h3>
<gl-loading-icon v-if="updateInProgress" />

View File

@ -211,7 +211,7 @@ export default {
<template>
<section class="gl-pb-4">
<div class="gl-display-flex gl-align-items-center gl-gap-3">
<h3 :class="{ 'gl-sr-only': isEditing }" class="gl-mb-0! gl-heading-scale-5">
<h3 :class="{ 'gl-sr-only': isEditing }" class="gl-mb-0! gl-heading-5">
{{ $options.i18n.dates }}
</h3>
<gl-button

View File

@ -209,7 +209,7 @@ export default {
<div>
<div class="gl-display-flex gl-align-items-center">
<!-- hide header when editing, since we then have a form label. Keep it reachable for screenreader nav -->
<h3 :class="{ 'gl-sr-only': isEditing }" class="gl-mb-0! gl-heading-scale-5">
<h3 :class="{ 'gl-sr-only': isEditing }" class="gl-mb-0! gl-heading-5">
{{ __('Parent') }}
</h3>
<gl-loading-icon

View File

@ -69,9 +69,13 @@ class Projects::ForksController < Projects::ApplicationController
@forked_project = fork_namespace.projects.find_by(path: project.path) # rubocop: disable CodeReuse/ActiveRecord
@forked_project = nil unless @forked_project && @forked_project.forked_from_project == project
@forked_project ||= fork_service.execute
unless @forked_project
@fork_response = fork_service.execute
if !@forked_project.saved? || !@forked_project.forked?
@forked_project ||= @fork_response[:project] if @fork_response.success?
end
if defined?(@fork_response) && @fork_response.error?
render :error
elsif @forked_project.import_in_progress?
redirect_to project_import_path(@forked_project, continue: continue_params)

View File

@ -29,7 +29,7 @@ module StatAnchorsHelper
elsif anchor.is_link
'stat-link'
else
"gl-button btn #{button_attribute(anchor)}"
button_attribute(anchor)
end
end
end

View File

@ -3,14 +3,11 @@
module Projects
class ForkService < BaseService
def execute(fork_to_project = nil)
forked_project = fork_to_project ? link_existing_project(fork_to_project) : fork_new_project
response = fork_to_project ? link_existing_project(fork_to_project) : fork_new_project
if forked_project&.saved?
refresh_forks_count
stream_audit_event(forked_project)
end
after_fork(response[:project]) if response.success?
forked_project
response
end
def valid_fork_targets(options = {})
@ -29,23 +26,39 @@ module Projects
private
def after_fork(project)
return unless project&.saved?
refresh_forks_count
stream_audit_event(project)
end
def link_existing_project(fork_to_project)
return if fork_to_project.forked?
if fork_to_project.forked?
return ServiceResponse.error(message: _('Project already forked'), reason: :already_forked)
end
build_fork_network_member(fork_to_project)
fork_to_project if link_fork_network(fork_to_project)
if link_fork_network(fork_to_project)
ServiceResponse.success(payload: { project: fork_to_project })
else
ServiceResponse.error(message: fork_to_project.errors.full_messages)
end
end
def fork_new_project
new_project = CreateService.new(current_user, new_fork_params).execute
return new_project unless new_project.persisted?
unless new_project.persisted?
return ServiceResponse.error(message: new_project.errors.full_messages)
end
new_project.project_feature.update!(
@project.project_feature.slice(ProjectFeature::FEATURES.map { |f| "#{f}_access_level" })
)
new_project
ServiceResponse.success(payload: { project: new_project })
end
def new_fork_params

View File

@ -1,11 +1,21 @@
- anchors = local_assigns.fetch(:anchors, [])
- project_buttons = local_assigns.fetch(:project_buttons, false)
- ff_reorg_enabled = Feature.enabled?(:project_overview_reorg)
- stat_text_classes = "stat-text d-flex gl-align-items-center #{'gl-px-0! gl-pb-2!' if ff_reorg_enabled}"
- return unless anchors.any?
%ul.nav.gl-row-gap-2.gl-column-gap-5
- anchors.each do |anchor|
%li.nav-item
= link_to_if(anchor.link, anchor.label, anchor.link, stat_anchor_attrs(anchor)) do
.stat-text.d-flex.gl-align-items-center{ class: "#{'btn gl-button btn-default disabled' if project_buttons} #{'gl-px-0! gl-pb-2!' if ff_reorg_enabled}" }= anchor.label
- if anchor.link # render actionable link/button
- if anchor.is_link || ff_reorg_enabled
= link_to(anchor.label, anchor.link, stat_anchor_attrs(anchor))
- else
= render Pajamas::ButtonComponent.new(href: anchor.link, button_options: stat_anchor_attrs(anchor)) do
= anchor.label
- elsif project_buttons # render disabled button
= render Pajamas::ButtonComponent.new(disabled: true, button_options: { classes: stat_text_classes }) do
= anchor.label
- else # render as text label
%div{ class: stat_text_classes }= anchor.label

View File

@ -1,5 +1,5 @@
- page_title _("Fork project")
- if @forked_project && !@forked_project.saved?
- if @fork_response.error?
= render Pajamas::AlertComponent.new(title: _('Fork Error!'),
variant: :danger,
alert_options: { class: 'gl-mt-5' },
@ -8,14 +8,10 @@
%p
= _("You tried to fork %{link_to_the_project} but it failed for the following reason:").html_safe % { link_to_the_project: link_to_project(@project) }
- if @forked_project && @forked_project.errors.any?
- @fork_response.errors.each do |error|
%p
&ndash;
- error = @forked_project.errors.full_messages.first
- if error.include?("already been taken")
= _('Name has already been taken')
- else
= error
= error
- c.with_actions do
= link_button_to _('Try to fork again'), new_project_fork_path(@project), title: _("Fork"), class: 'gl-alert-action', variant: :confirm

View File

@ -7,4 +7,19 @@ feature_categories:
description: Stores verification state for Geo replicated Pages deployments.
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/74905
milestone: '14.6'
gitlab_schema: gitlab_main
gitlab_schema: gitlab_main_cell
allow_cross_joins:
- gitlab_main_clusterwide
allow_cross_transactions:
- gitlab_main_clusterwide
allow_cross_foreign_keys:
- gitlab_main_clusterwide
desired_sharding_key:
project_id:
references: projects
backfill_via:
parent:
foreign_key: pages_deployment_id
table: pages_deployments
sharding_key: project_id
belongs_to: pages_deployment

View File

@ -128,7 +128,15 @@ class Gitlab::Seeder::Projects
project = Project.find_by_full_path(project_name)
User.offset(1).first(5).each do |user|
new_project = ::Projects::ForkService.new(project, user).execute
response = ::Projects::ForkService.new(project, user).execute
if response.error?
print 'F'
puts response.errors
next
end
new_project = response[:project]
if new_project.valid? && (new_project.valid_repo? || new_project.import_state.scheduled?)
print '.'

View File

@ -11,13 +11,20 @@ Sidekiq::Testing.inline! do
next unless source_project
Sidekiq::Worker.skipping_transaction_check do
fork_project = Projects::ForkService.new(
response = Projects::ForkService.new(
source_project,
user,
namespace: user.namespace,
skip_disk_validation: true
).execute
if response.error?
print 'F'
next
end
fork_project = response[:project]
# Seed-Fu runs this entire fixture in a transaction, so the `after_commit`
# hook won't run until after the fixture is loaded. That is too late
# since the Sidekiq::Testing block has already exited. Force clearing

View File

@ -9085,6 +9085,29 @@ The edge type for [`AmazonS3ConfigurationType`](#amazons3configurationtype).
| <a id="amazons3configurationtypeedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
| <a id="amazons3configurationtypeedgenode"></a>`node` | [`AmazonS3ConfigurationType`](#amazons3configurationtype) | The item at the end of the edge. |
#### `ApprovalPolicyConnection`
The connection type for [`ApprovalPolicy`](#approvalpolicy).
##### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="approvalpolicyconnectionedges"></a>`edges` | [`[ApprovalPolicyEdge]`](#approvalpolicyedge) | A list of edges. |
| <a id="approvalpolicyconnectionnodes"></a>`nodes` | [`[ApprovalPolicy]`](#approvalpolicy) | A list of nodes. |
| <a id="approvalpolicyconnectionpageinfo"></a>`pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. |
#### `ApprovalPolicyEdge`
The edge type for [`ApprovalPolicy`](#approvalpolicy).
##### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="approvalpolicyedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
| <a id="approvalpolicyedgenode"></a>`node` | [`ApprovalPolicy`](#approvalpolicy) | The item at the end of the edge. |
#### `ApprovalProjectRuleConnection`
The connection type for [`ApprovalProjectRule`](#approvalprojectrule).
@ -14747,6 +14770,26 @@ An API Fuzzing scan profile.
| <a id="apifuzzingscanprofilename"></a>`name` | [`String`](#string) | Unique name of the profile. |
| <a id="apifuzzingscanprofileyaml"></a>`yaml` | [`String`](#string) | Syntax highlighted HTML representation of the YAML. |
### `ApprovalPolicy`
Represents the approval policy.
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="approvalpolicyallgroupapprovers"></a>`allGroupApprovers` | [`[PolicyApprovalGroup!]`](#policyapprovalgroup) | All potential approvers of the group type, including groups inaccessible to the user. |
| <a id="approvalpolicydescription"></a>`description` | [`String!`](#string) | Description of the policy. |
| <a id="approvalpolicyeditpath"></a>`editPath` | [`String!`](#string) | URL of policy edit page. |
| <a id="approvalpolicyenabled"></a>`enabled` | [`Boolean!`](#boolean) | Indicates whether this policy is enabled. |
| <a id="approvalpolicygroupapprovers"></a>`groupApprovers` **{warning-solid}** | [`[Group!]`](#group) | **Deprecated** in 16.5. Use `allGroupApprovers`. |
| <a id="approvalpolicyname"></a>`name` | [`String!`](#string) | Name of the policy. |
| <a id="approvalpolicyroleapprovers"></a>`roleApprovers` | [`[MemberAccessLevelName!]`](#memberaccesslevelname) | Approvers of the role type. Users belonging to these role(s) alone will be approvers. |
| <a id="approvalpolicysource"></a>`source` | [`SecurityPolicySource!`](#securitypolicysource) | Source of the policy. Its fields depend on the source type. |
| <a id="approvalpolicyupdatedat"></a>`updatedAt` | [`Time!`](#time) | Timestamp of when the policy YAML was last updated. |
| <a id="approvalpolicyuserapprovers"></a>`userApprovers` | [`[UserCore!]`](#usercore) | Approvers of the user type. |
| <a id="approvalpolicyyaml"></a>`yaml` | [`String!`](#string) | YAML definition of the policy. |
### `ApprovalProjectRule`
Describes a project approval rule regarding who can approve merge requests.
@ -19581,6 +19624,22 @@ Returns [`AddOnPurchase`](#addonpurchase).
| ---- | ---- | ----------- |
| <a id="groupaddonpurchaseaddonname"></a>`addOnName` | [`String!`](#string) | AddOn name. |
##### `Group.approvalPolicies`
Approval Policies of the project.
Returns [`ApprovalPolicyConnection`](#approvalpolicyconnection).
This field returns a [connection](#connections). It accepts the
four standard [pagination arguments](#connection-pagination-arguments):
`before: String`, `after: String`, `first: Int`, and `last: Int`.
###### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="groupapprovalpoliciesrelationship"></a>`relationship` | [`SecurityPolicyRelationType`](#securitypolicyrelationtype) | Filter policies by the given policy relationship. |
##### `Group.autocompleteUsers`
Search users for autocompletion.
@ -20400,6 +20459,10 @@ four standard [pagination arguments](#connection-pagination-arguments):
Scan Result Policies of the project.
NOTE:
**Deprecated** in 16.9.
Use `approvalPolicies`.
Returns [`ScanResultPolicyConnection`](#scanresultpolicyconnection).
This field returns a [connection](#connections). It accepts the
@ -23369,6 +23432,22 @@ Returns [`AddOnPurchase`](#addonpurchase).
| ---- | ---- | ----------- |
| <a id="namespaceaddonpurchaseaddonname"></a>`addOnName` | [`String!`](#string) | AddOn name. |
##### `Namespace.approvalPolicies`
Approval Policies of the project.
Returns [`ApprovalPolicyConnection`](#approvalpolicyconnection).
This field returns a [connection](#connections). It accepts the
four standard [pagination arguments](#connection-pagination-arguments):
`before: String`, `after: String`, `first: Int`, and `last: Int`.
###### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="namespaceapprovalpoliciesrelationship"></a>`relationship` | [`SecurityPolicyRelationType`](#securitypolicyrelationtype) | Filter policies by the given policy relationship. |
##### `Namespace.complianceFrameworks`
Compliance frameworks available to projects in this namespace.
@ -23435,6 +23514,10 @@ four standard [pagination arguments](#connection-pagination-arguments):
Scan Result Policies of the project.
NOTE:
**Deprecated** in 16.9.
Use `approvalPolicies`.
Returns [`ScanResultPolicyConnection`](#scanresultpolicyconnection).
This field returns a [connection](#connections). It accepts the
@ -24649,6 +24732,22 @@ Returns [`[AlertManagementPayloadAlertField!]`](#alertmanagementpayloadalertfiel
| ---- | ---- | ----------- |
| <a id="projectalertmanagementpayloadfieldspayloadexample"></a>`payloadExample` | [`String!`](#string) | Sample payload for extracting alert fields for custom mappings. |
##### `Project.approvalPolicies`
Approval Policies of the project.
Returns [`ApprovalPolicyConnection`](#approvalpolicyconnection).
This field returns a [connection](#connections). It accepts the
four standard [pagination arguments](#connection-pagination-arguments):
`before: String`, `after: String`, `first: Int`, and `last: Int`.
###### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="projectapprovalpoliciesrelationship"></a>`relationship` | [`SecurityPolicyRelationType`](#securitypolicyrelationtype) | Filter policies by the given policy relationship. |
##### `Project.autocompleteUsers`
Search users for autocompletion.
@ -25769,6 +25868,10 @@ four standard [pagination arguments](#connection-pagination-arguments):
Scan Result Policies of the project.
NOTE:
**Deprecated** in 16.9.
Use `approvalPolicies`.
Returns [`ScanResultPolicyConnection`](#scanresultpolicyconnection).
This field returns a [connection](#connections). It accepts the
@ -33749,6 +33852,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
Implementations:
- [`ApprovalPolicy`](#approvalpolicy)
- [`ScanExecutionPolicy`](#scanexecutionpolicy)
- [`ScanResultPolicy`](#scanresultpolicy)

View File

@ -129,7 +129,7 @@ A personal access token can perform actions based on the assigned scopes.
| Scope | Access |
|--------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `api` | Grants complete read/write access to the API, including all groups and projects, the container registry, the dependency proxy, and the package registry. |
| `api` | Grants complete read/write access to the API, including all groups and projects, the container registry, the dependency proxy, and the package registry. Also grants complete read/write access to the registry and repository using Git over HTTP. |
| `read_user` | Grants read-only access to the authenticated user's profile through the `/user` API endpoint, which includes username, public email, and full name. Also grants access to read-only API endpoints under [`/users`](../../api/users.md). |
| `read_api` | Grants read access to the API, including all groups and projects, the container registry, and the package registry. ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/28944) in GitLab 12.10.) |
| `read_repository` | Grants read-only access to repositories on private projects using Git-over-HTTP or the Repository Files API. |

View File

@ -495,16 +495,16 @@ module API
not_found!('Source Branch') if fork_params[:branches].present? && !service.valid_fork_branch?(fork_params[:branches])
not_found!('Target Namespace') unless service.valid_fork_target?
forked_project = service.execute
result = service.execute
if forked_project.errors.any?
conflict!(forked_project.errors.messages)
else
present_project forked_project, {
if result.success?
present_project result[:project], {
with: Entities::Project,
user_can_admin_project: can?(current_user, :admin_project, forked_project),
user_can_admin_project: can?(current_user, :admin_project, result[:project]),
current_user: current_user
}
else
conflict!(result.message)
end
end
@ -719,10 +719,12 @@ module API
result = service.execute(user_project)
if result
if result.success?
present_project user_project.reset, with: Entities::Project, current_user: current_user
elsif user_project.forked?
render_api_error!("Project already forked", 409)
elsif result.reason == :already_forked
conflict!(result.message)
else
render_api_error!(result.message, 400)
end
end

View File

@ -31847,9 +31847,6 @@ msgstr ""
msgid "Name can't be blank"
msgstr ""
msgid "Name has already been taken"
msgstr ""
msgid "Name is already taken."
msgstr ""
@ -38275,6 +38272,9 @@ msgstr ""
msgid "Project already deleted"
msgstr ""
msgid "Project already forked"
msgstr ""
msgid "Project and wiki repositories"
msgstr ""

View File

@ -11,7 +11,7 @@ gem 'capybara', '~> 3.39.2'
gem 'capybara-screenshot', '~> 1.0.26'
gem 'rake', '~> 13', '>= 13.1.0'
gem 'rspec', '~> 3.12'
gem 'selenium-webdriver', '= 4.16.0'
gem 'selenium-webdriver', '= 4.17.0'
gem 'airborne', '~> 0.3.7', require: false # airborne is messing with rspec sandboxed mode so not requiring by default
gem 'rest-client', '~> 2.1.0'
gem 'rspec-retry', '~> 0.6.2', require: 'rspec/retry'
@ -39,7 +39,7 @@ gem 'chemlab-library-www-gitlab-com', '~> 0.1', '>= 0.1.1'
# dependencies for jenkins client
gem 'nokogiri', '~> 1.16'
gem 'deprecation_toolkit', '~> 2.1.0', require: false
gem 'deprecation_toolkit', '~> 2.2.0', require: false
gem 'factory_bot', '~> 6.3.0'

View File

@ -72,8 +72,8 @@ GEM
crass (1.0.6)
debug_inspector (1.1.0)
declarative (0.0.20)
deprecation_toolkit (2.1.0)
activesupport (>= 7.0)
deprecation_toolkit (2.2.0)
activesupport (>= 6.1)
diff-lcs (1.3)
domain_name (0.6.20240107)
erubi (1.12.0)
@ -301,7 +301,8 @@ GEM
sawyer (0.9.2)
addressable (>= 2.3.5)
faraday (>= 0.17.3, < 3)
selenium-webdriver (4.16.0)
selenium-webdriver (4.17.0)
base64 (~> 0.2)
rexml (~> 3.2, >= 3.2.5)
rubyzip (>= 1.2.2, < 3.0)
websocket (~> 1.0)
@ -350,7 +351,7 @@ DEPENDENCIES
capybara-screenshot (~> 1.0.26)
chemlab (~> 0.11, >= 0.11.1)
chemlab-library-www-gitlab-com (~> 0.1, >= 0.1.1)
deprecation_toolkit (~> 2.1.0)
deprecation_toolkit (~> 2.2.0)
factory_bot (~> 6.3.0)
faker (~> 3.2, >= 3.2.3)
faraday-retry (~> 2.2)
@ -375,7 +376,7 @@ DEPENDENCIES
rspec-retry (~> 0.6.2)
rspec_junit_formatter (~> 0.6.0)
ruby-debug-ide (~> 0.7.3)
selenium-webdriver (= 4.16.0)
selenium-webdriver (= 4.17.0)
slack-notifier (~> 2.4)
terminal-table (~> 3.0.2)
warning (~> 1.3)

View File

@ -35,6 +35,10 @@ module QA
it(
'after a push via the API creates a merge request',
quarantine: {
issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/403182',
type: :flaky
},
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/360490'
) do
commit = create(:commit,

View File

@ -25,7 +25,11 @@ module QA
end
context 'when developers and maintainers are not allowed to push to a protected branch' do
it 'user without push rights fails to push', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347757' do
it 'user without push rights fails to push', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347757',
quarantine: {
issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/426739',
type: :flaky
} do
create_protected_branch(allowed_to_push: {
roles: Resource::ProtectedBranch::Roles::NO_ONE
})

View File

@ -29,7 +29,11 @@ module QA
Runtime::Feature.disable(:rubygem_packages, project: project)
end
it 'publishes a Ruby gem', :reliable, testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347649' do
it 'publishes a Ruby gem', :reliable, testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347649',
quarantine: {
issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/366099',
type: :flaky
} do
Flow::Login.sign_in
Support::Retrier.retry_on_exception(max_attempts: 3, sleep_interval: 2) do

View File

@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Projects::ForksController, feature_category: :source_code_management do
let(:user) { create(:user) }
let(:project) { create(:project, :public, :repository) }
let(:forked_project) { Projects::ForkService.new(project, user, name: 'Some name').execute }
let(:forked_project) { Projects::ForkService.new(project, user, name: 'Some name').execute[:project] }
let(:group) { create(:group) }
before do
@ -271,6 +271,34 @@ RSpec.describe Projects::ForksController, feature_category: :source_code_managem
end
end
context 'when fork already exists' do
before do
forked_project
end
it 'responds with status 302' do
subject
expect(response).to have_gitlab_http_status(:found)
expect(response).to redirect_to(namespace_project_import_path(user.namespace, project))
end
end
context 'when fork process fails' do
before do
allow_next_instance_of(Projects::ForkService) do |instance|
allow(instance).to receive(:execute).and_return(ServiceResponse.error(message: 'Error'))
end
end
it 'responds with an error page' do
subject
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template(:error)
end
end
context 'continue params' do
let(:params) do
{

View File

@ -2,6 +2,8 @@
require 'fast_spec_helper'
PatternsList = Struct.new(:name, :patterns)
RSpec.describe '.gitlab/ci/rules.gitlab-ci.yml', feature_category: :tooling do
config = YAML.safe_load_file(
File.expand_path('../../.gitlab/ci/rules.gitlab-ci.yml', __dir__),
@ -58,4 +60,130 @@ RSpec.describe '.gitlab/ci/rules.gitlab-ci.yml', feature_category: :tooling do
end
end
end
describe 'patterns' do
foss_context = !Gitlab.ee?
no_matching_needed_files = (
[
'.byebug_history',
'.editorconfig',
'.eslintcache',
'.foreman',
'.git-blame-ignore-revs',
'.gitlab_kas_secret',
'.gitlab_shell_secret',
'.gitlab_workhorse_secret',
'.gitlab/agents/review-apps/config.yaml',
'.gitlab/changelog_config.yml',
'.gitlab/CODEOWNERS',
'.gitleaksignore',
'.gitpod.yml',
'.license_encryption_key.pub',
'.mailmap',
'.prettierignore',
'.projections.json.example',
'.rubocop_revert_ignores.txt',
'.ruby-version',
'.solargraph.yml.example',
'.solargraph.yml',
'.test_license_encryption_key.pub',
'.tool-versions',
'.vale.ini',
'.vscode/extensions.json',
'ee/lib/ee/gitlab/background_migration/.rubocop.yml',
'ee/LICENSE',
'Gemfile.checksum',
'gems/error_tracking_open_api/.openapi-generator/FILES',
'gems/error_tracking_open_api/.openapi-generator/VERSION',
'Guardfile',
'INSTALLATION_TYPE',
'lib/gitlab/background_migration/.rubocop.yml',
'lib/gitlab/ci/templates/.yamllint',
'LICENSE',
'Pipfile.lock',
'storybook/.env.template',
'yarn-error.log'
] +
Dir.glob('.bundle/**/*') +
Dir.glob('.github/*') +
Dir.glob('.gitlab/{issue,merge_request}_templates/**/*') +
Dir.glob('.gitlab/*.toml') +
Dir.glob('{,**/}.{DS_Store,eslintrc.yml,gitignore,gitkeep,keep}', File::FNM_DOTMATCH) +
Dir.glob('{,vendor/}gems/*/.*') +
Dir.glob('{.git,.lefthook,.ruby-lsp}/**/*') +
Dir.glob('{file_hooks,log}/**/*') +
Dir.glob('{metrics_server,sidekiq_cluster}/*') +
Dir.glob('{spec/fixtures,tmp}/**/*', File::FNM_DOTMATCH) +
Dir.glob('*.md') +
Dir.glob('changelogs/*') +
Dir.glob('doc/.{markdownlint,vale}/**/*') +
Dir.glob('keeps/**/*') +
Dir.glob('node_modules/**/*', File::FNM_DOTMATCH) +
Dir.glob('patches/*') +
Dir.glob('public/assets/**/.*') +
Dir.glob('qa/.{,**/}*') +
Dir.glob('qa/**/.gitlab-ci.yml') +
Dir.glob('shared/**/*') +
Dir.glob('workhorse/.*')
).freeze
no_matching_needed_files_ci_specific = (
[
'metrics.txt'
] +
Dir.glob('{auto_explain,crystalball,knapsack,rspec}/**/*') +
Dir.glob('coverage/**/*', File::FNM_DOTMATCH) +
Dir.glob('vendor/ruby/**/*', File::FNM_DOTMATCH)
).freeze
all_files = Dir.glob('{,**/}*', File::FNM_DOTMATCH) -
no_matching_needed_files -
no_matching_needed_files_ci_specific
all_files -= Dir.glob('ee/**/*', File::FNM_DOTMATCH) if foss_context
all_files.reject! { |f| File.directory?(f) }
# One loop to construct an array of PatternsList objects
patterns_lists = config.filter_map do |name, patterns|
next unless name.start_with?('.')
next unless name.end_with?('patterns')
# Ignore EE-only patterns list when in FOSS context
next if foss_context && patterns.all? { |pattern| pattern =~ %r|{?ee/| }
PatternsList.new(name, patterns)
end
# One loop to gather a { pattern => files } hash
patterns_files = patterns_lists.each_with_object({}) do |patterns_list, memo|
patterns_list.patterns.each do |pattern|
memo[pattern] ||= Dir.glob(pattern)
end
end
# Example: '.ci-patterns': [".gitlab-ci.yml", ".gitlab/ci/**/*", "scripts/rspec_helpers.sh"]
patterns_lists.each do |patterns_list|
describe "patterns list `#{patterns_list.name}`" do
patterns_list.patterns.each do |pattern|
pattern_files = patterns_files.fetch(pattern)
context "with `#{pattern}`" do
it 'matches' do
matching_files = (all_files & pattern_files)
expect(matching_files).not_to be_empty
end
end
end
end
end
describe 'missed matched files' do
all_matching_files = Set.new
patterns_files.each_value do |files|
all_matching_files.merge(files)
end
it 'does not miss files to match' do
expect(all_files - all_matching_files.to_a).to be_empty
end
end
end
end

View File

@ -77,7 +77,7 @@ describe('PackageTags', () => {
it('shows tag badge for medium or heigher resolutions', () => {
createComponent(mockTags);
const expectedStyle = [...defaultStyle, 'd-md-flex'];
const expectedStyle = [...defaultStyle, 'gl-md-display-flex!'];
expect(tagBadges().at(1).classes()).toEqual(expect.arrayContaining(expectedStyle));
});
@ -87,7 +87,7 @@ describe('PackageTags', () => {
tagDisplayLimit: 4,
});
const expectedStyleWithoutAppend = [...defaultStyle, 'd-md-flex'];
const expectedStyleWithoutAppend = [...defaultStyle, 'gl-md-display-flex!'];
const expectedStyleWithAppend = [...expectedStyleWithoutAppend, 'gl-mr-2'];
const allBadges = tagBadges();

View File

@ -25,7 +25,7 @@ RSpec.describe StatAnchorsHelper do
let(:anchor) { anchor_klass.new(false, nil, nil, 'btn-default') }
it 'returns the proper attributes' do
expect(subject[:class]).to include('gl-button btn btn-default')
expect(subject[:class]).to include('btn-default')
end
end
@ -33,7 +33,7 @@ RSpec.describe StatAnchorsHelper do
let(:anchor) { anchor_klass.new(false) }
it 'returns the proper attributes' do
expect(subject[:class]).to include('gl-button btn btn-dashed')
expect(subject[:class]).to include('btn-dashed')
end
end
end

View File

@ -331,7 +331,7 @@ RSpec.describe Gitlab::ClosingIssueExtractor do
end
context "with a cross-project fork reference" do
let(:forked_project) { Projects::ForkService.new(project, project2.creator).execute }
let(:forked_project) { Projects::ForkService.new(project, project2.creator).execute[:project] }
let(:fork_cross_reference) { issue.to_reference(forked_project) }
subject { described_class.new(forked_project, forked_project.creator) }

View File

@ -3513,6 +3513,24 @@ RSpec.describe API::Projects, :aggregate_failures, feature_category: :groups_and
expect(project_fork_target.forked_from_project.id).to eq(project_fork_source.id)
expect(project_fork_target).to be_forked
end
context 'when forking process fails' do
before do
allow_next_instance_of(Projects::ForkService) do |instance|
allow(instance).to receive(:execute).and_return(ServiceResponse.error(message: 'Error'))
end
end
it 'fails with 400 error' do
expect(project_fork_target).not_to be_forked
post api(path, admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['message']).to eq "Error"
expect(project_fork_target).not_to be_forked
end
end
end
end
@ -5039,8 +5057,13 @@ RSpec.describe API::Projects, :aggregate_failures, feature_category: :groups_and
post api("/projects/#{new_project.id}/fork", user)
expect(response).to have_gitlab_http_status(:conflict)
expect(json_response['message']['name']).to eq(['has already been taken'])
expect(json_response['message']['path']).to eq(['has already been taken'])
expect(json_response['message']).to match_array(
[
'Name has already been taken',
'Path has already been taken',
'Project namespace name has already been taken'
]
)
end
it 'fails if project to fork from does not exist' do
@ -5192,7 +5215,7 @@ RSpec.describe API::Projects, :aggregate_failures, feature_category: :groups_and
post api("/projects/#{project2.id}/fork", user2), params: { path: 'foobar' }
expect(response).to have_gitlab_http_status(:conflict)
expect(json_response['message']['path']).to eq(['has already been taken'])
expect(json_response['message']).to eq(['Path has already been taken'])
end
it 'accepts custom parameters for the target project' do
@ -5222,7 +5245,12 @@ RSpec.describe API::Projects, :aggregate_failures, feature_category: :groups_and
post api("/projects/#{project2.id}/fork", user2), params: { name: 'My Random Project' }
expect(response).to have_gitlab_http_status(:conflict)
expect(json_response['message']['name']).to eq(['has already been taken'])
expect(json_response['message']).to match_array(
[
'Name has already been taken',
'Project namespace name has already been taken'
]
)
end
it 'forks to the same namespace with alternative path and name' do
@ -5241,8 +5269,13 @@ RSpec.describe API::Projects, :aggregate_failures, feature_category: :groups_and
post api(path, user)
expect(response).to have_gitlab_http_status(:conflict)
expect(json_response['message']['path']).to eq(['has already been taken'])
expect(json_response['message']['name']).to eq(['has already been taken'])
expect(json_response['message']).to match_array(
[
'Name has already been taken',
'Path has already been taken',
'Project namespace name has already been taken'
]
)
end
it 'fails to fork with an unknown visibility level' do

View File

@ -461,7 +461,7 @@ RSpec.describe UsersController, feature_category: :user_management do
context 'forked project' do
let(:project) { create(:project) }
let(:forked_project) { Projects::ForkService.new(project, user).execute }
let(:forked_project) { Projects::ForkService.new(project, user).execute[:project] }
before do
sign_in(user)

View File

@ -24,7 +24,9 @@ RSpec.describe Projects::ForkService, feature_category: :source_code_management
end
describe '#execute' do
subject(:fork_of_project) { service.execute }
subject(:response) { service.execute }
let(:fork_of_project) { response[:project] }
before do
# NOTE: Avatar file is dropped after project reload. Explicitly re-add it for each test.
@ -37,8 +39,8 @@ RSpec.describe Projects::ForkService, feature_category: :source_code_management
end
it 'does not create a fork' do
is_expected.not_to be_persisted
expect(subject.errors[:forked_from_project_id]).to eq(['is forbidden'])
is_expected.to be_error
expect(response.errors).to eq(['Forked from project is forbidden'])
end
it 'does not create a fork network' do
@ -52,7 +54,7 @@ RSpec.describe Projects::ForkService, feature_category: :source_code_management
end
it 'creates a fork of the project' do
expect(fork_of_project).to be_persisted
is_expected.to be_success
expect(fork_of_project.errors).to be_empty
expect(fork_of_project.first_owner).to eq(user)
expect(fork_of_project.namespace).to eq(user.namespace)
@ -71,7 +73,7 @@ RSpec.describe Projects::ForkService, feature_category: :source_code_management
# https://gitlab.com/gitlab-org/gitlab-foss/issues/26158
it 'after forking the original project still has its avatar' do
# If we do not fork the project first we cannot detect the bug.
expect(fork_of_project).to be_persisted
is_expected.to be_success
expect(project.avatar.file).to be_exists
end
@ -107,10 +109,16 @@ RSpec.describe Projects::ForkService, feature_category: :source_code_management
let_it_be(:other_namespace) { create(:group).tap { |group| group.add_owner(user) } }
it 'creates a new project' do
fork_of_project = described_class.new(project, user, params).execute
fork_response = described_class.new(project, user, params).execute
expect(fork_response).to be_success
fork_of_project = fork_response[:project]
expect(fork_of_project).to be_persisted
fork_of_fork = described_class.new(fork_of_project, user, { namespace: other_namespace }).execute
fork_of_fork_response = described_class.new(fork_of_project, user, { namespace: other_namespace }).execute
expect(fork_of_fork_response).to be_success
fork_of_fork = fork_of_fork_response[:project]
expect(fork_of_fork).to be_persisted
expect(fork_of_fork).to be_valid
@ -122,7 +130,10 @@ RSpec.describe Projects::ForkService, feature_category: :source_code_management
let_it_be(:root_project) { create(:project, :public) }
it 'successfully creates a fork of the fork with correct visibility' do
fork_of_project = described_class.new(root_project, user, params).execute
fork_response = described_class.new(root_project, user, params).execute
expect(fork_response).to be_success
fork_of_project = fork_response[:project]
expect(fork_of_project).to be_persisted
root_project.update!(visibility_level: Gitlab::VisibilityLevel::INTERNAL)
@ -130,7 +141,10 @@ RSpec.describe Projects::ForkService, feature_category: :source_code_management
# Forked project visibility is not affected by root project visibility change
expect(fork_of_project).to have_attributes(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
fork_of_fork = described_class.new(fork_of_project, user, { namespace: other_namespace }).execute
fork_of_fork_response = described_class.new(fork_of_project, user, { namespace: other_namespace }).execute
expect(fork_of_fork_response).to be_success
fork_of_fork = fork_of_fork_response[:project]
expect(fork_of_fork).to be_persisted
expect(fork_of_fork).to be_valid
@ -139,7 +153,7 @@ RSpec.describe Projects::ForkService, feature_category: :source_code_management
end
it_behaves_like 'forks count cache refresh' do
let(:from_project) { described_class.new(project, user, { namespace: other_namespace }).execute }
let(:from_project) { described_class.new(project, user, { namespace: other_namespace }).execute[:project] }
let(:to_user) { user }
end
end
@ -149,8 +163,8 @@ RSpec.describe Projects::ForkService, feature_category: :source_code_management
existing_project = create(:project, namespace: namespace, path: project.path)
expect(existing_project).to be_persisted
expect(fork_of_project).not_to be_persisted
expect(fork_of_project.errors[:path]).to eq(['has already been taken'])
is_expected.to be_error
expect(response.errors).to include('Path has already been taken')
end
end
@ -167,17 +181,17 @@ RSpec.describe Projects::ForkService, feature_category: :source_code_management
end
it 'does not allow creation' do
fork_of_project
is_expected.to be_error
expect(fork_of_project).not_to be_persisted
expect(fork_of_project.errors.messages).to have_key(:base)
expect(fork_of_project.errors.messages[:base].first).to match('There is already a repository with that name on disk')
expect(response.errors).to include('There is already a repository with that name on disk')
end
context 'when repository disk validation is explicitly skipped' do
let(:params) { super().merge(skip_disk_validation: true) }
it 'allows fork project creation' do
is_expected.to be_success
expect(fork_of_project).to be_persisted
expect(fork_of_project.errors.messages).to be_empty
end
@ -189,6 +203,7 @@ RSpec.describe Projects::ForkService, feature_category: :source_code_management
it 'inherits default_git_depth from the origin project' do
project.update!(ci_default_git_depth: 42)
is_expected.to be_success
expect(fork_of_project).to be_persisted
expect(fork_of_project.ci_default_git_depth).to eq(42)
end
@ -198,6 +213,7 @@ RSpec.describe Projects::ForkService, feature_category: :source_code_management
it 'the fork has git depth set to 0' do
project.update!(ci_default_git_depth: nil)
is_expected.to be_success
expect(fork_of_project).to be_persisted
expect(fork_of_project.ci_default_git_depth).to eq(0)
end
@ -212,6 +228,7 @@ RSpec.describe Projects::ForkService, feature_category: :source_code_management
end
it 'creates fork with lowest level' do
is_expected.to be_success
expect(fork_of_project).to be_persisted
expect(fork_of_project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE)
end
@ -223,8 +240,8 @@ RSpec.describe Projects::ForkService, feature_category: :source_code_management
end
it "doesn't create a fork" do
expect(fork_of_project).not_to be_persisted
expect(fork_of_project.errors[:visibility_level]).to eq ['private has been restricted by your GitLab administrator']
is_expected.to be_error
expect(response.errors).to eq ['Visibility level private has been restricted by your GitLab administrator']
end
end
end
@ -235,8 +252,8 @@ RSpec.describe Projects::ForkService, feature_category: :source_code_management
end
it 'does not create a fork' do
expect(fork_of_project).not_to be_persisted
expect(fork_of_project.errors[:forked_from_project_id]).to eq(['is forbidden'])
is_expected.to be_error
expect(response.errors).to eq(['Forked from project is forbidden'])
end
end
@ -245,7 +262,8 @@ RSpec.describe Projects::ForkService, feature_category: :source_code_management
let_it_be_with_reload(:namespace) { create(:group).tap { |group| group.add_owner(user) } }
it 'creates a fork in the group' do
expect(fork_of_project).to be_persisted
is_expected.to be_success
expect(fork_of_project.first_owner).to eq(user)
expect(fork_of_project.namespace).to eq(namespace)
end
@ -255,8 +273,8 @@ RSpec.describe Projects::ForkService, feature_category: :source_code_management
existing_project = create(:project, :repository, path: project.path, namespace: namespace)
expect(existing_project).to be_persisted
expect(fork_of_project).not_to be_persisted
expect(fork_of_project.errors[:path]).to eq(['has already been taken'])
is_expected.to be_error
expect(response.errors).to include('Path has already been taken')
end
end
@ -265,6 +283,7 @@ RSpec.describe Projects::ForkService, feature_category: :source_code_management
let_it_be(:project) { create(:project, :public) }
it 'creates the project with the lower visibility level' do
is_expected.to be_success
expect(fork_of_project).to be_persisted
expect(fork_of_project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE)
end
@ -275,8 +294,8 @@ RSpec.describe Projects::ForkService, feature_category: :source_code_management
let_it_be(:namespace) { create(:group).tap { |group| group.add_developer(user) } }
it 'does not create a fork' do
expect(fork_of_project).not_to be_persisted
expect(fork_of_project.errors[:namespace]).to eq(['is not valid'])
is_expected.to be_error
expect(response.errors).to match_array(['Namespace is not valid', 'User is not allowed to import projects'])
end
end
end
@ -285,8 +304,9 @@ RSpec.describe Projects::ForkService, feature_category: :source_code_management
let(:params) { super().merge(path: 'forked', name: 'My Fork', description: 'Description', visibility: 'private') }
it 'sets optional attributes to specified values' do
expect(fork_of_project).to be_persisted
is_expected.to be_success
expect(fork_of_project).to be_persisted
expect(fork_of_project.path).to eq('forked')
expect(fork_of_project.name).to eq('My Fork')
expect(fork_of_project.description).to eq('Description')
@ -299,8 +319,9 @@ RSpec.describe Projects::ForkService, feature_category: :source_code_management
let(:params) { super().merge(visibility: 'unknown') }
it 'sets visibility level to private' do
expect(fork_of_project).to be_persisted
is_expected.to be_success
expect(fork_of_project).to be_persisted
expect(fork_of_project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE)
end
end
@ -311,8 +332,9 @@ RSpec.describe Projects::ForkService, feature_category: :source_code_management
let(:params) { super().merge(visibility: 'public') }
it 'sets visibility level to project visibility' do
expect(fork_of_project).to be_persisted
is_expected.to be_success
expect(fork_of_project).to be_persisted
expect(fork_of_project.visibility_level).to eq(Gitlab::VisibilityLevel::INTERNAL)
end
end
@ -322,8 +344,9 @@ RSpec.describe Projects::ForkService, feature_category: :source_code_management
let_it_be(:namespace) { create(:group, :private).tap { |group| group.add_owner(user) } }
it 'sets visibility level to target namespace visibility level' do
expect(fork_of_project).to be_persisted
is_expected.to be_success
expect(fork_of_project).to be_persisted
expect(fork_of_project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE)
end
end
@ -342,8 +365,9 @@ RSpec.describe Projects::ForkService, feature_category: :source_code_management
end
it 'copies project features visibility settings to the fork' do
expect(fork_of_project).to be_persisted
is_expected.to be_success
expect(fork_of_project).to be_persisted
expect(fork_of_project.project_feature.slice(attrs.keys)).to eq(attrs)
end
end
@ -369,7 +393,7 @@ RSpec.describe Projects::ForkService, feature_category: :source_code_management
end
it 'creates a new pool repository after the project is moved to a new shard' do
fork_before_move = subject
fork_before_move = fork_of_project
storage_move = create(
:project_repository_storage_move,
@ -379,8 +403,10 @@ RSpec.describe Projects::ForkService, feature_category: :source_code_management
)
Projects::UpdateRepositoryStorageService.new(storage_move).execute
fork_after_move = described_class.new(project.reload, user, namespace: group).execute
fork_after_move_response = described_class.new(project.reload, user, namespace: group).execute
expect(fork_after_move_response).to be_success
fork_after_move = fork_after_move_response[:project]
pool_repository_before_move = PoolRepository.joins(:shard)
.find_by(source_project: project, shards: { name: 'default' })
pool_repository_after_move = PoolRepository.joins(:shard)
@ -396,8 +422,9 @@ RSpec.describe Projects::ForkService, feature_category: :source_code_management
context 'when no pool exists' do
it 'creates a new object pool' do
expect { fork_of_project }.to change { PoolRepository.count }.by(1)
expect { response }.to change { PoolRepository.count }.by(1)
is_expected.to be_success
expect(fork_of_project.pool_repository).to eq(project.pool_repository)
end
@ -405,8 +432,9 @@ RSpec.describe Projects::ForkService, feature_category: :source_code_management
let_it_be(:project) { create(:project, :private, :repository) }
it 'does not create an object pool' do
expect { fork_of_project }.not_to change { PoolRepository.count }
expect { response }.not_to change { PoolRepository.count }
is_expected.to be_success
expect(fork_of_project.pool_repository).to be_nil
end
end
@ -416,8 +444,9 @@ RSpec.describe Projects::ForkService, feature_category: :source_code_management
let!(:pool_repository) { create(:pool_repository, source_project: project) }
it 'joins the object pool' do
expect { fork_of_project }.not_to change { PoolRepository.count }
expect { response }.not_to change { PoolRepository.count }
is_expected.to be_success
expect(fork_of_project.pool_repository).to eq(pool_repository)
end
end
@ -440,7 +469,7 @@ RSpec.describe Projects::ForkService, feature_category: :source_code_management
expect(forked_from_project(unlinked_fork)).to be_nil
expect(service.execute(unlinked_fork)).to be_nil
expect(service.execute(unlinked_fork)).to be_error
expect(forked_from_project(unlinked_fork)).to be_nil
end
@ -469,6 +498,21 @@ RSpec.describe Projects::ForkService, feature_category: :source_code_management
expect(project.forks_count).to eq(1)
end
context 'when user cannot fork' do
let(:another_user) { create(:user) }
it 'returns an error' do
expect(unlinked_fork.forked?).to be_falsey
expect(forked_from_project(unlinked_fork)).to be_nil
response = described_class.new(project, another_user, params).execute(unlinked_fork)
expect(response).to be_error
expect(response.errors).to eq ['Forked from project is forbidden']
expect(forked_from_project(unlinked_fork)).to be_nil
end
end
context 'if the fork is not allowed' do
let_it_be(:project) { create(:project, :private) }

View File

@ -48,7 +48,13 @@ module ProjectForksHelper
allow(service).to receive(:gitlab_shell).and_return(shell)
end
forked_project = service.execute(params[:target_project])
response = service.execute(params[:target_project])
# This helper is expected to return a valid result.
# This exception will be raised if someone tries to test failed states using fork_project method (not recommended).
raise ArgumentError, response.message if response.error?
forked_project = response[:project]
# Reload the both projects so they know about their newly created fork_network
if forked_project.persisted?