diff options
26 files changed, 744 insertions, 109 deletions
diff --git a/app/assets/javascripts/reports/codequality_report/constants.js b/app/assets/javascripts/reports/codequality_report/constants.js index 502977e714c..0c472b24471 100644 --- a/app/assets/javascripts/reports/codequality_report/constants.js +++ b/app/assets/javascripts/reports/codequality_report/constants.js @@ -15,3 +15,17 @@ export const SEVERITY_ICONS = { blocker: 'severity-critical', unknown: 'severity-unknown', }; + +// This is the icons mapping for the code Quality Merge-Request Widget Extension +// once the refactor_mr_widgets_extensions flag is activated the above SEVERITY_ICONS +// need be removed and this variable needs to be rename to SEVERITY_ICONS +// Rollout Issue: https://gitlab.com/gitlab-org/gitlab/-/issues/341759 + +export const SEVERITY_ICONS_EXTENSION = { + info: 'severityInfo', + minor: 'severityLow', + major: 'severityMedium', + critical: 'severityHigh', + blocker: 'severityCritical', + unknown: 'severityUnknown', +}; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue index 1621b6831a2..7435f578852 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue @@ -82,17 +82,8 @@ export default { return this.mr.shouldBeRebased; }, - sourceBranchProtected() { - if (this.glFeatures.mergeRequestWidgetGraphql) { - return this.stateData.sourceBranchProtected; - } - - return this.mr.sourceBranchProtected; - }, showResolveButton() { - return ( - this.mr.conflictResolutionPath && this.canPushToSourceBranch && !this.sourceBranchProtected - ); + return this.mr.conflictResolutionPath && this.canPushToSourceBranch; }, }, }; diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/index.js b/app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/index.js new file mode 100644 index 00000000000..d32db50874c --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/index.js @@ -0,0 +1,123 @@ +import { n__, s__, sprintf } from '~/locale'; +import axios from '~/lib/utils/axios_utils'; +import { EXTENSION_ICONS } from '~/vue_merge_request_widget/constants'; +import { SEVERITY_ICONS_EXTENSION } from '~/reports/codequality_report/constants'; +import { parseCodeclimateMetrics } from '~/reports/codequality_report/store/utils/codequality_parser'; +import { capitalizeFirstCharacter } from '~/lib/utils/text_utility'; + +export default { + name: 'WidgetCodeQuality', + props: ['codeQuality', 'blobPath'], + i18n: { + label: s__('ciReport|Code Quality'), + loading: s__('ciReport|Code Quality test metrics results are being parsed'), + error: s__('ciReport|Code Quality failed loading results'), + }, + expandEvent: 'i_testing_code_quality_widget_total', + computed: { + summary() { + const { newErrors, resolvedErrors, errorSummary } = this.collapsedData; + if (errorSummary.errored >= 1 && errorSummary.resolved >= 1) { + const improvements = sprintf( + n__( + '%{strongOpen}%{errors}%{strongClose} point', + '%{strongOpen}%{errors}%{strongClose} points', + resolvedErrors.length, + ), + { + errors: resolvedErrors.length, + strongOpen: '<strong>', + strongClose: '</strong>', + }, + false, + ); + + const degradations = sprintf( + n__( + '%{strongOpen}%{errors}%{strongClose} point', + '%{strongOpen}%{errors}%{strongClose} points', + newErrors.length, + ), + { errors: newErrors.length, strongOpen: '<strong>', strongClose: '</strong>' }, + false, + ); + return sprintf( + s__(`ciReport|Code Quality improved on ${improvements} and degraded on ${degradations}.`), + ); + } else if (errorSummary.resolved >= 1) { + const improvements = n__('%d point', '%d points', resolvedErrors.length); + return sprintf(s__(`ciReport|Code Quality improved on ${improvements}.`)); + } else if (errorSummary.errored >= 1) { + const degradations = n__('%d point', '%d points', newErrors.length); + return sprintf(s__(`ciReport|Code Quality degraded on ${degradations}.`)); + } + return s__(`ciReport|No changes to Code Quality.`); + }, + statusIcon() { + if (this.collapsedData.errorSummary?.errored >= 1) { + return EXTENSION_ICONS.warning; + } + return EXTENSION_ICONS.success; + }, + }, + methods: { + fetchCollapsedData() { + return Promise.all([this.fetchReport(this.codeQuality)]).then((values) => { + return { + resolvedErrors: parseCodeclimateMetrics( + values[0].resolved_errors, + this.blobPath.head_path, + ), + newErrors: parseCodeclimateMetrics(values[0].new_errors, this.blobPath.head_path), + existingErrors: parseCodeclimateMetrics( + values[0].existing_errors, + this.blobPath.head_path, + ), + errorSummary: values[0].summary, + }; + }); + }, + fetchFullData() { + const fullData = []; + + this.collapsedData.newErrors.map((e) => { + return fullData.push({ + text: `${capitalizeFirstCharacter(e.severity)} - ${e.description}`, + subtext: sprintf( + s__(`ciReport|in %{open_link}${e.file_path}:${e.line}%{close_link}`), + { + open_link: `<a class="gl-text-decoration-underline" href="${e.urlPath}">`, + close_link: '</a>', + }, + false, + ), + icon: { + name: SEVERITY_ICONS_EXTENSION[e.severity], + }, + }); + }); + + this.collapsedData.resolvedErrors.map((e) => { + return fullData.push({ + text: `${capitalizeFirstCharacter(e.severity)} - ${e.description}`, + subtext: sprintf( + s__(`ciReport|in %{open_link}${e.file_path}:${e.line}%{close_link}`), + { + open_link: `<a class="gl-text-decoration-underline" href="${e.urlPath}">`, + close_link: '</a>', + }, + false, + ), + icon: { + name: SEVERITY_ICONS_EXTENSION[e.severity], + }, + }); + }); + + return Promise.resolve(fullData); + }, + fetchReport(endpoint) { + return axios.get(endpoint).then((res) => res.data); + }, + }, +}; diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue index cd4d9398899..965746e79fb 100644 --- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue +++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue @@ -46,6 +46,7 @@ import mergeRequestQueryVariablesMixin from './mixins/merge_request_query_variab import getStateQuery from './queries/get_state.query.graphql'; import terraformExtension from './extensions/terraform'; import accessibilityExtension from './extensions/accessibility'; +import codeQualityExtension from './extensions/code_quality'; export default { // False positive i18n lint: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/25 @@ -241,6 +242,11 @@ export default { this.registerTerraformPlans(); } }, + shouldRenderCodeQuality(newVal) { + if (newVal) { + this.registerCodeQualityExtension(); + } + }, shouldShowAccessibilityReport(newVal) { if (newVal) { this.registerAccessibilityExtension(); @@ -491,6 +497,11 @@ export default { registerExtension(accessibilityExtension); } }, + registerCodeQualityExtension() { + if (this.shouldRenderCodeQuality && this.shouldShowExtension) { + registerExtension(codeQualityExtension); + } + }, }, }; </script> @@ -546,7 +557,7 @@ export default { </div> <extensions-container :mr="mr" /> <grouped-codequality-reports-app - v-if="shouldRenderCodeQuality" + v-if="shouldRenderCodeQuality && !shouldShowExtension" :head-blob-path="mr.headBlobPath" :base-blob-path="mr.baseBlobPath" :codequality-reports-path="mr.codequalityReportsPath" diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js index 994e0c23b44..eb07609d5d6 100644 --- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js +++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js @@ -32,9 +32,15 @@ export default class MergeRequestStore { this.setPaths(data); this.setData(data); + this.initCodeQualityReport(data); this.setGitpodData(data); } + initCodeQualityReport(data) { + this.blobPath = data.blob_path; + this.codeQuality = data.codequality_reports_path; + } + setData(data, isRebased) { this.initApprovals(); diff --git a/app/controllers/concerns/product_analytics_tracking.rb b/app/controllers/concerns/product_analytics_tracking.rb new file mode 100644 index 00000000000..03296d6b233 --- /dev/null +++ b/app/controllers/concerns/product_analytics_tracking.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module ProductAnalyticsTracking + include Gitlab::Tracking::Helpers + include RedisTracking + extend ActiveSupport::Concern + + class_methods do + def track_event(*controller_actions, name:, conditions: nil, destinations: [:redis_hll], &block) + custom_conditions = [:trackable_html_request?, *conditions] + + after_action only: controller_actions, if: custom_conditions do + route_events_to(destinations, name, &block) + end + end + end + + private + + def route_events_to(destinations, name, &block) + track_unique_redis_hll_event(name, &block) if destinations.include?(:redis_hll) + + if destinations.include?(:snowplow) && Feature.enabled?(:route_hll_to_snowplow, tracking_namespace_source, default_enabled: :yaml) + Gitlab::Tracking.event(self.class.to_s, name, namespace: tracking_namespace_source, user: current_user) + end + end +end diff --git a/app/controllers/jira_connect/events_controller.rb b/app/controllers/jira_connect/events_controller.rb index 1ea0a92662b..327192857f6 100644 --- a/app/controllers/jira_connect/events_controller.rb +++ b/app/controllers/jira_connect/events_controller.rb @@ -7,11 +7,13 @@ class JiraConnect::EventsController < JiraConnect::ApplicationController before_action :verify_asymmetric_atlassian_jwt! def installed - return head :ok if current_jira_installation + unless Feature.enabled?(:jira_connect_installation_update, default_enabled: :yaml) + return head :ok if current_jira_installation + end - installation = JiraConnectInstallation.new(event_params) + success = current_jira_installation ? update_installation : create_installation - if installation.save + if success head :ok else head :unprocessable_entity @@ -28,8 +30,24 @@ class JiraConnect::EventsController < JiraConnect::ApplicationController private - def event_params - params.permit(:clientKey, :sharedSecret, :baseUrl).transform_keys(&:underscore) + def create_installation + JiraConnectInstallation.new(create_params).save + end + + def update_installation + current_jira_installation.update(update_params) + end + + def create_params + transformed_params.permit(:client_key, :shared_secret, :base_url) + end + + def update_params + transformed_params.permit(:shared_secret, :base_url) + end + + def transformed_params + @transformed_params ||= params.transform_keys(&:underscore) end def verify_asymmetric_atlassian_jwt! @@ -43,7 +61,7 @@ class JiraConnect::EventsController < JiraConnect::ApplicationController def jwt_verification_claims { aud: jira_connect_base_url(protocol: 'https'), - iss: event_params[:client_key], + iss: transformed_params[:client_key], qsh: Atlassian::Jwt.create_query_string_hash(request.url, request.method, jira_connect_base_url) } end diff --git a/app/models/project.rb b/app/models/project.rb index 3ca719101d2..155ebe88d33 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -498,7 +498,10 @@ class Project < ApplicationRecord presence: true, project_path: true, length: { maximum: 255 } - validate :container_registry_project_path_validation + validates :path, + format: { with: Gitlab::Regex.oci_repository_path_regex, + message: Gitlab::Regex.oci_repository_path_regex_message }, + if: :path_changed? validates :project_feature, presence: true @@ -889,14 +892,6 @@ class Project < ApplicationRecord super end - def container_registry_project_path_validation - if Feature.enabled?(:restrict_special_characters_in_project_path, self, default_enabled: :yaml) && - path_changed? && - !path.match?(Gitlab::Regex.oci_repository_path_regex) - errors.add(:path, Gitlab::Regex.oci_repository_path_regex_message) - end - end - def parent_loaded? association(:namespace).loaded? end diff --git a/app/models/user.rb b/app/models/user.rb index 2905db7ce6c..b3bdc2c1c42 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -291,21 +291,7 @@ class User < ApplicationRecord end end after_commit(on: :update) do - if previous_changes.key?('email') - # Add the old primary email to Emails if not added already - this should be removed - # after the background migration for MR https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70872/ has completed, - # as the primary email is now added to Emails upon confirmation - # Issue to remove that: https://gitlab.com/gitlab-org/gitlab/-/issues/344134 - previous_confirmed_at = previous_changes.key?('confirmed_at') ? previous_changes['confirmed_at'][0] : confirmed_at - previous_email = previous_changes[:email][0] - if previous_confirmed_at && !emails.exists?(email: previous_email) - # rubocop: disable CodeReuse/ServiceClass - Emails::CreateService.new(self, user: self, email: previous_email).execute(confirmed_at: previous_confirmed_at) - # rubocop: enable CodeReuse/ServiceClass - end - - update_invalid_gpg_signatures - end + update_invalid_gpg_signatures if previous_changes.key?('email') end after_initialize :set_projects_limit diff --git a/app/views/admin/groups/_form.html.haml b/app/views/admin/groups/_form.html.haml index 91a018121c0..0c3ce1f3fa4 100644 --- a/app/views/admin/groups/_form.html.haml +++ b/app/views/admin/groups/_form.html.haml @@ -27,12 +27,9 @@ - if @group.new_record? .form-group.row .offset-sm-2.col-sm-10 - .gl-alert.gl-alert- - .gl-alert-container - = sprite_icon('information-o', size: 16, css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title') - .gl-alert-content - .gl-alert-body - = render 'shared/group_tips' + = render 'shared/global_alert', dismissible: false do + .gl-alert-body + = render 'shared/group_tips' .form-actions = f.submit _('Create group'), class: "gl-button btn btn-confirm" = link_to _('Cancel'), admin_groups_path, class: "gl-button btn btn-default btn-cancel" diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml index cd177db3ed0..1d711f366c4 100644 --- a/app/views/dashboard/todos/index.html.haml +++ b/app/views/dashboard/todos/index.html.haml @@ -2,6 +2,7 @@ - page_title _("To-Do List") - header_title _("To-Do List"), dashboard_todos_path += render_two_factor_auth_recovery_settings_check = render_dashboard_ultimate_trial(current_user) - add_page_specific_style 'page_bundles/todos' diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml index 133083f1103..a656b61dc8f 100644 --- a/app/views/layouts/_page.html.haml +++ b/app/views/layouts/_page.html.haml @@ -14,7 +14,6 @@ = dispensable_render "layouts/nav/classification_level_banner" = yield :flash_message = dispensable_render "shared/service_ping_consent" - = render_two_factor_auth_recovery_settings_check = dispensable_render_if_exists "layouts/header/ee_subscribable_banner" = dispensable_render_if_exists "layouts/header/seats_count_alert" = dispensable_render_if_exists "shared/namespace_storage_limit_alert" diff --git a/app/views/projects/_deletion_failed.html.haml b/app/views/projects/_deletion_failed.html.haml index 21c799f5bb6..b713b805009 100644 --- a/app/views/projects/_deletion_failed.html.haml +++ b/app/views/projects/_deletion_failed.html.haml @@ -1,10 +1,7 @@ - project = local_assigns.fetch(:project) - return unless project.delete_error.present? -.project-deletion-failed-message.gl-alert.gl-alert-warning - .gl-alert-container - = sprite_icon('warning', size: 16, css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title') - .gl-alert-content - .gl-alert-body - This project was scheduled for deletion, but failed with the following message: - = project.delete_error += render 'shared/global_alert', variant: :warning, dismissible: false, alert_class: 'project-deletion-failed-message' do + .gl-alert-body + This project was scheduled for deletion, but failed with the following message: + = project.delete_error diff --git a/app/views/projects/_new_project_fields.html.haml b/app/views/projects/_new_project_fields.html.haml index 2d0a4ae8605..1fb045544aa 100644 --- a/app/views/projects/_new_project_fields.html.haml +++ b/app/views/projects/_new_project_fields.html.haml @@ -35,8 +35,7 @@ - link_start_group_path = '<a href="%{path}">' % { path: new_group_path } - project_tip = s_('ProjectsNew|Want to house several dependent projects under the same namespace? %{link_start}Create a group.%{link_end}') % { link_start: link_start_group_path, link_end: '</a>' } = project_tip.html_safe -.gl-alert.gl-alert-success.gl-mb-4.gl-display-none.js-user-readme-repo - = sprite_icon('check-circle', size: 16, css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title') += render 'shared/global_alert', alert_class: "gl-mb-4 gl-display-none js-user-readme-repo", dismissible: false, variant: :success do .gl-alert-body - help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/profile/index', anchor: 'add-details-to-your-profile-with-a-readme') } = html_escape(_('%{project_path} is a project that you can use to add a README to your GitLab profile. Create a public project and initialize the repository with a README to get started. %{help_link_start}Learn more.%{help_link_end}')) % { project_path: "<strong>#{current_user.username} / #{current_user.username}</strong>".html_safe, help_link_start: help_link_start, help_link_end: '</a>'.html_safe } diff --git a/app/views/shared/_two_factor_auth_recovery_settings_check.html.haml b/app/views/shared/_two_factor_auth_recovery_settings_check.html.haml index e7239661313..f21acd26ada 100644 --- a/app/views/shared/_two_factor_auth_recovery_settings_check.html.haml +++ b/app/views/shared/_two_factor_auth_recovery_settings_check.html.haml @@ -1,6 +1,6 @@ = render 'shared/global_alert', variant: :warning, - alert_class: 'js-recovery-settings-callout', + alert_class: 'js-recovery-settings-callout gl-mt-5', alert_data: { feature_id: Users::CalloutsHelper::TWO_FACTOR_AUTH_RECOVERY_SETTINGS_CHECK, dismiss_endpoint: callouts_path, defer_links: 'true' }, close_button_data: { testid: 'close-account-recovery-regular-check-callout' } do .gl-alert-body diff --git a/config/feature_flags/development/restrict_special_characters_in_project_path.yml b/config/feature_flags/development/jira_connect_installation_update.yml index d2eff146605..a92a7dafc14 100644 --- a/config/feature_flags/development/restrict_special_characters_in_project_path.yml +++ b/config/feature_flags/development/jira_connect_installation_update.yml @@ -1,8 +1,8 @@ --- -name: restrict_special_characters_in_project_path -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/80055 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/353054 -milestone: '14.8' +name: jira_connect_installation_update +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/83038 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/356083 +milestone: '14.9' type: development -group: group::workspace +group: group::integrations default_enabled: false diff --git a/config/feature_flags/development/route_hll_to_snowplow.yml b/config/feature_flags/development/route_hll_to_snowplow.yml new file mode 100644 index 00000000000..3c8f7826a0a --- /dev/null +++ b/config/feature_flags/development/route_hll_to_snowplow.yml @@ -0,0 +1,8 @@ +--- +name: route_hll_to_snowplow +introduced_by_url: https://gitlab.com/gitlab-org/product-intelligence/-/issues/498 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/354442 +milestone: '14.9' +type: development +group: group::product intelligence +default_enabled: false diff --git a/doc/ci/jobs/index.md b/doc/ci/jobs/index.md index 39e14d0d20a..4fa251eb3cf 100644 --- a/doc/ci/jobs/index.md +++ b/doc/ci/jobs/index.md @@ -103,6 +103,9 @@ Job names must be 255 characters or less. [Introduced](https://gitlab.com/gitlab in GitLab 14.5, [with a feature flag](../../administration/feature_flags.md) named `ci_validate_job_length`. Enabled by default. To disable it, ask an administrator to [disable the feature flag](../../administration/feature_flags.md). +Use unique names for your jobs. If multiple jobs have the same name, +only one is added to the pipeline, and it's difficult to predict which one is chosen. + ## Group jobs in a pipeline If you have many similar jobs, your [pipeline graph](../pipelines/index.md#visualize-pipelines) becomes long and hard diff --git a/doc/development/performance.md b/doc/development/performance.md index 55453e31d3d..1e3e0570206 100644 --- a/doc/development/performance.md +++ b/doc/development/performance.md @@ -7,7 +7,38 @@ info: To determine the technical writer assigned to the Stage/Group associated w # Performance Guidelines This document describes various guidelines to follow to ensure good and -consistent performance of GitLab. +consistent performance of GitLab. Refer to the [Index](#performance-documentation) section below to navigate to Performance-related pages. + +## Performance Documentation + +- General: + - [Solving performance issues](#workflow) + - [Handbook performance page](https://about.gitlab.com/handbook/engineering/performance/) + - [Merge request performance guidelines](../development/merge_request_performance_guidelines.md) +- Backend: + - [Tooling](#tooling) + - Database: + - [Query performance guidelines](../development/query_performance.md) + - [Pagination performance guidelines](../development/database/pagination_performance_guidelines.md) + - [Keyset pagination performance](../development/database/keyset_pagination.md#performance) + - [Troubleshooting import/export performance issues](../development/import_export.md#troubleshooting-performance-issues) + - [Pipelines performance in the `gitlab` project](../development/pipelines.md#performance) +- Frontend: + - [Performance guidelines](../development/fe_guide/performance.md) + - [Performance dashboards and monitoring guidelines](../development/new_fe_guide/development/performance.md) + - [Browser performance testing guidelines](../user/project/merge_requests/browser_performance_testing.md) + - [`gdk measure` and `gdk measure-workflow`](https://gitlab.com/gitlab-org/gitlab-development-kit/-/blob/main/doc/gdk_commands.md#measure-performance) +- QA: + - [Load performance testing](../user/project/merge_requests/load_performance_testing.md) + - [GitLab Performance Tool project](https://gitlab.com/gitlab-org/quality/performance) + - [Review apps performance metrics](../development/testing_guide/review_apps.md#performance-metrics) +- Monitoring & Overview: + - [GitLab performance monitoring](../administration/monitoring/performance/index.md) + - [Development department performance indicators](https://about.gitlab.com/handbook/engineering/development/performance-indicators/) + - [Service measurement](../development/service_measurement.md) +- Self-managed administration and customer-focused: + - [File system performance benchmarking](../administration/operations/filesystem_benchmarking.md) + - [Sidekiq performance troubleshooting](../administration/troubleshooting/sidekiq.md) ## Workflow @@ -156,7 +187,7 @@ With the [Performance bar](../administration/monitoring/performance/performance_ you have the option to profile a request using Stackprof and immediately output the results to a [Speedscope flamegraph](profiling.md#speedscope-flamegraphs). -### RSpec profiling with Stackprof +### RSpec profiling with Stackprof To create a profile from a spec, identify (or create) a spec that exercises the troublesome code path, then run it using the `bin/rspec-stackprof` @@ -307,7 +338,7 @@ ProjectPolicy#access_allowed_to? (/Users/royzwambag/work/gitlab-development-kit/ code: | 793 | def access_allowed_to?(feature) 141 (0.2%) | 794 | return false unless project.project_feature - | 795 | + | 795 | 8 (0.0%) | 796 | case project.project_feature.access_level(feature) | 797 | when ProjectFeature::DISABLED | 798 | false diff --git a/locale/gitlab.pot b/locale/gitlab.pot index e86ca43d657..4ff3918efb6 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -350,6 +350,11 @@ msgid_plural "%d personal projects will be removed and cannot be restored." msgstr[0] "" msgstr[1] "" +msgid "%d point" +msgid_plural "%d points" +msgstr[0] "" +msgstr[1] "" + msgid "%d previously merged commit" msgid_plural "%d previously merged commits" msgstr[0] "" @@ -981,6 +986,11 @@ msgstr "" msgid "%{start} to %{end}" msgstr "" +msgid "%{strongOpen}%{errors}%{strongClose} point" +msgid_plural "%{strongOpen}%{errors}%{strongClose} points" +msgstr[0] "" +msgstr[1] "" + msgid "%{strongOpen}Warning:%{strongClose} SAML group links can cause GitLab to automatically remove members from groups." msgstr "" @@ -43509,6 +43519,15 @@ msgstr "" msgid "ciReport|Cluster Image Scanning" msgstr "" +msgid "ciReport|Code Quality" +msgstr "" + +msgid "ciReport|Code Quality failed loading results" +msgstr "" + +msgid "ciReport|Code Quality test metrics results are being parsed" +msgstr "" + msgid "ciReport|Code quality degraded" msgstr "" @@ -43616,6 +43635,9 @@ msgstr "" msgid "ciReport|New" msgstr "" +msgid "ciReport|No changes to Code Quality." +msgstr "" + msgid "ciReport|No changes to code quality" msgstr "" diff --git a/spec/controllers/concerns/product_analytics_tracking_spec.rb b/spec/controllers/concerns/product_analytics_tracking_spec.rb new file mode 100644 index 00000000000..250cc3cf2cf --- /dev/null +++ b/spec/controllers/concerns/product_analytics_tracking_spec.rb @@ -0,0 +1,171 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe ProductAnalyticsTracking, :snowplow do + include TrackingHelpers + include SnowplowHelpers + + let(:user) { create(:user) } + let!(:group) { create(:group) } + + before do + allow(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:track_event) + end + + controller(ApplicationController) do + include ProductAnalyticsTracking + + skip_before_action :authenticate_user!, only: :show + track_event(:index, :show, name: 'g_analytics_valuestream', destinations: [:redis_hll, :snowplow], + conditions: [:custom_condition_one?, :custom_condition_two?]) { |controller| controller.get_custom_id } + + def index + render html: 'index' + end + + def new + render html: 'new' + end + + def show + render html: 'show' + end + + def get_custom_id + 'some_custom_id' + end + + private + + def tracking_namespace_source + Group.first + end + + def custom_condition_one? + true + end + + def custom_condition_two? + true + end + end + + def expect_tracking(user: self.user) + expect(Gitlab::UsageDataCounters::HLLRedisCounter).to have_received(:track_event) + .with('g_analytics_valuestream', values: instance_of(String)) + + expect_snowplow_event( + category: anything, + action: 'g_analytics_valuestream', + namespace: group, + user: user + ) + end + + def expect_no_tracking + expect(Gitlab::UsageDataCounters::HLLRedisCounter).not_to receive(:track_event) + + expect_no_snowplow_event + end + + context 'when user is logged in' do + before do + sign_in(user) + end + + it 'tracks the event' do + get :index + + expect_tracking + end + + context 'when FF is disabled' do + before do + stub_feature_flags(route_hll_to_snowplow: false) + end + + it 'doesnt track snowplow event' do + get :index + + expect_no_snowplow_event + end + end + + it 'tracks the event if DNT is not enabled' do + stub_do_not_track('0') + + get :index + + expect_tracking + end + + it 'does not track the event if DNT is enabled' do + stub_do_not_track('1') + + get :index + + expect_no_tracking + end + + it 'does not track the event if the format is not HTML' do + get :index, format: :json + + expect_no_tracking + end + + it 'does not track the event if a custom condition returns false' do + allow(controller).to receive(:custom_condition_two?).and_return(false) + + get :index + + expect_no_tracking + end + + it 'does not track the event for untracked actions' do + get :new + + expect_no_tracking + end + end + + context 'when user is not logged in' do + let(:visitor_id) { SecureRandom.uuid } + + it 'tracks the event when there is a visitor id' do + cookies[:visitor_id] = { value: visitor_id, expires: 24.months } + + get :show, params: { id: 1 } + + expect_tracking(user: nil) + end + end + + context 'when user is not logged in and there is no visitor_id' do + it 'does not track the event' do + get :index + + expect_no_tracking + end + + it 'tracks the event when there is custom id' do + get :show, params: { id: 1 } + + expect_tracking(user: nil) + end + + it 'does not track the HLL event when there is no custom id' do + allow(controller).to receive(:get_custom_id).and_return(nil) + + get :show, params: { id: 2 } + + expect(Gitlab::UsageDataCounters::HLLRedisCounter).not_to receive(:track_event) + expect_snowplow_event( + category: anything, + action: 'g_analytics_valuestream', + namespace: group, + user: nil + ) + end + end +end diff --git a/spec/controllers/jira_connect/events_controller_spec.rb b/spec/controllers/jira_connect/events_controller_spec.rb index 2a70a2ea683..2129b24b2fb 100644 --- a/spec/controllers/jira_connect/events_controller_spec.rb +++ b/spec/controllers/jira_connect/events_controller_spec.rb @@ -43,14 +43,15 @@ RSpec.describe JiraConnect::EventsController do end describe '#installed' do - let(:client_key) { '1234' } - let(:shared_secret) { 'secret' } + let_it_be(:client_key) { '1234' } + let_it_be(:shared_secret) { 'secret' } + let_it_be(:base_url) { 'https://test.atlassian.net' } let(:params) do { clientKey: client_key, sharedSecret: shared_secret, - baseUrl: 'https://test.atlassian.net' + baseUrl: base_url } end @@ -77,11 +78,11 @@ RSpec.describe JiraConnect::EventsController do expect(installation.base_url).to eq('https://test.atlassian.net') end - context 'when it is a version update and shared_secret is not sent' do + context 'when the shared_secret param is missing' do let(:params) do { clientKey: client_key, - baseUrl: 'https://test.atlassian.net' + baseUrl: base_url } end @@ -90,13 +91,48 @@ RSpec.describe JiraConnect::EventsController do expect(response).to have_gitlab_http_status(:unprocessable_entity) end + end + + context 'when an installation already exists' do + let_it_be(:installation) { create(:jira_connect_installation, base_url: base_url, client_key: client_key, shared_secret: shared_secret) } + + it 'validates the JWT token in authorization header and returns 200 without creating a new installation', :aggregate_failures do + expect { subject }.not_to change { JiraConnectInstallation.count } + expect(response).to have_gitlab_http_status(:ok) + end + + context 'when parameters include a new shared secret and base_url' do + let(:shared_secret) { 'new_secret' } + let(:base_url) { 'https://new_test.atlassian.net' } - context 'and an installation exists' do - let!(:installation) { create(:jira_connect_installation, client_key: client_key, shared_secret: shared_secret) } + it 'updates the installation', :aggregate_failures do + subject - it 'validates the JWT token in authorization header and returns 200 without creating a new installation' do - expect { subject }.not_to change { JiraConnectInstallation.count } expect(response).to have_gitlab_http_status(:ok) + expect(installation.reload).to have_attributes( + shared_secret: shared_secret, + base_url: base_url + ) + end + + context 'when the `jira_connect_installation_update` feature flag is disabled' do + before do + stub_feature_flags(jira_connect_installation_update: false) + end + + it 'does not update the installation', :aggregate_failures do + expect { subject }.not_to change { installation.reload.attributes } + expect(response).to have_gitlab_http_status(:ok) + end + end + end + + context 'when the new base_url is invalid' do + let(:base_url) { 'invalid' } + + it 'renders 422', :aggregate_failures do + expect { subject }.not_to change { installation.reload.base_url } + expect(response).to have_gitlab_http_status(:unprocessable_entity) end end end diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_conflicts_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_conflicts_spec.js index bdc38eb0b22..7a92484695c 100644 --- a/spec/frontend/vue_mr_widget/components/states/mr_widget_conflicts_spec.js +++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_conflicts_spec.js @@ -224,8 +224,8 @@ describe('MRWidgetConflicts', () => { }); }); - it('should not allow you to resolve the conflicts', () => { - expect(findResolveButton().exists()).toBe(false); + it('should allow you to resolve the conflicts', () => { + expect(findResolveButton().exists()).toBe(true); }); }); diff --git a/spec/frontend/vue_mr_widget/extentions/code_quality/index_spec.js b/spec/frontend/vue_mr_widget/extentions/code_quality/index_spec.js new file mode 100644 index 00000000000..9a72e4a086b --- /dev/null +++ b/spec/frontend/vue_mr_widget/extentions/code_quality/index_spec.js @@ -0,0 +1,145 @@ +import MockAdapter from 'axios-mock-adapter'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; +import { trimText } from 'helpers/text_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import axios from '~/lib/utils/axios_utils'; +import extensionsContainer from '~/vue_merge_request_widget/components/extensions/container'; +import { registerExtension } from '~/vue_merge_request_widget/components/extensions'; +import codeQualityExtension from '~/vue_merge_request_widget/extensions/code_quality'; +import httpStatusCodes from '~/lib/utils/http_status'; +import { + codeQualityResponseNewErrors, + codeQualityResponseResolvedErrors, + codeQualityResponseResolvedAndNewErrors, + codeQualityResponseNoErrors, +} from './mock_data'; + +describe('Code Quality extension', () => { + let wrapper; + let mock; + + registerExtension(codeQualityExtension); + + const endpoint = '/root/repo/-/merge_requests/4/accessibility_reports.json'; + + const mockApi = (statusCode, data) => { + mock.onGet(endpoint).reply(statusCode, data); + }; + + const findToggleCollapsedButton = () => wrapper.findByTestId('toggle-button'); + const findAllExtensionListItems = () => wrapper.findAllByTestId('extension-list-item'); + + const createComponent = () => { + wrapper = mountExtended(extensionsContainer, { + propsData: { + mr: { + codeQuality: endpoint, + blobPath: { + head_path: 'example/path', + base_path: 'example/path', + }, + }, + }, + }); + }; + + beforeEach(() => { + mock = new MockAdapter(axios); + }); + + afterEach(() => { + wrapper.destroy(); + mock.restore(); + }); + + describe('summary', () => { + it('displays loading text', () => { + mockApi(httpStatusCodes.OK, codeQualityResponseNewErrors); + + createComponent(); + + expect(wrapper.text()).toBe('Code Quality test metrics results are being parsed'); + }); + + it('displays failed loading text', async () => { + mockApi(httpStatusCodes.INTERNAL_SERVER_ERROR); + + createComponent(); + + await waitForPromises(); + expect(wrapper.text()).toBe('Code Quality failed loading results'); + }); + + it('displays quality degradation', async () => { + mockApi(httpStatusCodes.OK, codeQualityResponseNewErrors); + + createComponent(); + + await waitForPromises(); + + expect(wrapper.text()).toBe('Code Quality degraded on 2 points.'); + }); + + it('displays quality improvement', async () => { + mockApi(httpStatusCodes.OK, codeQualityResponseResolvedErrors); + + createComponent(); + + await waitForPromises(); + + expect(wrapper.text()).toBe('Code Quality improved on 2 points.'); + }); + + it('displays quality improvement and degradation', async () => { + mockApi(httpStatusCodes.OK, codeQualityResponseResolvedAndNewErrors); + + createComponent(); + + await waitForPromises(); + + expect(wrapper.text()).toBe('Code Quality improved on 1 point and degraded on 1 point.'); + }); + + it('displays no detected errors', async () => { + mockApi(httpStatusCodes.OK, codeQualityResponseNoErrors); + + createComponent(); + + await waitForPromises(); + + expect(wrapper.text()).toBe('No changes to Code Quality.'); + }); + }); + + describe('expanded data', () => { + beforeEach(async () => { + mockApi(httpStatusCodes.OK, codeQualityResponseResolvedAndNewErrors); + + createComponent(); + + await waitForPromises(); + + findToggleCollapsedButton().trigger('click'); + + await waitForPromises(); + }); + + it('displays all report list items in viewport', async () => { + expect(findAllExtensionListItems()).toHaveLength(2); + }); + + it('displays report list item formatted', () => { + const text = { + newError: trimText(findAllExtensionListItems().at(0).text().replace(/\s+/g, ' ').trim()), + resolvedError: findAllExtensionListItems().at(1).text().replace(/\s+/g, ' ').trim(), + }; + + expect(text.newError).toContain( + "Minor - Parsing error: 'return' outside of function in index.js:12", + ); + expect(text.resolvedError).toContain( + "Minor - Parsing error: 'return' outside of function in index.js:12", + ); + }); + }); +}); diff --git a/spec/frontend/vue_mr_widget/extentions/code_quality/mock_data.js b/spec/frontend/vue_mr_widget/extentions/code_quality/mock_data.js new file mode 100644 index 00000000000..f5ad0ce7377 --- /dev/null +++ b/spec/frontend/vue_mr_widget/extentions/code_quality/mock_data.js @@ -0,0 +1,87 @@ +export const codeQualityResponseNewErrors = { + status: 'failed', + new_errors: [ + { + description: "Parsing error: 'return' outside of function", + severity: 'minor', + file_path: 'index.js', + line: 12, + }, + { + description: 'TODO found', + severity: 'minor', + file_path: '.gitlab-ci.yml', + line: 73, + }, + ], + resolved_errors: [], + existing_errors: [], + summary: { + total: 2, + resolved: 0, + errored: 2, + }, +}; + +export const codeQualityResponseResolvedErrors = { + status: 'failed', + new_errors: [], + resolved_errors: [ + { + description: "Parsing error: 'return' outside of function", + severity: 'minor', + file_path: 'index.js', + line: 12, + }, + { + description: 'TODO found', + severity: 'minor', + file_path: '.gitlab-ci.yml', + line: 73, + }, + ], + existing_errors: [], + summary: { + total: 2, + resolved: 2, + errored: 0, + }, +}; + +export const codeQualityResponseResolvedAndNewErrors = { + status: 'failed', + new_errors: [ + { + description: "Parsing error: 'return' outside of function", + severity: 'minor', + file_path: 'index.js', + line: 12, + }, + ], + resolved_errors: [ + { + description: "Parsing error: 'return' outside of function", + severity: 'minor', + file_path: 'index.js', + line: 12, + }, + ], + existing_errors: [], + summary: { + total: 2, + resolved: 1, + errored: 1, + }, +}; + +export const codeQualityResponseNoErrors = { + status: 'failed', + new_errors: [], + resolved_errors: [], + existing_errors: [], + summary: { + total: 0, + resolved: 0, + errored: 0, + }, +}; diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 21d12ce2856..fc7ac35ed41 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -631,38 +631,6 @@ RSpec.describe Project, factory_default: :keep do expect(project).not_to be_valid end - - context 'restrict_special_characters_in_project_path feature flag is disabled' do - before do - stub_feature_flags(restrict_special_characters_in_project_path: false) - end - - it "allows a path ending in '#{special_character}'" do - project = build(:project, path: "foo#{special_character}") - - expect(project).to be_valid - end - end - end - - context 'restrict_special_characters_in_project_path feature flag is disabled' do - before do - stub_feature_flags(restrict_special_characters_in_project_path: false) - end - - %w[. _].each do |special_character| - it "allows a path starting with '#{special_character}'" do - project = build(:project, path: "#{special_character}foo") - - expect(project).to be_valid - end - end - - it "rejects a path starting with '-'" do - project = build(:project, path: "-foo") - - expect(project).not_to be_valid - end end end end |