diff options
93 files changed, 1421 insertions, 144 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 4925f9ec268..79f99211bb1 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -131,7 +131,7 @@ workflow: variables: PG_VERSION: "12" - DEFAULT_CI_IMAGE: "${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-build-images/debian-${DEBIAN_VERSION}-ruby-${RUBY_VERSION}.patched-golang-${GO_VERSION}-rust-${RUST_VERSION}-node-16.14-postgresql-${PG_VERSION}:rubygems-3.2-git-2.36-lfs-2.9-chrome-${CHROME_VERSION}-yarn-1.22-graphicsmagick-1.3.36" + DEFAULT_CI_IMAGE: "${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-build-images/debian-${DEBIAN_VERSION}-ruby-${RUBY_VERSION}.patched-golang-${GO_VERSION}-rust-${RUST_VERSION}-node-16.14-postgresql-${PG_VERSION}:rubygems-${RUBYGEMS_VERSION}-git-2.36-lfs-2.9-chrome-${CHROME_VERSION}-yarn-1.22-graphicsmagick-1.3.36" # We set $GITLAB_DEPENDENCY_PROXY to another variable (since it's set at the group level and has higher precedence than .gitlab-ci.yml) # so that we can override $GITLAB_DEPENDENCY_PROXY_ADDRESS in workflow rules. GITLAB_DEPENDENCY_PROXY_ADDRESS: "${GITLAB_DEPENDENCY_PROXY}" @@ -151,6 +151,7 @@ variables: CHROME_VERSION: "109" DOCKER_VERSION: "23.0.1" RUBY_VERSION: "2.7" + RUBYGEMS_VERSION: "3.4" GO_VERSION: "1.18" RUST_VERSION: "1.65" diff --git a/.gitlab/ci/frontend.gitlab-ci.yml b/.gitlab/ci/frontend.gitlab-ci.yml index 6bf014a6324..59758d2aec7 100644 --- a/.gitlab/ci/frontend.gitlab-ci.yml +++ b/.gitlab/ci/frontend.gitlab-ci.yml @@ -3,7 +3,7 @@ - .default-retry - .default-before_script - .assets-compile-cache - image: ${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-build-images/debian-${DEBIAN_VERSION}-ruby-${RUBY_VERSION}-node-16.14:rubygems-3.2-git-2.33-lfs-2.9-yarn-1.22-graphicsmagick-1.3.36 + image: ${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-build-images/debian-${DEBIAN_VERSION}-ruby-${RUBY_VERSION}-node-16.14:rubygems-${RUBYGEMS_VERSION}-git-2.33-lfs-2.9-yarn-1.22-graphicsmagick-1.3.36 variables: SETUP_DB: "false" WEBPACK_VENDOR_DLL: "true" diff --git a/.rubocop.yml b/.rubocop.yml index 8f9a02bbadd..2de8a88633b 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -440,6 +440,12 @@ BackgroundMigration/FeatureCategory: Include: - 'lib/gitlab/background_migration/*.rb' +BackgroundMigration/MissingDictionaryFile: + Enabled: true + EnforcedSince: 20230307160251 + Include: + - 'db/post_migrate/*.rb' + # See https://gitlab.com/gitlab-org/gitlab/-/issues/373194 Gitlab/RSpec/AvoidSetup: Enabled: true diff --git a/.rubocop_todo/background_migration/missing_dictionary_file.yml b/.rubocop_todo/background_migration/missing_dictionary_file.yml new file mode 100644 index 00000000000..c065a1ac3aa --- /dev/null +++ b/.rubocop_todo/background_migration/missing_dictionary_file.yml @@ -0,0 +1,4 @@ +--- +# Grace period will be removed in https://gitlab.com/gitlab-org/gitlab/-/issues/395354 +BackgroundMigration/MissingDictionaryFile: + Details: grace period diff --git a/.rubocop_todo/rspec/missing_feature_category.yml b/.rubocop_todo/rspec/missing_feature_category.yml index faf8f9f7d4c..8a679bb9e96 100644 --- a/.rubocop_todo/rspec/missing_feature_category.yml +++ b/.rubocop_todo/rspec/missing_feature_category.yml @@ -1135,7 +1135,6 @@ RSpec/MissingFeatureCategory: - 'ee/spec/lib/omni_auth/strategies/kerberos_spec.rb' - 'ee/spec/lib/peek/views/elasticsearch_spec.rb' - 'ee/spec/lib/quality/seeders/vulnerabilities_spec.rb' - - 'ee/spec/lib/sidebars/groups/menus/administration_menu_spec.rb' - 'ee/spec/lib/sidebars/groups/menus/analytics_menu_spec.rb' - 'ee/spec/lib/sidebars/groups/menus/security_compliance_menu_spec.rb' - 'ee/spec/lib/slack/api_spec.rb' diff --git a/.rubocop_todo/style/if_unless_modifier.yml b/.rubocop_todo/style/if_unless_modifier.yml index cda00940294..19016646725 100644 --- a/.rubocop_todo/style/if_unless_modifier.yml +++ b/.rubocop_todo/style/if_unless_modifier.yml @@ -642,7 +642,6 @@ Style/IfUnlessModifier: - 'ee/lib/gitlab/usage/metrics/instrumentations/count_users_creating_ci_builds_metric.rb' - 'ee/lib/gitlab/usage/metrics/instrumentations/license_metric.rb' - 'ee/lib/omni_auth/strategies/group_saml.rb' - - 'ee/lib/sidebars/groups/menus/administration_menu.rb' - 'ee/lib/sidebars/groups/menus/analytics_menu.rb' - 'ee/lib/sidebars/groups/menus/security_compliance_menu.rb' - 'ee/lib/tasks/geo.rake' diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 8bf2fa93b54..5cdfcb682a1 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -60b05a6155531cdfd5e51b2ed3c6e888f105048b +d1ce57834e67383564e6d7faf7827be3a7dd8afe diff --git a/app/assets/javascripts/super_sidebar/components/context_switcher_toggle.vue b/app/assets/javascripts/super_sidebar/components/context_switcher_toggle.vue index 7a2cc47d3a7..e0b6870872c 100644 --- a/app/assets/javascripts/super_sidebar/components/context_switcher_toggle.vue +++ b/app/assets/javascripts/super_sidebar/components/context_switcher_toggle.vue @@ -11,6 +11,9 @@ export default { CollapseToggle: GlCollapseToggleDirective, }, props: { + /* + * Contains metadata about the current view, e.g. `id`, `title` and `avatar` + */ context: { type: Object, required: true, @@ -24,6 +27,9 @@ export default { collapseIcon() { return this.expanded ? 'chevron-up' : 'chevron-down'; }, + avatarShape() { + return this.context.avatar_shape || 'rect'; + }, }, }; </script> @@ -43,7 +49,7 @@ export default { <gl-avatar v-else :size="24" - shape="rect" + :shape="avatarShape" :entity-name="context.title" :entity-id="context.id" :src="context.avatar" diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 279830c4569..67fc73729a3 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -143,11 +143,24 @@ .dropdown-menu-toggle.dropdown-menu-toggle { justify-content: flex-start; overflow: hidden; + padding-top: #{$gl-padding-8 - 1}; + padding-bottom: #{$gl-padding-8 - 1}; padding-right: 25px; position: relative; text-overflow: ellipsis; + line-height: $gl-line-height; width: 160px; + &:hover { + @include gl-inset-border-1-gray-400; + } + + &:hover, + &:focus { + background-color: $gray-50; + border-color: $gray-400; + } + .gl-spinner { position: absolute; top: 9px; @@ -157,7 +170,7 @@ .dropdown-menu-toggle-icon { position: absolute; right: $gl-padding-8; - color: $gray-darkest; + color: $gray-500; } } diff --git a/app/assets/stylesheets/startup/startup-dark.scss b/app/assets/stylesheets/startup/startup-dark.scss index cc7fe8243d2..d4270235b0d 100644 --- a/app/assets/stylesheets/startup/startup-dark.scss +++ b/app/assets/stylesheets/startup/startup-dark.scss @@ -661,9 +661,12 @@ html { .dropdown-menu-toggle.dropdown-menu-toggle { justify-content: flex-start; overflow: hidden; + padding-top: 7px; + padding-bottom: 7px; padding-right: 25px; position: relative; text-overflow: ellipsis; + line-height: 16px; width: 160px; } .dropdown-menu { diff --git a/app/assets/stylesheets/startup/startup-general.scss b/app/assets/stylesheets/startup/startup-general.scss index 9fa1f6ed402..005b356aae7 100644 --- a/app/assets/stylesheets/startup/startup-general.scss +++ b/app/assets/stylesheets/startup/startup-general.scss @@ -661,9 +661,12 @@ html { .dropdown-menu-toggle.dropdown-menu-toggle { justify-content: flex-start; overflow: hidden; + padding-top: 7px; + padding-bottom: 7px; padding-right: 25px; position: relative; text-overflow: ellipsis; + line-height: 16px; width: 160px; } .dropdown-menu { diff --git a/app/helpers/dropdowns_helper.rb b/app/helpers/dropdowns_helper.rb index 427cbe18fbf..475ba3dcba8 100644 --- a/app/helpers/dropdowns_helper.rb +++ b/app/helpers/dropdowns_helper.rb @@ -54,7 +54,7 @@ module DropdownsHelper default_label = data_attr[:default_label] content_tag(:button, disabled: options[:disabled], class: "dropdown-menu-toggle #{options[:toggle_class] if options.key?(:toggle_class)}", id: (options[:id] if options.key?(:id)), type: "button", data: data_attr) do output = content_tag(:span, toggle_text, class: "dropdown-toggle-text #{'is-default' if toggle_text == default_label}") - output << sprite_icon('chevron-down', css_class: "dropdown-menu-toggle-icon gl-top-3") + output << sprite_icon('chevron-down', css_class: "dropdown-menu-toggle-icon") output.html_safe end end diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb index 563a58d6eb3..7f0a928c62c 100644 --- a/app/helpers/nav_helper.rb +++ b/app/helpers/nav_helper.rb @@ -97,7 +97,7 @@ module NavHelper def super_sidebar_supported? return true if @nav.nil? - %w(your_work project group profile).include?(@nav) + %w(your_work project group profile user_profile).include?(@nav) end def get_header_links diff --git a/app/helpers/sidebars_helper.rb b/app/helpers/sidebars_helper.rb index 9ea30b055c8..756040986db 100644 --- a/app/helpers/sidebars_helper.rb +++ b/app/helpers/sidebars_helper.rb @@ -90,7 +90,9 @@ module SidebarsHelper } end - def super_sidebar_nav_panel(nav: nil, project: nil, user: nil, group: nil, current_ref: nil, ref_type: nil) + def super_sidebar_nav_panel( + nav: nil, project: nil, user: nil, group: nil, current_ref: nil, ref_type: nil, + viewed_user: nil) context_adds = { route_is_active: method(:active_nav_link?), is_super_sidebar: true } case nav when 'project' @@ -102,6 +104,9 @@ module SidebarsHelper when 'profile' context = Sidebars::Context.new(current_user: user, container: user, **context_adds) Sidebars::UserSettings::Panel.new(context) + when 'user_profile' + context = Sidebars::Context.new(current_user: user, container: viewed_user, **context_adds) + Sidebars::UserProfile::Panel.new(context) else context = your_work_sidebar_context(user, **context_adds) Sidebars::YourWork::Panel.new(context) diff --git a/app/models/container_registry/data_repair_detail.rb b/app/models/container_registry/data_repair_detail.rb new file mode 100644 index 00000000000..09e617e69f5 --- /dev/null +++ b/app/models/container_registry/data_repair_detail.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module ContainerRegistry + class DataRepairDetail < ApplicationRecord + self.table_name = 'container_registry_data_repair_details' + self.primary_key = :project_id + + belongs_to :project, optional: false + end +end diff --git a/app/models/packages/rpm/repository_file.rb b/app/models/packages/rpm/repository_file.rb index 614ec9b3e56..bbd435691d2 100644 --- a/app/models/packages/rpm/repository_file.rb +++ b/app/models/packages/rpm/repository_file.rb @@ -13,7 +13,7 @@ module Packages enum status: { default: 0, pending_destruction: 1, processing: 2, error: 3 } - belongs_to :project, inverse_of: :repository_files + belongs_to :project, inverse_of: :rpm_repository_files validates :project, presence: true validates :file, presence: true diff --git a/app/models/project.rb b/app/models/project.rb index f07090d6131..af640bd8296 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -243,14 +243,22 @@ class Project < ApplicationRecord has_many :fork_network_projects, through: :fork_network, source: :projects # Packages - has_many :packages, class_name: 'Packages::Package' - has_many :package_files, through: :packages, class_name: 'Packages::PackageFile' + has_many :packages, + class_name: 'Packages::Package' + has_many :package_files, + through: :packages, class_name: 'Packages::PackageFile' # repository_files must be destroyed by ruby code in order to properly remove carrierwave uploads - has_many :repository_files, inverse_of: :project, class_name: 'Packages::Rpm::RepositoryFile', - dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent + has_many :rpm_repository_files, + inverse_of: :project, + class_name: 'Packages::Rpm::RepositoryFile', + dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent # debian_distributions and associated component_files must be destroyed by ruby code in order to properly remove carrierwave uploads - has_many :debian_distributions, class_name: 'Packages::Debian::ProjectDistribution', dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent - has_one :packages_cleanup_policy, class_name: 'Packages::Cleanup::Policy', inverse_of: :project + has_many :debian_distributions, + class_name: 'Packages::Debian::ProjectDistribution', + dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent + has_one :packages_cleanup_policy, + class_name: 'Packages::Cleanup::Policy', + inverse_of: :project has_one :import_state, autosave: true, class_name: 'ProjectImportState', inverse_of: :project has_one :import_export_upload, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index e18a48d2d5f..8b6286c5a4a 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -261,6 +261,7 @@ class ProjectPolicy < BasePolicy enable :reporter_access enable :developer_access enable :maintainer_access + enable :add_catalog_resource enable :change_namespace enable :change_visibility_level diff --git a/app/services/ci/catalog/add_resource_service.rb b/app/services/ci/catalog/add_resource_service.rb new file mode 100644 index 00000000000..1f53513b7d1 --- /dev/null +++ b/app/services/ci/catalog/add_resource_service.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +module Ci + module Catalog + class AddResourceService + include Gitlab::Allowable + + attr_reader :project, :current_user + + def initialize(project, user) + @current_user = user + @project = project + end + + def execute + raise Gitlab::Access::AccessDeniedError unless can?(current_user, :add_catalog_resource, project) + + validation_response = Ci::Catalog::ValidateResourceService.new(project, project.default_branch).execute + + if validation_response.success? + create_catalog_resource + else + ServiceResponse.error(message: validation_response.message) + end + end + + private + + def create_catalog_resource + catalog_resource = Ci::Catalog::Resource.new(project: project) + + if catalog_resource.valid? + catalog_resource.save! + ServiceResponse.success(payload: catalog_resource) + else + ServiceResponse.error(message: catalog_resource.errors.full_messages.join(', ')) + end + end + end + end +end diff --git a/app/services/ci/catalog/validate_resource_service.rb b/app/services/ci/catalog/validate_resource_service.rb new file mode 100644 index 00000000000..f166c220869 --- /dev/null +++ b/app/services/ci/catalog/validate_resource_service.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +module Ci + module Catalog + class ValidateResourceService + attr_reader :project + + def initialize(project, ref) + @project = project + @ref = ref + @errors = [] + end + + def execute + check_project_readme + check_project_description + + if errors.empty? + ServiceResponse.success + else + ServiceResponse.error(message: errors.join(' , ')) + end + end + + private + + attr_reader :ref, :errors + + def check_project_description + return if project.description.present? + + errors << 'Project must have a description' + end + + def check_project_readme + return if project_has_readme? + + errors << 'Project must have a README' + end + + def project_has_readme? + project.repository.blob_data_at(ref, 'README.md') + end + end + end +end diff --git a/app/services/import/github/cancel_project_import_service.rb b/app/services/import/github/cancel_project_import_service.rb index 616ae54451f..62cd0c95eaf 100644 --- a/app/services/import/github/cancel_project_import_service.rb +++ b/app/services/import/github/cancel_project_import_service.rb @@ -9,7 +9,7 @@ module Import if project.import_in_progress? project.import_state.cancel - metrics.track_import_state + metrics.track_canceled_import success(project: project) else diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml index 9e59f9d700f..d182cb9f391 100644 --- a/app/views/dashboard/todos/index.html.haml +++ b/app/views/dashboard/todos/index.html.haml @@ -62,7 +62,7 @@ = sort_options_hash[@sort] - else = sort_title_recently_created - = sprite_icon('chevron-down', css_class: 'dropdown-menu-toggle-icon gl-top-3') + = sprite_icon('chevron-down', css_class: 'dropdown-menu-toggle-icon') %ul.dropdown-menu.dropdown-menu-sort.dropdown-menu-right %li = link_to todos_filter_path(sort: sort_value_label_priority) do diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml index d03e24d2457..ec80c2ba353 100644 --- a/app/views/layouts/_page.html.haml +++ b/app/views/layouts/_page.html.haml @@ -5,7 +5,7 @@ -# Render the parent group sidebar while creating a new subgroup/project, see GroupsController#new. - group = @parent_group || @group - - sidebar_panel = super_sidebar_nav_panel(nav: nav, user: current_user, group: group, project: @project, current_ref: current_ref, ref_type: @ref_type) + - sidebar_panel = super_sidebar_nav_panel(nav: nav, user: current_user, group: group, project: @project, current_ref: current_ref, ref_type: @ref_type, viewed_user: @user) - sidebar_data = super_sidebar_context(current_user, group: group, project: @project, panel: sidebar_panel).to_json %aside.js-super-sidebar.super-sidebar.super-sidebar-loading{ data: { root_path: root_path, sidebar: sidebar_data, toggle_new_nav_endpoint: profile_preferences_url } } diff --git a/app/views/layouts/nav/sidebar/_user_profile.html.haml b/app/views/layouts/nav/sidebar/_user_profile.html.haml new file mode 100644 index 00000000000..b24334f48c4 --- /dev/null +++ b/app/views/layouts/nav/sidebar/_user_profile.html.haml @@ -0,0 +1 @@ += render partial: 'shared/nav/sidebar', object: Sidebars::UserProfile::Panel.new(Sidebars::Context.new(current_user: current_user, container: @user)) diff --git a/app/views/search/_results_status.html.haml b/app/views/search/_results_status.html.haml index 2321abb31df..4ab68caaf22 100644 --- a/app/views/search/_results_status.html.haml +++ b/app/views/search/_results_status.html.haml @@ -5,7 +5,8 @@ .gl-p-5.gl-display-flex .gl-md-display-flex.gl-text-left.gl-align-items-center.gl-flex-grow-1.gl-white-space-nowrap.gl-max-w-full - unless @search_service_presenter.without_count? - = search_entries_info(@search_objects, @scope, @search_term) + .gl-text-truncate + = search_entries_info(@search_objects, @scope, @search_term) - unless @search_service_presenter.show_snippets? - if @project - link_to_project = link_to(@project.full_name, @project, class: 'ml-md-1 gl-text-truncate search-wrap-f-md-down') diff --git a/app/views/shared/issuable/_label_dropdown.html.haml b/app/views/shared/issuable/_label_dropdown.html.haml index af63839d7c1..3c4ee01d04f 100644 --- a/app/views/shared/issuable/_label_dropdown.html.haml +++ b/app/views/shared/issuable/_label_dropdown.html.haml @@ -26,7 +26,7 @@ - apply_is_default_styles = (selected.nil? || selected.empty?) && !no_default_styles %span.dropdown-toggle-text{ class: ("is-default" if apply_is_default_styles) } = multi_label_name(selected, label_name) - = sprite_icon('chevron-down', css_class: "dropdown-menu-toggle-icon gl-top-3") + = sprite_icon('chevron-down', css_class: "dropdown-menu-toggle-icon") .dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable.dropdown-extended-height = render partial: "shared/issuable/label_page_default", locals: { title: dropdown_title, show_footer: show_footer, show_create: show_create } - if show_create && project && can?(current_user, :admin_label, project) diff --git a/app/views/shared/issuable/form/_type_selector.html.haml b/app/views/shared/issuable/form/_type_selector.html.haml index a94ef70b2d5..6d4cd83d55b 100644 --- a/app/views/shared/issuable/form/_type_selector.html.haml +++ b/app/views/shared/issuable/form/_type_selector.html.haml @@ -10,7 +10,7 @@ %button.dropdown-menu-toggle{ type: 'button', 'data-toggle' => 'dropdown' } %span.dropdown-toggle-text.is-default = issuable.issue_type.capitalize || _("Select type") - = sprite_icon('chevron-down', css_class: "dropdown-menu-toggle-icon gl-top-3") + = sprite_icon('chevron-down', css_class: "dropdown-menu-toggle-icon") .dropdown-menu.dropdown-menu-selectable.dropdown-select .dropdown-title.gl-display-flex %span.gl-ml-auto diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index 7ba3aa8f12e..c27086ded6f 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -6,6 +6,9 @@ - page_itemtype 'http://schema.org/Person' - add_page_specific_style 'page_bundles/profile' - link_classes = "flex-grow-1 mx-1 " +- if show_super_sidebar? + - @left_sidebar = true + - nav "user_profile" = content_for :meta_tags do = auto_discovery_link_tag(:atom, user_url(@user, format: :atom), title: "#{@user.name} activity") @@ -120,7 +123,7 @@ = @user.bio - if !profile_tabs.empty? && !Feature.enabled?(:profile_tabs_vue, current_user) - .scrolling-tabs-container + .scrolling-tabs-container{ class: [('gl-display-none' if show_super_sidebar?)] } .fade-left= sprite_icon('chevron-lg-left', size: 12) .fade-right= sprite_icon('chevron-lg-right', size: 12) %ul.nav-links.user-profile-nav.scrolling-tabs.nav.nav-tabs diff --git a/config/feature_flags/development/disable_update_max_seats_worker.yml b/config/feature_flags/development/disable_update_max_seats_worker.yml new file mode 100644 index 00000000000..ae0fe482ee9 --- /dev/null +++ b/config/feature_flags/development/disable_update_max_seats_worker.yml @@ -0,0 +1,8 @@ +--- +name: disable_update_max_seats_worker +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/114127 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/382725 +milestone: '15.10' +type: development +group: group::utilization +default_enabled: false diff --git a/config/metrics/counts_28d/20230306134018_github_import_project_cancelled_monthly.yml b/config/metrics/counts_28d/20230306134018_github_import_project_cancelled_monthly.yml new file mode 100644 index 00000000000..f398fa3c16f --- /dev/null +++ b/config/metrics/counts_28d/20230306134018_github_import_project_cancelled_monthly.yml @@ -0,0 +1,26 @@ +--- +key_path: redis_hll_counters.importer.github_import_project_cancelled_monthly +description: The number of github projects that were cancelled monthly +product_section: dev +product_stage: manage +product_group: import +product_category: importers +value_type: number +status: active +milestone: "15.10" +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/113724 +time_frame: 28d +data_source: redis_hll +data_category: optional +instrumentation_class: RedisHLLMetric +options: + events: + - github_import_project_cancelled +performance_indicator_type: [] +distribution: +- ce +- ee +tier: +- free +- premium +- ultimate diff --git a/config/metrics/counts_28d/20230306134609_github_import_project_partially_completed_monthly.yml b/config/metrics/counts_28d/20230306134609_github_import_project_partially_completed_monthly.yml new file mode 100644 index 00000000000..10344cffe5f --- /dev/null +++ b/config/metrics/counts_28d/20230306134609_github_import_project_partially_completed_monthly.yml @@ -0,0 +1,26 @@ +--- +key_path: redis_hll_counters.importer.github_import_project_partially_completed_monthly +description: The number of github projects that were partially completed monthly +product_section: dev +product_stage: manage +product_group: import +product_category: importers +value_type: number +status: active +milestone: "15.10" +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/113724 +time_frame: 28d +data_source: redis_hll +data_category: optional +instrumentation_class: RedisHLLMetric +options: + events: + - github_import_project_partially_completed +performance_indicator_type: [] +distribution: +- ce +- ee +tier: +- free +- premium +- ultimate diff --git a/config/metrics/counts_7d/20230306133608_github_import_project_cancelled_weekly.yml b/config/metrics/counts_7d/20230306133608_github_import_project_cancelled_weekly.yml new file mode 100644 index 00000000000..2d76d88e2e7 --- /dev/null +++ b/config/metrics/counts_7d/20230306133608_github_import_project_cancelled_weekly.yml @@ -0,0 +1,26 @@ +--- +key_path: redis_hll_counters.importer.github_import_project_cancelled_weekly +description: The number of github projects that were cancelled weekly +product_section: dev +product_stage: manage +product_group: import +product_category: importers +value_type: number +status: active +milestone: "15.10" +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/113724 +time_frame: 7d +data_source: redis_hll +data_category: optional +instrumentation_class: RedisHLLMetric +options: + events: + - github_import_project_cancelled +performance_indicator_type: [] +distribution: +- ce +- ee +tier: +- free +- premium +- ultimate diff --git a/config/metrics/counts_7d/20230306134308_github_import_project_partially_completed_weekly.yml b/config/metrics/counts_7d/20230306134308_github_import_project_partially_completed_weekly.yml new file mode 100644 index 00000000000..9bc42eafd23 --- /dev/null +++ b/config/metrics/counts_7d/20230306134308_github_import_project_partially_completed_weekly.yml @@ -0,0 +1,26 @@ +--- +key_path: redis_hll_counters.importer.github_import_project_partially_completed_weekly +description: The number of github projects that were partially completed weekly +product_section: dev +product_stage: manage +product_group: import +product_category: importers +value_type: number +status: active +milestone: "15.10" +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/113724 +time_frame: 7d +data_source: redis_hll +data_category: optional +instrumentation_class: RedisHLLMetric +options: + events: + - github_import_project_partially_completed +performance_indicator_type: [] +distribution: +- ce +- ee +tier: +- free +- premium +- ultimate diff --git a/data/deprecations/15-9-rails-error-tracking.yml b/data/deprecations/15-9-rails-error-tracking.yml index 42ec004afa9..4d9fc371213 100644 --- a/data/deprecations/15-9-rails-error-tracking.yml +++ b/data/deprecations/15-9-rails-error-tracking.yml @@ -1,12 +1,12 @@ - title: "Error Tracking UI in GitLab Rails is deprecated" announcement_milestone: "15.9" - removal_milestone: "16.0" + removal_milestone: "16.6" breaking_change: true reporter: kbychu stage: monitor issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/389991 body: | - The [Error Tracking UI](https://docs.gitlab.com/ee/operations/error_tracking.html) is deprecated in 15.9 and will be removed in 16.0. In future versions, you should use the [GitLab Observability UI](https://gitlab.com/gitlab-org/opstrace/opstrace-ui/), which will gradually be made available on GitLab.com over the next few releases. + The [Error Tracking UI](https://docs.gitlab.com/ee/operations/error_tracking.html) is deprecated in 15.9 and will be removed in 16.6 (milestone might change) once GitLab Observability UI is made available. In future versions, you should use the [GitLab Observability UI](https://gitlab.com/gitlab-org/opstrace/opstrace-ui/), which will gradually be made available on GitLab.com over the next few releases. During the transition to the GitLab Observability UI, we will migrate the [GitLab Observability Backend](https://gitlab.com/gitlab-org/opstrace/opstrace) from a per-cluster deployment model to a per-tenant deployment model. Because [Integrated Error Tracking](https://docs.gitlab.com/ee/operations/error_tracking.html#integrated-error-tracking) is in Open Beta, we will not migrate any existing user data. For more details about the migration, see the direction pages for: diff --git a/db/docs/container_registry_data_repair_details.yml b/db/docs/container_registry_data_repair_details.yml new file mode 100644 index 00000000000..c258fff4832 --- /dev/null +++ b/db/docs/container_registry_data_repair_details.yml @@ -0,0 +1,10 @@ +--- +table_name: container_registry_data_repair_details +classes: +- ContainerRegistry::DataRepairDetail +feature_categories: +- container_registry +description: Contains details for the container registry data repair +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/113029 +milestone: '15.10' +gitlab_schema: gitlab_main diff --git a/db/migrate/20230308163018_create_container_registry_data_repair_details.rb b/db/migrate/20230308163018_create_container_registry_data_repair_details.rb new file mode 100644 index 00000000000..b682eb6ee7e --- /dev/null +++ b/db/migrate/20230308163018_create_container_registry_data_repair_details.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class CreateContainerRegistryDataRepairDetails < Gitlab::Database::Migration[2.1] + enable_lock_retries! + + def change + create_table :container_registry_data_repair_details, id: false do |t| + t.integer :missing_count, default: 0 + t.references :project, + primary_key: true, + default: nil, + index: false, + foreign_key: { to_table: :projects, on_delete: :cascade } + t.timestamps_with_timezone null: false + end + end +end diff --git a/db/schema_migrations/20230308163018 b/db/schema_migrations/20230308163018 new file mode 100644 index 00000000000..19815455ef3 --- /dev/null +++ b/db/schema_migrations/20230308163018 @@ -0,0 +1 @@ +f9132e8d1d39307fc4f9ef17c6e044bab636d17ae7a7e5207f26ab3e38441638
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 5fc3ebcb69c..8686191f19d 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -14523,6 +14523,13 @@ CREATE TABLE container_expiration_policies ( CONSTRAINT container_expiration_policies_name_regex_keep CHECK ((char_length(name_regex_keep) <= 255)) ); +CREATE TABLE container_registry_data_repair_details ( + missing_count integer DEFAULT 0, + project_id bigint NOT NULL, + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone NOT NULL +); + CREATE TABLE container_repositories ( id integer NOT NULL, project_id integer NOT NULL, @@ -26505,6 +26512,9 @@ ALTER TABLE ONLY compliance_management_frameworks ALTER TABLE ONLY container_expiration_policies ADD CONSTRAINT container_expiration_policies_pkey PRIMARY KEY (project_id); +ALTER TABLE ONLY container_registry_data_repair_details + ADD CONSTRAINT container_registry_data_repair_details_pkey PRIMARY KEY (project_id); + ALTER TABLE ONLY container_repositories ADD CONSTRAINT container_repositories_pkey PRIMARY KEY (id); @@ -36229,6 +36239,9 @@ ALTER TABLE ONLY packages_debian_project_component_files ALTER TABLE ONLY namespace_aggregation_schedules ADD CONSTRAINT fk_rails_b565c8d16c FOREIGN KEY (namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE; +ALTER TABLE ONLY container_registry_data_repair_details + ADD CONSTRAINT fk_rails_b70d8111d9 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE; + ALTER TABLE batched_background_migration_job_transition_logs ADD CONSTRAINT fk_rails_b7523a175b FOREIGN KEY (batched_background_migration_job_id) REFERENCES batched_background_migration_jobs(id) ON DELETE CASCADE; diff --git a/doc/update/deprecations.md b/doc/update/deprecations.md index c57892fd43d..4bef8e29df7 100644 --- a/doc/update/deprecations.md +++ b/doc/update/deprecations.md @@ -210,17 +210,17 @@ Users on self-managed instances should update their pipelines to ensure they do </div> -<div class="deprecation removal-160 breaking-change"> +<div class="deprecation removal-166 breaking-change"> ### Error Tracking UI in GitLab Rails is deprecated -Planned removal: GitLab <span class="removal-milestone">16.0</span> <span class="removal-date"></span> +Planned removal: GitLab <span class="removal-milestone">16.6</span> <span class="removal-date"></span> WARNING: This is a [breaking change](https://docs.gitlab.com/ee/development/deprecation_guidelines/). Review the details carefully before upgrading. -The [Error Tracking UI](https://docs.gitlab.com/ee/operations/error_tracking.html) is deprecated in 15.9 and will be removed in 16.0. In future versions, you should use the [GitLab Observability UI](https://gitlab.com/gitlab-org/opstrace/opstrace-ui/), which will gradually be made available on GitLab.com over the next few releases. +The [Error Tracking UI](https://docs.gitlab.com/ee/operations/error_tracking.html) is deprecated in 15.9 and will be removed in 16.6 (milestone might change) once GitLab Observability UI is made available. In future versions, you should use the [GitLab Observability UI](https://gitlab.com/gitlab-org/opstrace/opstrace-ui/), which will gradually be made available on GitLab.com over the next few releases. During the transition to the GitLab Observability UI, we will migrate the [GitLab Observability Backend](https://gitlab.com/gitlab-org/opstrace/opstrace) from a per-cluster deployment model to a per-tenant deployment model. Because [Integrated Error Tracking](https://docs.gitlab.com/ee/operations/error_tracking.html#integrated-error-tracking) is in Open Beta, we will not migrate any existing user data. For more details about the migration, see the direction pages for: diff --git a/doc/update/index.md b/doc/update/index.md index 2117ba07055..e2e46086e9d 100644 --- a/doc/update/index.md +++ b/doc/update/index.md @@ -269,7 +269,7 @@ and [Helm Chart deployments](https://docs.gitlab.com/charts/). They come with ap - **Upgrade to patch release 15.9.3 or later**. This provides fixes for two database migration bugs: - Patch releases 15.9.0, 15.9.1, 15.9.2 have [a bug that can cause data loss](#user-profile-data-loss-bug-in-159x) from the user profile fields. - The second [bug fix](https://gitlab.com/gitlab-org/gitlab/-/issues/394760) ensures it is possible to upgrade directly from 15.4.x. -- As part of the [CI Partitioning effort](../architecture/blueprints/ci_data_decay/pipeline_partitioning.md), a [new Foreign Key](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/107547) was added to `ci_builds_needs`. On GitLab instances with large CI tables, adding this constraint can take longer than usual. Make sure that this migration is finished before upgrading to 15.9. +- As part of the [CI Partitioning effort](../architecture/blueprints/ci_data_decay/pipeline_partitioning.md), a [new Foreign Key](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/107547) was added to `ci_builds_needs`. On GitLab instances with large CI tables, adding this constraint can take longer than usual. - Praefect's metadata verifier's [invalid metadata deletion behavior](../administration/gitaly/praefect.md#enable-deletions) is now enabled by default. The metadata verifier processes replica records in the Praefect database and verifies the replicas actually exist on the Gitaly nodes. If the replica doesn't exist, its @@ -1461,7 +1461,7 @@ If your organization uses these fields either wait for the bug fix to be release Organizations that are already running earlier patch levels of GitLab 15.6, 15.7, or 15.8 can proceed with steps 2 and 3. -If you have already upgraded to GitLab 15.9 following these instructions, your instance will not be affected by this bug, and you don't need to apply the 15.9.x patch when it is released. +If you have already upgraded to GitLab 15.9 following these instructions, your instance will not be affected by this bug, and you don't need to apply the 15.9.x patch when it is released. See [issue 393216](https://gitlab.com/gitlab-org/gitlab/-/issues/393216) for more information. diff --git a/doc/user/group/saml_sso/index.md b/doc/user/group/saml_sso/index.md index 2d0e642b3ef..8e574c4fbd1 100644 --- a/doc/user/group/saml_sso/index.md +++ b/doc/user/group/saml_sso/index.md @@ -325,10 +325,7 @@ After you have configured your identity provider, you can: To change the identity provider: -- If the `NameID` is not identical in the existing and new identity providers, - tell users to: - 1. [Unlink the current SAML identity](#unlinking-accounts). - 1. [Link their identity](#user-access-and-management) to the new identity provider. +- If the `NameID` is not identical in the existing and new identity providers, [change the NameID for users](#change-nameid-for-one-or-more-users). - If the `NameID` is identical, users do not have to make any changes. ### Migrate to a different identity provider @@ -340,19 +337,17 @@ users cannot access any of the SAML groups. To mitigate this, you can disable To migrate identity providers: 1. [Configure](#configure-your-identity-provider) the group with the new identity provider. -1. Tell users to: - 1. [Unlink their account from the group](#unlinking-accounts). - 1. [Link their account to the new SAML app](#linking-saml-to-your-existing-gitlabcom-account). +1. [Change the NameID for users](#change-nameid-for-one-or-more-users). ### Change email domains To migrate users to a new email domain, tell users to: 1. Add their new email as the primary email to their accounts and verify it. -1. [Unlink their account from the group](#unlinking-accounts). -1. [Link their account to the group](#linking-saml-to-your-existing-gitlabcom-account). 1. Optional. Remove their old email from the account. +If the NameID is configured with the email address, [change the NameID for users](#change-nameid-for-one-or-more-users). + ## User access and management > - SAML user provisioning [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/268142) in GitLab 13.7. @@ -395,7 +390,8 @@ On subsequent visits, you should be able to go [sign in to GitLab.com with SAML] > Update of SAML identities using the SAML API [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/227841) in GitLab 15.5. -Group owners can update the SAML identities for their group members using the [SAML API](../../../api/saml.md). +Group owners can update the SAML identities for their group members using the [SAML API](../../../api/saml.md#update-extern_uid-field-for-a-saml-identity). +If [SCIM](scim_setup.md) is configured, group owners can update the SCIM identities using the [SCIM API](../../../api/scim.md#update-extern_uid-field-for-a-scim-identity). Alternatively, ask the users to reconnect their SAML account. diff --git a/lib/generators/batched_background_migration/batched_background_migration_generator.rb b/lib/generators/batched_background_migration/batched_background_migration_generator.rb index 96c2b160744..c68ed52c1a0 100644 --- a/lib/generators/batched_background_migration/batched_background_migration_generator.rb +++ b/lib/generators/batched_background_migration/batched_background_migration_generator.rb @@ -64,5 +64,10 @@ module BatchedBackgroundMigration def feature_category options[:feature_category] end + + def current_milestone + version = Gem::Version.new(File.read('VERSION')) + version.release.segments.first(2).join('.') + end end end diff --git a/lib/generators/batched_background_migration/templates/batched_background_migration_dictionary.template b/lib/generators/batched_background_migration/templates/batched_background_migration_dictionary.template index 7cc7091191d..8aa08e15f48 100644 --- a/lib/generators/batched_background_migration/templates/batched_background_migration_dictionary.template +++ b/lib/generators/batched_background_migration/templates/batched_background_migration_dictionary.template @@ -3,4 +3,4 @@ migration_job_name: <%= class_name %> description: # Please capture what <%= class_name %> does feature_category: <%= feature_category %> introduced_by_url: # URL of the MR (or issue/commit) that introduced the migration -milestone: +milestone: <%= current_milestone %> diff --git a/lib/gitlab/exception_log_formatter.rb b/lib/gitlab/exception_log_formatter.rb index ce802b562f0..52ad67d6f8b 100644 --- a/lib/gitlab/exception_log_formatter.rb +++ b/lib/gitlab/exception_log_formatter.rb @@ -17,6 +17,10 @@ module Gitlab payload['exception.backtrace'] = Rails.backtrace_cleaner.clean(exception.backtrace) end + if exception.cause + payload['exception.cause_class'] = exception.cause.class.name + end + if sql = find_sql(exception) payload['exception.sql'] = sql end diff --git a/lib/gitlab/i18n.rb b/lib/gitlab/i18n.rb index 971737e21af..a1b6e937396 100644 --- a/lib/gitlab/i18n.rb +++ b/lib/gitlab/i18n.rb @@ -44,28 +44,28 @@ module Gitlab TRANSLATION_LEVELS = { 'bg' => 0, 'cs_CZ' => 0, - 'da_DK' => 34, - 'de' => 16, + 'da_DK' => 33, + 'de' => 15, 'en' => 100, 'eo' => 0, - 'es' => 33, + 'es' => 32, 'fil_PH' => 0, 'fr' => 99, 'gl_ES' => 0, 'id_ID' => 0, 'it' => 1, 'ja' => 31, - 'ko' => 20, - 'nb_NO' => 23, + 'ko' => 19, + 'nb_NO' => 22, 'nl_NL' => 0, 'pl_PL' => 3, - 'pt_BR' => 57, - 'ro_RO' => 91, - 'ru' => 26, - 'si_LK' => 11, + 'pt_BR' => 56, + 'ro_RO' => 89, + 'ru' => 25, + 'si_LK' => 10, 'tr_TR' => 10, - 'uk' => 55, - 'zh_CN' => 98, + 'uk' => 53, + 'zh_CN' => 96, 'zh_HK' => 1, 'zh_TW' => 98 }.freeze diff --git a/lib/gitlab/import/import_failure_service.rb b/lib/gitlab/import/import_failure_service.rb index 8257f9f5db4..714d9b3edd9 100644 --- a/lib/gitlab/import/import_failure_service.rb +++ b/lib/gitlab/import/import_failure_service.rb @@ -89,13 +89,8 @@ module Gitlab ) end - def import_metrics - @import_metrics ||= Gitlab::Import::Metrics.new("#{project.import_type}_importer", project) - end - def track_metrics - import_metrics.track_failed_import - import_metrics.track_import_state + Gitlab::Import::Metrics.new("#{project.import_type}_importer", project).track_failed_import end end end diff --git a/lib/gitlab/import/metrics.rb b/lib/gitlab/import/metrics.rb index 82f8cf8dbc6..e457d9ec57c 100644 --- a/lib/gitlab/import/metrics.rb +++ b/lib/gitlab/import/metrics.rb @@ -26,25 +26,20 @@ module Gitlab observe_histogram projects_counter.increment track_finish_metric - track_import_state end def track_failed_import return unless project.github_import? track_usage_event(:github_import_project_failure, project.id) + track_import_state('github') end - def track_import_state + def track_canceled_import return unless project.github_import? - Gitlab::Tracking.event( - importer, - 'create', - label: 'github_import_project_state', - project: project, - extra: { import_type: 'github', state: project.beautified_import_status_name } - ) + track_usage_event(:github_import_project_cancelled, project.id) + track_import_state('github') end def issues_counter @@ -88,7 +83,24 @@ module Gitlab def track_finish_metric return unless project.github_import? - track_usage_event(:github_import_project_success, project.id) + track_import_state('github') + + case project.beautified_import_status_name + when 'partially completed' + track_usage_event(:github_import_project_partially_completed, project.id) + when 'completed' + track_usage_event(:github_import_project_success, project.id) + end + end + + def track_import_state(type) + Gitlab::Tracking.event( + importer, + 'create', + label: "#{type}_import_project_state", + project: project, + extra: { import_type: type, state: project.beautified_import_status_name } + ) end end end diff --git a/lib/gitlab/usage_data_counters/known_events/importer_events.yml b/lib/gitlab/usage_data_counters/known_events/importer_events.yml index ece8cd6aa45..3050fff5d92 100644 --- a/lib/gitlab/usage_data_counters/known_events/importer_events.yml +++ b/lib/gitlab/usage_data_counters/known_events/importer_events.yml @@ -9,3 +9,9 @@ - name: github_import_project_failure redis_slot: import aggregation: weekly +- name: github_import_project_cancelled + redis_slot: import + aggregation: weekly +- name: github_import_project_partially_completed + redis_slot: import + aggregation: weekly diff --git a/lib/sidebars/user_profile/base_menu.rb b/lib/sidebars/user_profile/base_menu.rb new file mode 100644 index 00000000000..5594d7c3c65 --- /dev/null +++ b/lib/sidebars/user_profile/base_menu.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module Sidebars + module UserProfile + class BaseMenu < ::Sidebars::Menu + override :render? + def render? + can?(context.current_user, :read_user_profile, context.container) + end + end + end +end diff --git a/lib/sidebars/user_profile/menus/activity_menu.rb b/lib/sidebars/user_profile/menus/activity_menu.rb new file mode 100644 index 00000000000..95e99a3dcbc --- /dev/null +++ b/lib/sidebars/user_profile/menus/activity_menu.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Sidebars + module UserProfile + module Menus + class ActivityMenu < ::Sidebars::UserProfile::BaseMenu + override :link + def link + user_activity_path(context.container) + end + + override :title + def title + s_('UserProfile|Activity') + end + + override :active_routes + def active_routes + { path: 'users#activity' } + end + end + end + end +end diff --git a/lib/sidebars/user_profile/menus/contributed_projects_menu.rb b/lib/sidebars/user_profile/menus/contributed_projects_menu.rb new file mode 100644 index 00000000000..0644982a143 --- /dev/null +++ b/lib/sidebars/user_profile/menus/contributed_projects_menu.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Sidebars + module UserProfile + module Menus + class ContributedProjectsMenu < ::Sidebars::UserProfile::BaseMenu + override :link + def link + user_contributed_projects_path(context.container) + end + + override :title + def title + s_('UserProfile|Contributed projects') + end + + override :active_routes + def active_routes + { path: 'users#contributed' } + end + end + end + end +end diff --git a/lib/sidebars/user_profile/menus/followers_menu.rb b/lib/sidebars/user_profile/menus/followers_menu.rb new file mode 100644 index 00000000000..53411c21a86 --- /dev/null +++ b/lib/sidebars/user_profile/menus/followers_menu.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +module Sidebars + module UserProfile + module Menus + class FollowersMenu < ::Sidebars::UserProfile::BaseMenu + include Gitlab::Utils::StrongMemoize + + override :link + def link + user_followers_path(context.container) + end + + override :title + def title + s_('UserProfile|Followers') + end + + override :active_routes + def active_routes + { path: 'users#followers' } + end + + override :has_pill? + def has_pill? + context.container.followers.any? + end + strong_memoize_attr :has_pill? + + override :pill_count + def pill_count + context.container.followers.count + end + strong_memoize_attr :pill_count + end + end + end +end diff --git a/lib/sidebars/user_profile/menus/following_menu.rb b/lib/sidebars/user_profile/menus/following_menu.rb new file mode 100644 index 00000000000..5da76a0100a --- /dev/null +++ b/lib/sidebars/user_profile/menus/following_menu.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +module Sidebars + module UserProfile + module Menus + class FollowingMenu < ::Sidebars::UserProfile::BaseMenu + include Gitlab::Utils::StrongMemoize + + override :link + def link + user_following_path(context.container) + end + + override :title + def title + s_('UserProfile|Following') + end + + override :active_routes + def active_routes + { path: 'users#following' } + end + + override :has_pill? + def has_pill? + context.container.followees.any? + end + strong_memoize_attr :has_pill? + + override :pill_count + def pill_count + context.container.followees.count + end + strong_memoize_attr :pill_count + end + end + end +end diff --git a/lib/sidebars/user_profile/menus/groups_menu.rb b/lib/sidebars/user_profile/menus/groups_menu.rb new file mode 100644 index 00000000000..38c2c3f042d --- /dev/null +++ b/lib/sidebars/user_profile/menus/groups_menu.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Sidebars + module UserProfile + module Menus + class GroupsMenu < ::Sidebars::UserProfile::BaseMenu + override :link + def link + user_groups_path(context.container) + end + + override :title + def title + s_('UserProfile|Groups') + end + + override :active_routes + def active_routes + { path: 'users#groups' } + end + end + end + end +end diff --git a/lib/sidebars/user_profile/menus/overview_menu.rb b/lib/sidebars/user_profile/menus/overview_menu.rb new file mode 100644 index 00000000000..ebf184e7e4e --- /dev/null +++ b/lib/sidebars/user_profile/menus/overview_menu.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Sidebars + module UserProfile + module Menus + class OverviewMenu < ::Sidebars::UserProfile::BaseMenu + override :link + def link + user_path(context.container) + end + + override :title + def title + s_('UserProfile|Overview') + end + + override :active_routes + def active_routes + { path: 'users#show' } + end + end + end + end +end diff --git a/lib/sidebars/user_profile/menus/personal_projects_menu.rb b/lib/sidebars/user_profile/menus/personal_projects_menu.rb new file mode 100644 index 00000000000..14fcddf2cc2 --- /dev/null +++ b/lib/sidebars/user_profile/menus/personal_projects_menu.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Sidebars + module UserProfile + module Menus + class PersonalProjectsMenu < ::Sidebars::UserProfile::BaseMenu + override :link + def link + user_projects_path(context.container) + end + + override :title + def title + s_('UserProfile|Personal projects') + end + + override :active_routes + def active_routes + { path: 'users#projects' } + end + end + end + end +end diff --git a/lib/sidebars/user_profile/menus/snippets_menu.rb b/lib/sidebars/user_profile/menus/snippets_menu.rb new file mode 100644 index 00000000000..a8e5077098c --- /dev/null +++ b/lib/sidebars/user_profile/menus/snippets_menu.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Sidebars + module UserProfile + module Menus + class SnippetsMenu < ::Sidebars::UserProfile::BaseMenu + override :link + def link + user_snippets_path(context.container) + end + + override :title + def title + s_('UserProfile|Snippets') + end + + override :active_routes + def active_routes + { path: 'users#snippets' } + end + end + end + end +end diff --git a/lib/sidebars/user_profile/menus/starred_projects_menu.rb b/lib/sidebars/user_profile/menus/starred_projects_menu.rb new file mode 100644 index 00000000000..762b451f4a1 --- /dev/null +++ b/lib/sidebars/user_profile/menus/starred_projects_menu.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Sidebars + module UserProfile + module Menus + class StarredProjectsMenu < ::Sidebars::UserProfile::BaseMenu + override :link + def link + user_starred_projects_path(context.container) + end + + override :title + def title + s_('UserProfile|Starred projects') + end + + override :active_routes + def active_routes + { path: 'users#starred' } + end + end + end + end +end diff --git a/lib/sidebars/user_profile/panel.rb b/lib/sidebars/user_profile/panel.rb new file mode 100644 index 00000000000..9a595fdf64c --- /dev/null +++ b/lib/sidebars/user_profile/panel.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +module Sidebars + module UserProfile + class Panel < ::Sidebars::Panel + include UsersHelper + + override :configure_menus + def configure_menus + add_menus + end + + override :aria_label + def aria_label + s_('UserProfile|User profile navigation') + end + + override :super_sidebar_context_header + def super_sidebar_context_header + @super_sidebar_context_header ||= { + title: user_name, + avatar: context.container.avatar_url, + avatar_shape: 'circle' + } + end + + private + + def user + context.container + end + + def user_name + return user_display_name(user) if user.blocked? || !user.confirmed? + + user.name + end + + def add_menus + add_menu(Sidebars::UserProfile::Menus::OverviewMenu.new(context)) + add_menu(Sidebars::UserProfile::Menus::ActivityMenu.new(context)) + add_menu(Sidebars::UserProfile::Menus::GroupsMenu.new(context)) + add_menu(Sidebars::UserProfile::Menus::ContributedProjectsMenu.new(context)) + add_menu(Sidebars::UserProfile::Menus::PersonalProjectsMenu.new(context)) + add_menu(Sidebars::UserProfile::Menus::StarredProjectsMenu.new(context)) + add_menu(Sidebars::UserProfile::Menus::SnippetsMenu.new(context)) + add_menu(Sidebars::UserProfile::Menus::FollowersMenu.new(context)) + add_menu(Sidebars::UserProfile::Menus::FollowingMenu.new(context)) + end + end + end +end diff --git a/lib/tasks/gitlab/db.rake b/lib/tasks/gitlab/db.rake index befc2113b1e..72fe190bc8f 100644 --- a/lib/tasks/gitlab/db.rake +++ b/lib/tasks/gitlab/db.rake @@ -458,7 +458,7 @@ namespace :gitlab do Rails.application.eager_load! version = Gem::Version.new(File.read('VERSION')) - milestone = version.release.segments[0..1].join('.') + milestone = version.release.segments.first(2).join('.') classes = {} diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 21316a79726..62debd10678 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -3627,9 +3627,6 @@ msgstr "" msgid "AdminUsers|user cap" msgstr "" -msgid "Administration" -msgstr "" - msgid "Administrators" msgstr "" @@ -47224,6 +47221,9 @@ msgstr "" msgid "UserProfile|User ID: %{id}" msgstr "" +msgid "UserProfile|User profile navigation" +msgstr "" + msgid "UserProfile|View all" msgstr "" diff --git a/package.json b/package.json index 03552da4056..4364eb97805 100644 --- a/package.json +++ b/package.json @@ -225,7 +225,7 @@ "cheerio": "^1.0.0-rc.9", "commander": "^2.20.3", "custom-jquery-matchers": "^2.1.0", - "eslint": "8.34.0", + "eslint": "8.35.0", "eslint-import-resolver-jest": "3.0.2", "eslint-import-resolver-webpack": "0.13.2", "eslint-plugin-import": "^2.27.5", diff --git a/qa/qa/flow/saml.rb b/qa/qa/flow/saml.rb index 8a0ebb9f551..1cf29e75c67 100644 --- a/qa/qa/flow/saml.rb +++ b/qa/qa/flow/saml.rb @@ -19,8 +19,6 @@ module QA end def enable_saml_sso(group, saml_idp_service, enforce_sso: false, default_membership_role: 'Guest') - Runtime::Feature.enable(:group_administration_nav_item) - page.visit Runtime::Scenario.gitlab_address Page::Main::Login.perform(&:sign_in_using_credentials) unless Page::Main::Menu.perform(&:signed_in?) diff --git a/rubocop/cop/background_migration/missing_dictionary_file.rb b/rubocop/cop/background_migration/missing_dictionary_file.rb new file mode 100644 index 00000000000..9158b268bf9 --- /dev/null +++ b/rubocop/cop/background_migration/missing_dictionary_file.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +require_relative '../../migration_helpers' + +module RuboCop + module Cop + module BackgroundMigration + # Checks the batched background migration has the corresponding dictionary file + class MissingDictionaryFile < RuboCop::Cop::Base + include MigrationHelpers + + MSG = "Missing %{file_name}. " \ + "Use the generator 'batched_background_migration' to create dictionary files automatically. " \ + "For more details refer: https://docs.gitlab.com/ee/development/database/batched_background_migrations.html#generator" + + DICTIONARY_DIR = "db/docs/batched_background_migrations" + + def_node_matcher :batched_background_migration_name_node, <<~PATTERN + `(send nil? :queue_batched_background_migration $_ ...) + PATTERN + + def_node_matcher :migration_constant_value, <<~PATTERN + `(casgn nil? %const_name ({sym|str} $_)) + PATTERN + + def on_class(node) + return unless time_enforced?(node) && in_post_deployment_migration?(node) + + migration_name_node = batched_background_migration_name_node(node) + return unless migration_name_node + + migration_name = if migration_name_node.const_name.present? + migration_constant_value(node, const_name: migration_name_node.const_name.to_sym) + else + migration_name_node.value + end + + return if dictionary_file?(migration_name) + + add_offense(node, message: format(MSG, file_name: dictionary_file_path(migration_name))) + end + + private + + def dictionary_file?(migration_class_name) + File.exist?(dictionary_file_path(migration_class_name)) + end + + def dictionary_file_path(migration_class_name) + File.join(rails_root, DICTIONARY_DIR, "#{migration_class_name.underscore}.yml") + end + + def rails_root + @rails_root ||= File.expand_path('../../..', __dir__) + end + end + end + end +end diff --git a/spec/frontend/super_sidebar/components/context_switcher_toggle_spec.js b/spec/frontend/super_sidebar/components/context_switcher_toggle_spec.js new file mode 100644 index 00000000000..7172b60d0fa --- /dev/null +++ b/spec/frontend/super_sidebar/components/context_switcher_toggle_spec.js @@ -0,0 +1,50 @@ +import { GlAvatar } from '@gitlab/ui'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import ContextSwitcherToggle from '~/super_sidebar/components/context_switcher_toggle.vue'; + +describe('ContextSwitcherToggle component', () => { + let wrapper; + + const context = { + id: 1, + title: 'Title', + avatar: '/path/to/avatar.png', + }; + + const findGlAvatar = () => wrapper.getComponent(GlAvatar); + + const createWrapper = (props = {}) => { + wrapper = shallowMountExtended(ContextSwitcherToggle, { + propsData: { + context, + expanded: false, + ...props, + }, + }); + }; + + describe('with an avatar', () => { + it('passes the correct props to GlAvatar', () => { + createWrapper(); + const avatar = findGlAvatar(); + + expect(avatar.props('shape')).toBe('rect'); + expect(avatar.props('entityName')).toBe(context.title); + expect(avatar.props('entityId')).toBe(context.id); + expect(avatar.props('src')).toBe(context.avatar); + }); + + it('renders the avatar with a custom shape', () => { + const customShape = 'circle'; + createWrapper({ + context: { + ...context, + avatar_shape: customShape, + }, + }); + const avatar = findGlAvatar(); + + expect(avatar.props('shape')).toBe(customShape); + }); + }); +}); diff --git a/spec/helpers/sidebars_helper_spec.rb b/spec/helpers/sidebars_helper_spec.rb index f23de35877b..b602fe3548e 100644 --- a/spec/helpers/sidebars_helper_spec.rb +++ b/spec/helpers/sidebars_helper_spec.rb @@ -218,6 +218,10 @@ RSpec.describe SidebarsHelper, feature_category: :navigation do expect(helper.super_sidebar_nav_panel(nav: 'profile')).to be_a(Sidebars::UserSettings::Panel) end + it 'returns User profile Panel for user profile nav' do + expect(helper.super_sidebar_nav_panel(nav: 'user_profile')).to be_a(Sidebars::UserProfile::Panel) + end + it 'returns "Your Work" Panel for your_work nav', :use_clean_rails_memory_store_caching do expect(helper.super_sidebar_nav_panel(nav: 'your_work', user: user)).to be_a(Sidebars::YourWork::Panel) end diff --git a/spec/lib/generators/batched_background_migration/batched_background_migration_generator_spec.rb b/spec/lib/generators/batched_background_migration/batched_background_migration_generator_spec.rb index 017548b6fb5..d533bcf0039 100644 --- a/spec/lib/generators/batched_background_migration/batched_background_migration_generator_spec.rb +++ b/spec/lib/generators/batched_background_migration/batched_background_migration_generator_spec.rb @@ -25,7 +25,7 @@ RSpec.describe BatchedBackgroundMigration::BatchedBackgroundMigrationGenerator, let(:expected_migration_spec_file) { load_expected_file('queue_my_batched_migration_spec.txt') } let(:expected_migration_job_file) { load_expected_file('my_batched_migration.txt') } let(:expected_migration_job_spec_file) { load_expected_file('my_batched_migration_spec_matcher.txt') } - let(:expected_migration_dictionary) { load_expected_file('my_batched_migration_dictionary.txt') } + let(:expected_migration_dictionary) { load_expected_file('my_batched_migration_dictionary_matcher.txt') } it 'generates expected files' do run_generator %w[my_batched_migration --table_name=projects --column_name=id --feature_category=database] @@ -48,7 +48,8 @@ RSpec.describe BatchedBackgroundMigration::BatchedBackgroundMigrationGenerator, end assert_file('db/docs/batched_background_migrations/my_batched_migration.yml') do |migration_dictionary| - expect(migration_dictionary).to eq(expected_migration_dictionary) + # Regex is used to match the dynamically generated 'milestone' in the dictionary + expect(migration_dictionary).to match(/#{expected_migration_dictionary}/) end end end diff --git a/spec/lib/generators/batched_background_migration/expected_files/my_batched_migration_dictionary.txt b/spec/lib/generators/batched_background_migration/expected_files/my_batched_migration_dictionary_matcher.txt index 0b350961abd..6280d35177e 100644 --- a/spec/lib/generators/batched_background_migration/expected_files/my_batched_migration_dictionary.txt +++ b/spec/lib/generators/batched_background_migration/expected_files/my_batched_migration_dictionary_matcher.txt @@ -2,5 +2,5 @@ migration_job_name: MyBatchedMigration description: # Please capture what MyBatchedMigration does feature_category: database -introduced_by_url: # URL of the MR (or issue/commit) that introduced the migration -milestone: +introduced_by_url: # URL of the MR \(or issue/commit\) that introduced the migration +milestone: [0-9\.]+ diff --git a/spec/lib/gitlab/exception_log_formatter_spec.rb b/spec/lib/gitlab/exception_log_formatter_spec.rb index 7dda56f0bf5..82166971603 100644 --- a/spec/lib/gitlab/exception_log_formatter_spec.rb +++ b/spec/lib/gitlab/exception_log_formatter_spec.rb @@ -45,6 +45,12 @@ RSpec.describe Gitlab::ExceptionLogFormatter do allow(exception).to receive(:cause).and_return(ActiveRecord::StatementInvalid.new(sql: 'SELECT "users".* FROM "users" WHERE "users"."id" = 1 AND "users"."foo" = $1')) end + it 'adds the cause_class to payload' do + described_class.format!(exception, payload) + + expect(payload['exception.cause_class']).to eq('ActiveRecord::StatementInvalid') + end + it 'adds the normalized SQL query to payload' do described_class.format!(exception, payload) diff --git a/spec/lib/gitlab/import/import_failure_service_spec.rb b/spec/lib/gitlab/import/import_failure_service_spec.rb index fc02c6bd4ca..eb71b307b8d 100644 --- a/spec/lib/gitlab/import/import_failure_service_spec.rb +++ b/spec/lib/gitlab/import/import_failure_service_spec.rb @@ -141,7 +141,6 @@ RSpec.describe Gitlab::Import::ImportFailureService, :aggregate_failures do .with("#{project.import_type}_importer", project) .and_return(metrics_double) expect(metrics_double).to receive(:track_failed_import) - expect(metrics_double).to receive(:track_import_state) service.execute end diff --git a/spec/lib/gitlab/import/metrics_spec.rb b/spec/lib/gitlab/import/metrics_spec.rb index f0140a975b1..1a988af0dbd 100644 --- a/spec/lib/gitlab/import/metrics_spec.rb +++ b/spec/lib/gitlab/import/metrics_spec.rb @@ -11,7 +11,6 @@ RSpec.describe Gitlab::Import::Metrics, :aggregate_failures do subject { described_class.new(importer, project) } before do - allow(Gitlab::Metrics).to receive(:counter) { counter } allow(counter).to receive(:increment) allow(histogram).to receive(:observe) end @@ -42,6 +41,13 @@ RSpec.describe Gitlab::Import::Metrics, :aggregate_failures do context 'when project is not a github import' do it 'does not emit importer metrics' do expect(subject).not_to receive(:track_usage_event) + expect_no_snowplow_event( + category: :test_importer, + action: 'create', + label: 'github_import_project_state', + project: project, + extra: { import_type: 'github', state: 'failed' } + ) subject.track_failed_import end @@ -50,22 +56,15 @@ RSpec.describe Gitlab::Import::Metrics, :aggregate_failures do context 'when project is a github import' do before do project.import_type = 'github' + allow(project).to receive(:import_status).and_return('failed') end it 'emits importer metrics' do expect(subject).to receive(:track_usage_event).with(:github_import_project_failure, project.id) subject.track_failed_import - end - end - end - - describe '#track_import_state' do - context 'when project is not a github import' do - it 'does not emit importer metrics' do - subject.track_import_state - expect_no_snowplow_event( + expect_snowplow_event( category: :test_importer, action: 'create', label: 'github_import_project_state', @@ -74,50 +73,64 @@ RSpec.describe Gitlab::Import::Metrics, :aggregate_failures do ) end end + end + describe '#track_finished_import' do context 'when project is a github import' do before do project.import_type = 'github' - allow(project).to receive(:import_status).and_return('failed') + allow(Gitlab::Metrics).to receive(:counter) { counter } + allow(Gitlab::Metrics).to receive(:histogram) { histogram } + allow(project).to receive(:beautified_import_status_name).and_return('completed') end it 'emits importer metrics' do - subject.track_import_state + expect(Gitlab::Metrics).to receive(:counter).with( + :test_importer_imported_projects_total, + 'The number of imported projects' + ) + + expect(Gitlab::Metrics).to receive(:histogram).with( + :test_importer_total_duration_seconds, + 'Total time spent importing projects, in seconds', + {}, + described_class::IMPORT_DURATION_BUCKETS + ) + + expect(counter).to receive(:increment) + + subject.track_finished_import expect_snowplow_event( category: :test_importer, action: 'create', label: 'github_import_project_state', project: project, - extra: { import_type: 'github', state: 'failed' } + extra: { import_type: 'github', state: 'completed' } ) - end - end - end - describe '#track_finished_import' do - before do - allow(Gitlab::Metrics).to receive(:histogram) { histogram } - end - - it 'emits importer metrics' do - expect(Gitlab::Metrics).to receive(:counter).with( - :test_importer_imported_projects_total, - 'The number of imported projects' - ) + expect(subject.duration).not_to be_nil + end - expect(Gitlab::Metrics).to receive(:histogram).with( - :test_importer_total_duration_seconds, - 'Total time spent importing projects, in seconds', - {}, - described_class::IMPORT_DURATION_BUCKETS - ) + context 'when import is partially completed' do + before do + allow(project).to receive(:beautified_import_status_name).and_return('partially completed') + end - expect(counter).to receive(:increment) + it 'emits snowplow metrics' do + expect(subject).to receive(:track_usage_event).with(:github_import_project_partially_completed, project.id) - subject.track_finished_import + subject.track_finished_import - expect(subject.duration).not_to be_nil + expect_snowplow_event( + category: :test_importer, + action: 'create', + label: 'github_import_project_state', + project: project, + extra: { import_type: 'github', state: 'partially completed' } + ) + end + end end context 'when project is not a github import' do @@ -126,7 +139,6 @@ RSpec.describe Gitlab::Import::Metrics, :aggregate_failures do subject.track_finished_import - expect(histogram).to have_received(:observe).with({ importer: :test_importer }, anything) expect_no_snowplow_event( category: :test_importer, action: 'create', @@ -136,23 +148,41 @@ RSpec.describe Gitlab::Import::Metrics, :aggregate_failures do ) end end + end + + describe '#track_cancelled_import' do + context 'when project is not a github import' do + it 'does not emit importer metrics' do + expect(subject).not_to receive(:track_usage_event) + expect_no_snowplow_event( + category: :test_importer, + action: 'create', + label: 'github_import_project_state', + project: project, + extra: { import_type: 'github', state: 'canceled' } + ) + + subject.track_canceled_import + end + end context 'when project is a github import' do before do project.import_type = 'github' - allow(project).to receive(:import_status).and_return('finished') - allow(project).to receive(:import_finished?).and_return(true) + allow(project).to receive(:import_status).and_return('canceled') end - it 'emits snowplow metrics' do - subject.track_finished_import + it 'emits importer metrics' do + expect(subject).to receive(:track_usage_event).with(:github_import_project_cancelled, project.id) + + subject.track_canceled_import expect_snowplow_event( category: :test_importer, action: 'create', label: 'github_import_project_state', project: project, - extra: { import_type: 'github', state: 'completed' } + extra: { import_type: 'github', state: 'canceled' } ) end end diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index ebd2db63fe5..ca784ead25a 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -704,7 +704,7 @@ project: - project_registry - packages - package_files -- repository_files +- rpm_repository_files - packages_cleanup_policy - alerting_setting - project_setting diff --git a/spec/lib/sidebars/user_profile/menus/activity_menu_spec.rb b/spec/lib/sidebars/user_profile/menus/activity_menu_spec.rb new file mode 100644 index 00000000000..44492380f11 --- /dev/null +++ b/spec/lib/sidebars/user_profile/menus/activity_menu_spec.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Sidebars::UserProfile::Menus::ActivityMenu, feature_category: :navigation do + it_behaves_like 'User profile menu', + title: s_('UserProfile|Activity'), + active_route: 'users#activity' do + let(:link) { "/users/#{user.username}/activity" } + end +end diff --git a/spec/lib/sidebars/user_profile/menus/contributed_projects_menu_spec.rb b/spec/lib/sidebars/user_profile/menus/contributed_projects_menu_spec.rb new file mode 100644 index 00000000000..a5371c36fd3 --- /dev/null +++ b/spec/lib/sidebars/user_profile/menus/contributed_projects_menu_spec.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Sidebars::UserProfile::Menus::ContributedProjectsMenu, feature_category: :navigation do + it_behaves_like 'User profile menu', + title: s_('UserProfile|Contributed projects'), + active_route: 'users#contributed' do + let(:link) { "/users/#{user.username}/contributed" } + end +end diff --git a/spec/lib/sidebars/user_profile/menus/followers_menu_spec.rb b/spec/lib/sidebars/user_profile/menus/followers_menu_spec.rb new file mode 100644 index 00000000000..1b3efbf4ceb --- /dev/null +++ b/spec/lib/sidebars/user_profile/menus/followers_menu_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Sidebars::UserProfile::Menus::FollowersMenu, feature_category: :navigation do + it_behaves_like 'User profile menu', + title: s_('UserProfile|Followers'), + active_route: 'users#followers' do + let(:link) { "/users/#{user.username}/followers" } + end + + it_behaves_like 'Followers/followees counts', :followers +end diff --git a/spec/lib/sidebars/user_profile/menus/following_menu_spec.rb b/spec/lib/sidebars/user_profile/menus/following_menu_spec.rb new file mode 100644 index 00000000000..167961f085e --- /dev/null +++ b/spec/lib/sidebars/user_profile/menus/following_menu_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Sidebars::UserProfile::Menus::FollowingMenu, feature_category: :navigation do + it_behaves_like 'User profile menu', + title: s_('UserProfile|Following'), + active_route: 'users#following' do + let(:link) { "/users/#{user.username}/following" } + end + + it_behaves_like 'Followers/followees counts', :followees +end diff --git a/spec/lib/sidebars/user_profile/menus/groups_menu_spec.rb b/spec/lib/sidebars/user_profile/menus/groups_menu_spec.rb new file mode 100644 index 00000000000..6c48ad2e8d0 --- /dev/null +++ b/spec/lib/sidebars/user_profile/menus/groups_menu_spec.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Sidebars::UserProfile::Menus::GroupsMenu, feature_category: :navigation do + it_behaves_like 'User profile menu', + title: s_('UserProfile|Groups'), + active_route: 'users#groups' do + let(:link) { "/users/#{user.username}/groups" } + end +end diff --git a/spec/lib/sidebars/user_profile/menus/overview_menu_spec.rb b/spec/lib/sidebars/user_profile/menus/overview_menu_spec.rb new file mode 100644 index 00000000000..e34f59cddf0 --- /dev/null +++ b/spec/lib/sidebars/user_profile/menus/overview_menu_spec.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Sidebars::UserProfile::Menus::OverviewMenu, feature_category: :navigation do + it_behaves_like 'User profile menu', + title: s_('UserProfile|Overview'), + active_route: 'users#show' do + let(:link) { "/#{user.username}" } + end +end diff --git a/spec/lib/sidebars/user_profile/menus/personal_projects_menu_spec.rb b/spec/lib/sidebars/user_profile/menus/personal_projects_menu_spec.rb new file mode 100644 index 00000000000..ba2c3d11b88 --- /dev/null +++ b/spec/lib/sidebars/user_profile/menus/personal_projects_menu_spec.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Sidebars::UserProfile::Menus::PersonalProjectsMenu, feature_category: :navigation do + it_behaves_like 'User profile menu', + title: s_('UserProfile|Personal projects'), + active_route: 'users#projects' do + let(:link) { "/users/#{user.username}/projects" } + end +end diff --git a/spec/lib/sidebars/user_profile/menus/snippets_menu_spec.rb b/spec/lib/sidebars/user_profile/menus/snippets_menu_spec.rb new file mode 100644 index 00000000000..2760d172a51 --- /dev/null +++ b/spec/lib/sidebars/user_profile/menus/snippets_menu_spec.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Sidebars::UserProfile::Menus::SnippetsMenu, feature_category: :navigation do + it_behaves_like 'User profile menu', + title: s_('UserProfile|Snippets'), + active_route: 'users#snippets' do + let(:link) { "/users/#{user.username}/snippets" } + end +end diff --git a/spec/lib/sidebars/user_profile/menus/starred_projects_menu_spec.rb b/spec/lib/sidebars/user_profile/menus/starred_projects_menu_spec.rb new file mode 100644 index 00000000000..e205d1f5492 --- /dev/null +++ b/spec/lib/sidebars/user_profile/menus/starred_projects_menu_spec.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Sidebars::UserProfile::Menus::StarredProjectsMenu, feature_category: :navigation do + it_behaves_like 'User profile menu', + title: s_('UserProfile|Starred projects'), + active_route: 'users#starred' do + let(:link) { "/users/#{user.username}/starred" } + end +end diff --git a/spec/lib/sidebars/user_profile/panel_spec.rb b/spec/lib/sidebars/user_profile/panel_spec.rb new file mode 100644 index 00000000000..af261dce3f3 --- /dev/null +++ b/spec/lib/sidebars/user_profile/panel_spec.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Sidebars::UserProfile::Panel, feature_category: :navigation do + let_it_be(:current_user) { create(:user) } + let_it_be(:user) { create(:user) } + + let(:context) { Sidebars::Context.new(current_user: current_user, container: user) } + + subject { described_class.new(context) } + + it 'implements #aria_label' do + expect(subject.aria_label).to eq(s_('UserProfile|User profile navigation')) + end + + it 'implements #super_sidebar_context_header' do + expect(subject.super_sidebar_context_header).to eq({ + title: user.name, + avatar: user.avatar_url, + avatar_shape: 'circle' + }) + end +end diff --git a/spec/models/container_registry/data_repair_detail_spec.rb b/spec/models/container_registry/data_repair_detail_spec.rb new file mode 100644 index 00000000000..92833553a1e --- /dev/null +++ b/spec/models/container_registry/data_repair_detail_spec.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ContainerRegistry::DataRepairDetail, type: :model, feature_category: :container_registry do + let_it_be(:project) { create(:project) } + + subject { described_class.new(project: project) } + + it { is_expected.to belong_to(:project).required } +end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index e7390c99b64..92b23045b08 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -137,6 +137,7 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do it { is_expected.to have_many(:reviews).inverse_of(:project) } it { is_expected.to have_many(:packages).class_name('Packages::Package') } it { is_expected.to have_many(:package_files).class_name('Packages::PackageFile') } + it { is_expected.to have_many(:rpm_repository_files).class_name('Packages::Rpm::RepositoryFile').inverse_of(:project).dependent(:destroy) } it { is_expected.to have_many(:debian_distributions).class_name('Packages::Debian::ProjectDistribution').dependent(:destroy) } it { is_expected.to have_one(:packages_cleanup_policy).class_name('Packages::Cleanup::Policy').inverse_of(:project) } it { is_expected.to have_many(:pipeline_artifacts).dependent(:restrict_with_error) } diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb index 7b2b0122768..778ddfe9d71 100644 --- a/spec/policies/project_policy_spec.rb +++ b/spec/policies/project_policy_spec.rb @@ -3042,6 +3042,26 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do end end + describe 'add_catalog_resource' do + using RSpec::Parameterized::TableSyntax + + let(:current_user) { public_send(role) } + + where(:role, :allowed) do + :owner | true + :maintainer | false + :developer | false + :reporter | false + :guest | false + end + + with_them do + it do + expect(subject.can?(:add_catalog_resource)).to be(allowed) + end + end + end + describe 'read_code' do let(:current_user) { create(:user) } diff --git a/spec/rubocop/cop/background_migration/missing_dictionary_file_spec.rb b/spec/rubocop/cop/background_migration/missing_dictionary_file_spec.rb new file mode 100644 index 00000000000..32b958426b9 --- /dev/null +++ b/spec/rubocop/cop/background_migration/missing_dictionary_file_spec.rb @@ -0,0 +1,137 @@ +# frozen_string_literal: true + +require 'rubocop_spec_helper' +require_relative '../../../../rubocop/cop/background_migration/missing_dictionary_file' + +RSpec.describe RuboCop::Cop::BackgroundMigration::MissingDictionaryFile, feature_category: :database do + let(:config) do + RuboCop::Config.new( + 'BackgroundMigration/MissingDictionaryFile' => { + 'EnforcedSince' => 20230307160251 + } + ) + end + + context 'for non post migrations' do + before do + allow(cop).to receive(:in_post_deployment_migration?).and_return(false) + end + + it 'does not throw any offense' do + expect_no_offenses(<<~RUBY) + class QueueMyMigration < Gitlab::Database::Migration[2.1] + MIGRATION = 'MyMigration' + + def up + queue_batched_background_migration( + MIGRATION, + :users, + :id + ) + end + end + RUBY + end + end + + context 'for post migrations' do + before do + allow(cop).to receive(:in_post_deployment_migration?).and_return(true) + end + + context 'without enqueuing batched migrations' do + it 'does not throw any offense' do + expect_no_offenses(<<~RUBY) + class CreateTestTable < Gitlab::Database::Migration[2.1] + def change + create_table(:tests) + end + end + RUBY + end + end + + context 'with enqueuing batched migration' do + let(:rails_root) { File.expand_path('../../../..', __dir__) } + let(:dictionary_file_path) { File.join(rails_root, 'db/docs/batched_background_migrations/my_migration.yml') } + + context 'for migrations before enforced time' do + before do + allow(cop).to receive(:version).and_return(20230307160250) + end + + it 'does not throw any offenses' do + expect_no_offenses(<<~RUBY) + class QueueMyMigration < Gitlab::Database::Migration[2.1] + MIGRATION = 'MyMigration' + + def up + queue_batched_background_migration( + MIGRATION, + :users, + :id + ) + end + end + RUBY + end + end + + context 'for migrations after enforced time' do + before do + allow(cop).to receive(:version).and_return(20230307160252) + end + + it 'throws offense on not having the appropriate dictionary file with migration name as a constant' do + expect_offense(<<~RUBY) + class QueueMyMigration < Gitlab::Database::Migration[2.1] + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{format("Missing %{file_name}. Use the generator 'batched_background_migration' to create dictionary files automatically. For more details refer: https://docs.gitlab.com/ee/development/database/batched_background_migrations.html#generator", file_name: dictionary_file_path)} + MIGRATION = 'MyMigration' + + def up + queue_batched_background_migration( + MIGRATION, + :users, + :id + ) + end + end + RUBY + end + + it 'throws offense on not having the appropriate dictionary file with migration name as a variable' do + expect_offense(<<~RUBY) + class QueueMyMigration < Gitlab::Database::Migration[2.1] + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{format("Missing %{file_name}. Use the generator 'batched_background_migration' to create dictionary files automatically. For more details refer: https://docs.gitlab.com/ee/development/database/batched_background_migrations.html#generator", file_name: dictionary_file_path)} + def up + queue_batched_background_migration( + 'MyMigration', + :users, + :id + ) + end + end + RUBY + end + + it 'does not throw offense with appropriate dictionary file' do + expect(File).to receive(:exist?).with(dictionary_file_path).and_return(true) + + expect_no_offenses(<<~RUBY) + class QueueMyMigration < Gitlab::Database::Migration[2.1] + MIGRATION = 'MyMigration' + + def up + queue_batched_background_migration( + MIGRATION, + :users, + :id + ) + end + end + RUBY + end + end + end + end +end diff --git a/spec/services/ci/catalog/add_resource_service_spec.rb b/spec/services/ci/catalog/add_resource_service_spec.rb new file mode 100644 index 00000000000..ecb939e3c2d --- /dev/null +++ b/spec/services/ci/catalog/add_resource_service_spec.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Ci::Catalog::AddResourceService, feature_category: :pipeline_composition do + let_it_be(:project) { create(:project, :repository, description: 'Our components') } + let_it_be(:user) { create(:user) } + + let(:service) { described_class.new(project, user) } + + describe '#execute' do + context 'with an unauthorized user' do + it 'raises an AccessDeniedError' do + expect { service.execute }.to raise_error(Gitlab::Access::AccessDeniedError) + end + end + + context 'with an authorized user' do + before do + project.add_owner(user) + end + + context 'and a valid project' do + it 'creates a catalog resource' do + response = service.execute + + expect(response.payload.project).to eq(project) + end + end + + context 'with an invalid project' do + let_it_be(:project) { create(:project, :repository) } + + it 'does not create a catalog resource' do + response = service.execute + + expect(response.message).to eq('Project must have a description') + end + end + + context 'with an invalid catalog resource' do + it 'does not save the catalog resource' do + catalog_resource = instance_double(::Ci::Catalog::Resource, + valid?: false, + errors: instance_double(ActiveModel::Errors, full_messages: ['not valid'])) + allow(::Ci::Catalog::Resource).to receive(:new).and_return(catalog_resource) + + response = service.execute + + expect(response.message).to eq('not valid') + end + end + end + end +end diff --git a/spec/services/ci/catalog/validate_resource_service_spec.rb b/spec/services/ci/catalog/validate_resource_service_spec.rb new file mode 100644 index 00000000000..3bee37b7e55 --- /dev/null +++ b/spec/services/ci/catalog/validate_resource_service_spec.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Ci::Catalog::ValidateResourceService, feature_category: :pipeline_composition do + describe '#execute' do + context 'with a project that has a README and a description' do + it 'is valid' do + project = create(:project, :repository, description: 'Component project') + response = described_class.new(project, project.default_branch).execute + + expect(response).to be_success + end + end + + context 'with a project that has neither a description nor a README' do + it 'is not valid' do + project = create(:project, :empty_repo) + project.repository.create_file( + project.creator, + 'ruby.rb', + 'I like this', + message: 'Ruby like this', + branch_name: 'master' + ) + response = described_class.new(project, project.default_branch).execute + + expect(response.message).to eq('Project must have a README , Project must have a description') + end + end + + context 'with a project that has a description but not a README' do + it 'is not valid' do + project = create(:project, :empty_repo, description: 'project with no README') + project.repository.create_file( + project.creator, + 'text.txt', + 'I do not like this', + message: 'only text like text', + branch_name: 'master' + ) + response = described_class.new(project, project.default_branch).execute + + expect(response.message).to eq('Project must have a README') + end + end + + context 'with a project that has a README and not a description' do + it 'is not valid' do + project = create(:project, :repository) + response = described_class.new(project, project.default_branch).execute + + expect(response.message).to eq('Project must have a description') + end + end + end +end diff --git a/spec/services/import/github/cancel_project_import_service_spec.rb b/spec/services/import/github/cancel_project_import_service_spec.rb index 17d5e0fa443..d8ea303fa50 100644 --- a/spec/services/import/github/cancel_project_import_service_spec.rb +++ b/spec/services/import/github/cancel_project_import_service_spec.rb @@ -22,7 +22,7 @@ RSpec.describe Import::Github::CancelProjectImportService, feature_category: :im .to receive(:new) .with(:github_importer, project) .and_return(metrics_double) - expect(metrics_double).to receive(:track_import_state) + expect(metrics_double).to receive(:track_canceled_import) import_cancel.execute end diff --git a/spec/support/rspec_order_todo.yml b/spec/support/rspec_order_todo.yml index 039459e6cd2..74b80c4864c 100644 --- a/spec/support/rspec_order_todo.yml +++ b/spec/support/rspec_order_todo.yml @@ -1575,7 +1575,6 @@ - './ee/spec/lib/omni_auth/strategies/group_saml_spec.rb' - './ee/spec/lib/omni_auth/strategies/kerberos_spec.rb' - './ee/spec/lib/peek/views/elasticsearch_spec.rb' -- './ee/spec/lib/sidebars/groups/menus/administration_menu_spec.rb' - './ee/spec/lib/sidebars/groups/menus/analytics_menu_spec.rb' - './ee/spec/lib/sidebars/groups/menus/epics_menu_spec.rb' - './ee/spec/lib/sidebars/groups/menus/security_compliance_menu_spec.rb' diff --git a/spec/support/shared_contexts/navbar_structure_context.rb b/spec/support/shared_contexts/navbar_structure_context.rb index 3af0b30bca0..8b03d8a2770 100644 --- a/spec/support/shared_contexts/navbar_structure_context.rb +++ b/spec/support/shared_contexts/navbar_structure_context.rb @@ -140,6 +140,7 @@ RSpec.shared_context 'group navbar structure' do _('CI/CD'), _('Applications'), _('Packages and registries'), + s_('UsageQuota|Usage Quotas'), _('Domain Verification') ] } @@ -154,15 +155,6 @@ RSpec.shared_context 'group navbar structure' do } end - let(:administration_nav_item) do - { - nav_item: _('Administration'), - nav_sub_items: [ - s_('UsageQuota|Usage Quotas') - ] - } - end - let(:security_and_compliance_nav_item) do { nav_item: _('Security and Compliance'), diff --git a/spec/support/shared_examples/lib/sidebars/user_profile/user_profile_menus_shared_examples.rb b/spec/support/shared_examples/lib/sidebars/user_profile/user_profile_menus_shared_examples.rb new file mode 100644 index 00000000000..92c51a03b31 --- /dev/null +++ b/spec/support/shared_examples/lib/sidebars/user_profile/user_profile_menus_shared_examples.rb @@ -0,0 +1,89 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'User profile menu' do |title:, active_route:| + let_it_be(:current_user) { build(:user) } + let_it_be(:user) { build(:user) } + + let(:context) { Sidebars::Context.new(current_user: current_user, container: user) } + + subject { described_class.new(context) } + + it 'does not contain any sub menu' do + expect(subject.has_items?).to be false + end + + it 'renders the correct link' do + expect(subject.link).to match link + end + + it 'renders the correct title' do + expect(subject.title).to eq title + end + + it 'defines correct active route' do + expect(subject.active_routes[:path]).to be active_route + end + + it 'renders if user is logged in' do + expect(subject.render?).to be true + end + + [:blocked, :banned].each do |trait| + context "when viewed user is #{trait}" do + let_it_be(:viewed_user) { build(:user, trait) } + let(:context) { Sidebars::Context.new(current_user: user, container: viewed_user) } + + context 'when user is not logged in' do + it 'is not allowed to view the menu item' do + expect(described_class.new(Sidebars::Context.new(current_user: nil, + container: viewed_user)).render?).to be false + end + end + + context 'when current user has permission' do + before do + allow(Ability).to receive(:allowed?).with(user, :read_user_profile, viewed_user).and_return(true) + end + + it 'is allowed to view the menu item' do + expect(described_class.new(context).render?).to be true + end + end + + context 'when current user does not have permission' do + it 'is not allowed to view the menu item' do + expect(described_class.new(context).render?).to be false + end + end + end + end +end + +RSpec.shared_examples 'Followers/followees counts' do |symbol| + let_it_be(:current_user) { build(:user) } + let_it_be(:user) { build(:user) } + + let(:context) { Sidebars::Context.new(current_user: current_user, container: user) } + + subject { described_class.new(context) } + + context 'when there are items' do + before do + allow(user).to receive(symbol).and_return([1, 2]) + end + + it 'renders the pill' do + expect(subject.has_pill?).to be(true) + end + + it 'returns the count' do + expect(subject.pill_count).to be(2) + end + end + + context 'when there are no items' do + it 'does not render the pill' do + expect(subject.has_pill?).to be(false) + end + end +end diff --git a/yarn.lock b/yarn.lock index 70556d0941b..a60bc36fced 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1157,10 +1157,10 @@ resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.17.11.tgz#9be796d93ae27b636da32d960899a4912bca27a1" integrity sha512-N9vXqLP3eRL8BqSy8yn4Y98cZI2pZ8fyuHx6lKjiG2WABpT2l01TXdzq5Ma2ZUBzfB7tx5dXVhge8X9u0S70ZQ== -"@eslint/eslintrc@^1.4.1": - version "1.4.1" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.4.1.tgz#af58772019a2d271b7e2d4c23ff4ddcba3ccfb3e" - integrity sha512-XXrH9Uarn0stsyldqDYq8r++mROmWRI1xKMXa640Bb//SY1+ECYX6VzT6Lcx5frD0V30XieqJ0oX9I2Xj5aoMA== +"@eslint/eslintrc@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.0.0.tgz#943309d8697c52fc82c076e90c1c74fbbe69dbff" + integrity sha512-fluIaaV+GyV24CCu/ggiHdV+j4RNh85yQnAYS/G2mZODZgGmmlrgCydjUcV3YvxCm9x8nMAfThsqTni4KiXT4A== dependencies: ajv "^6.12.4" debug "^4.3.2" @@ -1172,6 +1172,11 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" +"@eslint/js@8.35.0": + version "8.35.0" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.35.0.tgz#b7569632b0b788a0ca0e438235154e45d42813a7" + integrity sha512-JXdzbRiWclLVoD8sNUjR443VVlYqiYmDVT6rGUEIEHU5YJW0gaVZwV2xgM7D4arkvASqD0IlLUVjHiFuxaftRw== + "@gitlab/at.js@1.5.7": version "1.5.7" resolved "https://registry.yarnpkg.com/@gitlab/at.js/-/at.js-1.5.7.tgz#1ee6f838cc4410a1d797770934df91d90df8179e" @@ -5664,12 +5669,13 @@ eslint-visitor-keys@^3.3.0: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz#f6480fa6b1f30efe2d1968aa8ac745b862469826" integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA== -eslint@8.34.0: - version "8.34.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.34.0.tgz#fe0ab0ef478104c1f9ebc5537e303d25a8fb22d6" - integrity sha512-1Z8iFsucw+7kSqXNZVslXS8Ioa4u2KM7GPwuKtkTFAqZ/cHMcEaR+1+Br0wLlot49cNxIiZk5wp8EAbPcYZxTg== +eslint@8.35.0: + version "8.35.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.35.0.tgz#fffad7c7e326bae606f0e8f436a6158566d42323" + integrity sha512-BxAf1fVL7w+JLRQhWl2pzGeSiGqbWumV4WNvc9Rhp6tiCtm4oHnyPBSEtMGZwrQgudFQ+otqzWoPB7x+hxoWsw== dependencies: - "@eslint/eslintrc" "^1.4.1" + "@eslint/eslintrc" "^2.0.0" + "@eslint/js" "8.35.0" "@humanwhocodes/config-array" "^0.11.8" "@humanwhocodes/module-importer" "^1.0.1" "@nodelib/fs.walk" "^1.2.8" @@ -5683,7 +5689,7 @@ eslint@8.34.0: eslint-utils "^3.0.0" eslint-visitor-keys "^3.3.0" espree "^9.4.0" - esquery "^1.4.0" + esquery "^1.4.2" esutils "^2.0.2" fast-deep-equal "^3.1.3" file-entry-cache "^6.0.1" @@ -5730,6 +5736,13 @@ esquery@^1.4.0: dependencies: estraverse "^5.1.0" +esquery@^1.4.2: + version "1.5.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.5.0.tgz#6ce17738de8577694edd7361c57182ac8cb0db0b" + integrity sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg== + dependencies: + estraverse "^5.1.0" + esrecurse@^4.1.0, esrecurse@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" |