Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.rubocop_todo/layout/line_length.yml1
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_create.js6
-rw-r--r--app/assets/javascripts/super_sidebar/components/menu_section.vue2
-rw-r--r--app/assets/stylesheets/framework/files.scss1
-rw-r--r--app/assets/stylesheets/page_bundles/web_ide_loader.scss38
-rw-r--r--app/controllers/projects/settings/branch_rules_controller.rb4
-rw-r--r--app/controllers/projects/settings/repository_controller.rb1
-rw-r--r--app/graphql/mutations/users/set_namespace_commit_email.rb44
-rw-r--r--app/graphql/resolvers/blobs_resolver.rb14
-rw-r--r--app/graphql/resolvers/last_commit_resolver.rb3
-rw-r--r--app/graphql/resolvers/paginated_tree_resolver.rb12
-rw-r--r--app/graphql/resolvers/tree_resolver.rb8
-rw-r--r--app/graphql/types/mutation_type.rb1
-rw-r--r--app/graphql/types/ref_type_enum.rb11
-rw-r--r--app/models/analytics/cycle_analytics/aggregation.rb4
-rw-r--r--app/models/blob.rb1
-rw-r--r--app/models/repository.rb5
-rw-r--r--app/models/tree.rb16
-rw-r--r--app/models/user.rb6
-rw-r--r--app/policies/user_policy.rb1
-rw-r--r--app/presenters/blob_presenter.rb29
-rw-r--r--app/presenters/tree_entry_presenter.rb17
-rw-r--r--app/services/users/set_namespace_commit_email_service.rb87
-rw-r--r--app/views/devise/shared/_signup_box.html.haml3
-rw-r--r--app/views/devise/shared/_signup_omniauth_provider_list.haml4
-rw-r--r--app/views/devise/shared/_signup_omniauth_providers.haml2
-rw-r--r--app/views/ide/_show.html.haml3
-rw-r--r--app/views/projects/blame/_page.html.haml2
-rw-r--r--app/views/projects/branches/index.html.haml6
-rw-r--r--app/views/projects/settings/repository/show.html.haml3
-rw-r--r--app/views/shared/_ide_root.html.haml9
-rw-r--r--config/application.rb1
-rw-r--r--config/feature_flags/development/branch_rules.yml8
-rw-r--r--config/feature_flags/development/comment_on_files.yml2
-rw-r--r--config/feature_flags/development/compare_project_authorization_linear_cte.yml8
-rw-r--r--config/metrics/counts_28d/20230613085814_i_quickactions_unlink_monthly.yml24
-rw-r--r--config/metrics/counts_7d/20230606094621_i_quickactions_unlink_weekly.yml2
-rw-r--r--db/migrate/20230531054422_add_index_on_packages_id_id_to_package_build_infos.rb15
-rw-r--r--db/schema_migrations/202305310544221
-rw-r--r--db/structure.sql2
-rw-r--r--doc/administration/audit_event_streaming.md36
-rw-r--r--doc/administration/instance_limits.md2
-rw-r--r--doc/api/graphql/reference/index.md62
-rw-r--r--doc/api/packages.md68
-rw-r--r--doc/api/rest/index.md1
-rw-r--r--doc/ci/introduction/index.md3
-rw-r--r--doc/user/infrastructure/clusters/connect/index.md11
-rw-r--r--doc/user/infrastructure/clusters/connect/new_civo_cluster.md3
-rw-r--r--doc/user/infrastructure/clusters/connect/new_eks_cluster.md3
-rw-r--r--doc/user/infrastructure/clusters/connect/new_gke_cluster.md3
-rw-r--r--doc/user/infrastructure/iac/index.md6
-rw-r--r--doc/user/infrastructure/iac/terraform_state.md8
-rw-r--r--lib/api/project_packages.rb50
-rw-r--r--lib/extracts_ref.rb30
-rw-r--r--lib/gitlab/git/tree.rb2
-rw-r--r--lib/gitlab/pagination/cursor_based_keyset.rb3
-rw-r--r--lib/gitlab/project_authorizations.rb136
-rw-r--r--locale/gitlab.pot17
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/add_new_branch_rule_spec.rb10
-rw-r--r--spec/features/projects/settings/branch_rules_settings_spec.rb9
-rw-r--r--spec/features/projects/settings/repository_settings_spec.rb9
-rw-r--r--spec/features/users/signup_spec.rb116
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/packages/pipelines.json6
-rw-r--r--spec/graphql/mutations/users/set_namespace_commit_email_spec.rb75
-rw-r--r--spec/graphql/resolvers/blobs_resolver_spec.rb89
-rw-r--r--spec/graphql/resolvers/last_commit_resolver_spec.rb24
-rw-r--r--spec/lib/extracts_ref_spec.rb59
-rw-r--r--spec/lib/gitlab/pagination/cursor_based_keyset_spec.rb9
-rw-r--r--spec/lib/gitlab/project_authorizations_spec.rb50
-rw-r--r--spec/policies/user_policy_spec.rb4
-rw-r--r--spec/presenters/blob_presenter_spec.rb26
-rw-r--r--spec/presenters/tree_entry_presenter_spec.rb16
-rw-r--r--spec/requests/api/graphql/users/set_namespace_commit_email_spec.rb106
-rw-r--r--spec/requests/api/project_packages_spec.rb243
-rw-r--r--spec/services/users/set_namespace_commit_email_service_spec.rb195
-rw-r--r--spec/support/shared_examples/features/work_items_shared_examples.rb7
77 files changed, 1652 insertions, 254 deletions
diff --git a/.rubocop_todo/layout/line_length.yml b/.rubocop_todo/layout/line_length.yml
index edaf6d4b669..5dcfc9a16a0 100644
--- a/.rubocop_todo/layout/line_length.yml
+++ b/.rubocop_todo/layout/line_length.yml
@@ -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'
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 8855008082f..5583c38cdf4 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-d3bb9a8fe1d9ff265edf3920c348b2d5993ca0a8
+fa4f0dbedd76758c0b0422da6e54441a5ac80d18
diff --git a/app/assets/javascripts/protected_branches/protected_branch_create.js b/app/assets/javascripts/protected_branches/protected_branch_create.js
index cdbe39fd5e0..a11201627a4 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_create.js
+++ b/app/assets/javascripts/protected_branches/protected_branch_create.js
@@ -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() {
diff --git a/app/assets/javascripts/super_sidebar/components/menu_section.vue b/app/assets/javascripts/super_sidebar/components/menu_section.vue
index 2fbdc5dc7e4..b5a8241a286 100644
--- a/app/assets/javascripts/super_sidebar/components/menu_section.vue
+++ b/app/assets/javascripts/super_sidebar/components/menu_section.vue
@@ -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"
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index 287a20a56c5..2e88b45d646 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -514,7 +514,6 @@ span.idiff {
}
.blame-commit {
- padding: 5px 10px;
width: 400px;
flex: none;
background: $gray-light;
diff --git a/app/assets/stylesheets/page_bundles/web_ide_loader.scss b/app/assets/stylesheets/page_bundles/web_ide_loader.scss
new file mode 100644
index 00000000000..f922cadc235
--- /dev/null
+++ b/app/assets/stylesheets/page_bundles/web_ide_loader.scss
@@ -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);
+ }
+}
diff --git a/app/controllers/projects/settings/branch_rules_controller.rb b/app/controllers/projects/settings/branch_rules_controller.rb
index 0a415b60124..68ef7f49cc3 100644
--- a/app/controllers/projects/settings/branch_rules_controller.rb
+++ b/app/controllers/projects/settings/branch_rules_controller.rb
@@ -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
diff --git a/app/controllers/projects/settings/repository_controller.rb b/app/controllers/projects/settings/repository_controller.rb
index a6f4e2fcd73..38b23b24c9a 100644
--- a/app/controllers/projects/settings/repository_controller.rb
+++ b/app/controllers/projects/settings/repository_controller.rb
@@ -12,7 +12,6 @@ module Projects
urgency :low, [:show, :create_deploy_token]
def show
- push_frontend_feature_flag(:branch_rules, @project)
render_show
end
diff --git a/app/graphql/mutations/users/set_namespace_commit_email.rb b/app/graphql/mutations/users/set_namespace_commit_email.rb
new file mode 100644
index 00000000000..72ef0635bb3
--- /dev/null
+++ b/app/graphql/mutations/users/set_namespace_commit_email.rb
@@ -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
diff --git a/app/graphql/resolvers/blobs_resolver.rb b/app/graphql/resolvers/blobs_resolver.rb
index 0b8180dbce7..546eeb76ff5 100644
--- a/app/graphql/resolvers/blobs_resolver.rb
+++ b/app/graphql/resolvers/blobs_resolver.rb
@@ -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
diff --git a/app/graphql/resolvers/last_commit_resolver.rb b/app/graphql/resolvers/last_commit_resolver.rb
index 00c43bdfee6..acf7826ab13 100644
--- a/app/graphql/resolvers/last_commit_resolver.rb
+++ b/app/graphql/resolvers/last_commit_resolver.rb
@@ -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
diff --git a/app/graphql/resolvers/paginated_tree_resolver.rb b/app/graphql/resolvers/paginated_tree_resolver.rb
index 8fd80b1a9b9..de48fbafb04 100644
--- a/app/graphql/resolvers/paginated_tree_resolver.rb
+++ b/app/graphql/resolvers/paginated_tree_resolver.rb
@@ -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
diff --git a/app/graphql/resolvers/tree_resolver.rb b/app/graphql/resolvers/tree_resolver.rb
index 553f9aa6cd9..6b88f120d1b 100644
--- a/app/graphql/resolvers/tree_resolver.rb
+++ b/app/graphql/resolvers/tree_resolver.rb
@@ -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
diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb
index 62ab1086ed3..16c46d172f3 100644
--- a/app/graphql/types/mutation_type.rb
+++ b/app/graphql/types/mutation_type.rb
@@ -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
diff --git a/app/graphql/types/ref_type_enum.rb b/app/graphql/types/ref_type_enum.rb
new file mode 100644
index 00000000000..f56d4cd512a
--- /dev/null
+++ b/app/graphql/types/ref_type_enum.rb
@@ -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
diff --git a/app/models/analytics/cycle_analytics/aggregation.rb b/app/models/analytics/cycle_analytics/aggregation.rb
index fa165ae9600..0f8e184933e 100644
--- a/app/models/analytics/cycle_analytics/aggregation.rb
+++ b/app/models/analytics/cycle_analytics/aggregation.rb
@@ -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)
diff --git a/app/models/blob.rb b/app/models/blob.rb
index e6496e21175..7c88833b19d 100644
--- a/app/models/blob.rb
+++ b/app/models/blob.rb
@@ -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
diff --git a/app/models/repository.rb b/app/models/repository.rb
index acb795f174d..b21df6baf0e 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -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)
diff --git a/app/models/tree.rb b/app/models/tree.rb
index c6adf5c263c..8622eb793c1 100644
--- a/app/models/tree.rb
+++ b/app/models/tree.rb
@@ -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
diff --git a/app/models/user.rb b/app/models/user.rb
index 8afe4a9c704..3ac58b75879 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -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
diff --git a/app/policies/user_policy.rb b/app/policies/user_policy.rb
index 1078eda38e7..2fd198b8cf4 100644
--- a/app/policies/user_policy.rb
+++ b/app/policies/user_policy.rb
@@ -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
diff --git a/app/presenters/blob_presenter.rb b/app/presenters/blob_presenter.rb
index f25436c54be..cd473152b41 100644
--- a/app/presenters/blob_presenter.rb
+++ b/app/presenters/blob_presenter.rb
@@ -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
diff --git a/app/presenters/tree_entry_presenter.rb b/app/presenters/tree_entry_presenter.rb
index 0b313d81360..3f4a9f13c36 100644
--- a/app/presenters/tree_entry_presenter.rb
+++ b/app/presenters/tree_entry_presenter.rb
@@ -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
diff --git a/app/services/users/set_namespace_commit_email_service.rb b/app/services/users/set_namespace_commit_email_service.rb
new file mode 100644
index 00000000000..30ee597120d
--- /dev/null
+++ b/app/services/users/set_namespace_commit_email_service.rb
@@ -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
diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml
index 178f320ea9f..684ade87720 100644
--- a/app/views/devise/shared/_signup_box.html.haml
+++ b/app/views/devise/shared/_signup_box.html.haml
@@ -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
diff --git a/app/views/devise/shared/_signup_omniauth_provider_list.haml b/app/views/devise/shared/_signup_omniauth_provider_list.haml
index cd0d43614de..e8c82e456ae 100644
--- a/app/views/devise/shared/_signup_omniauth_provider_list.haml
+++ b/app/views/devise/shared/_signup_omniauth_provider_list.haml
@@ -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
diff --git a/app/views/devise/shared/_signup_omniauth_providers.haml b/app/views/devise/shared/_signup_omniauth_providers.haml
index d2a47974e01..5ec3c7a4150 100644
--- a/app/views/devise/shared/_signup_omniauth_providers.haml
+++ b/app/views/devise/shared/_signup_omniauth_providers.haml
@@ -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"
diff --git a/app/views/ide/_show.html.haml b/app/views/ide/_show.html.haml
index eb6d5668807..4b16c0199ba 100644
--- a/app/views/ide/_show.html.haml
+++ b/app/views/ide/_show.html.haml
@@ -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') }
diff --git a/app/views/projects/blame/_page.html.haml b/app/views/projects/blame/_page.html.haml
index 92fb99c30a6..10f27c3f620 100644
--- a/app/views/projects/blame/_page.html.haml
+++ b/app/views/projects/blame/_page.html.haml
@@ -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
diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml
index 64adf97b1b5..8992753c676 100644
--- a/app/views/projects/branches/index.html.haml
+++ b/app/views/projects/branches/index.html.haml
@@ -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 } }
diff --git a/app/views/projects/settings/repository/show.html.haml b/app/views/projects/settings/repository/show.html.haml
index 12404180362..36ace52df13 100644
--- a/app/views/projects/settings/repository/show.html.haml
+++ b/app/views/projects/settings/repository/show.html.haml
@@ -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"
diff --git a/app/views/shared/_ide_root.html.haml b/app/views/shared/_ide_root.html.haml
index 848ff1e5728..db3e76e188c 100644
--- a/app/views/shared/_ide_root.html.haml
+++ b/app/views/shared/_ide_root.html.haml
@@ -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
diff --git a/config/application.rb b/config/application.rb
index 5aca9b999a7..9764e13082c 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -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"
diff --git a/config/feature_flags/development/branch_rules.yml b/config/feature_flags/development/branch_rules.yml
deleted file mode 100644
index 2c1eefb8681..00000000000
--- a/config/feature_flags/development/branch_rules.yml
+++ /dev/null
@@ -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
diff --git a/config/feature_flags/development/comment_on_files.yml b/config/feature_flags/development/comment_on_files.yml
index fc79ee249a4..3482486eefa 100644
--- a/config/feature_flags/development/comment_on_files.yml
+++ b/config/feature_flags/development/comment_on_files.yml
@@ -5,4 +5,4 @@ rollout_issue_url:
milestone: '16.1'
type: development
group: group::code review
-default_enabled: false
+default_enabled: true
diff --git a/config/feature_flags/development/compare_project_authorization_linear_cte.yml b/config/feature_flags/development/compare_project_authorization_linear_cte.yml
new file mode 100644
index 00000000000..7032e6f64f4
--- /dev/null
+++ b/config/feature_flags/development/compare_project_authorization_linear_cte.yml
@@ -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
diff --git a/config/metrics/counts_28d/20230613085814_i_quickactions_unlink_monthly.yml b/config/metrics/counts_28d/20230613085814_i_quickactions_unlink_monthly.yml
new file mode 100644
index 00000000000..ff405926ba8
--- /dev/null
+++ b/config/metrics/counts_28d/20230613085814_i_quickactions_unlink_monthly.yml
@@ -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
diff --git a/config/metrics/counts_7d/20230606094621_i_quickactions_unlink_weekly.yml b/config/metrics/counts_7d/20230606094621_i_quickactions_unlink_weekly.yml
index 5e64ea64cd0..1fae5e95512 100644
--- a/config/metrics/counts_7d/20230606094621_i_quickactions_unlink_weekly.yml
+++ b/config/metrics/counts_7d/20230606094621_i_quickactions_unlink_weekly.yml
@@ -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
diff --git a/db/migrate/20230531054422_add_index_on_packages_id_id_to_package_build_infos.rb b/db/migrate/20230531054422_add_index_on_packages_id_id_to_package_build_infos.rb
new file mode 100644
index 00000000000..15eac952a88
--- /dev/null
+++ b/db/migrate/20230531054422_add_index_on_packages_id_id_to_package_build_infos.rb
@@ -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
diff --git a/db/schema_migrations/20230531054422 b/db/schema_migrations/20230531054422
new file mode 100644
index 00000000000..f038ca979e0
--- /dev/null
+++ b/db/schema_migrations/20230531054422
@@ -0,0 +1 @@
+5fadce4dbc2280ca1d68f8271f4d44ea3c492769b65ebb1d8f2ae94cfb6d6c75 \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 50c0796c2ef..13f9521fd6f 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -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);
diff --git a/doc/administration/audit_event_streaming.md b/doc/administration/audit_event_streaming.md
index 351febad33c..f4d0d7d780f 100644
--- a/doc/administration/audit_event_streaming.md
+++ b/doc/administration/audit_event_streaming.md
@@ -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.
diff --git a/doc/administration/instance_limits.md b/doc/administration/instance_limits.md
index 03c7c51251b..df364a3f737 100644
--- a/doc/administration/instance_limits.md
+++ b/doc/administration/instance_limits.md
@@ -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):
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index cb451eaab82..f15247d986f 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -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.
diff --git a/doc/api/packages.md b/doc/api/packages.md
index 86eaf3028cf..efb27a29e0f 100644
--- a/doc/api/packages.md
+++ b/doc/api/packages.md
@@ -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.
diff --git a/doc/api/rest/index.md b/doc/api/rest/index.md
index 3a2e49cdcf9..947142e3a50 100644
--- a/doc/api/rest/index.md
+++ b/doc/api/rest/index.md
@@ -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. |
diff --git a/doc/ci/introduction/index.md b/doc/ci/introduction/index.md
index bf07af5e761..895aa8551d7 100644
--- a/doc/ci/introduction/index.md
+++ b/doc/ci/introduction/index.md
@@ -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.
diff --git a/doc/user/infrastructure/clusters/connect/index.md b/doc/user/infrastructure/clusters/connect/index.md
index b571a65b50a..01911f2b889 100644
--- a/doc/user/infrastructure/clusters/connect/index.md
+++ b/doc/user/infrastructure/clusters/connect/index.md
@@ -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
diff --git a/doc/user/infrastructure/clusters/connect/new_civo_cluster.md b/doc/user/infrastructure/clusters/connect/new_civo_cluster.md
index 45445c0a0f5..7c8065b6143 100644
--- a/doc/user/infrastructure/clusters/connect/new_civo_cluster.md
+++ b/doc/user/infrastructure/clusters/connect/new_civo_cluster.md
@@ -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**.
diff --git a/doc/user/infrastructure/clusters/connect/new_eks_cluster.md b/doc/user/infrastructure/clusters/connect/new_eks_cluster.md
index 4516ea538a9..19bcce581e9 100644
--- a/doc/user/infrastructure/clusters/connect/new_eks_cluster.md
+++ b/doc/user/infrastructure/clusters/connect/new_eks_cluster.md
@@ -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**.
diff --git a/doc/user/infrastructure/clusters/connect/new_gke_cluster.md b/doc/user/infrastructure/clusters/connect/new_gke_cluster.md
index c8d2fb674b2..25a0a7149e0 100644
--- a/doc/user/infrastructure/clusters/connect/new_gke_cluster.md
+++ b/doc/user/infrastructure/clusters/connect/new_gke_cluster.md
@@ -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**.
diff --git a/doc/user/infrastructure/iac/index.md b/doc/user/infrastructure/iac/index.md
index d4fc345f494..12ad207e4f8 100644
--- a/doc/user/infrastructure/iac/index.md
+++ b/doc/user/infrastructure/iac/index.md
@@ -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:
diff --git a/doc/user/infrastructure/iac/terraform_state.md b/doc/user/infrastructure/iac/terraform_state.md
index 1b0065fd165..455f2ce19e8 100644
--- a/doc/user/infrastructure/iac/terraform_state.md
+++ b/doc/user/infrastructure/iac/terraform_state.md
@@ -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.
diff --git a/lib/api/project_packages.rb b/lib/api/project_packages.rb
index 158ba7465f4..43bd15931ef 100644
--- a/lib/api/project_packages.rb
+++ b/lib/api/project_packages.rb
@@ -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
diff --git a/lib/extracts_ref.rb b/lib/extracts_ref.rb
index 5f73b474956..49ec564eb8d 100644
--- a/lib/extracts_ref.rb
+++ b/lib/extracts_ref.rb
@@ -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)
diff --git a/lib/gitlab/git/tree.rb b/lib/gitlab/git/tree.rb
index e437f99dab3..df3d8165ef2 100644
--- a/lib/gitlab/git/tree.rb
+++ b/lib/gitlab/git/tree.rb
@@ -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
diff --git a/lib/gitlab/pagination/cursor_based_keyset.rb b/lib/gitlab/pagination/cursor_based_keyset.rb
index a21d0228082..ee8259cc671 100644
--- a/lib/gitlab/pagination/cursor_based_keyset.rb
+++ b/lib/gitlab/pagination/cursor_based_keyset.rb
@@ -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
diff --git a/lib/gitlab/project_authorizations.rb b/lib/gitlab/project_authorizations.rb
index eedea2f0997..0fcb8321dae 100644
--- a/lib/gitlab/project_authorizations.rb
+++ b/lib/gitlab/project_authorizations.rb
@@ -12,62 +12,56 @@ module Gitlab
end
def calculate
- cte = if Feature.enabled?(:linear_project_authorization, user)
- linear_cte
- else
- recursive_cte
- 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,
+ 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
- # 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
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 1427ffea5eb..f21cfa184d5 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -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 ""
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/add_new_branch_rule_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/add_new_branch_rule_spec.rb
index 3d68de30d57..82074919ad4 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/add_new_branch_rule_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/add_new_branch_rule_spec.rb
@@ -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!
diff --git a/spec/features/projects/settings/branch_rules_settings_spec.rb b/spec/features/projects/settings/branch_rules_settings_spec.rb
index a6ecfa2d231..5ef80521401 100644
--- a/spec/features/projects/settings/branch_rules_settings_spec.rb
+++ b/spec/features/projects/settings/branch_rules_settings_spec.rb
@@ -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
diff --git a/spec/features/projects/settings/repository_settings_spec.rb b/spec/features/projects/settings/repository_settings_spec.rb
index a801f633882..2439e624dd6 100644
--- a/spec/features/projects/settings/repository_settings_spec.rb
+++ b/spec/features/projects/settings/repository_settings_spec.rb
@@ -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
diff --git a/spec/features/users/signup_spec.rb b/spec/features/users/signup_spec.rb
index ccecd8bfcad..850dd0bbc5d 100644
--- a/spec/features/users/signup_spec.rb
+++ b/spec/features/users/signup_spec.rb
@@ -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
- before do
- enforce_terms
- 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)
+ include_examples 'signup process'
- select 'Software Developer', from: 'user_role'
- click_button 'Get started!'
-
- created_user = User.find_by_username(new_user.username)
+ # 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
- 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)
+ before do
+ stub_feature_flags(restyle_login_page: false)
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
diff --git a/spec/fixtures/api/schemas/public_api/v4/packages/pipelines.json b/spec/fixtures/api/schemas/public_api/v4/packages/pipelines.json
new file mode 100644
index 00000000000..3432503212a
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v4/packages/pipelines.json
@@ -0,0 +1,6 @@
+{
+ "type": "array",
+ "items": {
+ "$ref": "../pipeline.json"
+ }
+}
diff --git a/spec/graphql/mutations/users/set_namespace_commit_email_spec.rb b/spec/graphql/mutations/users/set_namespace_commit_email_spec.rb
new file mode 100644
index 00000000000..6d8e15ac791
--- /dev/null
+++ b/spec/graphql/mutations/users/set_namespace_commit_email_spec.rb
@@ -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
diff --git a/spec/graphql/resolvers/blobs_resolver_spec.rb b/spec/graphql/resolvers/blobs_resolver_spec.rb
index 26eb6dc0abe..0d725f00d43 100644
--- a/spec/graphql/resolvers/blobs_resolver_spec.rb
+++ b/spec/graphql/resolvers/blobs_resolver_spec.rb
@@ -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
diff --git a/spec/graphql/resolvers/last_commit_resolver_spec.rb b/spec/graphql/resolvers/last_commit_resolver_spec.rb
index 5ac6ad59864..82bbdd4487c 100644
--- a/spec/graphql/resolvers/last_commit_resolver_spec.rb
+++ b/spec/graphql/resolvers/last_commit_resolver_spec.rb
@@ -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
diff --git a/spec/lib/extracts_ref_spec.rb b/spec/lib/extracts_ref_spec.rb
index 93a09bf5a0a..ac403ad642a 100644
--- a/spec/lib/extracts_ref_spec.rb
+++ b/spec/lib/extracts_ref_spec.rb
@@ -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
diff --git a/spec/lib/gitlab/pagination/cursor_based_keyset_spec.rb b/spec/lib/gitlab/pagination/cursor_based_keyset_spec.rb
index dc62fcb4478..7cee65c13f7 100644
--- a/spec/lib/gitlab/pagination/cursor_based_keyset_spec.rb
+++ b/spec/lib/gitlab/pagination/cursor_based_keyset_spec.rb
@@ -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
diff --git a/spec/lib/gitlab/project_authorizations_spec.rb b/spec/lib/gitlab/project_authorizations_spec.rb
index b076bb65fb5..f3dcdfe2a9d 100644
--- a/spec/lib/gitlab/project_authorizations_spec.rb
+++ b/spec/lib/gitlab/project_authorizations_spec.rb
@@ -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,9 +423,53 @@ 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
+
+ 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'
diff --git a/spec/policies/user_policy_spec.rb b/spec/policies/user_policy_spec.rb
index 94b7e295167..9a2caeb7435 100644
--- a/spec/policies/user_policy_spec.rb
+++ b/spec/policies/user_policy_spec.rb
@@ -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
diff --git a/spec/presenters/blob_presenter_spec.rb b/spec/presenters/blob_presenter_spec.rb
index f10150b819a..e776716bd2d 100644
--- a/spec/presenters/blob_presenter_spec.rb
+++ b/spec/presenters/blob_presenter_spec.rb
@@ -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 }
diff --git a/spec/presenters/tree_entry_presenter_spec.rb b/spec/presenters/tree_entry_presenter_spec.rb
index de84f36c5e6..0abf372b704 100644
--- a/spec/presenters/tree_entry_presenter_spec.rb
+++ b/spec/presenters/tree_entry_presenter_spec.rb
@@ -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
diff --git a/spec/requests/api/graphql/users/set_namespace_commit_email_spec.rb b/spec/requests/api/graphql/users/set_namespace_commit_email_spec.rb
new file mode 100644
index 00000000000..1db6f83ce4f
--- /dev/null
+++ b/spec/requests/api/graphql/users/set_namespace_commit_email_spec.rb
@@ -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
diff --git a/spec/requests/api/project_packages_spec.rb b/spec/requests/api/project_packages_spec.rb
index c003ae9cd48..b84b7e9c52d 100644
--- a/spec/requests/api/project_packages_spec.rb
+++ b/spec/requests/api/project_packages_spec.rb
@@ -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)
diff --git a/spec/services/users/set_namespace_commit_email_service_spec.rb b/spec/services/users/set_namespace_commit_email_service_spec.rb
new file mode 100644
index 00000000000..4f64d454ecb
--- /dev/null
+++ b/spec/services/users/set_namespace_commit_email_service_spec.rb
@@ -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
diff --git a/spec/support/shared_examples/features/work_items_shared_examples.rb b/spec/support/shared_examples/features/work_items_shared_examples.rb
index ab57f4e2c55..128bd28410c 100644
--- a/spec/support/shared_examples/features/work_items_shared_examples.rb
+++ b/spec/support/shared_examples/features/work_items_shared_examples.rb
@@ -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"]')