Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2023-06-14 15:09:43 +00:00
parent 9223573b85
commit 7a33080fff
77 changed files with 1653 additions and 255 deletions

View File

@ -3534,7 +3534,6 @@ Layout/LineLength:
- 'spec/features/user_sorts_things_spec.rb'
- 'spec/features/users/login_spec.rb'
- 'spec/features/users/overview_spec.rb'
- 'spec/features/users/signup_spec.rb'
- 'spec/features/users/user_browses_projects_on_user_page_spec.rb'
- 'spec/features/webauthn_spec.rb'
- 'spec/finders/access_requests_finder_spec.rb'

View File

@ -1 +1 @@
d3bb9a8fe1d9ff265edf3920c348b2d5993ca0a8
fa4f0dbedd76758c0b0422da6e54441a5ac80d18

View File

@ -95,11 +95,7 @@ export default class ProtectedBranchCreate {
}
hasProtectedBranchSuccessAlert() {
return (
window.gon?.features?.branchRules &&
this.isLocalStorageAvailable &&
localStorage.getItem(IS_PROTECTED_BRANCH_CREATED)
);
return this.isLocalStorageAvailable && localStorage.getItem(IS_PROTECTED_BRANCH_CREATED);
}
createSuccessAlert() {

View File

@ -71,7 +71,7 @@ export default {
<component :is="tag">
<hr v-if="separated" aria-hidden="true" class="gl-mx-4 gl-my-2" />
<button
class="gl-rounded-base gl-relative gl-display-flex gl-align-items-center gl-mb-2 gl-py-2 gl-px-0 gl-text-black-normal! gl-hover-bg-t-gray-a-08 gl-focus-bg-t-gray-a-08 gl-text-decoration-none! gl-appearance-none gl-border-0 gl-bg-transparent gl-text-left gl-w-full gl-focus--focus"
class="gl-rounded-base gl-relative gl-display-flex gl-align-items-center gl-line-height-normal gl-mb-2 gl-py-3 gl-px-0 gl-text-black-normal! gl-hover-bg-t-gray-a-08 gl-focus-bg-t-gray-a-08 gl-text-decoration-none! gl-appearance-none gl-border-0 gl-bg-transparent gl-text-left gl-w-full gl-focus--focus"
:class="computedLinkClasses"
data-qa-selector="menu_section_button"
:data-qa-section-name="item.title"

View File

@ -514,7 +514,6 @@ span.idiff {
}
.blame-commit {
padding: 5px 10px;
width: 400px;
flex: none;
background: $gray-light;

View File

@ -0,0 +1,38 @@
.web-ide-loader {
max-width: 400px;
}
.web-ide-loader .tanuki-logo {
width: 50px;
height: 50px;
}
.web-ide-loader .tanuki,
.web-ide-loader .right-cheek,
.web-ide-loader .chin,
.web-ide-loader .left-cheek {
animation: animate-tanuki 1.5s infinite;
}
.web-ide-loader .right-cheek {
animation-delay: 0.35s;
}
.web-ide-loader .chin {
animation-delay: 0.7s;
}
.web-ide-loader .left-cheek {
animation-delay: 1.05s;
}
@keyframes animate-tanuki {
0%,
50% {
filter: brightness(1) grayscale(0);
}
25% {
filter: brightness(1.2) grayscale(0.2);
}
}

View File

@ -7,9 +7,7 @@ module Projects
feature_category :source_code_management
def index
render_404 unless Feature.enabled?(:branch_rules, project)
end
def index; end
end
end
end

View File

@ -12,7 +12,6 @@ module Projects
urgency :low, [:show, :create_deploy_token]
def show
push_frontend_feature_flag(:branch_rules, @project)
render_show
end

View File

@ -0,0 +1,44 @@
# frozen_string_literal: true
module Mutations
module Users
class SetNamespaceCommitEmail < BaseMutation
graphql_name 'UserSetNamespaceCommitEmail'
argument :namespace_id,
::Types::GlobalIDType[::Namespace],
required: true,
description: 'ID of the namespace to set the namespace commit email for.'
argument :email_id,
::Types::GlobalIDType[::Email],
required: false,
description: 'ID of the email to set.'
field :namespace_commit_email,
Types::Users::NamespaceCommitEmailType,
null: true,
description: 'User namespace commit email after mutation.'
authorize :read_namespace
def resolve(args)
namespace = authorized_find!(args[:namespace_id])
args[:email_id] = args[:email_id].model_id
result = ::Users::SetNamespaceCommitEmailService.new(current_user, namespace, args[:email_id], {}).execute
{
namespace_commit_email: result.payload[:namespace_commit_email],
errors: result.errors
}
end
private
def find_object(id)
GitlabSchema.object_from_id(
id, expected_type: [::Namespace, ::Namespaces::UserNamespace, ::Namespaces::ProjectNamespace]).sync
end
end
end
end

View File

@ -17,6 +17,10 @@ module Resolvers
required: false,
default_value: nil,
description: 'Commit ref to get the blobs from. Default value is HEAD.'
argument :ref_type, Types::RefTypeEnum,
required: false,
default_value: nil,
description: 'Type of ref.'
# We fetch blobs from Gitaly efficiently but it still scales O(N) with the
# number of paths being fetched, so apply a scaling limit to that.
@ -24,7 +28,7 @@ module Resolvers
super + (args[:paths] || []).size
end
def resolve(paths:, ref:)
def resolve(paths:, ref:, ref_type:)
authorize!(repository.container)
return [] if repository.empty?
@ -32,7 +36,13 @@ module Resolvers
ref ||= repository.root_ref
validate_ref(ref)
repository.blobs_at(paths.map { |path| [ref, path] })
ref = ExtractsRef.qualify_ref(ref, ref_type)
repository.blobs_at(paths.map { |path| [ref, path] }).tap do |blobs|
blobs.each do |blob|
blob.ref_type = ref_type
end
end
end
private

View File

@ -11,7 +11,8 @@ module Resolvers
def resolve(**args)
# Ensure merge commits can be returned by sending nil to Gitaly instead of '/'
path = tree.path == '/' ? nil : tree.path
commit = Gitlab::Git::Commit.last_for_path(tree.repository, tree.sha, path, literal_pathspec: true)
commit = Gitlab::Git::Commit.last_for_path(tree.repository,
ExtractsRef.qualify_ref(tree.sha, tree.ref_type), path, literal_pathspec: true)
::Commit.new(commit, tree.repository.project) if commit
end

View File

@ -18,6 +18,9 @@ module Resolvers
argument :ref, GraphQL::Types::String,
required: false,
description: 'Commit ref to get the tree for. Default value is HEAD.'
argument :ref_type, Types::RefTypeEnum,
required: false,
description: 'Type of ref.'
alias_method :repository, :object
@ -25,7 +28,6 @@ module Resolvers
return if repository.empty?
cursor = args.delete(:after)
args[:ref] ||= :head
pagination_params = {
limit: @field.max_page_size || 100,
@ -33,9 +35,11 @@ module Resolvers
}
tree = repository.tree(
args[:ref], args[:path], recursive: args[:recursive],
skip_flat_paths: false,
pagination_params: pagination_params
args[:ref].presence || :head,
args[:path], recursive: args[:recursive],
skip_flat_paths: false,
pagination_params: pagination_params,
ref_type: args[:ref_type]
)
next_cursor = tree.cursor&.next_cursor

View File

@ -17,14 +17,18 @@ module Resolvers
argument :ref, GraphQL::Types::String,
required: false,
description: 'Commit ref to get the tree for. Default value is HEAD.'
argument :ref_type, Types::RefTypeEnum,
required: false,
description: 'Type of ref.'
alias_method :repository, :object
def resolve(**args)
return unless repository.exists?
args[:ref] ||= :head
repository.tree(args[:ref], args[:path], recursive: args[:recursive])
ref = (args[:ref].presence || :head)
repository.tree(ref, args[:path], recursive: args[:recursive], ref_type: args[:ref_type])
end
end
end

View File

@ -182,6 +182,7 @@ module Types
mount_mutation Mutations::Pages::MarkOnboardingComplete
mount_mutation Mutations::SavedReplies::Destroy
mount_mutation Mutations::Uploads::Delete
mount_mutation Mutations::Users::SetNamespaceCommitEmail
end
end

View File

@ -0,0 +1,11 @@
# frozen_string_literal: true
module Types
class RefTypeEnum < BaseEnum
graphql_name 'RefType'
description 'Type of ref'
value 'HEADS', description: 'Ref type for branches.', value: 'heads'
value 'TAGS', description: 'Ref type for tags.', value: 'tags'
end
end

View File

@ -17,11 +17,13 @@ class Analytics::CycleAnalytics::Aggregation < ApplicationRecord
end
def consistency_check_cursor_for(model)
return {} if self["last_consistency_check_#{model.issuable_model.table_name}_issuable_id"].nil?
{
:start_event_timestamp => self["last_consistency_check_#{model.issuable_model.table_name}_start_event_timestamp"],
:end_event_timestamp => self["last_consistency_check_#{model.issuable_model.table_name}_end_event_timestamp"],
model.issuable_id_column => self["last_consistency_check_#{model.issuable_model.table_name}_issuable_id"]
}.compact
}
end
def refresh_last_run(mode)

View File

@ -71,6 +71,7 @@ class Blob < SimpleDelegator
].freeze
attr_reader :container
attr_accessor :ref_type
delegate :repository, to: :container, allow_nil: true
delegate :project, to: :repository, allow_nil: true

View File

@ -691,7 +691,7 @@ class Repository
@head_tree ||= Tree.new(self, root_ref, nil, skip_flat_paths: skip_flat_paths)
end
def tree(sha = :head, path = nil, recursive: false, skip_flat_paths: true, pagination_params: nil)
def tree(sha = :head, path = nil, recursive: false, skip_flat_paths: true, pagination_params: nil, ref_type: nil)
if sha == :head
return if empty? || root_ref.nil?
@ -699,10 +699,11 @@ class Repository
return head_tree(skip_flat_paths: skip_flat_paths)
else
sha = head_commit.sha
ref_type = nil
end
end
Tree.new(self, sha, path, recursive: recursive, skip_flat_paths: skip_flat_paths, pagination_params: pagination_params)
Tree.new(self, sha, path, recursive: recursive, skip_flat_paths: skip_flat_paths, pagination_params: pagination_params, ref_type: ref_type)
end
def blob_at_branch(branch_name, path)

View File

@ -3,17 +3,25 @@
class Tree
include Gitlab::Utils::StrongMemoize
attr_accessor :repository, :sha, :path, :entries, :cursor
attr_accessor :repository, :sha, :path, :entries, :cursor, :ref_type
def initialize(repository, sha, path = '/', recursive: false, skip_flat_paths: true, pagination_params: nil)
def initialize(
repository, sha, path = '/', recursive: false, skip_flat_paths: true, pagination_params: nil,
ref_type: nil)
path = '/' if path.blank?
@repository = repository
@sha = sha
@path = path
@ref_type = ExtractsRef.ref_type(ref_type)
git_repo = @repository.raw_repository
@entries, @cursor = Gitlab::Git::Tree.where(git_repo, @sha, @path, recursive, skip_flat_paths, pagination_params)
ref = ExtractsRef.qualify_ref(@sha, ref_type)
@entries, @cursor = Gitlab::Git::Tree.where(git_repo, ref, @path, recursive, skip_flat_paths, pagination_params)
@entries.each do |entry|
entry.ref_type = self.ref_type
end
end
def readme_path

View File

@ -2302,6 +2302,12 @@ class User < ApplicationRecord
}
end
def namespace_commit_email_for_namespace(namespace)
return if namespace.nil?
namespace_commit_emails.find_by(namespace: namespace)
end
protected
# override, from Devise::Validatable

View File

