diff options
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"]') |