diff --git a/.gitlab/CODEOWNERS b/.gitlab/CODEOWNERS index c0c2499a257..acacd7c6c04 100644 --- a/.gitlab/CODEOWNERS +++ b/.gitlab/CODEOWNERS @@ -451,7 +451,7 @@ lib/gitlab/checks/** /doc/administration/monitoring/prometheus/index.md @axil /doc/administration/monitoring/prometheus/pgbouncer_exporter.md @aqualls /doc/administration/monitoring/prometheus/postgres_exporter.md @aqualls -/doc/administration/monitoring/prometheus/registry_exporter.md @marcel.amirault +/doc/administration/monitoring/prometheus/registry_exporter.md @phillipwells /doc/administration/monitoring/prometheus/web_exporter.md @jglassman1 /doc/administration/nfs.md @axil /doc/administration/object_storage.md @axil @@ -461,6 +461,7 @@ lib/gitlab/checks/** /doc/administration/operations/moving_repositories.md @eread /doc/administration/package_information/ @axil /doc/administration/packages/ @marcel.amirault +/doc/administration/packages/index.md @phillipwells /doc/administration/polling.md @axil /doc/administration/postgresql/ @aqualls /doc/administration/postgresql/multiple_databases.md @lciutacu @@ -583,8 +584,8 @@ lib/gitlab/checks/** /doc/api/notification_settings.md @msedlakjakubowski /doc/api/oauth2.md @jglassman1 /doc/api/openapi/ @eread @ashrafkhamis -/doc/api/packages.md @marcel.amirault -/doc/api/packages/ @marcel.amirault +/doc/api/packages.md @phillipwells +/doc/api/packages/ @phillipwells /doc/api/personal_access_tokens.md @eread /doc/api/pipeline_schedules.md @marcel.amirault /doc/api/pipeline_triggers.md @marcel.amirault @@ -657,6 +658,7 @@ lib/gitlab/checks/** /doc/ci/docker/using_docker_images.md @fneill /doc/ci/environments/ @phillipwells /doc/ci/examples/deployment/ @phillipwells +/doc/ci/examples/semantic-release.md @phillipwells /doc/ci/interactive_web_terminal/ @fneill /doc/ci/large_repositories/ @fneill /doc/ci/resource_groups/ @phillipwells @@ -730,7 +732,9 @@ lib/gitlab/checks/** /doc/development/navigation_sidebar.md @sselhorn /doc/development/omnibus.md @axil /doc/development/organization/ @lciutacu -/doc/development/packages/ @marcel.amirault +/doc/development/packages/ @phillipwells +/doc/development/packages/cleanup_policies.md @marcel.amirault +/doc/development/packages/dependency_proxy.md @marcel.amirault /doc/development/permissions.md @jglassman1 /doc/development/policies.md @jglassman1 /doc/development/project_templates.md @aqualls @msedlakjakubowski @@ -833,7 +837,7 @@ lib/gitlab/checks/** /doc/user/admin_area/settings/incident_management_rate_limits.md @msedlakjakubowski /doc/user/admin_area/settings/index.md @aqualls @msedlakjakubowski /doc/user/admin_area/settings/instance_template_repository.md @aqualls @msedlakjakubowski -/doc/user/admin_area/settings/package_registry_rate_limits.md @marcel.amirault +/doc/user/admin_area/settings/package_registry_rate_limits.md @phillipwells /doc/user/admin_area/settings/project_integration_management.md @eread @ashrafkhamis /doc/user/admin_area/settings/push_event_activities_limit.md @aqualls @msedlakjakubowski /doc/user/admin_area/settings/rate_limit_on_issues_creation.md @msedlakjakubowski @@ -883,7 +887,10 @@ lib/gitlab/checks/** /doc/user/okrs.md @msedlakjakubowski /doc/user/operations_dashboard/ @phillipwells /doc/user/organization/ @lciutacu -/doc/user/packages/ @marcel.amirault +/doc/user/packages/ @phillipwells +/doc/user/packages/container_registry/ @marcel.amirault +/doc/user/packages/dependency_proxy/ @marcel.amirault +/doc/user/packages/harbor_container_registry/ @marcel.amirault /doc/user/permissions.md @jglassman1 /doc/user/product_analytics/ @lciutacu /doc/user/profile/account/ @jglassman1 diff --git a/.rubocop_todo/layout/argument_alignment.yml b/.rubocop_todo/layout/argument_alignment.yml index 4299f928a2b..6fac8aff19c 100644 --- a/.rubocop_todo/layout/argument_alignment.yml +++ b/.rubocop_todo/layout/argument_alignment.yml @@ -977,14 +977,6 @@ Layout/ArgumentAlignment: - 'ee/app/models/incident_management/escalation_rule.rb' - 'ee/app/models/integrations/github/status_notifier.rb' - 'ee/app/models/status_page/project_setting.rb' - - 'ee/app/models/vulnerabilities/external_issue_link.rb' - - 'ee/app/models/vulnerabilities/feedback.rb' - - 'ee/app/models/vulnerabilities/finding/evidence.rb' - - 'ee/app/models/vulnerabilities/issue_link.rb' - - 'ee/app/models/vulnerabilities/merge_request_link.rb' - - 'ee/app/models/vulnerabilities/read.rb' - - 'ee/app/models/vulnerabilities/stat_diff.rb' - - 'ee/app/models/vulnerabilities/statistic.rb' - 'ee/app/services/analytics/devops_adoption/enabled_namespaces/bulk_find_or_create_service.rb' - 'ee/app/services/audit_events/streaming/event_type_filters/destroy_service.rb' - 'ee/app/services/auto_merge/merge_train_service.rb' diff --git a/.rubocop_todo/rspec/factory_bot/avoid_create.yml b/.rubocop_todo/rspec/factory_bot/avoid_create.yml index e994e3e516c..d98ad31f754 100644 --- a/.rubocop_todo/rspec/factory_bot/avoid_create.yml +++ b/.rubocop_todo/rspec/factory_bot/avoid_create.yml @@ -586,8 +586,8 @@ RSpec/FactoryBot/AvoidCreate: - 'spec/views/projects/hooks/edit.html.haml_spec.rb' - 'spec/views/projects/hooks/index.html.haml_spec.rb' - 'spec/views/projects/imports/new.html.haml_spec.rb' - - 'spec/views/projects/issues/_issue.html.haml_spec.rb' - 'spec/views/projects/issues/_service_desk_info_content.html.haml_spec.rb' + - 'spec/views/projects/issues/service_desk/_issue.html.haml_spec.rb' - 'spec/views/projects/issues/show.html.haml_spec.rb' - 'spec/views/projects/jobs/_build.html.haml_spec.rb' - 'spec/views/projects/jobs/_generic_commit_status.html.haml_spec.rb' diff --git a/Gemfile b/Gemfile index db1a0d640e5..02131fd2388 100644 --- a/Gemfile +++ b/Gemfile @@ -193,7 +193,7 @@ gem 'asciidoctor', '~> 2.0.18' gem 'asciidoctor-include-ext', '~> 0.4.0', require: false gem 'asciidoctor-plantuml', '~> 0.0.16' gem 'asciidoctor-kroki', '~> 0.8.0', require: false -gem 'rouge', '~> 4.1.0' +gem 'rouge', '~> 4.1.2' gem 'truncato', '~> 0.7.12' gem 'nokogiri', '~> 1.15' diff --git a/Gemfile.checksum b/Gemfile.checksum index 343678274f1..0ffbd70075e 100644 --- a/Gemfile.checksum +++ b/Gemfile.checksum @@ -518,7 +518,7 @@ {"name":"rexml","version":"3.2.5","platform":"ruby","checksum":"a33c3bf95fda7983ec7f05054f3a985af41dbc25a0339843bd2479e93cabb123"}, {"name":"rinku","version":"2.0.0","platform":"ruby","checksum":"3e695aaf9f24baba3af45823b5c427b58a624582132f18482320e2737f9f8a85"}, {"name":"rotp","version":"6.2.0","platform":"ruby","checksum":"239a2eefba6f1bd4157b2c735d0f975598e0ef94823eea2f35d103d2e5cc0787"}, -{"name":"rouge","version":"4.1.1","platform":"ruby","checksum":"41cc3ed28de7a9f5c0145bcdbeae8f5c16133065d570e21393aac935a235fd4b"}, +{"name":"rouge","version":"4.1.2","platform":"ruby","checksum":"3b4ca60e4ac6e36be2deb0359cba04278ba15bdd2b1fbbb66bbc19cae517d55f"}, {"name":"rqrcode","version":"0.7.0","platform":"ruby","checksum":"8b3a5cba9cc199ba2d781a7c767cb55679f29a3621aa0506a799cec3760d16a1"}, {"name":"rqrcode-rails3","version":"0.1.7","platform":"ruby","checksum":"6f0582f26485123e5ed6f2a8a2871f00d86d353e0f58c8429a5a13212bcf48c4"}, {"name":"rspec","version":"3.12.0","platform":"ruby","checksum":"ccc41799a43509dc0be84070e3f0410ac95cbd480ae7b6c245543eb64162399c"}, diff --git a/Gemfile.lock b/Gemfile.lock index a6d9a889e2d..793fedad216 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1270,7 +1270,7 @@ GEM rexml (3.2.5) rinku (2.0.0) rotp (6.2.0) - rouge (4.1.1) + rouge (4.1.2) rqrcode (0.7.0) chunky_png rqrcode-rails3 (0.1.7) @@ -1885,7 +1885,7 @@ DEPENDENCIES responders (~> 3.0) retriable (~> 3.1.2) rexml (~> 3.2.5) - rouge (~> 4.1.0) + rouge (~> 4.1.2) rqrcode-rails3 (~> 0.1.7) rspec-benchmark (~> 0.6.0) rspec-parameterized (~> 1.0) diff --git a/app/assets/javascripts/ci/artifacts/utils.js b/app/assets/javascripts/ci/artifacts/utils.js index 2ed78261ade..74ade7d48aa 100644 --- a/app/assets/javascripts/ci/artifacts/utils.js +++ b/app/assets/javascripts/ci/artifacts/utils.js @@ -4,7 +4,7 @@ import { ARCHIVE_FILE_TYPE, METADATA_FILE_TYPE, JOB_STATUS_GROUP_SUCCESS } from export const totalArtifactsSizeForJob = (job) => numberToHumanSize( job.artifacts.nodes - .map((artifact) => artifact.size) + .map((artifact) => Number(artifact.size)) .reduce((total, artifact) => total + artifact, 0), ); diff --git a/app/assets/javascripts/usage_quotas/storage/components/project_storage_detail.vue b/app/assets/javascripts/usage_quotas/storage/components/project_storage_detail.vue index beff3b4c0c3..ce487beca07 100644 --- a/app/assets/javascripts/usage_quotas/storage/components/project_storage_detail.vue +++ b/app/assets/javascripts/usage_quotas/storage/components/project_storage_detail.vue @@ -82,7 +82,15 @@ export default { />

- {{ item.storageType.name }} + {{ item.storageType.name }} + import { numberToHumanSize } from '~/lib/utils/number_utils'; -import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { PROJECT_STORAGE_TYPES } from '../constants'; import { descendingStorageUsageSort } from '../utils'; export default { - mixins: [glFeatureFlagMixin()], + name: 'UsageGraph', props: { rootStorageStatistics: { required: true, @@ -36,49 +35,49 @@ export default { return [ { - id: 'repositorySize', + id: 'repository', style: this.usageStyle(this.barRatio(repositorySize)), class: 'gl-bg-data-viz-blue-500', size: repositorySize, }, { - id: 'lfsObjectsSize', + id: 'lfsObjects', style: this.usageStyle(this.barRatio(lfsObjectsSize)), class: 'gl-bg-data-viz-orange-600', size: lfsObjectsSize, }, { - id: 'packagesSize', + id: 'packages', style: this.usageStyle(this.barRatio(packagesSize)), class: 'gl-bg-data-viz-aqua-500', size: packagesSize, }, { - id: 'containerRegistrySize', + id: 'containerRegistry', style: this.usageStyle(this.barRatio(containerRegistrySize)), class: 'gl-bg-data-viz-aqua-800', size: containerRegistrySize, }, { - id: 'buildArtifactsSize', + id: 'buildArtifacts', style: this.usageStyle(this.barRatio(buildArtifactsSize)), class: 'gl-bg-data-viz-green-500', size: buildArtifactsSize, }, { - id: 'pipelineArtifactsSize', + id: 'pipelineArtifacts', style: this.usageStyle(this.barRatio(pipelineArtifactsSize)), class: 'gl-bg-data-viz-green-800', size: pipelineArtifactsSize, }, { - id: 'wikiSize', + id: 'wiki', style: this.usageStyle(this.barRatio(wikiSize)), class: 'gl-bg-data-viz-magenta-500', size: wikiSize, }, { - id: 'snippetsSize', + id: 'snippets', style: this.usageStyle(this.barRatio(snippetsSize)), class: 'gl-bg-data-viz-orange-800', size: snippetsSize, diff --git a/app/assets/javascripts/usage_quotas/storage/constants.js b/app/assets/javascripts/usage_quotas/storage/constants.js index 8e3eaff4496..f08e8db26b9 100644 --- a/app/assets/javascripts/usage_quotas/storage/constants.js +++ b/app/assets/javascripts/usage_quotas/storage/constants.js @@ -26,44 +26,44 @@ export const PROJECT_TABLE_LABEL_USAGE = s__('UsageQuota|Usage'); export const PROJECT_STORAGE_TYPES = [ { - id: 'containerRegistrySize', + id: 'containerRegistry', name: __('Container Registry'), description: s__( 'UsageQuota|Gitlab-integrated Docker Container Registry for storing Docker Images.', ), }, { - id: 'buildArtifactsSize', + id: 'buildArtifacts', name: __('Job artifacts'), description: s__('UsageQuota|Job artifacts created by CI/CD.'), }, { - id: 'pipelineArtifactsSize', + id: 'pipelineArtifacts', name: __('Pipeline artifacts'), description: s__('UsageQuota|Pipeline artifacts created by CI/CD.'), }, { - id: 'lfsObjectsSize', + id: 'lfsObjects', name: __('LFS'), description: s__('UsageQuota|Audio samples, videos, datasets, and graphics.'), }, { - id: 'packagesSize', + id: 'packages', name: __('Packages'), description: s__('UsageQuota|Code packages and container images.'), }, { - id: 'repositorySize', + id: 'repository', name: __('Repository'), description: s__('UsageQuota|Git repository.'), }, { - id: 'snippetsSize', + id: 'snippets', name: __('Snippets'), description: s__('UsageQuota|Shared bits of code and text.'), }, { - id: 'wikiSize', + id: 'wiki', name: __('Wiki'), description: s__('UsageQuota|Wiki content.'), }, diff --git a/app/assets/javascripts/usage_quotas/storage/queries/project_storage.query.graphql b/app/assets/javascripts/usage_quotas/storage/queries/project_storage.query.graphql index d254f576219..85a181d3e01 100644 --- a/app/assets/javascripts/usage_quotas/storage/queries/project_storage.query.graphql +++ b/app/assets/javascripts/usage_quotas/storage/queries/project_storage.query.graphql @@ -1,6 +1,14 @@ query getProjectStorageStatistics($fullPath: ID!) { project(fullPath: $fullPath) { id + statisticsDetailsPaths { + containerRegistry + buildArtifacts + packages + repository + snippets + wiki + } statistics { containerRegistrySize buildArtifactsSize diff --git a/app/assets/javascripts/usage_quotas/storage/utils.js b/app/assets/javascripts/usage_quotas/storage/utils.js index 443788f650d..0460cd0a9b2 100644 --- a/app/assets/javascripts/usage_quotas/storage/utils.js +++ b/app/assets/javascripts/usage_quotas/storage/utils.js @@ -1,17 +1,23 @@ import { numberToHumanSize } from '~/lib/utils/number_utils'; import { PROJECT_STORAGE_TYPES } from './constants'; -export const getStorageTypesFromProjectStatistics = (projectStatistics, helpLinks = {}) => +export const getStorageTypesFromProjectStatistics = ( + projectStatistics, + helpLinks = {}, + statisticsDetailsPaths = {}, +) => PROJECT_STORAGE_TYPES.reduce((types, currentType) => { - const helpPathKey = currentType.id.replace(`Size`, ``); - const helpPath = helpLinks[helpPathKey]; + const helpPath = helpLinks[currentType.id]; + const value = projectStatistics[`${currentType.id}Size`]; + const detailsPath = statisticsDetailsPaths[currentType.id]; return types.concat({ storageType: { ...currentType, helpPath, + detailsPath, }, - value: projectStatistics[currentType.id], + value, }); }, []); @@ -27,7 +33,11 @@ export const parseGetProjectStorageResults = (data, helpLinks) => { return {}; } const { storageSize } = projectStatistics; - const storageTypes = getStorageTypesFromProjectStatistics(projectStatistics, helpLinks); + const storageTypes = getStorageTypesFromProjectStatistics( + projectStatistics, + helpLinks, + data?.project?.statisticsDetailsPaths, + ); return { storage: { diff --git a/app/models/personal_access_token.rb b/app/models/personal_access_token.rb index 40370359c01..80825c4bf08 100644 --- a/app/models/personal_access_token.rb +++ b/app/models/personal_access_token.rb @@ -79,10 +79,6 @@ class PersonalAccessToken < ApplicationRecord fuzzy_search(query, [:name]) end - def project_access_token? - user&.project_bot? - end - protected def validate_scopes diff --git a/app/services/resource_access_tokens/create_service.rb b/app/services/resource_access_tokens/create_service.rb index e6f6f63a116..b184c2f8f58 100644 --- a/app/services/resource_access_tokens/create_service.rb +++ b/app/services/resource_access_tokens/create_service.rb @@ -17,6 +17,8 @@ module ResourceAccessTokens access_level = params[:access_level] || Gitlab::Access::MAINTAINER return error("Could not provision owner access to project access token") if do_not_allow_owner_access_level_for_project_bot?(access_level) + return error(s_('AccessTokens|Access token limit reached')) if reached_access_token_limit? + user = create_user return error(user.errors.full_messages.to_sentence) unless user.persisted? @@ -45,6 +47,10 @@ module ResourceAccessTokens attr_reader :resource_type, :resource + def reached_access_token_limit? + false + end + def username_and_email_generator Gitlab::Utils::UsernameAndEmailGenerator.new( username_prefix: "#{resource_type}_#{resource.id}_bot", diff --git a/app/views/admin/sessions/new.html.haml b/app/views/admin/sessions/new.html.haml index 4fc30cbaecf..7301b0f6e04 100644 --- a/app/views/admin/sessions/new.html.haml +++ b/app/views/admin/sessions/new.html.haml @@ -8,7 +8,7 @@ - if any_form_based_providers_enabled? = render 'devise/shared/tabs_ldap', show_password_form: allow_admin_mode_password_authentication_for_web?, render_signup_link: false - else - = render 'devise/shared/tab_single', tab_title: page_title + = render 'devise/shared/tab_single', tab_title: page_title if Feature.disabled?(:restyle_login_page, @project) .tab-content - if allow_admin_mode_password_authentication_for_web? || ldap_sign_in_enabled? || crowd_enabled? = render 'admin/sessions/signin_box' diff --git a/app/views/admin/sessions/two_factor.html.haml b/app/views/admin/sessions/two_factor.html.haml index 69f8a8a3a97..bfe66e2477e 100644 --- a/app/views/admin/sessions/two_factor.html.haml +++ b/app/views/admin/sessions/two_factor.html.haml @@ -5,11 +5,10 @@ .col-md-5.new-session-forms-container .login-page #signin-container{ class: ('borderless' if Feature.enabled?(:restyle_login_page, @project)) } - = render 'devise/shared/tab_single', tab_title: _('Enter admin mode') - .tab-content - .login-box.tab-pane.gl-p-5.active{ id: 'login-pane', role: 'tabpanel' } - .login-body - - if current_user.two_factor_enabled? - = render 'admin/sessions/two_factor_otp' - - if current_user.two_factor_webauthn_enabled? - = render 'authentication/authenticate', render_remember_me: false, target_path: admin_session_path + = render 'devise/shared/tab_single', tab_title: _('Enter admin mode') if Feature.disabled?(:restyle_login_page, @project) + .login-box.gl-p-5 + .login-body + - if current_user.two_factor_enabled? + = render 'admin/sessions/two_factor_otp' + - if current_user.two_factor_webauthn_enabled? + = render 'authentication/authenticate', render_remember_me: false, target_path: admin_session_path diff --git a/app/views/dashboard/projects/_blank_state_admin_welcome.html.haml b/app/views/dashboard/projects/_blank_state_admin_welcome.html.haml index 855177fd836..94f956896d6 100644 --- a/app/views/dashboard/projects/_blank_state_admin_welcome.html.haml +++ b/app/views/dashboard/projects/_blank_state_admin_welcome.html.haml @@ -4,7 +4,7 @@ - if has_start_trial? = render_if_exists "dashboard/projects/blank_state_ee_trial" - = link_to new_project_path, class: link_classes do + = link_to new_project_path, class: link_classes, data: { qa_selector: 'new_project_button' } do .blank-state-icon = custom_icon("add_new_project", size: 50) .blank-state-body.gl-sm-pl-6 diff --git a/app/views/dashboard/projects/_blank_state_welcome.html.haml b/app/views/dashboard/projects/_blank_state_welcome.html.haml index 2b13e2ba9a5..08b914a218d 100644 --- a/app/views/dashboard/projects/_blank_state_welcome.html.haml +++ b/app/views/dashboard/projects/_blank_state_welcome.html.haml @@ -2,7 +2,7 @@ .gl-display-flex.gl-flex-wrap.gl-justify-content-space-between - if current_user.can_create_project? - = link_to new_project_path, class: link_classes do + = link_to new_project_path, class: link_classes, data: { qa_selector: 'new_project_button' } do .blank-state-icon = custom_icon("add_new_project", size: 50) .blank-state-body.gl-sm-pl-6 diff --git a/app/views/projects/settings/access_tokens/_form.html.haml b/app/views/projects/settings/access_tokens/_form.html.haml new file mode 100644 index 00000000000..919462a0f62 --- /dev/null +++ b/app/views/projects/settings/access_tokens/_form.html.haml @@ -0,0 +1,14 @@ +- type = local_assigns.fetch(:type) + += render 'shared/access_tokens/form', + ajax: true, + type: type, + path: project_settings_access_tokens_path(@project), + resource: @project, + token: @resource_access_token, + scopes: @scopes, + access_levels: ProjectMember.permissible_access_level_roles(current_user, @project), + default_access_level: Gitlab::Access::GUEST, + prefix: :resource_access_token, + description_prefix: :project_access_token, + help_path: help_page_path('user/project/settings/project_access_tokens', anchor: 'scopes-for-a-project-access-token') diff --git a/app/views/projects/settings/access_tokens/index.html.haml b/app/views/projects/settings/access_tokens/index.html.haml index 26c08fcdfe4..df517b5d642 100644 --- a/app/views/projects/settings/access_tokens/index.html.haml +++ b/app/views/projects/settings/access_tokens/index.html.haml @@ -27,18 +27,8 @@ #js-new-access-token-app{ data: { access_token_type: type } } - if current_user.can?(:create_resource_access_tokens, @project) - = render 'shared/access_tokens/form', - ajax: true, - type: type, - path: project_settings_access_tokens_path(@project), - resource: @project, - token: @resource_access_token, - scopes: @scopes, - access_levels: ProjectMember.permissible_access_level_roles(current_user, @project), - default_access_level: Gitlab::Access::GUEST, - prefix: :resource_access_token, - description_prefix: :project_access_token, - help_path: help_page_path('user/project/settings/project_access_tokens', anchor: 'scopes-for-a-project-access-token') + = render_if_exists 'projects/settings/access_tokens/form', + type: type #js-access-token-table-app{ data: { access_token_type: type, access_token_type_plural: type_plural, initial_active_access_tokens: @active_access_tokens.to_json, no_active_tokens_message: _('This project has no active access tokens.'), show_role: true } } diff --git a/config/feature_flags/development/normalize_links_for_sanitizing.yml b/config/feature_flags/development/normalize_links_for_sanitizing.yml new file mode 100644 index 00000000000..204a401f279 --- /dev/null +++ b/config/feature_flags/development/normalize_links_for_sanitizing.yml @@ -0,0 +1,8 @@ +--- +name: normalize_links_for_sanitizing +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/119040 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/413779 +milestone: '16.1' +type: development +group: group::source code +default_enabled: false diff --git a/doc/development/i18n/index.md b/doc/development/i18n/index.md index 0f9688ec26d..b7e385a774a 100644 --- a/doc/development/i18n/index.md +++ b/doc/development/i18n/index.md @@ -31,7 +31,7 @@ See [Externalization for GitLab](externalization.md). ## Translate strings -The translation process is managed at [https://translate.gitlab.com](https://translate.gitlab.com) +The translation process is managed at [https://crowdin.com/project/gitlab-ee](https://crowdin.com/project/gitlab-ee) using [Crowdin](https://crowdin.com/). You must create a Crowdin account before you can submit translations. Once you are signed in, select the language you wish to contribute translations to. diff --git a/lib/gitlab/utils/sanitize_node_link.rb b/lib/gitlab/utils/sanitize_node_link.rb index 9c34302f75e..26b409893ee 100644 --- a/lib/gitlab/utils/sanitize_node_link.rb +++ b/lib/gitlab/utils/sanitize_node_link.rb @@ -51,7 +51,9 @@ module Gitlab begin node[attr] = node[attr].strip + uri = Addressable::URI.parse(node[attr]) + uri = uri.normalize if ::Feature.enabled?(:normalize_links_for_sanitizing) next unless uri.scheme next if safe_protocol?(uri.scheme) diff --git a/lib/tasks/gitlab/tw/codeowners.rake b/lib/tasks/gitlab/tw/codeowners.rake index 3b45b217b08..afe2c564247 100644 --- a/lib/tasks/gitlab/tw/codeowners.rake +++ b/lib/tasks/gitlab/tw/codeowners.rake @@ -57,7 +57,7 @@ namespace :tw do # CodeOwnerRule.new('Observability', ''), CodeOwnerRule.new('Optimize', '@lciutacu'), CodeOwnerRule.new('Organization', '@lciutacu'), - CodeOwnerRule.new('Package Registry', '@marcel.amirault'), + CodeOwnerRule.new('Package Registry', '@phillipwells'), CodeOwnerRule.new('Pipeline Authoring', '@marcel.amirault'), CodeOwnerRule.new('Pipeline Execution', '@marcel.amirault'), CodeOwnerRule.new('Pipeline Security', '@marcel.amirault'), diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 3c261e0e4fa..27c345d6dd7 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -1868,9 +1868,6 @@ msgstr "" msgid "AI-generated test file" msgstr "" -msgid "AISummary|Experiment" -msgstr "" - msgid "AISummary|Generates a summary of all comments" msgstr "" @@ -1898,9 +1895,6 @@ msgstr "" msgid "AI|Code Explanation" msgstr "" -msgid "AI|Describe the issue" -msgstr "" - msgid "AI|Enabling these features is your acceptance of the %{link_start}GitLab Testing Agreement%{link_end}." msgstr "" @@ -1916,7 +1910,7 @@ msgstr "" msgid "AI|Features that use third-party AI services require transmission of data, including personal data." msgstr "" -msgid "AI|For example: It should be possible to forecast into the future using our value stream analytics charts. This would allow organizations to better understand how they are trending on important metrics." +msgid "AI|For example: Organizations should be able to forecast into the future by using value stream analytics charts. This feature would help them understand how their metrics are trending." msgstr "" msgid "AI|Helpful" @@ -1925,6 +1919,9 @@ msgstr "" msgid "AI|I don't see how I can help. Please give better instructions!" msgstr "" +msgid "AI|Populate issue description" +msgstr "" + msgid "AI|Responses generated by AI" msgstr "" @@ -1958,6 +1955,9 @@ msgstr "" msgid "AI|What does the selected code mean?" msgstr "" +msgid "AI|Write a brief description and have AI fill in the details." +msgstr "" + msgid "AI|Write a summary to fill out the selected issue template" msgstr "" @@ -2366,6 +2366,9 @@ msgstr "" msgid "AccessTokens|Access Tokens" msgstr "" +msgid "AccessTokens|Access token limit reached" +msgstr "" + msgid "AccessTokens|Are you sure?" msgstr "" @@ -2423,6 +2426,9 @@ msgstr "" msgid "AccessTokens|You can generate a personal access token for each application you use that needs access to the GitLab API." msgstr "" +msgid "AccessTokens|You can only have one active project access token with a trial license. You cannot generate a new token until the existing token is deleted, or you upgrade your subscription." +msgstr "" + msgid "AccessTokens|Your feed token authenticates you when your RSS reader loads a personalized RSS feed or when your calendar application loads a personalized calendar. It is visible in those feed URLs." msgstr "" @@ -40108,9 +40114,18 @@ msgstr "" msgid "ScanResultPolicy|Choose criteria type" msgstr "" +msgid "ScanResultPolicy|Except" +msgstr "" + +msgid "ScanResultPolicy|License is:" +msgstr "" + msgid "ScanResultPolicy|License scanning allows only one criteria: Status" msgstr "" +msgid "ScanResultPolicy|Matching" +msgstr "" + msgid "ScanResultPolicy|Maximum number of severity-criteria is one" msgstr "" @@ -40132,6 +40147,9 @@ msgstr "" msgid "ScanResultPolicy|Select a scan type before adding criteria" msgstr "" +msgid "ScanResultPolicy|Select license types" +msgstr "" + msgid "ScanResultPolicy|Severity is:" msgstr "" @@ -40141,24 +40159,15 @@ msgstr "" msgid "ScanResultPolicy|When %{scanType} %{scanners} runs against the %{branches} and find(s) %{vulnerabilitiesNumber} %{boldDescription} of the following criteria:" msgstr "" -msgid "ScanResultPolicy|When %{scanType} find any license %{matchType} %{licenseType} in an open merge request targeting the %{branches} and the licences match all of the following criteria" +msgid "ScanResultPolicy|When %{scanType} in an open merge request targeting the %{branches} and the licenses match all of the following criteria:" msgstr "" msgid "ScanResultPolicy|When %{scanners} find scanner specified conditions in an open merge request targeting the %{branches} and match %{boldDescription} of the following criteria" msgstr "" -msgid "ScanResultPolicy|except" -msgstr "" - msgid "ScanResultPolicy|license status" msgstr "" -msgid "ScanResultPolicy|license type" -msgstr "" - -msgid "ScanResultPolicy|matching" -msgstr "" - msgid "ScanResultPolicy|matching type" msgstr "" diff --git a/qa/qa/page/dashboard/projects.rb b/qa/qa/page/dashboard/projects.rb index 10529ed69e1..4a81d34b32e 100644 --- a/qa/qa/page/dashboard/projects.rb +++ b/qa/qa/page/dashboard/projects.rb @@ -17,6 +17,14 @@ module QA element :new_project_button end + view 'app/views/dashboard/projects/_blank_state_welcome.html.haml' do + element :new_project_button + end + + view 'app/views/dashboard/projects/_blank_state_admin_welcome.html.haml' do + element :new_project_button + end + def has_project_with_access_role?(project_name, access_role) within_element(:project_content, text: project_name) do has_element?(:user_role_content, text: access_role) diff --git a/spec/frontend/ci/artifacts/utils_spec.js b/spec/frontend/ci/artifacts/utils_spec.js new file mode 100644 index 00000000000..17b4a9f162b --- /dev/null +++ b/spec/frontend/ci/artifacts/utils_spec.js @@ -0,0 +1,16 @@ +import getJobArtifactsResponse from 'test_fixtures/graphql/ci/artifacts/graphql/queries/get_job_artifacts.query.graphql.json'; +import { numberToHumanSize } from '~/lib/utils/number_utils'; +import { totalArtifactsSizeForJob } from '~/ci/artifacts/utils'; + +const job = getJobArtifactsResponse.data.project.jobs.nodes[0]; +const artifacts = job.artifacts.nodes; + +describe('totalArtifactsSizeForJob', () => { + it('adds artifact sizes together', () => { + expect(totalArtifactsSizeForJob(job)).toBe( + numberToHumanSize( + Number(artifacts[0].size) + Number(artifacts[1].size) + Number(artifacts[2].size), + ), + ); + }); +}); diff --git a/spec/frontend/usage_quotas/storage/components/project_storage_detail_spec.js b/spec/frontend/usage_quotas/storage/components/project_storage_detail_spec.js index 15758c94436..37fc9602315 100644 --- a/spec/frontend/usage_quotas/storage/components/project_storage_detail_spec.js +++ b/spec/frontend/usage_quotas/storage/components/project_storage_detail_spec.js @@ -26,7 +26,7 @@ describe('ProjectStorageDetail', () => { ); }; - const generateStorageType = (id = 'buildArtifactsSize') => { + const generateStorageType = (id = 'buildArtifacts') => { return { storageType: { id, @@ -56,7 +56,7 @@ describe('ProjectStorageDetail', () => { expect(wrapper.findByTestId(`${id}-description`).text()).toBe(description); expect(wrapper.findByTestId(`${id}-icon`).props('name')).toBe(id); expect(wrapper.findByTestId(`${id}-help-link`).attributes('href')).toBe( - projectHelpLinks[id.replace(`Size`, ``)], + projectHelpLinks[id], ); }, ); @@ -74,6 +74,14 @@ describe('ProjectStorageDetail', () => { }); }); + describe('with details links', () => { + it.each(storageTypes)('each $storageType.id', (item) => { + const shouldExist = Boolean(item.storageType.detailsPath && item.value); + const detailsLink = wrapper.findByTestId(`${item.storageType.id}-details-link`); + expect(detailsLink.exists()).toBe(shouldExist); + }); + }); + describe('without storage types', () => { beforeEach(() => { createComponent({ storageTypes: [] }); diff --git a/spec/frontend/usage_quotas/storage/components/storage_type_icon_spec.js b/spec/frontend/usage_quotas/storage/components/storage_type_icon_spec.js index ebe4c4b7f4e..92c24400e76 100644 --- a/spec/frontend/usage_quotas/storage/components/storage_type_icon_spec.js +++ b/spec/frontend/usage_quotas/storage/components/storage_type_icon_spec.js @@ -18,11 +18,11 @@ describe('StorageTypeIcon', () => { describe('rendering icon', () => { it.each` expected | provided - ${'doc-image'} | ${'lfsObjectsSize'} - ${'snippet'} | ${'snippetsSize'} - ${'infrastructure-registry'} | ${'repositorySize'} - ${'package'} | ${'packagesSize'} - ${'disk'} | ${'wikiSize'} + ${'doc-image'} | ${'lfsObjects'} + ${'snippet'} | ${'snippets'} + ${'infrastructure-registry'} | ${'repository'} + ${'package'} | ${'packages'} + ${'disk'} | ${'wiki'} ${'disk'} | ${'anything-else'} `( 'renders icon with name of $expected when name prop is $provided', diff --git a/spec/frontend/usage_quotas/storage/mock_data.js b/spec/frontend/usage_quotas/storage/mock_data.js index b4b02f77b52..8a7f941151b 100644 --- a/spec/frontend/usage_quotas/storage/mock_data.js +++ b/spec/frontend/usage_quotas/storage/mock_data.js @@ -9,25 +9,27 @@ export const projectData = { storageTypes: [ { storageType: { - id: 'containerRegistrySize', + id: 'containerRegistry', name: 'Container Registry', description: 'Gitlab-integrated Docker Container Registry for storing Docker Images.', helpPath: '/container_registry', + detailsPath: 'http://localhost/frontend-fixtures/builds-project/container_registry', }, - value: 3_900_000, + value: 3900000, }, { storageType: { - id: 'buildArtifactsSize', + id: 'buildArtifacts', name: 'Job artifacts', description: 'Job artifacts created by CI/CD.', helpPath: '/build-artifacts', + detailsPath: 'http://localhost/frontend-fixtures/builds-project/-/artifacts', }, value: 400000, }, { storageType: { - id: 'pipelineArtifactsSize', + id: 'pipelineArtifacts', name: 'Pipeline artifacts', description: 'Pipeline artifacts created by CI/CD.', helpPath: '/pipeline-artifacts', @@ -36,7 +38,7 @@ export const projectData = { }, { storageType: { - id: 'lfsObjectsSize', + id: 'lfsObjects', name: 'LFS', description: 'Audio samples, videos, datasets, and graphics.', helpPath: '/lsf-objects', @@ -45,37 +47,41 @@ export const projectData = { }, { storageType: { - id: 'packagesSize', + id: 'packages', name: 'Packages', description: 'Code packages and container images.', helpPath: '/packages', + detailsPath: 'http://localhost/frontend-fixtures/builds-project/-/packages', }, value: 3800000, }, { storageType: { - id: 'repositorySize', + id: 'repository', name: 'Repository', description: 'Git repository.', helpPath: '/repository', + detailsPath: 'http://localhost/frontend-fixtures/builds-project/-/tree/master', }, value: 3900000, }, { storageType: { - id: 'snippetsSize', + id: 'snippets', name: 'Snippets', description: 'Shared bits of code and text.', helpPath: '/snippets', + detailsPath: 'http://localhost/frontend-fixtures/builds-project/-/snippets', }, value: 0, }, { storageType: { - id: 'wikiSize', + id: 'wiki', name: 'Wiki', description: 'Wiki content.', helpPath: '/wiki', + detailsPath: 'http://localhost/frontend-fixtures/builds-project/-/wikis/pages', }, value: 300000, }, diff --git a/spec/frontend/usage_quotas/storage/utils_spec.js b/spec/frontend/usage_quotas/storage/utils_spec.js index 8fdd307c008..e3a271adc57 100644 --- a/spec/frontend/usage_quotas/storage/utils_spec.js +++ b/spec/frontend/usage_quotas/storage/utils_spec.js @@ -12,7 +12,10 @@ import { } from './mock_data'; describe('getStorageTypesFromProjectStatistics', () => { - const projectStatistics = mockGetProjectStorageStatisticsGraphQLResponse.data.project.statistics; + const { + statistics: projectStatistics, + statisticsDetailsPaths, + } = mockGetProjectStorageStatisticsGraphQLResponse.data.project; describe('matches project statistics value with matching storage type', () => { const typesWithStats = getStorageTypesFromProjectStatistics(projectStatistics); @@ -22,29 +25,39 @@ describe('getStorageTypesFromProjectStatistics', () => { storageType: expect.objectContaining({ id, }), - value: projectStatistics[id], + value: projectStatistics[`${id}Size`], }); }); }); it('adds helpPath to a relevant type', () => { - const trimTypeId = (id) => id.replace('Size', ''); const helpLinks = PROJECT_STORAGE_TYPES.reduce((acc, { id }) => { - const key = trimTypeId(id); return { ...acc, - [key]: `url://${id}`, + [id]: `url://${id}`, }; }, {}); const typesWithStats = getStorageTypesFromProjectStatistics(projectStatistics, helpLinks); typesWithStats.forEach((type) => { - const key = trimTypeId(type.storageType.id); + const key = type.storageType.id; expect(type.storageType.helpPath).toBe(helpLinks[key]); }); }); + + it('adds details page path', () => { + const typesWithStats = getStorageTypesFromProjectStatistics( + projectStatistics, + {}, + statisticsDetailsPaths, + ); + typesWithStats.forEach((type) => { + expect(type.storageType.detailsPath).toBe(statisticsDetailsPaths[type.storageType.id]); + }); + }); }); + describe('parseGetProjectStorageResults', () => { it('parses project statistics correctly', () => { expect( diff --git a/spec/lib/gitlab/utils/sanitize_node_link_spec.rb b/spec/lib/gitlab/utils/sanitize_node_link_spec.rb index 1fc10bc3aa8..54f86353d88 100644 --- a/spec/lib/gitlab/utils/sanitize_node_link_spec.rb +++ b/spec/lib/gitlab/utils/sanitize_node_link_spec.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true -require 'fast_spec_helper' +# TODO: change to fast_spec_helper in scope of https://gitlab.com/gitlab-org/gitlab/-/issues/413779 +require 'spec_helper' require 'html/pipeline' require 'addressable' @@ -27,9 +28,13 @@ RSpec.describe Gitlab::Utils::SanitizeNodeLink do "  javascript:" ] - invalid_schemes.each do |scheme| - context "with the scheme: #{scheme}" do - describe "#remove_unsafe_links" do + describe "#remove_unsafe_links" do + subject { object.remove_unsafe_links(env, remove_invalid_links: true) } + + let(:env) { { node: node } } + + invalid_schemes.each do |scheme| + context "with the scheme: #{scheme}" do tags = { a: { doc: HTML::Pipeline.parse("foo"), @@ -55,19 +60,67 @@ RSpec.describe Gitlab::Utils::SanitizeNodeLink do tags.each do |tag, opts| context "<#{tag}> tags" do - it "removes the unsafe link" do - node = opts[:node_to_check].call(opts[:doc]) + let(:node) { opts[:node_to_check].call(opts[:doc]) } - expect { object.remove_unsafe_links({ node: node }, remove_invalid_links: true) } - .to change { node[opts[:attr]] } + it "removes the unsafe link" do + expect { subject }.to change { node[opts[:attr]] } expect(node[opts[:attr]]).to be_blank end end end end + end - describe "#safe_protocol?" do + context 'when URI is valid' do + let(:doc) { HTML::Pipeline.parse("foo") } + let(:node) { doc.children.first } + + it 'does not remove it' do + subject + + expect(node[:href]).to eq('http://example.com') + end + end + + context 'when URI is invalid' do + let(:doc) { HTML::Pipeline.parse("foo") } + let(:node) { doc.children.first } + + it 'removes the link' do + subject + + expect(node[:href]).to be_nil + end + end + + context 'when URI is encoded but still invalid' do + let(:doc) { HTML::Pipeline.parse("foo") } + let(:node) { doc.children.first } + + it 'removes the link' do + subject + + expect(node[:href]).to be_nil + end + + context 'when feature flag "normalize_links_for_sanitizing" is disabled' do + before do + stub_feature_flags(normalize_links_for_sanitizing: false) + end + + it 'does not remove it' do + subject + + expect(node[:href]).to eq('http://example%EF%BC%9A%E7%BD%91') + end + end + end + end + + describe "#safe_protocol?" do + invalid_schemes.each do |scheme| + context "with the scheme: #{scheme}" do let(:doc) { HTML::Pipeline.parse("foo") } let(:node) { doc.children.first } let(:uri) { Addressable::URI.parse(node['href']) } @@ -78,4 +131,14 @@ RSpec.describe Gitlab::Utils::SanitizeNodeLink do end end end + + describe '#sanitize_unsafe_links' do + let(:env) { { node: 'node' } } + + it 'makes a call to #remove_unsafe_links_method' do + expect(object).to receive(:remove_unsafe_links).with(env) + + object.sanitize_unsafe_links(env) + end + end end diff --git a/spec/views/admin/sessions/new.html.haml_spec.rb b/spec/views/admin/sessions/new.html.haml_spec.rb index 18a673b1c31..c1ed8d4f4ef 100644 --- a/spec/views/admin/sessions/new.html.haml_spec.rb +++ b/spec/views/admin/sessions/new.html.haml_spec.rb @@ -19,8 +19,7 @@ RSpec.describe 'admin/sessions/new.html.haml' do it 'shows enter password form' do render - expect(rendered).to have_selector('[data-testid="sign-in-tab"]') - expect(rendered).to have_css('#login-pane.active') + expect(rendered).to have_css('.login-box') expect(rendered).to have_selector('[data-testid="password-field"]') end @@ -29,7 +28,7 @@ RSpec.describe 'admin/sessions/new.html.haml' do render - expect(rendered).not_to have_css('#login-pane') + expect(rendered).not_to have_css('.login-box') expect(rendered).to have_content _('No authentication methods configured.') end end diff --git a/spec/views/admin/sessions/two_factor.html.haml_spec.rb b/spec/views/admin/sessions/two_factor.html.haml_spec.rb index 6503c08b84c..9ac9356b91a 100644 --- a/spec/views/admin/sessions/two_factor.html.haml_spec.rb +++ b/spec/views/admin/sessions/two_factor.html.haml_spec.rb @@ -24,7 +24,7 @@ RSpec.describe 'admin/sessions/two_factor.html.haml' do it 'shows enter otp form' do render - expect(rendered).to have_css('#login-pane.active') + expect(rendered).to have_css('.login-box') expect(rendered).to have_field('user[otp_attempt]') end end diff --git a/spec/views/projects/issues/service_desk/_issue.html.haml_spec.rb b/spec/views/projects/issues/service_desk/_issue.html.haml_spec.rb index fab872d4019..ee582ee9927 100644 --- a/spec/views/projects/issues/service_desk/_issue.html.haml_spec.rb +++ b/spec/views/projects/issues/service_desk/_issue.html.haml_spec.rb @@ -14,7 +14,7 @@ RSpec.describe 'projects/issues/service_desk/_issue.html.haml', feature_category describe 'timestamp', :freeze_time do context 'when issue is open' do - let(:issue) { create(:issue, updated_at: 1.day.ago) } # rubocop:disable RSpec/FactoryBot/AvoidCreate + let(:issue) { create(:issue, updated_at: 1.day.ago) } it 'shows last updated date' do expect(rendered).to have_content("updated #{format_timestamp(1.day.ago)}") @@ -22,7 +22,7 @@ RSpec.describe 'projects/issues/service_desk/_issue.html.haml', feature_category end context 'when issue is closed' do - let(:issue) { create(:issue, :closed, closed_at: 2.days.ago, updated_at: 1.day.ago) } # rubocop:disable RSpec/FactoryBot/AvoidCreate + let(:issue) { create(:issue, :closed, closed_at: 2.days.ago, updated_at: 1.day.ago) } it 'shows closed date' do expect(rendered).to have_content("closed #{format_timestamp(2.days.ago)}") @@ -30,7 +30,7 @@ RSpec.describe 'projects/issues/service_desk/_issue.html.haml', feature_category end context 'when issue is closed but closed_at is empty' do - let(:issue) { create(:issue, :closed, closed_at: nil, updated_at: 1.day.ago) } # rubocop:disable RSpec/FactoryBot/AvoidCreate + let(:issue) { create(:issue, :closed, closed_at: nil, updated_at: 1.day.ago) } it 'shows last updated date' do expect(rendered).to have_content("updated #{format_timestamp(1.day.ago)}") @@ -40,7 +40,7 @@ RSpec.describe 'projects/issues/service_desk/_issue.html.haml', feature_category context 'when issue is service desk issue' do let_it_be(:email) { 'user@example.com' } let_it_be(:obfuscated_email) { 'us*****@e*****.c**' } - let_it_be(:issue) { create(:issue, author: User.support_bot, service_desk_reply_to: email) } # rubocop:disable RSpec/FactoryBot/AvoidCreate + let_it_be(:issue) { create(:issue, author: User.support_bot, service_desk_reply_to: email) } context 'with anonymous user' do it 'obfuscates service_desk_reply_to email for anonymous user' do @@ -49,7 +49,7 @@ RSpec.describe 'projects/issues/service_desk/_issue.html.haml', feature_category end context 'with signed in user' do - let_it_be(:user) { create(:user) } # rubocop:disable RSpec/FactoryBot/AvoidCreate + let_it_be(:user) { create(:user) } before do allow(view).to receive(:current_user).and_return(user)