@ -31,6 +31,7 @@ class UserPolicy < BasePolicy
enable :read_user_groups
enable :read_saved_replies
enable :read_user_email_address
enable :admin_user_email_address
end
rule { default }.enable :read_user_profile

View File

@ -56,23 +56,23 @@ class BlobPresenter < Gitlab::View::Presenter::Delegated
end
def web_url
url_helpers.project_blob_url(project, ref_qualified_path)
url_helpers.project_blob_url(*path_params)
end
def web_path
url_helpers.project_blob_path(project, ref_qualified_path)
url_helpers.project_blob_path(*path_params)
end
def edit_blob_path
url_helpers.project_edit_blob_path(project, ref_qualified_path)
url_helpers.project_edit_blob_path(*path_params)
end
def raw_path
url_helpers.project_raw_path(project, ref_qualified_path)
url_helpers.project_raw_path(*path_params)
end
def replace_path
url_helpers.project_update_blob_path(project, ref_qualified_path)
url_helpers.project_update_blob_path(*path_params)
end
def pipeline_editor_path
@ -164,6 +164,18 @@ class BlobPresenter < Gitlab::View::Presenter::Delegated
private
def path_params
if ref_type.present?
[project, ref_qualified_path, { ref_type: ref_type }]
else
[project, ref_qualified_path]
end
end
def ref_type
blob.ref_type
end
def url_helpers
Gitlab::Routing.url_helpers
end
@ -179,7 +191,12 @@ class BlobPresenter < Gitlab::View::Presenter::Delegated
end
def ref_qualified_path
File.join(blob.commit_id, blob.path)
# If `ref_type` is present the commit_id will include the ref qualifier e.g. `refs/heads/`.
# We only accept/return unqualified refs so we need to remove the qualifier from the `commit_id`.
commit_id = ExtractsRef.unqualify_ref(blob.commit_id, ref_type)
File.join(commit_id, blob.path)
end
def load_all_blob_data

View File

@ -4,10 +4,23 @@ class TreeEntryPresenter < Gitlab::View::Presenter::Delegated
presents nil, as: :tree
def web_url
Gitlab::Routing.url_helpers.project_tree_url(tree.repository.project, File.join(tree.commit_id, tree.path))
Gitlab::Routing.url_helpers.project_tree_url(tree.repository.project, ref_qualified_path,
ref_type: tree.ref_type)
end
def web_path
Gitlab::Routing.url_helpers.project_tree_path(tree.repository.project, File.join(tree.commit_id, tree.path))
Gitlab::Routing.url_helpers.project_tree_path(tree.repository.project, ref_qualified_path,
ref_type: tree.ref_type)
end
private
def ref_qualified_path
# If `ref_type` is present the commit_id will include the ref qualifier e.g. `refs/heads/`.
# We only accept/return unqualified refs so we need to remove the qualifier from the `commit_id`.
commit_id = ExtractsRef.unqualify_ref(tree.commit_id, ref_type)
File.join(commit_id, tree.path)
end
end

View File

@ -0,0 +1,87 @@
# frozen_string_literal: true
module Users
class SetNamespaceCommitEmailService
include Gitlab::Allowable
attr_reader :current_user, :target_user, :namespace, :email_id
def initialize(current_user, namespace, email_id, params)
@current_user = current_user
@target_user = params.delete(:user) || current_user
@namespace = namespace
@email_id = email_id
end
def execute
return error(_('Namespace must be provided.')) if namespace.nil?
unless can?(current_user, :admin_user_email_address, target_user)
return error(_("User doesn't exist or you don't have permission to change namespace commit emails."))
end
unless can?(target_user, :read_namespace, namespace)
return error(_("Namespace doesn't exist or you don't have permission."))
end
email = target_user.emails.find_by(id: email_id) unless email_id.nil? # rubocop: disable CodeReuse/ActiveRecord
existing_namespace_commit_email = target_user.namespace_commit_email_for_namespace(namespace)
if existing_namespace_commit_email.nil?
return error(_('Email must be provided.')) if email.nil?
create_namespace_commit_email(email)
elsif email_id.nil?
remove_namespace_commit_email(existing_namespace_commit_email)
else
update_namespace_commit_email(existing_namespace_commit_email, email)
end
end
private
def remove_namespace_commit_email(namespace_commit_email)
namespace_commit_email.destroy
success(nil)
end
def create_namespace_commit_email(email)
namespace_commit_email = ::Users::NamespaceCommitEmail.new(
user: target_user,
namespace: namespace,
email: email
)
save_namespace_commit_email(namespace_commit_email)
end
def update_namespace_commit_email(namespace_commit_email, email)
namespace_commit_email.email = email
save_namespace_commit_email(namespace_commit_email)
end
def save_namespace_commit_email(namespace_commit_email)
if !namespace_commit_email.save
error_in_save(namespace_commit_email)
else
success(namespace_commit_email)
end
end
def success(namespace_commit_email)
ServiceResponse.success(payload: {
namespace_commit_email: namespace_commit_email
})
end
def error(message)
ServiceResponse.error(message: message)
end
def error_in_save(namespace_commit_email)
return error(_('Failed to save namespace commit email.')) if namespace_commit_email.errors.empty?
error(namespace_commit_email.errors.full_messages.to_sentence)
end
end
end

View File

@ -75,4 +75,5 @@
.gl-pt-5
= markdown_field(Gitlab::CurrentSettings.current_application_settings, :sign_in_text)
= render 'devise/shared/terms_of_service_notice', button_text: button_text
= yield :omniauth_providers_bottom if show_omniauth_providers
= yield :omniauth_providers_bottom if show_omniauth_providers

View File

@ -4,7 +4,7 @@
= _("Register with:")
.gl-text-center.gl-ml-auto.gl-mr-auto
- providers.each do |provider|
= link_to omniauth_authorize_path(:user, provider, register_omniauth_params(local_assigns)), method: :post, class: "btn gl-button btn-default gl-w-full gl-mb-4 js-oauth-login #{qa_selector_for_provider(provider)}", data: { provider: provider }, id: "oauth-login-#{provider}" do
= button_to omniauth_authorize_path(:user, provider, register_omniauth_params(local_assigns)), class: "btn gl-button btn-default gl-w-full gl-mb-4 js-oauth-login #{qa_selector_for_provider(provider)}", data: { provider: provider, track_action: "#{provider}_sso", track_label: tracking_label }, id: "oauth-login-#{provider}" do
- if provider_has_icon?(provider)
= provider_image_tag(provider)
%span.gl-button-text
@ -14,7 +14,7 @@
= _("Create an account using:")
.gl-display-flex.gl-justify-content-between.gl-flex-wrap
- providers.each do |provider|
= link_to omniauth_authorize_path(:user, provider, register_omniauth_params(local_assigns)), method: :post, class: "btn gl-button btn-default gl-w-full gl-mb-4 js-oauth-login #{qa_selector_for_provider(provider)}", data: { provider: provider }, id: "oauth-login-#{provider}" do
= button_to omniauth_authorize_path(:user, provider, register_omniauth_params(local_assigns)), class: "btn gl-button btn-default gl-w-full gl-mb-4 js-oauth-login #{qa_selector_for_provider(provider)}", data: { provider: provider, track_action: "#{provider}_sso", track_label: tracking_label }, id: "oauth-login-#{provider}" do
- if provider_has_icon?(provider)
= provider_image_tag(provider)
%span.gl-button-text

View File

@ -1,4 +1,4 @@
- if Feature.disabled?(:restyle_login_page, @project)
.omniauth-divider.gl-display-flex.gl-align-items-center
= _("or")
= render 'devise/shared/signup_omniauth_provider_list', providers: enabled_button_based_providers
= render 'devise/shared/signup_omniauth_provider_list', providers: enabled_button_based_providers, tracking_label: "free_registration"

View File

@ -1,4 +1,5 @@
- page_title _("IDE"), @project.full_name
- add_page_specific_style 'page_bundles/web_ide_loader'
- unless use_new_web_ide?
- add_page_specific_style 'page_bundles/build'
@ -9,4 +10,4 @@
- data = ide_data(project: @project, fork_info: @fork_info, params: params)
= render partial: 'shared/ide_root', locals: { data: data, loading_text: _('Loading the GitLab IDE...') }
= render partial: 'shared/ide_root', locals: { data: data, loading_text: _('Loading the GitLab IDE') }

View File

@ -7,7 +7,7 @@
- line_count = blame_group[:lines].count
.tr{ class: ('last-row' if groups_length == index) }
.td.blame-commit.commit{ class: commit_data.age_map_class }
.td.blame-commit.commit.gl-py-3.gl-px-4{ class: commit_data.age_map_class }
= commit_data.author_avatar
.commit-row-title

View File

@ -1,7 +1,7 @@
- add_page_specific_style 'page_bundles/branches'
- page_title _('Branches')
- add_to_breadcrumbs(_('Repository'), project_tree_path(@project))
- is_branch_rules_available = (can? current_user, :maintainer_access, @project) && Feature.enabled?(:branch_rules, @project)
- can_access_branch_rules = can?(current_user, :maintainer_access, @project)
- can_push_code = (can? current_user, :push_code, @project)
-# Possible values for variables passed down from the projects/branches_controller.rb
@ -24,7 +24,7 @@
sorted_by: @sort }
}
- if is_branch_rules_available
- if can_access_branch_rules
= link_to project_settings_repository_path(@project, anchor: 'js-branch-rules'), class: 'gl-button btn btn-default' do
= s_('Branches|View branch rules')
@ -38,7 +38,7 @@
= render_if_exists 'projects/commits/mirror_status'
- if is_branch_rules_available
- if can_access_branch_rules
= render 'branch_rules_info'
.js-branch-list{ data: { diverging_counts_endpoint: diverging_commit_counts_namespace_project_branches_path(@project.namespace, @project, format: :json), default_branch: @project.default_branch } }

View File

@ -4,8 +4,7 @@
- @force_desktop_expanded_sidebar = true
= render "projects/branch_defaults/show"
- if Feature.enabled?(:branch_rules, @project)
= render "projects/branch_rules/show"
= render "projects/branch_rules/show"
= render_if_exists "projects/push_rules/index"
= render "projects/mirrors/mirror_repos"

View File

@ -3,9 +3,8 @@
-# Fix for iOS 13+, the height of the page is actually less than
-# 100vh because of the presence of the bottom bar
- @body_class = 'gl-max-h-full gl-fixed'
#ide.gl--flex-center.gl-h-full{ data: data }
.gl-text-center
= gl_loading_icon(size: 'md')
%h2.clgray= loading_text
#ide.gl-h-full{ data: data }
.web-ide-loader.gl-display-flex.gl-justify-content-center.gl-align-items-center.gl-flex-direction-column.gl-h-full.gl-mr-auto.gl-ml-auto
= brand_header_logo
%h3.clblack.gl-mt-6= loading_text

View File

@ -336,6 +336,7 @@ module Gitlab
config.assets.precompile << "page_bundles/todos.css"
config.assets.precompile << "page_bundles/tree.css"
config.assets.precompile << "page_bundles/users.css"
config.assets.precompile << "page_bundles/web_ide_loader.css"
config.assets.precompile << "page_bundles/wiki.css"
config.assets.precompile << "page_bundles/work_items.css"
config.assets.precompile << "page_bundles/xterm.css"

View File

@ -1,8 +0,0 @@
---
name: branch_rules
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/88279
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/363170
milestone: '15.1'
type: development
group: group::source code
default_enabled: true

View File

@ -5,4 +5,4 @@ rollout_issue_url:
milestone: '16.1'
type: development
group: group::code review
default_enabled: false
default_enabled: true

View File

@ -0,0 +1,8 @@
---
name: compare_project_authorization_linear_cte
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/122886
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/414310
milestone: '16.1'
type: development
group: group::authentication and authorization
default_enabled: false

View File

@ -0,0 +1,24 @@
---
key_path: redis_hll_counters.quickactions.i_quickactions_unlink_monthly
description: Count of MAU using the `/unlink` quick action
product_section: dev
product_stage: plan
product_group: product_planning
value_type: number
status: active
milestone: "16.1"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/123485
time_frame: 28d
data_source: redis_hll
data_category: optional
instrumentation_class: RedisHLLMetric
options:
events:
- i_quickactions_unlink
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate

View File

@ -1,6 +1,5 @@
---
key_path: redis_hll_counters.quickactions.i_quickactions_unlink_weekly
name: quickactions_unlink_weekly
description: Count of WAU using the `/unlink` quick action
product_section: dev
product_stage: plan
@ -16,7 +15,6 @@ instrumentation_class: RedisHLLMetric
options:
events:
- i_quickactions_unlink
performance_indicator_type: []
distribution:
- ce
- ee

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
class AddIndexOnPackagesIdIdToPackageBuildInfos < Gitlab::Database::Migration[2.1]
disable_ddl_transaction!
INDEX_NAME = 'index_packages_build_infos_package_id_id'
def up
add_concurrent_index :packages_build_infos, [:package_id, :id], name: INDEX_NAME
end
def down
remove_concurrent_index_by_name :packages_build_infos, name: INDEX_NAME
end
end

View File

@ -0,0 +1 @@
5fadce4dbc2280ca1d68f8271f4d44ea3c492769b65ebb1d8f2ae94cfb6d6c75

View File

@ -32019,6 +32019,8 @@ CREATE INDEX index_p_ci_runner_machine_builds_on_runner_machine_id ON ONLY p_ci_
CREATE INDEX index_packages_build_infos_on_pipeline_id ON packages_build_infos USING btree (pipeline_id);
CREATE INDEX index_packages_build_infos_package_id_id ON packages_build_infos USING btree (package_id, id);
CREATE INDEX index_packages_build_infos_package_id_pipeline_id_id ON packages_build_infos USING btree (package_id, pipeline_id, id);
CREATE UNIQUE INDEX index_packages_composer_cache_namespace_and_sha ON packages_composer_cache_files USING btree (namespace_id, file_sha256);

View File

@ -44,8 +44,8 @@ Streaming destinations receive **all** audit event data, which could include sen
Users with the Owner role for a group can add streaming destinations for it:
1. On the top bar, select **Main menu > Groups** and find your group.
1. On the left sidebar, select **Security and Compliance > Audit events**.
1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your group.
1. Select **Secure > Audit events**.
1. On the main area, select **Streams** tab.
1. Select **Add streaming destination** to show the section for adding destinations.
1. Enter the destination URL to add.
@ -70,7 +70,8 @@ Prerequisites:
To add a streaming destination for an instance:
1. On the top bar, select **Main menu > Admin**.
1. On the left sidebar, expand the top-most chevron (**{chevron-down}**).
1. Select **Admin Area**.
1. On the left sidebar, select **Monitoring > Audit events**.
1. On the main area, select **Streams** tab.
1. Select **Add streaming destination** to show the section for adding destinations.
@ -180,8 +181,8 @@ Users with the Owner role for a group can list streaming destinations.
To list the streaming destinations for a top-level group:
1. On the top bar, select **Main menu > Groups** and find your group.
1. On the left sidebar, select **Security and Compliance > Audit events**.
1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your group.
1. Select **Secure > Audit events**.
1. On the main area, select **Streams** tab.
1. To the right of the item, select **Edit** (**{pencil}**) to see all the custom HTTP headers.
@ -199,7 +200,8 @@ Prerequisites:
To list the streaming destinations for an instance:
1. On the top bar, select **Main menu > Admin**.
1. On the left sidebar, expand the top-most chevron (**{chevron-down}**).
1. Select **Admin Area**.
1. On the left sidebar, select **Monitoring > Audit events**.
1. On the main area, select **Streams** tab.
@ -276,8 +278,8 @@ Users with the Owner role for a group and instance administrators can update str
To update a streaming destinations custom HTTP headers:
1. On the top bar, select **Main menu > Groups** and find your group.
1. On the left sidebar, select **Security and Compliance > Audit events**.
1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your group.
1. Select **Secure > Audit events**.
1. On the main area, select **Streams** tab.
1. To the right of the item, select **Edit** (**{pencil}**).
1. Locate the **Custom HTTP headers** table.
@ -368,15 +370,15 @@ When the last destination is successfully deleted, streaming is disabled for the
To delete a streaming destination:
1. On the top bar, select **Main menu > Groups** and find your group.
1. On the left sidebar, select **Security and Compliance > Audit events**.
1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your group.
1. Select **Secure > Audit events**.
1. On the main area, select the **Streams** tab.
1. To the right of the item, select **Delete** (**{remove}**).
To delete only the custom HTTP headers for a streaming destination:
1. On the top bar, select **Main menu > Groups** and find your group.
1. On the left sidebar, select **Security and Compliance > Audit events**.
1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your group.
1. Select **Secure > Audit events**.
1. On the main area, select the **Streams** tab.
1. To the right of the item, **Edit** (**{pencil}**).
1. Locate the **Custom HTTP headers** table.
@ -398,7 +400,8 @@ Prerequisites:
To delete the streaming destinations for an instance:
1. On the top bar, select **Main menu > Admin**.
1. On the left sidebar, expand the top-most chevron (**{chevron-down}**).
1. Select **Admin Area**.
1. On the left sidebar, select **Monitoring > Audit events**.
1. On the main area, select the **Streams** tab.
1. To the right of the item, select **Delete** (**{remove}**).
@ -484,8 +487,8 @@ the destination's value when [listing streaming destinations](#list-streaming-de
Users with the Owner role for a group can list streaming destinations and see the verification tokens:
1. On the top bar, select **Main menu > Groups** and find your group.
1. On the left sidebar, select **Security and Compliance > Audit events**.
1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your group.
1. Select **Secure > Audit events**.
1. On the main area, select the **Streams**.
1. View the verification token on the right side of each item.
@ -503,7 +506,8 @@ Prerequisites:
To list streaming destinations for an instance and see the verification tokens:
1. On the top bar, select **Main menu > Admin**.
1. On the left sidebar, expand the top-most chevron (**{chevron-down}**).
1. Select **Admin Area**.
1. On the left sidebar, select **Monitoring > Audit events**.
1. On the main area, select the **Streams**.
1. View the verification token on the right side of each item.

View File

@ -781,7 +781,7 @@ Plan.default.actual_limits.update!(dast_profile_schedules: 50)
### Maximum size and depth of CI/CD configuration YAML files
The default maximum size of a CI/CD configuration YAML file is 1 megabyte and the
The default maximum size of a single CI/CD configuration YAML file is 1 megabyte and the
default depth is 100.
You can change these limits in the [GitLab Rails console](operations/rails_console.md#starting-a-rails-console-session):

View File

@ -6817,6 +6817,26 @@ Input type: `UserPreferencesUpdateInput`
| <a id="mutationuserpreferencesupdateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| <a id="mutationuserpreferencesupdateuserpreferences"></a>`userPreferences` | [`UserPreferences`](#userpreferences) | User preferences after mutation. |
### `Mutation.userSetNamespaceCommitEmail`
Input type: `UserSetNamespaceCommitEmailInput`
#### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationusersetnamespacecommitemailclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationusersetnamespacecommitemailemailid"></a>`emailId` | [`EmailID`](#emailid) | ID of the email to set. |
| <a id="mutationusersetnamespacecommitemailnamespaceid"></a>`namespaceId` | [`NamespaceID!`](#namespaceid) | ID of the namespace to set the namespace commit email for. |
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationusersetnamespacecommitemailclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationusersetnamespacecommitemailerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| <a id="mutationusersetnamespacecommitemailnamespacecommitemail"></a>`namespaceCommitEmail` | [`NamespaceCommitEmail`](#namespacecommitemail) | User namespace commit email after mutation. |
### `Mutation.vulnerabilityConfirm`
Input type: `VulnerabilityConfirmInput`
@ -7548,6 +7568,29 @@ The edge type for [`AuditEventStreamingHeader`](#auditeventstreamingheader).
| <a id="auditeventstreamingheaderedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
| <a id="auditeventstreamingheaderedgenode"></a>`node` | [`AuditEventStreamingHeader`](#auditeventstreamingheader) | The item at the end of the edge. |
#### `AuditEventsStreamingInstanceHeaderConnection`
The connection type for [`AuditEventsStreamingInstanceHeader`](#auditeventsstreaminginstanceheader).
##### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="auditeventsstreaminginstanceheaderconnectionedges"></a>`edges` | [`[AuditEventsStreamingInstanceHeaderEdge]`](#auditeventsstreaminginstanceheaderedge) | A list of edges. |
| <a id="auditeventsstreaminginstanceheaderconnectionnodes"></a>`nodes` | [`[AuditEventsStreamingInstanceHeader]`](#auditeventsstreaminginstanceheader) | A list of nodes. |
| <a id="auditeventsstreaminginstanceheaderconnectionpageinfo"></a>`pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. |
#### `AuditEventsStreamingInstanceHeaderEdge`
The edge type for [`AuditEventsStreamingInstanceHeader`](#auditeventsstreaminginstanceheader).
##### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="auditeventsstreaminginstanceheaderedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
| <a id="auditeventsstreaminginstanceheaderedgenode"></a>`node` | [`AuditEventsStreamingInstanceHeader`](#auditeventsstreaminginstanceheader) | The item at the end of the edge. |
#### `AwardEmojiConnection`
The connection type for [`AwardEmoji`](#awardemoji).
@ -16838,6 +16881,7 @@ Represents an external resource to send instance audit events to.
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="instanceexternalauditeventdestinationdestinationurl"></a>`destinationUrl` | [`String!`](#string) | External destination to send audit events to. |
| <a id="instanceexternalauditeventdestinationheaders"></a>`headers` | [`AuditEventsStreamingInstanceHeaderConnection!`](#auditeventsstreaminginstanceheaderconnection) | List of additional HTTP headers sent with each event. (see [Connections](#connections)) |
| <a id="instanceexternalauditeventdestinationid"></a>`id` | [`ID!`](#id) | ID of the destination. |
| <a id="instanceexternalauditeventdestinationverificationtoken"></a>`verificationToken` | [`String!`](#string) | Verification token to validate source of event. |
@ -21712,6 +21756,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
| ---- | ---- | ----------- |
| <a id="repositoryblobspaths"></a>`paths` | [`[String!]!`](#string) | Array of desired blob paths. |
| <a id="repositoryblobsref"></a>`ref` | [`String`](#string) | Commit ref to get the blobs from. Default value is HEAD. |
| <a id="repositoryblobsreftype"></a>`refType` | [`RefType`](#reftype) | Type of ref. |
##### `Repository.branchNames`
@ -21756,6 +21801,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
| <a id="repositorypaginatedtreepath"></a>`path` | [`String`](#string) | Path to get the tree for. Default value is the root of the repository. |
| <a id="repositorypaginatedtreerecursive"></a>`recursive` | [`Boolean`](#boolean) | Used to get a recursive tree. Default is false. |
| <a id="repositorypaginatedtreeref"></a>`ref` | [`String`](#string) | Commit ref to get the tree for. Default value is HEAD. |
| <a id="repositorypaginatedtreereftype"></a>`refType` | [`RefType`](#reftype) | Type of ref. |
##### `Repository.tree`
@ -21770,6 +21816,7 @@ Returns [`Tree`](#tree).
| <a id="repositorytreepath"></a>`path` | [`String`](#string) | Path to get the tree for. Default value is the root of the repository. |
| <a id="repositorytreerecursive"></a>`recursive` | [`Boolean`](#boolean) | Used to get a recursive tree. Default is false. |
| <a id="repositorytreeref"></a>`ref` | [`String`](#string) | Commit ref to get the tree for. Default value is HEAD. |
| <a id="repositorytreereftype"></a>`refType` | [`RefType`](#reftype) | Type of ref. |
### `RepositoryBlob`
@ -26023,6 +26070,15 @@ Project member relation.
| <a id="projectmemberrelationinvited_groups"></a>`INVITED_GROUPS` | Invited Groups members. |
| <a id="projectmemberrelationshared_into_ancestors"></a>`SHARED_INTO_ANCESTORS` | Shared Into Ancestors members. |
### `RefType`
Type of ref.
| Value | Description |
| ----- | ----------- |
| <a id="reftypeheads"></a>`HEADS` | Ref type for branches. |
| <a id="reftypetags"></a>`TAGS` | Ref type for tags. |
### `RegistryState`
State of a Geo registry.
@ -27026,6 +27082,12 @@ Duration between two instants, represented as a fractional number of seconds.
For example: 12.3334.
### `EmailID`
A `EmailID` is a global ID. It is encoded as a string.
An example `EmailID` is: `"gid://gitlab/Email/1"`.
### `EnvironmentID`
A `EnvironmentID` is a global ID. It is encoded as a string.

View File

@ -330,6 +330,74 @@ Example response:
By default, the `GET` request returns 20 results, because the API is [paginated](rest/index.md#pagination).
## List package pipelines
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/341950) in GitLab 16.1.
Get a list of pipelines for a single package. The results are sorted by `id` in descending order.
The results are [paginated](rest/index.md#keyset-based-pagination) and return up to 20 records per page.
```plaintext
GET /projects/:id/packages/:package_id/pipelines
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding) |
| `package_id` | integer | yes | ID of a package. |
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/:id/packages/:package_id/pipelines"
```
Example response:
```json
[
{
"id": 1,
"iid": 1,
"project_id": 9,
"sha": "2b6127f6bb6f475c4e81afcc2251e3f941e554f9",
"ref": "mytag",
"status": "failed",
"source": "push",
"created_at": "2023-02-01T12:19:21.895Z",
"updated_at": "2023-02-01T14:00:05.922Z",
"web_url": "http://gdk.test:3001/feature-testing/composer-repository/-/pipelines/1",
"user": {
"id": 1,
"username": "root",
"name": "Administrator",
"state": "active",
"avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon",
"web_url": "http://gdk.test:3001/root"
}
},
{
"id": 2,
"iid": 2,
"project_id": 9,
"sha": "e564015ac6cb3d8617647802c875b27d392f72a6",
"ref": "master",
"status": "canceled",
"source": "push",
"created_at": "2023-02-01T12:23:23.694Z",
"updated_at": "2023-02-01T12:26:28.635Z",
"web_url": "http://gdk.test:3001/feature-testing/composer-repository/-/pipelines/2",
"user": {
"id": 1,
"username": "root",
"name": "Administrator",
"state": "active",
"avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon",
"web_url": "http://gdk.test:3001/root"
}
}
]
```
## Delete a project package
Deletes a project package.

View File

@ -491,6 +491,7 @@ options:
| [Group audit events](../audit_events.md#retrieve-all-group-audit-events) | `order_by=id`, `sort=desc` only | Authenticated users only. |
| [Groups](../groups.md#list-groups) | `order_by=name`, `sort=asc` only | Unauthenticated users only. |
| [Instance audit events](../audit_events.md#retrieve-all-instance-audit-events) | `order_by=id`, `sort=desc` only | Authenticated users only. |
| [Package pipelines](../packages.md#list-package-pipelines) | `order_by=id`, `sort=desc` only | Authenticated users only. |
| [Project jobs](../jobs.md#list-project-jobs) | `order_by=id`, `sort=desc` only | Authenticated users only. |
| [Project audit events](../audit_events.md#retrieve-all-project-audit-events) | `order_by=id`, `sort=desc` only | Authenticated users only. |
| [Projects](../projects.md) | `order_by=id` only | Authenticated and unauthenticated users. |

View File

@ -61,8 +61,7 @@ of the changes.
## Continuous Deployment
[Continuous Deployment](https://www.airpair.com/continuous-deployment/posts/continuous-deployment-for-practical-people)
is another step beyond Continuous Integration, similar to
Continuous Deployment is another step beyond Continuous Integration, similar to
Continuous Delivery. The difference is that instead of deploying your
application manually, you set it to be deployed automatically.
Human intervention is not required.

View File

@ -34,17 +34,18 @@ your cluster's level.
**Project-level clusters:**
1. On the top bar, select **Main menu > Projects** and find your project.
1. On the left sidebar, select **Infrastructure > Kubernetes clusters**.
1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your project.
1. Select **Operate > Kubernetes clusters**.
**Group-level clusters:**
1. On the top bar, select **Main menu > Groups** and find your group.
1. On the left sidebar, select **Kubernetes**.
1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your group.
1. Select **Operate > Kubernetes clusters**.
**Instance-level clusters:**
1. On the top bar, select **Main menu > Admin**.
1. On the left sidebar, expand the top-most chevron (**{chevron-down}**).
1. Select **Admin Area**.
1. On the left sidebar, select **Kubernetes**.
## Security implications for clusters connected with certificates

View File

@ -35,7 +35,8 @@ Start by [importing the example project by URL](../../../project/import/repo_by_
To import the project:
1. In GitLab, on the top bar, select **Main menu > Projects > View all projects**.
1. In GitLab, on the left sidebar, expand the top-most chevron (**{chevron-down}**).
1. Select **View all your projects**..
1. On the right of the page, select **New project**.
1. Select **Import project**.
1. Select **Repository by URL**.

View File

@ -34,7 +34,8 @@ Start by [importing the example project by URL](../../../project/import/repo_by_
To import the project:
1. In GitLab, on the top bar, select **Main menu > Projects > View all projects**.
1. In GitLab, on the left sidebar, expand the top-most chevron (**{chevron-down}**).
1. Select **View all your projects**.
1. On the right of the page, select **New project**.
1. Select **Import project**.
1. Select **Repository by URL**.

View File

@ -41,7 +41,8 @@ Start by [importing the example project by URL](../../../project/import/repo_by_
To import the project:
1. In GitLab, on the top bar, select **Main menu > Projects > View all projects**.
1. In GitLab, on the left sidebar, expand the top-most chevron (**{chevron-down}**).
1. Select **View all your projects**.
1. On the right of the page, select **New project**.
1. Select **Import project**.
1. Select **Repository by URL**.

View File

@ -64,8 +64,8 @@ In each GitLab major release (for example, 15.0), the latest templates replace t
To use a Terraform template:
1. On the top bar, select **Main menu > Projects** and find the project you want to integrate with Terraform.
1. On the left sidebar, select **Repository > Files**.
1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your project you want to integrate with Terraform.
1. Select **Code > Repository**.
1. Edit your `.gitlab-ci.yml` file, use the `include` attribute to fetch the Terraform template:
```yaml
@ -73,7 +73,7 @@ To use a Terraform template:
# To fetch the latest template, use:
- template: Terraform.latest.gitlab-ci.yml
# To fetch the advanced latest template, use:
- template: Terraform/Base.latest.gitlab-ci.yml
- template: Terraform/Base.latest.gitlab-ci.yml
# To fetch the stable template, use:
- template: Terraform.gitlab-ci.yml
# To fetch the advanced stable template, use:

View File

@ -116,8 +116,8 @@ inconsistent. Instead, use a remote storage resource.
[initialized for CI/CD](#initialize-a-terraform-state-as-a-backend-by-using-gitlab-cicd).
1. Copy a pre-populated Terraform `init` command:
1. On the top bar, select **Main menu > Projects** and find your project.
1. On the left sidebar, select **Infrastructure > Terraform states**.
1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your project.
1. Select **Operate > Terraform states**.
1. Next to the environment you want to use, select **Actions**
(**{ellipsis_v}**) and select **Copy Terraform init command**.
@ -294,8 +294,8 @@ To read the Terraform state in the target project, you need at least the Develop
To view Terraform state files:
1. On the top bar, select **Main menu > Projects** and find your project.
1. On the left sidebar, select **Infrastructure > Terraform states**.
1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your project.
1. Select **Operate > Terraform states**.
[An epic exists](https://gitlab.com/groups/gitlab-org/-/epics/4563) to track improvements to this UI.

View File

@ -2,8 +2,11 @@
module API
class ProjectPackages < ::API::Base
include Gitlab::Utils::StrongMemoize
include PaginationParams
PIPELINE_COLUMNS = %i[id iid project_id sha ref status source created_at updated_at user_id].freeze
before do
authorize_packages_access!(user_project)
end
@ -12,6 +15,13 @@ module API
urgency :low
helpers ::API::Helpers::PackagesHelpers
helpers do
def package
strong_memoize(:package) do # rubocop:disable Gitlab/StrongMemoizeAttr
::Packages::PackageFinder.new(user_project, declared_params[:package_id]).execute
end
end
end
params do
requires :id, types: [String, Integer], desc: 'The ID or URL-encoded path of the project'
@ -66,14 +76,45 @@ module API
end
route_setting :authentication, job_token_allowed: true
get ':id/packages/:package_id' do
package = ::Packages::PackageFinder
.new(user_project, params[:package_id]).execute
render_api_error!('Package not found', 404) unless package.default?
present package, with: ::API::Entities::Package, user: current_user, namespace: user_project.namespace
end
desc 'Get the pipelines for a single project package' do
detail 'This feature was introduced in GitLab 16.1'
success code: 200, model: ::API::Entities::Package::Pipeline
failure [
{ code: 401, message: 'Unauthorized' },
{ code: 403, message: 'Forbidden' },
{ code: 404, message: 'Not Found' }
]
tags %w[project_packages]
end
params do
use :pagination
requires :package_id, type: Integer, desc: 'The ID of a package'
optional :cursor, type: String, desc: 'Cursor for obtaining the next set of records'
# Overrides the original definition to add the `values: 1..20` restriction
optional :per_page, type: Integer, default: 20,
desc: 'Number of items per page', documentation: { example: 20 },
values: 1..20
end
route_setting :authentication, job_token_allowed: true
get ':id/packages/:package_id/pipelines' do
not_found!('Package not found') unless package.default?
params[:pagination] = 'keyset' # keyset is the only available pagination
pipelines = paginate_with_strategies(
package.build_infos.without_empty_pipelines,
paginator_params: { per_page: declared_params[:per_page], cursor: declared_params[:cursor] }
) do |results|
::Ci::Pipeline.id_in(results.map(&:pipeline_id)).select(PIPELINE_COLUMNS).order_id_desc
end
present pipelines, with: ::API::Entities::Package::Pipeline, user: current_user
end
desc 'Delete a project package' do
detail 'This feature was introduced in GitLab 11.9'
success code: 204
@ -90,9 +131,6 @@ module API
delete ':id/packages/:package_id' do
authorize_destroy_package!(user_project)
package = ::Packages::PackageFinder
.new(user_project, params[:package_id]).execute
destroy_conditionally!(package) do |package|
::Packages::MarkPackageForDestructionService.new(container: package, current_user: current_user).execute
end

View File

@ -7,6 +7,28 @@ module ExtractsRef
InvalidPathError = Class.new(StandardError)
BRANCH_REF_TYPE = 'heads'
TAG_REF_TYPE = 'tags'
REF_TYPES = [BRANCH_REF_TYPE, TAG_REF_TYPE].freeze
def self.ref_type(type)
return unless REF_TYPES.include?(type)
type
end
def self.qualify_ref(ref, type)
validated_type = ref_type(type)
return ref unless validated_type
%(refs/#{validated_type}/#{ref})
end
def self.unqualify_ref(ref, type)
validated_type = ref_type(type)
return ref unless validated_type
ref.sub(%r{^refs/#{validated_type}/}, '')
end
# Given a string containing both a Git tree-ish, such as a branch or tag, and
# a filesystem path joined by forward slashes, attempts to separate the two.
#
@ -60,7 +82,6 @@ module ExtractsRef
#
# If the :id parameter appears to be requesting a specific response format,
# that will be handled as well.
#
# rubocop:disable Gitlab/ModuleWithInstanceVariables
def assign_ref_vars
@id, @ref, @path = extract_ref_path
@ -70,7 +91,7 @@ module ExtractsRef
return unless @ref.present?
@commit = if ref_type
@fully_qualified_ref = %(refs/#{ref_type}/#{@ref})
@fully_qualified_ref = ExtractsRef.qualify_ref(@ref, ref_type)
@repo.commit(@fully_qualified_ref)
else
@repo.commit(@ref)
@ -90,9 +111,7 @@ module ExtractsRef
end
def ref_type
return unless params[:ref_type].present?
params[:ref_type] == TAG_REF_TYPE ? TAG_REF_TYPE : BRANCH_REF_TYPE
ExtractsRef.ref_type(params[:ref_type])
end
private
@ -156,6 +175,7 @@ module ExtractsRef
raise NotImplementedError
end
# deprecated in favor of ExtractsRef::RequestedRef
def ambiguous_ref?(project, ref)
return false unless ref
return true if project.repository.ambiguous_ref?(ref)

View File

@ -6,7 +6,7 @@ module Gitlab
include Gitlab::EncodingHelper
extend Gitlab::Git::WrapsGitalyErrors
attr_accessor :id, :type, :mode, :commit_id, :submodule_url
attr_accessor :id, :type, :mode, :commit_id, :submodule_url, :ref_type
attr_writer :name, :path, :flat_path
class << self

View File

@ -6,7 +6,8 @@ module Gitlab
SUPPORTED_ORDERING = {
Group => { name: :asc },
AuditEvent => { id: :desc },
::Ci::Build => { id: :desc }
::Ci::Build => { id: :desc },
::Packages::BuildInfo => { id: :desc }
}.freeze
# Relation types that are enforced in this list

View File

@ -12,62 +12,56 @@ module Gitlab
end
def calculate
cte = if Feature.enabled?(:linear_project_authorization, user)
linear_cte
else
recursive_cte
end
if Feature.enabled?(:compare_project_authorization_linear_cte, user)
linear_relation = calculate_with_linear_query
recursive_relation = calculate_with_recursive_query
recursive_set = Set.new(recursive_relation.to_a.pluck(:project_id, :access_level))
linear_set = Set.new(linear_relation.to_a.pluck(:project_id, :access_level))
if linear_set == recursive_set
Gitlab::AppJsonLogger.info(event: 'linear_authorized_projects_check',
user_id: user.id,
matching_results: true)
return calculate_with_linear_query
else
Gitlab::AppJsonLogger.warn(event: 'linear_authorized_projects_check',
user_id: user.id,
matching_results: false)
end
end
cte_alias = cte.table.alias(Group.table_name)
projects = Project.arel_table
links = ProjectGroupLink.arel_table
relations = [
# The project a user has direct access to.
user.projects_with_active_memberships.select_for_project_authorization,
# The personal projects of the user.
user.personal_projects.select_project_owner_for_project_authorization,
# Projects that belong directly to any of the groups the user has
# access to.
Namespace
.unscoped
.select([alias_as_column(projects[:id], 'project_id'),
cte_alias[:access_level]])
.from(cte_alias)
.joins(:projects),
# Projects shared with any of the namespaces the user has access to.
Namespace
.unscoped
.select([
links[:project_id],
least(cte_alias[:access_level], links[:group_access], 'access_level')
])
.from(cte_alias)
.joins('INNER JOIN project_group_links ON project_group_links.group_id = namespaces.id')
.joins('INNER JOIN projects ON projects.id = project_group_links.project_id')
.joins('INNER JOIN namespaces p_ns ON p_ns.id = projects.namespace_id')
.where('p_ns.share_with_group_lock IS FALSE')
]
Gitlab::AppJsonLogger.info(event: 'linear_authorized_projects_check_with_flag',
feature_flag_status: Feature.enabled?(:linear_project_authorization, user))
if Feature.enabled?(:linear_project_authorization, user)
ProjectAuthorization
.unscoped
.with(cte.to_arel)
.select_from_union(relations)
calculate_with_linear_query
else
ProjectAuthorization
.unscoped
.with
.recursive(cte.to_arel)
.select_from_union(relations)
calculate_with_recursive_query
end
end
private
def calculate_with_linear_query
cte = linear_cte
cte_alias = cte.table.alias(Group.table_name)
ProjectAuthorization
.unscoped
.with(cte.to_arel)
.select_from_union(relations(cte_alias: cte_alias))
end
def calculate_with_recursive_query
cte = recursive_cte
cte_alias = cte.table.alias(Group.table_name)
ProjectAuthorization
.unscoped
.with
.recursive(cte.to_arel)
.select_from_union(relations(cte_alias: cte_alias))
end
# Builds a recursive CTE that gets all the groups the current user has
# access to, including any nested groups and any shared groups.
def recursive_cte
@ -83,9 +77,11 @@ module Gitlab
# Namespaces shared with any of the group
cte << Group.select([namespaces[:id],
least(members[:access_level],
group_group_links[:group_access],
'access_level')])
least(
members[:access_level],
group_group_links[:group_access],
'access_level'
)])
.joins(join_group_group_links)
.joins(join_members_on_group_group_links)
@ -187,5 +183,45 @@ module Gitlab
def alias_as_column(value, alias_to)
Arel::Nodes::As.new(value, Arel::Nodes::SqlLiteral.new(alias_to))
end
def relations(cte_alias:)
[
user.projects_with_active_memberships.select_for_project_authorization,
user.personal_projects.select_project_owner_for_project_authorization,
projects_belonging_directy_to_any_groups_user_has_access_to(cte_alias: cte_alias),
projects_shared_with_namespaces_user_has_access_to(cte_alias: cte_alias)
]
end
def projects_shared_with_namespaces_user_has_access_to(cte_alias:)
Namespace
.unscoped
.select([
links[:project_id],
least(cte_alias[:access_level], links[:group_access], 'access_level')
])
.from(cte_alias)
.joins('INNER JOIN project_group_links ON project_group_links.group_id = namespaces.id')
.joins('INNER JOIN projects ON projects.id = project_group_links.project_id')
.joins('INNER JOIN namespaces p_ns ON p_ns.id = projects.namespace_id')
.where('p_ns.share_with_group_lock IS FALSE')
end
def projects_belonging_directy_to_any_groups_user_has_access_to(cte_alias:)
Namespace
.unscoped
.select([alias_as_column(projects[:id], 'project_id'),
cte_alias[:access_level]])
.from(cte_alias)
.joins(:projects)
end
def projects
Project.arel_table
end
def links
ProjectGroupLink.arel_table
end
end
end

View File

@ -16799,6 +16799,9 @@ msgstr ""
msgid "Email display name"
msgstr ""
msgid "Email must be provided."
msgstr ""
msgid "Email not verified. Please verify your email in Salesforce."
msgstr ""
@ -18803,6 +18806,9 @@ msgstr ""
msgid "Failed to save merge conflicts resolutions. Please try again!"
msgstr ""
msgid "Failed to save namespace commit email."
msgstr ""
msgid "Failed to save new settings"
msgstr ""
@ -27346,7 +27352,7 @@ msgstr ""
msgid "Loading snippet"
msgstr ""
msgid "Loading the GitLab IDE..."
msgid "Loading the GitLab IDE"
msgstr ""
msgid "Loading, please wait."
@ -29705,6 +29711,12 @@ msgstr ""
msgid "Namespace Limits"
msgstr ""
msgid "Namespace doesn't exist or you don't have permission."
msgstr ""
msgid "Namespace must be provided."
msgstr ""
msgid "Namespace or group to import repository into does not exist."
msgstr ""
@ -49493,6 +49505,9 @@ msgstr ""
msgid "User does not have permission to create a Security Policy project."
msgstr ""
msgid "User doesn't exist or you don't have permission to change namespace commit emails."
msgstr ""
msgid "User has already been deactivated"
msgstr ""

View File

@ -3,10 +3,6 @@
module QA
RSpec.describe 'Create' do
describe 'Branch Rules Overview', product_group: :source_code,
feature_flag: {
name: 'branch_rules',
scope: :project
},
quarantine: {
issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/403583',
type: :flaky
@ -22,8 +18,6 @@ module QA
end
before do
Runtime::Feature.enable(:branch_rules, project: project)
Flow::Login.sign_in
Resource::Repository::Commit.fabricate_via_api! do |commit|
@ -35,10 +29,6 @@ module QA
end
end
after do
Runtime::Feature.disable(:branch_rules, project: project)
end
it 'adds a new branch rule', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/397587' do
project.visit!

View File

@ -45,14 +45,5 @@ RSpec.describe 'Projects > Settings > Repository > Branch rules settings', featu
expect(page).to have_content('Branch rules')
end
end
context 'branch_rules feature flag disabled' do
it 'does not render branch rules content' do
stub_feature_flags(branch_rules: false)
request
expect(page).to have_gitlab_http_status(:not_found)
end
end
end
end

View File

@ -10,7 +10,6 @@ RSpec.describe 'Projects > Settings > Repository settings', feature_category: :g
let(:role) { :developer }
before do
stub_feature_flags(branch_rules: false)
stub_feature_flags(mirror_only_branches_match_regex: false)
project.add_role(user, role)
sign_in(user)
@ -43,15 +42,7 @@ RSpec.describe 'Projects > Settings > Repository settings', feature_category: :g
end
context 'Branch rules', :js do
context 'branch_rules feature flag disabled', :js do
it 'does not render branch rules settings' do
visit project_settings_repository_path(project)
expect(page).not_to have_content('Branch rules')
end
end
it 'renders branch rules settings' do
stub_feature_flags(branch_rules: true)
visit project_settings_repository_path(project)
expect(page).to have_content('Branch rules')
end

View File

@ -3,10 +3,8 @@
require 'spec_helper'
RSpec.shared_examples 'Signup name validation' do |field, max_length, label|
flag_values = [true, false]
flag_values.each do |val|
shared_examples 'signup validation' do
before do
stub_feature_flags(restyle_login_page: val)
visit new_user_registration_path
end
@ -42,6 +40,18 @@ RSpec.shared_examples 'Signup name validation' do |field, max_length, label|
end
end
end
include_examples 'signup validation'
# Inline `shared_example 'signup validation'` again after feature flag
# `restyle_login_page` was removed.
context 'with feature flag restyle_login_page disabled' do
before do
stub_feature_flags(restyle_login_page: false)
end
include_examples 'signup validation'
end
end
RSpec.describe 'Signup', :js, feature_category: :user_profile do
@ -49,25 +59,32 @@ RSpec.describe 'Signup', :js, feature_category: :user_profile do
let(:new_user) { build_stubbed(:user) }
def fill_in_signup_form
fill_in 'new_user_username', with: new_user.username
fill_in 'new_user_email', with: new_user.email
fill_in 'new_user_first_name', with: new_user.first_name
fill_in 'new_user_last_name', with: new_user.last_name
fill_in 'new_user_password', with: new_user.password
let(:terms_text) do
<<~TEXT.squish
By clicking Register or registering through a third party you accept the
Terms of Use and acknowledge the Privacy Policy and Cookie Policy
TEXT
end
def confirm_email
new_user_token = User.find_by_email(new_user.email).confirmation_token
shared_examples 'signup process' do
def fill_in_signup_form
fill_in 'new_user_username', with: new_user.username
fill_in 'new_user_email', with: new_user.email
fill_in 'new_user_first_name', with: new_user.first_name
fill_in 'new_user_last_name', with: new_user.last_name
fill_in 'new_user_password', with: new_user.password
visit user_confirmation_path(confirmation_token: new_user_token)
end
wait_for_all_requests
end
def confirm_email
new_user_token = User.find_by_email(new_user.email).confirmation_token
visit user_confirmation_path(confirmation_token: new_user_token)
end
flag_values = [true, false]
flag_values.each do |val|
before do
stub_feature_flags(arkose_labs_signup_challenge: false)
stub_feature_flags(restyle_login_page: val)
stub_application_setting(require_admin_approval_after_user_signup: false)
end
@ -162,7 +179,8 @@ RSpec.describe 'Signup', :js, feature_category: :user_profile do
expect(page).to have_content("Invalid input, please avoid emojis")
end
it 'shows a pending message if the username availability is being fetched' do
it 'shows a pending message if the username availability is being fetched',
quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/31484' do
fill_in 'new_user_username', with: 'new-user'
expect(find('.username > .validation-pending')).not_to have_css '.hide'
@ -263,7 +281,10 @@ RSpec.describe 'Signup', :js, feature_category: :user_profile do
expect { click_button 'Register' }.to change { User.count }.by(1)
expect(page).to have_current_path new_user_session_path, ignore_query: true
expect(page).to have_content("You have signed up successfully. However, we could not sign you in because your account is awaiting approval from your GitLab administrator")
expect(page).to have_content(<<~TEXT.squish)
You have signed up successfully. However, we could not sign you in
because your account is awaiting approval from your GitLab administrator
TEXT
end
end
end
@ -305,13 +326,26 @@ RSpec.describe 'Signup', :js, feature_category: :user_profile do
it 'renders text that the user confirms terms by signing in' do
visit new_user_registration_path
expect(page).to have_content(/By clicking Register, I agree that I have read and accepted the Terms of Use and Privacy Policy/)
expect(page).to have_content(terms_text)
fill_in_signup_form
click_button 'Register'
expect(page).to have_current_path users_sign_up_welcome_path, ignore_query: true
expect(page).to have_current_path(users_sign_up_welcome_path), ignore_query: true
visit new_project_path
select 'Software Developer', from: 'user_role'
click_button 'Get started!'
created_user = User.find_by_username(new_user.username)
expect(created_user.software_developer_role?).to be_truthy
expect(created_user.setup_for_company).to be_nil
expect(page).to have_current_path(new_project_path)
end
it_behaves_like 'Signup name validation', 'new_user_first_name', 127, 'First name'
it_behaves_like 'Signup name validation', 'new_user_last_name', 127, 'Last name'
end
context 'when reCAPTCHA and invisible captcha are enabled' do
@ -337,7 +371,8 @@ RSpec.describe 'Signup', :js, feature_category: :user_profile do
expect { click_button 'Register' }.not_to change { User.count }
expect(page).to have_content(_('There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.'))
expect(page).to have_content("Minimum length is #{Gitlab::CurrentSettings.minimum_password_length} characters")
expect(page).to have_content(
"Minimum length is #{Gitlab::CurrentSettings.minimum_password_length} characters")
end
end
@ -357,7 +392,6 @@ RSpec.describe 'Signup', :js, feature_category: :user_profile do
visit new_user_registration_path
fill_in_signup_form
wait_for_all_requests
click_button 'Register'
@ -393,34 +427,22 @@ RSpec.describe 'Signup', :js, feature_category: :user_profile do
end
end
context 'when terms are enforced' do
include_examples 'signup process'
# Inline `shared_example 'signup process'` again after feature flag
# `restyle_login_page` was removed.
context 'with feature flag restyle_login_page disabled' do
let(:terms_text) do
<<~TEXT.squish
By clicking Register, I agree that I have read and accepted the Terms of
Use and Privacy Policy
TEXT
end
before do
enforce_terms
stub_feature_flags(restyle_login_page: false)
end
it 'renders text that the user confirms terms by signing in' do
visit new_user_registration_path
expect(page).to have_content(/By clicking Register, I agree that I have read and accepted the Terms of Use and Privacy Policy/)
fill_in_signup_form
click_button 'Register'
visit new_project_path
expect(page).to have_current_path(users_sign_up_welcome_path)
select 'Software Developer', from: 'user_role'
click_button 'Get started!'
created_user = User.find_by_username(new_user.username)
expect(created_user.software_developer_role?).to be_truthy
expect(created_user.setup_for_company).to be_nil
expect(page).to have_current_path(new_project_path)
end
it_behaves_like 'Signup name validation', 'new_user_first_name', 127, 'First name'
it_behaves_like 'Signup name validation', 'new_user_last_name', 127, 'Last name'
include_examples 'signup process'
end
end

View File

@ -0,0 +1,6 @@
{
"type": "array",
"items": {
"$ref": "../pipeline.json"
}
}

View File

@ -0,0 +1,75 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Mutations::Users::SetNamespaceCommitEmail, feature_category: :user_profile do
include GraphqlHelpers
let(:current_user) { create(:user) }
let(:group) { create(:group) }
let(:email) { create(:email, user: current_user) }
let(:input) { {} }
let(:namespace_id) { group.to_global_id }
let(:email_id) { email.to_global_id }
shared_examples 'success' do
it 'creates namespace commit email with correct values' do
expect(resolve_mutation[:namespace_commit_email])
.to have_attributes({ namespace_id: namespace_id.model_id.to_i, email_id: email_id.model_id.to_i })
end
end
describe '#resolve' do
subject(:resolve_mutation) do
described_class.new(object: nil, context: { current_user: current_user }, field: nil).resolve(
namespace_id: namespace_id,
email_id: email_id
)
end
context 'when current_user does not have permission' do
it 'raises an error' do
expect { resolve_mutation }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
.with_message(Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR)
end
end
context 'when the user has permission' do
before do
group.add_reporter(current_user)
end
context 'when the email does not belong to the target user' do
let(:email_id) { create(:email).to_global_id }
it 'returns the validation error' do
expect(resolve_mutation[:errors]).to contain_exactly("Email must be provided.")
end
end
context 'when namespace is a group' do
it_behaves_like 'success'
end
context 'when namespace is a user' do
let(:namespace_id) { current_user.namespace.to_global_id }
it_behaves_like 'success'
end
context 'when namespace is a project' do
let_it_be(:project) { create(:project) }
let(:namespace_id) { project.project_namespace.to_global_id }
before do
project.add_reporter(current_user)
end
it_behaves_like 'success'
end
end
end
specify { expect(described_class).to require_graphql_authorizations(:read_namespace) }
end

View File

@ -2,8 +2,9 @@
require 'spec_helper'
RSpec.describe Resolvers::BlobsResolver do
RSpec.describe Resolvers::BlobsResolver, feature_category: :source_code_management do
include GraphqlHelpers
include RepoHelpers
describe '.resolver_complexity' do
it 'adds one per path being resolved' do
@ -59,15 +60,89 @@ RSpec.describe Resolvers::BlobsResolver do
end
end
context 'specifying a different ref' do
context 'when specifying a branch ref' do
let(:ref) { 'add-pdf-file' }
let(:args) { { paths: paths, ref: ref, ref_type: ref_type } }
let(:paths) { ['files/pdf/test.pdf', 'README.md'] }
it 'returns the specified blobs for that ref' do
is_expected.to contain_exactly(
have_attributes(path: 'files/pdf/test.pdf'),
have_attributes(path: 'README.md')
)
context 'and no ref_type is specified' do
let(:ref_type) { nil }
it 'returns the specified blobs for that ref' do
is_expected.to contain_exactly(
have_attributes(path: 'files/pdf/test.pdf'),
have_attributes(path: 'README.md')
)
end
context 'and a tag with the same name exists' do
let(:ref) { SecureRandom.uuid }
before do
project.repository.create_branch(ref)
create_file_in_repo(project, ref, ref, 'branch_file', 'Test file', commit_message: 'Add new content')
project.repository.add_tag(project.owner, sample_commit.id, ref)
end
it 'returns the specified blobs for the tag' do
is_expected.to contain_exactly(
have_attributes(path: 'README.md')
)
end
end
end
context 'and ref_type is for branches' do
let(:args) { { paths: paths, ref: ref, ref_type: 'heads' } }
it 'returns nothing' do
is_expected.to contain_exactly(
have_attributes(path: 'files/pdf/test.pdf'),
have_attributes(path: 'README.md')
)
end
end
context 'and ref_type is for tags' do
let(:args) { { paths: paths, ref: ref, ref_type: 'tags' } }
it 'returns nothing' do
is_expected.to be_empty
end
end
end
context 'when specifying a tag ref' do
let(:ref) { 'v1.0.0' }
let(:args) { { paths: paths, ref: ref, ref_type: ref_type } }
context 'and no ref_type is specified' do
let(:ref_type) { nil }
it 'returns the specified blobs for that ref' do
is_expected.to contain_exactly(
have_attributes(path: 'README.md')
)
end
end
context 'and ref_type is for tags' do
let(:ref_type) { 'tags' }
it 'returns the specified blobs for that ref' do
is_expected.to contain_exactly(
have_attributes(path: 'README.md')
)
end
end
context 'and ref_type is for branches' do
let(:ref_type) { 'heads' }
it 'returns nothing' do
is_expected.to be_empty
end
end
end

View File

@ -61,5 +61,29 @@ RSpec.describe Resolvers::LastCommitResolver do
expect(commit).to be_nil
end
end
context 'when the ref is ambiguous' do
let(:ambiguous_ref) { 'v1.0.0' }
before do
project.repository.create_branch(ambiguous_ref)
end
context 'when tree is for a tag' do
let(:tree) { repository.tree(ambiguous_ref, ref_type: 'tags') }
it 'resolves commit' do
expect(commit.id).to eq(repository.find_tag(ambiguous_ref).dereferenced_target.id)
end
end
context 'when tree is for a branch' do
let(:tree) { repository.tree(ambiguous_ref, ref_type: 'heads') }
it 'resolves commit' do
expect(commit.id).to eq(repository.find_branch(ambiguous_ref).target)
end
end
end
end
end

View File

@ -57,5 +57,64 @@ RSpec.describe ExtractsRef do
end
end
describe '#ref_type' do
let(:params) { ActionController::Parameters.new(ref_type: 'heads') }
it 'delegates to .ref_type' do
expect(described_class).to receive(:ref_type).with('heads')
ref_type
end
end
describe '.ref_type' do
subject { described_class.ref_type(ref_type) }
context 'when ref_type is nil' do
let(:ref_type) { nil }
it { is_expected.to eq(nil) }
end
context 'when ref_type is heads' do
let(:ref_type) { 'heads' }
it { is_expected.to eq('heads') }
end
context 'when ref_type is tags' do
let(:ref_type) { 'tags' }
it { is_expected.to eq('tags') }
end
context 'when ref_type is invalid' do
let(:ref_type) { 'invalid' }
it { is_expected.to eq(nil) }
end
end
describe '.qualify_ref' do
subject { described_class.qualify_ref(ref, ref_type) }
context 'when ref_type is nil' do
let(:ref_type) { nil }
it { is_expected.to eq(ref) }
end
context 'when ref_type valid' do
let(:ref_type) { 'heads' }
it { is_expected.to eq("refs/#{ref_type}/#{ref}") }
end
context 'when ref_type is invalid' do
let(:ref_type) { 'invalid' }
it { is_expected.to eq(ref) }
end
end
it_behaves_like 'extracts refs'
end

View File

@ -14,6 +14,10 @@ RSpec.describe Gitlab::Pagination::CursorBasedKeyset do
expect(subject.available_for_type?(Ci::Build.all)).to be_truthy
end
it 'returns true for Packages::BuildInfo' do
expect(subject.available_for_type?(Packages::BuildInfo.all)).to be_truthy
end
it 'return false for other types of relations' do
expect(subject.available_for_type?(User.all)).to be_falsey
end
@ -56,6 +60,7 @@ RSpec.describe Gitlab::Pagination::CursorBasedKeyset do
it 'return false for other types of relations' do
expect(subject.available?(cursor_based_request_context, User.all)).to be_falsey
expect(subject.available?(cursor_based_request_context, Ci::Build.all)).to be_falsey
expect(subject.available?(cursor_based_request_context, Packages::BuildInfo.all)).to be_falsey
end
end
@ -70,6 +75,10 @@ RSpec.describe Gitlab::Pagination::CursorBasedKeyset do
it 'returns true for AuditEvent' do
expect(subject.available?(cursor_based_request_context, AuditEvent.all)).to be_truthy
end
it 'returns true for Packages::BuildInfo' do
expect(subject.available?(cursor_based_request_context, Packages::BuildInfo.all)).to be_truthy
end
end
context 'with other order-by columns' do

View File

@ -9,8 +9,10 @@ RSpec.describe Gitlab::ProjectAuthorizations, feature_category: :system_access d
end
end
let(:service) { described_class.new(user) }
subject(:authorizations) do
described_class.new(user).calculate
service.calculate
end
# Inline this shared example while cleaning up feature flag linear_project_authorization
@ -421,7 +423,34 @@ RSpec.describe Gitlab::ProjectAuthorizations, feature_category: :system_access d
end
end
context 'when feature_flag linear_project_authorization_is disabled' do
context 'it compares values for correctness' do
let_it_be(:user) { create(:user) }
context 'when values returned by the queries are the same' do
it 'logs a message indicating that the values are the same' do
expect(Gitlab::AppJsonLogger).to receive(:info).with(event: 'linear_authorized_projects_check',
user_id: user.id,
matching_results: true)
service.calculate
end
end
context 'when values returned by queries are diffrent' do
before do
create(:project_authorization)
allow(service).to receive(:calculate_with_linear_query).and_return(ProjectAuthorization.all)
end
it 'logs a message indicating that the values are different' do
expect(Gitlab::AppJsonLogger).to receive(:warn).with(event: 'linear_authorized_projects_check',
user_id: user.id,
matching_results: false)
service.calculate
end
end
end
context 'when feature_flag linear_project_authorization is disabled' do
before do
stub_feature_flags(linear_project_authorization: false)
end
@ -429,5 +458,22 @@ RSpec.describe Gitlab::ProjectAuthorizations, feature_category: :system_access d
it_behaves_like 'project authorizations'
end
context 'when feature_flag compare_project_authorization_linear_cte is disabled' do
before do
stub_feature_flags(compare_project_authorization_linear_cte: false)
end
it_behaves_like 'project authorizations'
end
context 'when feature_flag linear_project_authorization and compare_project_authorization_linear_cte are disabled' do
before do
stub_feature_flags(linear_project_authorization: false)
stub_feature_flags(compare_project_authorization_linear_cte: false)
end
it_behaves_like 'project authorizations'
end
it_behaves_like 'project authorizations'
end

View File

@ -253,10 +253,12 @@ RSpec.describe UserPolicy do
context 'when admin mode is enabled', :enable_admin_mode do
it { is_expected.to be_allowed(:read_user_email_address) }
it { is_expected.to be_allowed(:admin_user_email_address) }
end
context 'when admin mode is disabled' do
it { is_expected.not_to be_allowed(:read_user_email_address) }
it { is_expected.not_to be_allowed(:admin_user_email_address) }
end
end
@ -265,10 +267,12 @@ RSpec.describe UserPolicy do
subject { described_class.new(current_user, current_user) }
it { is_expected.to be_allowed(:read_user_email_address) }
it { is_expected.to be_allowed(:admin_user_email_address) }
end
context "requesting a different user's" do
it { is_expected.not_to be_allowed(:read_user_email_address) }
it { is_expected.not_to be_allowed(:admin_user_email_address) }
end
end
end

View File

@ -31,6 +31,32 @@ RSpec.describe BlobPresenter do
it { expect(presenter.replace_path).to eq("/#{project.full_path}/-/update/#{blob.commit_id}/#{blob.path}") }
end
context 'when blob has ref_type' do
before do
blob.ref_type = 'heads'
end
describe '#web_url' do
it { expect(presenter.web_url).to eq("http://localhost/#{project.full_path}/-/blob/#{blob.commit_id}/#{blob.path}?ref_type=heads") }
end
describe '#web_path' do
it { expect(presenter.web_path).to eq("/#{project.full_path}/-/blob/#{blob.commit_id}/#{blob.path}?ref_type=heads") }
end
describe '#edit_blob_path' do
it { expect(presenter.edit_blob_path).to eq("/#{project.full_path}/-/edit/#{blob.commit_id}/#{blob.path}?ref_type=heads") }
end
describe '#raw_path' do
it { expect(presenter.raw_path).to eq("/#{project.full_path}/-/raw/#{blob.commit_id}/#{blob.path}?ref_type=heads") }
end
describe '#replace_path' do
it { expect(presenter.replace_path).to eq("/#{project.full_path}/-/update/#{blob.commit_id}/#{blob.path}?ref_type=heads") }
end
end
describe '#can_current_user_push_to_branch' do
let(:branch_exists) { true }

View File

@ -17,4 +17,20 @@ RSpec.describe TreeEntryPresenter do
describe '#web_path' do
it { expect(presenter.web_path).to eq("/#{project.full_path}/-/tree/#{tree.commit_id}/#{tree.path}") }
end
context 'when blob has ref_type' do
before do
tree.ref_type = 'heads'
end
describe '.web_url' do
it { expect(presenter.web_url).to eq("http://localhost/#{project.full_path}/-/tree/#{tree.commit_id}/#{tree.path}?ref_type=heads") }
end
describe '#web_path' do
it {
expect(presenter.web_path).to eq("/#{project.full_path}/-/tree/#{tree.commit_id}/#{tree.path}?ref_type=heads")
}
end
end
end

View File

@ -0,0 +1,106 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Setting namespace commit email', feature_category: :user_profile do
include GraphqlHelpers
let(:current_user) { create(:user) }
let(:group) { create(:group, :public) }
let(:email) { create(:email, :confirmed, user: current_user) }
let(:input) { {} }
let(:namespace_id) { group.to_global_id }
let(:email_id) { email.to_global_id }
let(:resource_or_permission_error) do
"The resource that you are attempting to access does not exist or you don't have permission to perform this action"
end
let(:mutation) do
variables = {
namespace_id: namespace_id,
email_id: email_id
}
graphql_mutation(:user_set_namespace_commit_email, variables.merge(input),
<<-QL.strip_heredoc
namespaceCommitEmail {
email {
id
}
}
errors
QL
)
end
def mutation_response
graphql_mutation_response(:user_set_namespace_commit_email)
end
shared_examples 'success' do
it 'creates a namespace commit email' do
post_graphql_mutation(mutation, current_user: current_user)
expect(mutation_response.dig('namespaceCommitEmail', 'email', 'id')).to eq(email.to_global_id.to_s)
expect(graphql_errors).to be_nil
end
end
before do
group.add_reporter(current_user)
end
context 'when current_user is nil' do
it 'returns the top level error' do
post_graphql_mutation(mutation, current_user: nil)
expect(graphql_errors.first).to match a_hash_including(
'message' => resource_or_permission_error)
end
end
context 'when the user cannot access the namespace' do
let(:namespace_id) { create(:group).to_global_id }
it 'returns the top level error' do
post_graphql_mutation(mutation, current_user: current_user)
expect(graphql_errors).not_to be_empty
expect(graphql_errors.first).to match a_hash_including(
'message' => resource_or_permission_error)
end
end
context 'when the service returns an error' do
let(:email_id) { create(:email).to_global_id }
it 'returns the error' do
post_graphql_mutation(mutation, current_user: current_user)
expect(mutation_response['errors']).to contain_exactly("Email must be provided.")
expect(mutation_response['namespaceCommitEmail']).to be_nil
end
end
context 'when namespace is a group' do
it_behaves_like 'success'
end
context 'when namespace is a user' do
let(:namespace_id) { current_user.namespace.to_global_id }
it_behaves_like 'success'
end
context 'when namespace is a project' do
let_it_be(:project) { create(:project) }
let(:namespace_id) { project.project_namespace.to_global_id }
before do
project.add_reporter(current_user)
end
it_behaves_like 'success'
end
end

View File

@ -3,9 +3,11 @@
require 'spec_helper'
RSpec.describe API::ProjectPackages, feature_category: :package_registry do
let_it_be(:project) { create(:project, :public) }
using RSpec::Parameterized::TableSyntax
let(:user) { create(:user) }
let_it_be_with_reload(:project) { create(:project, :public) }
let_it_be(:user) { create(:user) }
let!(:package1) { create(:npm_package, :last_downloaded_at, project: project, version: '3.1.0', name: "@#{project.root_namespace.path}/foo1") }
let(:package_url) { "/projects/#{project.id}/packages/#{package1.id}" }
let!(:package2) { create(:nuget_package, project: project, version: '2.0.4') }
@ -101,7 +103,7 @@ RSpec.describe API::ProjectPackages, feature_category: :package_registry do
end
context 'project is private' do
let(:project) { create(:project, :private) }
let_it_be(:project) { create(:project, :private) }
context 'for unauthenticated user' do
it_behaves_like 'rejects packages access', :project, :no_type, :not_found
@ -235,7 +237,7 @@ RSpec.describe API::ProjectPackages, feature_category: :package_registry do
expect do
get api(package_url, user)
end.not_to exceed_query_limit(control)
end.not_to exceed_query_limit(control).with_threshold(4)
end
end
@ -286,7 +288,7 @@ RSpec.describe API::ProjectPackages, feature_category: :package_registry do
end
context 'project is private' do
let(:project) { create(:project, :private) }
let_it_be(:project) { create(:project, :private) }
it 'returns 404 for non authenticated user' do
get api(package_url)
@ -362,6 +364,235 @@ RSpec.describe API::ProjectPackages, feature_category: :package_registry do
end
end
describe 'GET /projects/:id/packages/:package_id/pipelines' do
let(:package_pipelines_url) { "/projects/#{project.id}/packages/#{package1.id}/pipelines" }
let(:tokens) do
{
personal_access_token: personal_access_token.token,
job_token: job.token
}
end
let_it_be(:personal_access_token) { create(:personal_access_token) }
let_it_be(:user) { personal_access_token.user }
let_it_be(:job) { create(:ci_build, :running, user: user, project: project) }
let(:headers) { {} }
subject { get api(package_pipelines_url) }
shared_examples 'returns package pipelines' do |expected_status|
it 'returns the first page of package pipelines' do
subject
expect(response).to have_gitlab_http_status(expected_status)
expect(response).to match_response_schema('public_api/v4/packages/pipelines')
expect(json_response.length).to eq(3)
expect(json_response.pluck('id')).to eq(pipelines.reverse.map(&:id))
end
end
context 'without the need for a license' do
context 'when the package does not exist' do
let(:package_pipelines_url) { "/projects/#{project.id}/packages/0/pipelines" }
it_behaves_like 'returning response status', :not_found
end
context 'when there are no pipelines for the package' do
let(:package_pipelines_url) { "/projects/#{project.id}/packages/#{package2.id}/pipelines" }
it 'returns an empty response' do
subject
expect(response).to have_gitlab_http_status(:success)
expect(response).to match_response_schema('public_api/v4/packages/pipelines')
expect(json_response.length).to eq(0)
end
end
context 'with valid package and pipelines' do
let!(:pipelines) do
create_list(:ci_pipeline, 3, user: user, project: project).each do |pipeline|
create(:package_build_info, package: package1, pipeline: pipeline)
end
end
where(:visibility, :user_role, :member, :token_type, :valid_token, :shared_examples_name, :expected_status) do
:public | :developer | true | :personal_access_token | true | 'returns package pipelines' | :success
:public | :guest | true | :personal_access_token | true | 'returns package pipelines' | :success
:public | :developer | true | :personal_access_token | false | 'returning response status' | :unauthorized
:public | :guest | true | :personal_access_token | false | 'returning response status' | :unauthorized
:public | :developer | false | :personal_access_token | true | 'returns package pipelines' | :success
:public | :guest | false | :personal_access_token | true | 'returns package pipelines' | :success
:public | :developer | false | :personal_access_token | false | 'returning response status' | :unauthorized
:public | :guest | false | :personal_access_token | false | 'returning response status' | :unauthorized
:public | :anonymous | false | nil | true | 'returns package pipelines' | :success
:private | :developer | true | :personal_access_token | true | 'returns package pipelines' | :success
:private | :guest | true | :personal_access_token | true | 'returning response status' | :forbidden
:private | :developer | true | :personal_access_token | false | 'returning response status' | :unauthorized
:private | :guest | true | :personal_access_token | false | 'returning response status' | :unauthorized
:private | :developer | false | :personal_access_token | true | 'returning response status' | :not_found
:private | :guest | false | :personal_access_token | true | 'returning response status' | :not_found
:private | :developer | false | :personal_access_token | false | 'returning response status' | :unauthorized
:private | :guest | false | :personal_access_token | false | 'returning response status' | :unauthorized
:private | :anonymous | false | nil | true | 'returning response status' | :not_found
:public | :developer | true | :job_token | true | 'returns package pipelines' | :success
:public | :guest | true | :job_token | true | 'returns package pipelines' | :success
:public | :developer | true | :job_token | false | 'returning response status' | :unauthorized
:public | :guest | true | :job_token | false | 'returning response status' | :unauthorized
:public | :developer | false | :job_token | true | 'returns package pipelines' | :success
:public | :guest | false | :job_token | true | 'returns package pipelines' | :success
:public | :developer | false | :job_token | false | 'returning response status' | :unauthorized
:public | :guest | false | :job_token | false | 'returning response status' | :unauthorized
:private | :developer | true | :job_token | true | 'returns package pipelines' | :success
# TODO uncomment the spec below when https://gitlab.com/gitlab-org/gitlab/-/issues/370998 is resolved
# :private | :guest | true | :job_token | true | 'returning response status' | :forbidden
:private | :developer | true | :job_token | false | 'returning response status' | :unauthorized
:private | :guest | true | :job_token | false | 'returning response status' | :unauthorized
:private | :developer | false | :job_token | true | 'returning response status' | :not_found
:private | :guest | false | :job_token | true | 'returning response status' | :not_found
:private | :developer | false | :job_token | false | 'returning response status' | :unauthorized
:private | :guest | false | :job_token | false | 'returning response status' | :unauthorized
end
with_them do
subject { get api(package_pipelines_url), headers: headers }
let(:invalid_token) { 'invalid-token123' }
let(:token) { valid_token ? tokens[token_type] : invalid_token }
let(:headers) do
case token_type
when :personal_access_token
{ Gitlab::Auth::AuthFinders::PRIVATE_TOKEN_HEADER => token }
when :job_token
{ Gitlab::Auth::AuthFinders::JOB_TOKEN_HEADER => token }
when nil
{}
end
end
before do
project.update!(visibility: visibility.to_s)
project.send("add_#{user_role}", user) if member && user_role != :anonymous
end
it_behaves_like params[:shared_examples_name], params[:expected_status]
end
end
context 'pagination' do
shared_context 'setup pipeline records' do
let!(:pipelines) do
create_list(:package_build_info, 21, :with_pipeline, package: package1)
end
end
shared_examples 'returns the default number of pipelines' do
it do
subject
expect(json_response.size).to eq(20)
end
end
shared_examples 'returns an error about the invalid per_page value' do
it do
subject
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['error']).to match(/per_page does not have a valid value/)
end
end
context 'without pagination params' do
include_context 'setup pipeline records'
it_behaves_like 'returns the default number of pipelines'
end
context 'with valid per_page value' do
let(:per_page) { 11 }
subject { get api(package_pipelines_url, user), params: { per_page: per_page } }
include_context 'setup pipeline records'
it 'returns the correct number of pipelines' do
subject
expect(json_response.size).to eq(per_page)
end
end
context 'with invalid pagination params' do
subject { get api(package_pipelines_url, user), params: { per_page: per_page } }
context 'with non-positive per_page' do
let(:per_page) { -2 }
it_behaves_like 'returns an error about the invalid per_page value'
end
context 'with a too high value for per_page' do
let(:per_page) { 21 }
it_behaves_like 'returns an error about the invalid per_page value'
end
end
context 'with valid pagination params' do
let_it_be(:package1) { create(:npm_package, :last_downloaded_at, project: project) }
let_it_be(:build_info1) { create(:package_build_info, :with_pipeline, package: package1) }
let_it_be(:build_info2) { create(:package_build_info, :with_pipeline, package: package1) }
let_it_be(:build_info3) { create(:package_build_info, :with_pipeline, package: package1) }
let(:pipeline1) { build_info1.pipeline }
let(:pipeline2) { build_info2.pipeline }
let(:pipeline3) { build_info3.pipeline }
let(:per_page) { 2 }
context 'with no cursor supplied' do
subject { get api(package_pipelines_url, user), params: { per_page: per_page } }
it 'returns first 2 pipelines' do
subject
expect(json_response.pluck('id')).to contain_exactly(pipeline3.id, pipeline2.id)
end
end
context 'with a cursor parameter' do
let(:cursor) { Base64.urlsafe_encode64(Gitlab::Json.dump(cursor_attributes)) }
subject { get api(package_pipelines_url, user), params: { per_page: per_page, cursor: cursor } }
before do
subject
end
context 'with a cursor for the next page' do
let(:cursor_attributes) { { "id" => build_info2.id, "_kd" => "n" } }
it 'returns the next page of records' do
expect(json_response.pluck('id')).to contain_exactly(pipeline1.id)
end
end
context 'with a cursor for the previous page' do
let(:cursor_attributes) { { "id" => build_info1.id, "_kd" => "p" } }
it 'returns the previous page of records' do
expect(json_response.pluck('id')).to contain_exactly(pipeline3.id, pipeline2.id)
end
end
end
end
end
end
end
describe 'DELETE /projects/:id/packages/:package_id' do
context 'without the need for a license' do
context 'project is public' do
@ -379,7 +610,7 @@ RSpec.describe API::ProjectPackages, feature_category: :package_registry do
end
context 'project is private' do
let(:project) { create(:project, :private) }
let_it_be(:project) { create(:project, :private) }
before do
expect(::Packages::Maven::Metadata::SyncWorker).not_to receive(:perform_async)

View File

@ -0,0 +1,195 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Users::SetNamespaceCommitEmailService, feature_category: :user_profile do
include AfterNextHelpers
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:email) { create(:email, user: user) }
let_it_be(:existing_achievement) { create(:achievement, namespace: group) }
let(:namespace) { group }
let(:current_user) { user }
let(:target_user) { user }
let(:email_id) { email.id }
let(:params) { { user: target_user } }
let(:service) { described_class.new(current_user, namespace, email_id, params) }
before_all do
group.add_reporter(user)
end
shared_examples 'success' do
it 'creates namespace commit email' do
result = service.execute
expect(result.payload[:namespace_commit_email]).to be_a(Users::NamespaceCommitEmail)
expect(result.payload[:namespace_commit_email]).to be_persisted
end
end
describe '#execute' do
context 'when current_user is not provided' do
let(:current_user) { nil }
it 'returns error message' do
expect(service.execute.message)
.to eq("User doesn't exist or you don't have permission to change namespace commit emails.")
end
end
context 'when current_user does not have permission to change namespace commit emails' do
let(:target_user) { create(:user) }
it 'returns error message' do
expect(service.execute.message)
.to eq("User doesn't exist or you don't have permission to change namespace commit emails.")
end
end
context 'when target_user does not have permission to access the namespace' do
let(:namespace) { create(:group) }
it 'returns error message' do
expect(service.execute.message).to eq("Namespace doesn't exist or you don't have permission.")
end
end
context 'when namespace is not provided' do
let(:namespace) { nil }
it 'returns error message' do
expect(service.execute.message).to eq('Namespace must be provided.')
end
end
context 'when target user is not current user' do
context 'when current user is an admin' do
let(:current_user) { create(:user, :admin) }
context 'when admin mode is enabled', :enable_admin_mode do
it 'creates namespace commit email' do
result = service.execute
expect(result.payload[:namespace_commit_email]).to be_a(Users::NamespaceCommitEmail)
expect(result.payload[:namespace_commit_email]).to be_persisted
end
end
context 'when admin mode is not enabled' do
it 'returns error message' do
expect(service.execute.message)
.to eq("User doesn't exist or you don't have permission to change namespace commit emails.")
end
end
end
context 'when current user is not an admin' do
let(:current_user) { create(:user) }
it 'returns error message' do
expect(service.execute.message)
.to eq("User doesn't exist or you don't have permission to change namespace commit emails.")
end
end
end
context 'when namespace commit email does not exist' do
context 'when email_id is not provided' do
let(:email_id) { nil }
it 'returns error message' do
expect(service.execute.message).to eq('Email must be provided.')
end
end
context 'when model save fails' do
before do
allow_next(::Users::NamespaceCommitEmail).to receive(:save).and_return(false)
end
it 'returns error message' do
expect(service.execute.message).to eq('Failed to save namespace commit email.')
end
end
context 'when namepsace is a group' do
it_behaves_like 'success'
end
context 'when namespace is a user' do
let(:namespace) { current_user.namespace }
it_behaves_like 'success'
end
context 'when namespace is a project' do
let_it_be(:project) { create(:project) }
let(:namespace) { project.project_namespace }
before do
project.add_reporter(current_user)
end
it_behaves_like 'success'
end
end
context 'when namespace commit email already exists' do
let!(:existing_namespace_commit_email) do
create(:namespace_commit_email,
user: target_user,
namespace: namespace,
email: create(:email, user: target_user))
end
context 'when email_id is not provided' do
let(:email_id) { nil }
it 'destroys the namespace commit email' do
result = service.execute
expect(result.message).to be_nil
expect(result.payload[:namespace_commit_email]).to be_nil
end
end
context 'and email_id is provided' do
let(:email_id) { create(:email, user: current_user).id }
it 'updates namespace commit email' do
result = service.execute
existing_namespace_commit_email.reload
expect(result.payload[:namespace_commit_email]).to eq(existing_namespace_commit_email)
expect(existing_namespace_commit_email.email_id).to eq(email_id)
end
end
context 'when model save fails' do
before do
allow_any_instance_of(::Users::NamespaceCommitEmail).to receive(:save).and_return(false) # rubocop:disable RSpec/AnyInstanceOf
end
it 'returns generic error message' do
expect(service.execute.message).to eq('Failed to save namespace commit email.')
end
context 'with model errors' do
before do
allow_any_instance_of(::Users::NamespaceCommitEmail).to receive_message_chain(:errors, :empty?).and_return(false) # rubocop:disable RSpec/AnyInstanceOf
allow_any_instance_of(::Users::NamespaceCommitEmail).to receive_message_chain(:errors, :full_messages, :to_sentence).and_return('Model error') # rubocop:disable RSpec/AnyInstanceOf
end
it 'returns the model error message' do
expect(service.execute.message).to eq('Model error')
end
end
end
end
end
end

View File

@ -80,10 +80,13 @@ RSpec.shared_examples 'work items comments' do |type|
it 'shows work item note actions' do
set_comment
click_button "Comment"
send_keys([modifier_key, :enter])
wait_for_requests
page.within(".main-notes-list") do
expect(page).to have_content comment
end
page.within('.timeline-entry.note.note-wrapper.note-comment:last-child') do
expect(page).to have_selector('[data-testid="work-item-note-actions"]')