From 9b3b16421d88373e195c1ec355f780c4af17eac3 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Tue, 2 Apr 2024 15:09:49 +0000 Subject: [PATCH] Add latest changes from gitlab-org/gitlab@master --- .haml-lint.yml | 1 + .rubocop_todo/gitlab/rails/safe_format.yml | 59 +++++++ Dangerfile | 2 + .../diffs/components/no_changes.vue | 53 +++--- .../pajamas/checkbox_component.html.haml | 2 +- app/components/pajamas/checkbox_component.rb | 3 + app/helpers/merge_requests_helper.rb | 2 +- .../integrations/mattermost_slash_commands.rb | 4 + .../_enforcement_checkbox.html.haml | 3 +- .../_setting_checkbox.html.haml | 20 +++ .../_setting_label_checkbox.html.haml | 17 -- app/workers/integrations/execute_worker.rb | 2 +- app/workers/web_hook_worker.rb | 2 +- .../wiki_content_background_job.yml | 9 ++ doc/api/group_access_tokens.md | 3 - doc/api/project_access_tokens.md | 3 - doc/development/ai_architecture.md | 66 +++++++- doc/development/cascading_settings.md | 28 ++-- .../project/integrations/webhook_events.md | 3 +- .../form_builders/gitlab_ui_form_builder.rb | 4 +- lib/gitlab/hook_data/wiki_page_builder.rb | 20 ++- lib/gitlab/web_hooks.rb | 32 ++++ locale/gitlab.pot | 3 + package.json | 4 +- rubocop/cop/gitlab/rails/safe_format.rb | 133 +++++++++++++++ .../pajamas/checkbox_component_spec.rb | 8 +- .../frontend/__mocks__/@cubejs-client/core.js | 2 +- .../diffs/components/no_changes_spec.js | 7 +- .../gitlab_ui_form_builder_spec.rb | 3 +- .../hook_data/wiki_page_builder_spec.rb | 20 +++ .../mattermost_slash_commands_spec.rb | 8 + spec/models/wiki_page_spec.rb | 20 ++- .../cop/gitlab/rails/safe_format_spec.rb | 151 ++++++++++++++++++ .../integrations/execute_worker_spec.rb | 90 +++++++++++ spec/workers/web_hook_worker_spec.rb | 95 +++++++++++ yarn.lock | 18 +-- 36 files changed, 801 insertions(+), 99 deletions(-) create mode 100644 .rubocop_todo/gitlab/rails/safe_format.yml create mode 100644 app/views/shared/namespaces/cascading_settings/_setting_checkbox.html.haml delete mode 100644 app/views/shared/namespaces/cascading_settings/_setting_label_checkbox.html.haml create mode 100644 config/feature_flags/gitlab_com_derisk/wiki_content_background_job.yml create mode 100644 rubocop/cop/gitlab/rails/safe_format.rb create mode 100644 spec/lib/gitlab/hook_data/wiki_page_builder_spec.rb create mode 100644 spec/rubocop/cop/gitlab/rails/safe_format_spec.rb diff --git a/.haml-lint.yml b/.haml-lint.yml index 4a2d3a9434928..faa858687b7fc 100644 --- a/.haml-lint.yml +++ b/.haml-lint.yml @@ -118,6 +118,7 @@ linters: - Cop/ProjectPathHelper - Gitlab/FeatureAvailableUsage - Gitlab/Json + - Gitlab/Rails/SafeFormat - GitlabSecurity/PublicSend - Layout/FirstHashElementIndentation - Layout/EmptyLineAfterGuardClause diff --git a/.rubocop_todo/gitlab/rails/safe_format.yml b/.rubocop_todo/gitlab/rails/safe_format.yml new file mode 100644 index 0000000000000..4e2fcd05454d1 --- /dev/null +++ b/.rubocop_todo/gitlab/rails/safe_format.yml @@ -0,0 +1,59 @@ +--- +# Cop supports --autocorrect. +Gitlab/Rails/SafeFormat: + Details: grace period + Exclude: + - 'app/controllers/profiles/two_factor_auths_controller.rb' + - 'app/graphql/types/project_type.rb' + - 'app/helpers/auth_helper.rb' + - 'app/helpers/emails_helper.rb' + - 'app/helpers/groups/group_members_helper.rb' + - 'app/helpers/groups_helper.rb' + - 'app/helpers/import_helper.rb' + - 'app/helpers/members_helper.rb' + - 'app/helpers/merge_requests_helper.rb' + - 'app/helpers/profiles_helper.rb' + - 'app/helpers/projects_helper.rb' + - 'app/helpers/reminder_emails_helper.rb' + - 'app/helpers/search_helper.rb' + - 'app/helpers/sourcegraph_helper.rb' + - 'app/helpers/whats_new_helper.rb' + - 'app/models/integrations/apple_app_store.rb' + - 'app/models/integrations/asana.rb' + - 'app/models/integrations/bamboo.rb' + - 'app/models/integrations/beyond_identity.rb' + - 'app/models/integrations/bugzilla.rb' + - 'app/models/integrations/clickup.rb' + - 'app/models/integrations/confluence.rb' + - 'app/models/integrations/custom_issue_tracker.rb' + - 'app/models/integrations/datadog.rb' + - 'app/models/integrations/discord.rb' + - 'app/models/integrations/ewm.rb' + - 'app/models/integrations/external_wiki.rb' + - 'app/models/integrations/google_play.rb' + - 'app/models/integrations/hangouts_chat.rb' + - 'app/models/integrations/irker.rb' + - 'app/models/integrations/jenkins.rb' + - 'app/models/integrations/mattermost.rb' + - 'app/models/integrations/pivotaltracker.rb' + - 'app/models/integrations/redmine.rb' + - 'app/models/integrations/unify_circuit.rb' + - 'app/models/integrations/youtrack.rb' + - 'app/presenters/ci/pipeline_presenter.rb' + - 'app/presenters/key_presenter.rb' + - 'app/presenters/project_presenter.rb' + - 'app/services/jira/requests/base.rb' + - 'app/services/security/ci_configuration/base_create_service.rb' + - 'ee/app/components/namespaces/free_user_cap/enforcement_alert_component.rb' + - 'ee/app/components/namespaces/free_user_cap/usage_quota_alert_component.rb' + - 'ee/app/components/namespaces/free_user_cap/usage_quota_trial_alert_component.rb' + - 'ee/app/helpers/ee/application_helper.rb' + - 'ee/app/helpers/ee/import_helper.rb' + - 'ee/app/helpers/ee/members_helper.rb' + - 'ee/app/helpers/ee/search_helper.rb' + - 'ee/app/helpers/push_rules_helper.rb' + - 'ee/app/models/integrations/git_guardian.rb' + - 'ee/app/models/integrations/github.rb' + - 'ee/lib/gitlab/licenses/submit_license_usage_data_banner.rb' + - 'ee/lib/gitlab/manual_quarterly_co_term_banner.rb' + - 'spec/helpers/profiles_helper_spec.rb' diff --git a/Dangerfile b/Dangerfile index 0bb0036fa765c..2c63e5a79dc8f 100644 --- a/Dangerfile +++ b/Dangerfile @@ -13,7 +13,9 @@ Gitlab::Dangerfiles.for_project(self, project_name) do |gitlab_dangerfiles| gitlab_dangerfiles.import_plugins gitlab_dangerfiles.config.ci_only_rules = ProjectHelper::CI_ONLY_RULES gitlab_dangerfiles.config.files_to_category = ProjectHelper::CATEGORIES + gitlab_dangerfiles.config.excluded_required_codeowners_sections_for_roulette.push('Database') + gitlab_dangerfiles.config.included_optional_codeowners_sections_for_roulette.push('Backend Static Code Analysis') gitlab_dangerfiles.import_dangerfiles(except: %w[simple_roulette]) end diff --git a/app/assets/javascripts/diffs/components/no_changes.vue b/app/assets/javascripts/diffs/components/no_changes.vue index ab5f31a1fb7c8..b5d31cafedcbb 100644 --- a/app/assets/javascripts/diffs/components/no_changes.vue +++ b/app/assets/javascripts/diffs/components/no_changes.vue @@ -1,12 +1,18 @@ diff --git a/app/components/pajamas/checkbox_component.html.haml b/app/components/pajamas/checkbox_component.html.haml index 9e3d4e68a4233..52cc92f80acdf 100644 --- a/app/components/pajamas/checkbox_component.html.haml +++ b/app/components/pajamas/checkbox_component.html.haml @@ -1,4 +1,4 @@ -.gl-form-checkbox.custom-control.custom-checkbox +.gl-form-checkbox.custom-control.custom-checkbox{ content_wrapper_options } = form.check_box(method, formatted_input_options, checked_value, diff --git a/app/components/pajamas/checkbox_component.rb b/app/components/pajamas/checkbox_component.rb index d9987b7653c36..5d51a8fb0e386 100644 --- a/app/components/pajamas/checkbox_component.rb +++ b/app/components/pajamas/checkbox_component.rb @@ -20,6 +20,7 @@ def initialize( help_text: nil, label_options: {}, checkbox_options: {}, + content_wrapper_options: {}, checked_value: '1', unchecked_value: '0' ) @@ -29,6 +30,7 @@ def initialize( @help_text_argument = help_text @label_options = label_options @input_options = checkbox_options + @content_wrapper_options = content_wrapper_options @checked_value = checked_value @unchecked_value = unchecked_value @value = checked_value if checkbox_options[:multiple] @@ -43,6 +45,7 @@ def initialize( :help_text_argument, :label_options, :input_options, + :content_wrapper_options, :checked_value, :unchecked_value, :value diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb index 583b4ce76b032..a9a8964212fd7 100644 --- a/app/helpers/merge_requests_helper.rb +++ b/app/helpers/merge_requests_helper.rb @@ -188,7 +188,7 @@ def diffs_tab_pane_data(project, merge_request, params) current_user_data: @current_user_data, update_current_user_path: @update_current_user_path, project_path: project_path(merge_request.project), - changes_empty_state_illustration: image_path('illustrations/merge_request_changes_empty.svg'), + changes_empty_state_illustration: image_path('illustrations/empty-state/empty-commit-md.svg'), is_fluid_layout: fluid_layout.to_s, dismiss_endpoint: callouts_path, show_suggest_popover: show_suggest_popover?.to_s, diff --git a/app/models/integrations/mattermost_slash_commands.rb b/app/models/integrations/mattermost_slash_commands.rb index b8cf4fc73084c..b43c779244531 100644 --- a/app/models/integrations/mattermost_slash_commands.rb +++ b/app/models/integrations/mattermost_slash_commands.rb @@ -30,6 +30,10 @@ def self.to_param 'mattermost_slash_commands' end + def avatar_url + ActionController::Base.helpers.image_path('illustrations/third-party-logos/integrations-logos/mattermost.svg') + end + def configure(user, params) token = ::Mattermost::Command.new(user) .create(command(params)) diff --git a/app/views/shared/namespaces/cascading_settings/_enforcement_checkbox.html.haml b/app/views/shared/namespaces/cascading_settings/_enforcement_checkbox.html.haml index 68a4d01087261..4c07598c48cf8 100644 --- a/app/views/shared/namespaces/cascading_settings/_enforcement_checkbox.html.haml +++ b/app/views/shared/namespaces/cascading_settings/_enforcement_checkbox.html.haml @@ -13,4 +13,5 @@ = form.gitlab_ui_checkbox_component lock_attribute, label, help_text: help_text, - checkbox_options: { checked: group.namespace_settings.public_send(lock_attribute), data: { testid: 'enforce-for-all-subgroups-checkbox' } } + checkbox_options: { checked: group.namespace_settings.public_send(lock_attribute), data: { testid: 'enforce-for-all-subgroups-checkbox' } }, + content_wrapper_options: { class: 'gl-pl-7!' } diff --git a/app/views/shared/namespaces/cascading_settings/_setting_checkbox.html.haml b/app/views/shared/namespaces/cascading_settings/_setting_checkbox.html.haml new file mode 100644 index 0000000000000..a414a6af10eb1 --- /dev/null +++ b/app/views/shared/namespaces/cascading_settings/_setting_checkbox.html.haml @@ -0,0 +1,20 @@ +- attribute = local_assigns.fetch(:attribute, nil) +- group = local_assigns.fetch(:group, nil) +- settings_path_helper = local_assigns.fetch(:settings_path_helper, nil) +- form = local_assigns.fetch(:form, nil) +- setting_locked = local_assigns.fetch(:setting_locked, false) +- help_text = local_assigns.fetch(:help_text, nil) +- checked = local_assigns.fetch(:checked, false) +- klass = local_assigns.fetch(:class, nil) + +- return unless attribute && group && form && settings_path_helper + += form.gitlab_ui_checkbox_component attribute, checkbox_options: { checked: checked, disabled: setting_locked, class: klass } do |c| + = c.with_label attribute, class: 'custom-control-label', aria: { disabled: setting_locked } do + = render 'shared/namespaces/cascading_settings/setting_label_container' do + = yield + - if setting_locked + = render 'shared/namespaces/cascading_settings/lock_icon', local_assigns + - if help_text + = c.with_help_text do + = help_text diff --git a/app/views/shared/namespaces/cascading_settings/_setting_label_checkbox.html.haml b/app/views/shared/namespaces/cascading_settings/_setting_label_checkbox.html.haml deleted file mode 100644 index 83d602aba2185..0000000000000 --- a/app/views/shared/namespaces/cascading_settings/_setting_label_checkbox.html.haml +++ /dev/null @@ -1,17 +0,0 @@ -- attribute = local_assigns.fetch(:attribute, nil) -- group = local_assigns.fetch(:group, nil) -- settings_path_helper = local_assigns.fetch(:settings_path_helper, nil) -- form = local_assigns.fetch(:form, nil) -- setting_locked = local_assigns.fetch(:setting_locked, false) -- help_text = local_assigns.fetch(:help_text, nil) - -- return unless attribute && group && form && settings_path_helper - -= form.label attribute, class: 'custom-control-label', aria: { disabled: setting_locked } do - = render 'shared/namespaces/cascading_settings/setting_label_container' do - = yield - - if setting_locked - = render 'shared/namespaces/cascading_settings/lock_icon', local_assigns - - if help_text - %p.help-text - = help_text diff --git a/app/workers/integrations/execute_worker.rb b/app/workers/integrations/execute_worker.rb index 6fe1937a222d3..c54f5073a4f8f 100644 --- a/app/workers/integrations/execute_worker.rb +++ b/app/workers/integrations/execute_worker.rb @@ -15,7 +15,7 @@ class ExecuteWorker # rubocop:disable Scalability/IdempotentWorker def perform(hook_id, data) return if ::Gitlab::SilentMode.enabled? - data = data.with_indifferent_access + data = Gitlab::WebHooks.prepare_data(data) integration = Integration.find_by_id(hook_id) return unless integration diff --git a/app/workers/web_hook_worker.rb b/app/workers/web_hook_worker.rb index cea0816f5a6d0..8ea4d283b3bd4 100644 --- a/app/workers/web_hook_worker.rb +++ b/app/workers/web_hook_worker.rb @@ -17,7 +17,7 @@ def perform(hook_id, data, hook_name, params = {}) hook = WebHook.find_by_id(hook_id) return unless hook - data = data.with_indifferent_access + data = Gitlab::WebHooks.prepare_data(data) params.symbolize_keys! # Before executing the hook, reapply any recursion detection UUID that was initially diff --git a/config/feature_flags/gitlab_com_derisk/wiki_content_background_job.yml b/config/feature_flags/gitlab_com_derisk/wiki_content_background_job.yml new file mode 100644 index 0000000000000..13fedf973ebd2 --- /dev/null +++ b/config/feature_flags/gitlab_com_derisk/wiki_content_background_job.yml @@ -0,0 +1,9 @@ +--- +name: wiki_content_background_job +feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/367628 +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/146973 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/452495 +milestone: '16.11' +group: group::knowledge +type: gitlab_com_derisk +default_enabled: false diff --git a/doc/api/group_access_tokens.md b/doc/api/group_access_tokens.md index 83f6e6f0a9b53..257bd420930ea 100644 --- a/doc/api/group_access_tokens.md +++ b/doc/api/group_access_tokens.md @@ -150,9 +150,6 @@ POST /groups/:id/access_tokens/:token_id/rotate | `token_id` | integer | yes | ID of the access token | | `expires_at` | date | no | Expiration date of the access token in ISO format (`YYYY-MM-DD`). [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/416795) in GitLab 16.6. | -NOTE: -Non-administrators can rotate their own tokens. Administrators can rotate tokens of any user in the group. - ```shell curl --request POST --header "PRIVATE-TOKEN: " "https://gitlab.example.com/api/v4/groups//access_tokens//rotate" ``` diff --git a/doc/api/project_access_tokens.md b/doc/api/project_access_tokens.md index 44741c8551ddb..4dc4af2e1d552 100644 --- a/doc/api/project_access_tokens.md +++ b/doc/api/project_access_tokens.md @@ -159,9 +159,6 @@ POST /projects/:id/access_tokens/:token_id/rotate | `token_id` | integer | yes | ID of the project access token | | `expires_at` | date | no | Expiration date of the access token in ISO format (`YYYY-MM-DD`). [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/416795) in GitLab 16.6. | -NOTE: -Non-administrators can rotate their own tokens. Administrators can rotate tokens of any user in the project. - ```shell curl --request POST --header "PRIVATE-TOKEN: " "https://gitlab.example.com/api/v4/projects//access_tokens//rotate" ``` diff --git a/doc/development/ai_architecture.md b/doc/development/ai_architecture.md index 974d5bf30c353..318097f26fc4f 100644 --- a/doc/development/ai_architecture.md +++ b/doc/development/ai_architecture.md @@ -6,16 +6,65 @@ info: Any user with at least the Maintainer role can merge updates to this conte # AI Architecture -GitLab has created a common set of tools to support our product groups and their utilization of AI. Our goals with this common architecture are: +This document describes architecture shared by the GitLab Duo AI features. For historical motivation and goals of this architecture, see the [AI Gateway Architecture blueprint](../architecture/blueprints/ai_gateway/index.md). + +## Introduction + +The following diagram shows a simplified view of how the different components in GitLab interact. + +```plantuml +@startuml +!theme cloudscape-design +skinparam componentStyle rectangle + +package Clients { + [IDEs, Code Editors, Language Server] as IDE + [GitLab Web Frontend] as GLWEB +} + +[GitLab.com] as GLCOM +[Self-Managed/Dedicated] as SMI +[CustomersDot API] as CD +[AI Gateway] as AIGW + +package Models { + [3rd party models (Anthropic,VertexAI)] as THIRD + [GitLab Native Models] as GLNM +} + +Clients -down-> GLCOM : REST/Websockets +Clients -down-> SMI : REST/Websockets +SMI -right-> CD : License + JWT Sync +GLCOM -down-> AIGW : Prompts + Telemetry + JWT (REST) + +SMI -down-> AIGW : Prompts + Telemetry + JWT (REST) +AIGW -up-> GLCOM : JWKS public key sync +AIGW -up-> CD : JWKS public key sync +AIGW -down-> Models : prompts +@enduml +``` + +- **AI Abstraction layer** - Every GitLab instance (Self-Managed, GitLab.com, ..) contains an [AI Abstraction layer](ai_features/index.md) which provides a framework for implementing new AI features in the monolith. This layer adds contextual information to the request and does request pre/post processing. -1. Increase the velocity of feature teams by providing a set of high quality, ready to use tools -1. Ability to switch underlying technologies quickly and easily +### Systems -AI is moving very quickly, and we need to be able to keep pace with changes in the area. We have built an [abstraction layer](ai_features/index.md) to do this, allowing us to take a more "pluggable" approach to the underlying models, data stores, and other technologies. +- [GitLab instances](https://gitlab.com/gitlab-org/gitlab) - GitLab monolith that powers all types of GitLab instances +- [CustomersDot](https://gitlab.com/gitlab-org/customers-gitlab-com) - Allows customers to buy and upgrade subscriptions by adding more seats and add/edit payment records. It also manages self-managed licenses. +- [AI Gateway](https://gitlab.com/gitlab-org/modelops/applied-ml/code-suggestions/ai-assist) - System that provides unified interface for invoking models. Deployed in Google Cloud Run (using [Runway](https://gitlab.com/gitlab-com/gl-infra/platform/runway)). +- Extensions + - [Language Server](https://gitlab.com/gitlab-org/editor-extensions/gitlab-lsp) (powers code suggestions in VS Code, VisualStudio and Neovim) + - [VS Code](https://gitlab.com/gitlab-org/gitlab-vscode-extension) + - [JetBrains](https://gitlab.com/gitlab-org/editor-extensions/gitlab-jetbrains-plugin) + - [Visual Studio](https://gitlab.com/gitlab-org/editor-extensions/gitlab-visual-studio-extension) + - [Neovim](https://gitlab.com/gitlab-org/editor-extensions/gitlab.vim) -The following diagram from the [architecture blueprint](../architecture/blueprints/ai_gateway/index.md) shows a simplified view of how the different components in GitLab interact. The abstraction layer helps avoid code duplication within the REST APIs. +### Difference between how GitLab.com and Self-Managed/Dedicated access AI Gateway -![architecture diagram](../architecture/blueprints/ai_gateway/img/architecture.png) +- GitLab.com + - GitLab.com instances self-issue JWT Auth token signed with a private key. +- Other types of instances + - Self-Managed and Dedicated regularly synchronise their licenses and AI Access tokens with CustomersDot. + - Self-Managed and Dedicated instances route traffic to appropriate AI Gateway. ## SaaS-based AI abstraction layer @@ -128,3 +177,8 @@ Code Suggestions acceptance rates are _highly_ sensitive to latency. While writi In a worst case with sufficient latency, the IDE could be issuing a string of requests, each of which is then ignored as the user proceeds without waiting for the response. This adds no value for the user, while still putting load on our services. See our discussions [here](https://gitlab.com/gitlab-org/gitlab/-/issues/418955) around how we plan to iterate on latency for this feature. + +## Future changes to the architecture + +- We plan on deploying [AI Gateway](https://gitlab.com/gitlab-org/modelops/applied-ml/code-suggestions/ai-assist) in different regions to improve latency (see the ed epic [Multi-region support for AI Gateway](https://gitlab.com/groups/gitlab-com/gl-infra/-/epics/1206)). +- We would like to centralize telemetry. However, centralizing AI (or, Cloud Connector) telemetry is a difficult and unsolved problem as of now. diff --git a/doc/development/cascading_settings.md b/doc/development/cascading_settings.md index 930522a3ab500..5c4c1cfed9147 100644 --- a/doc/development/cascading_settings.md +++ b/doc/development/cascading_settings.md @@ -136,7 +136,7 @@ Renders the enforcement checkbox. | `setting_locked` | If the setting is locked by an ancestor group or administrator setting. Can be calculated with [`cascading_namespace_setting_locked?`](https://gitlab.com/gitlab-org/gitlab/-/blob/c2736823b8e922e26fd35df4f0cd77019243c858/app/helpers/namespaces_helper.rb#L86). | `Boolean` | `true` | | `help_text` | Text shown below the checkbox. | `String` | `false` (Subgroups cannot change this setting.) | -[`_setting_label_checkbox.html.haml`](https://gitlab.com/gitlab-org/gitlab/-/blob/c2736823b8e922e26fd35df4f0cd77019243c858/app/views/shared/namespaces/cascading_settings/_setting_label_checkbox.html.haml) +[`_setting_checkbox.html.haml`](https://gitlab.com/gitlab-org/gitlab/-/blob/e915f204f9eb5930760722ce28b4db60b1159677/app/views/shared/namespaces/cascading_settings/_setting_checkbox.html.haml) Renders the label for a checkbox setting. @@ -184,20 +184,18 @@ This function should be imported and called in the [page-specific JavaScript](fe = form_for @group do |f| .form-group{ data: { testid: 'delayed-project-removal-form-group' } } - .gl-form-checkbox.custom-control.custom-checkbox - = f.check_box :delayed_project_removal, checked: @group.namespace_settings.delayed_project_removal?, disabled: delayed_project_removal_locked, class: 'custom-control-input' - = render 'shared/namespaces/cascading_settings/setting_label_checkbox', attribute: :delayed_project_removal, - group: @group, - form: f, - setting_locked: delayed_project_removal_locked, - settings_path_helper: -> (locked_ancestor) { edit_group_path(locked_ancestor, anchor: 'js-permissions-settings') }, - help_text: s_('Settings|Projects will be permanently deleted after a 7-day delay. Inherited by subgroups.') do - = s_('Settings|Enable delayed project deletion') - = render 'shared/namespaces/cascading_settings/enforcement_checkbox', - attribute: :delayed_project_removal, - group: @group, - form: f, - setting_locked: delayed_project_removal_locked + = render 'shared/namespaces/cascading_settings/setting_checkbox', attribute: :delayed_project_removal, + group: @group, + form: f, + setting_locked: delayed_project_removal_locked, + settings_path_helper: -> (locked_ancestor) { edit_group_path(locked_ancestor, anchor: 'js-permissions-settings') }, + help_text: s_('Settings|Projects will be permanently deleted after a 7-day delay. Inherited by subgroups.') do + = s_('Settings|Enable delayed project deletion') + = render 'shared/namespaces/cascading_settings/enforcement_checkbox', + attribute: :delayed_project_removal, + group: @group, + form: f, + setting_locked: delayed_project_removal_locked %fieldset.form-group = render 'shared/namespaces/cascading_settings/setting_label_fieldset', attribute: :merge_method, diff --git a/doc/user/project/integrations/webhook_events.md b/doc/user/project/integrations/webhook_events.md index cf9d6daf70e3c..351ae566e934b 100644 --- a/doc/user/project/integrations/webhook_events.md +++ b/doc/user/project/integrations/webhook_events.md @@ -1118,7 +1118,8 @@ Payload example: "slug": "awesome", "url": "http://example.com/root/awesome-project/-/wikis/awesome", "action": "create", - "diff_url": "http://example.com/root/awesome-project/-/wikis/home/diff?version_id=78ee4a6705abfbff4f4132c6646dbaae9c8fb6ec" + "diff_url": "http://example.com/root/awesome-project/-/wikis/home/diff?version_id=78ee4a6705abfbff4f4132c6646dbaae9c8fb6ec", + "version_id": "3ad67c972065298d226dd80b2b03e0fc2421e731" } } ``` diff --git a/lib/gitlab/form_builders/gitlab_ui_form_builder.rb b/lib/gitlab/form_builders/gitlab_ui_form_builder.rb index ea98f6b2eec45..d89eaebdcb7f0 100644 --- a/lib/gitlab/form_builders/gitlab_ui_form_builder.rb +++ b/lib/gitlab/form_builders/gitlab_ui_form_builder.rb @@ -25,6 +25,7 @@ def gitlab_ui_checkbox_component( checked_value: '1', unchecked_value: '0', label_options: {}, + content_wrapper_options: {}, &block ) Pajamas::CheckboxComponent.new( @@ -35,7 +36,8 @@ def gitlab_ui_checkbox_component( checkbox_options: format_options(checkbox_options), checked_value: checked_value, unchecked_value: unchecked_value, - label_options: format_options(label_options) + label_options: format_options(label_options), + content_wrapper_options: content_wrapper_options ).render_in(@template, &block) end diff --git a/lib/gitlab/hook_data/wiki_page_builder.rb b/lib/gitlab/hook_data/wiki_page_builder.rb index 6f1a9fbea1eec..060c1ef58f17f 100644 --- a/lib/gitlab/hook_data/wiki_page_builder.rb +++ b/lib/gitlab/hook_data/wiki_page_builder.rb @@ -6,16 +6,34 @@ class WikiPageBuilder < BaseBuilder alias_method :wiki_page, :object def build + project_id = wiki_page.wiki.id + return legacy_build unless Feature.enabled?(:wiki_content_background_job, Project.actor_from_id(project_id)) + wiki_page .attributes + .except(:content) .merge( - 'content' => absolute_image_urls(wiki_page.content) + version_id: wiki_page.version&.id ) end + def page_content + absolute_image_urls(wiki_page.content) + end + def uploads_prefix wiki_page.wiki.wiki_base_path end + + private + + def legacy_build + wiki_page + .attributes + .merge( + 'content' => absolute_image_urls(wiki_page.content) + ) + end end end end diff --git a/lib/gitlab/web_hooks.rb b/lib/gitlab/web_hooks.rb index 031f69f3679f7..d024ed4550c22 100644 --- a/lib/gitlab/web_hooks.rb +++ b/lib/gitlab/web_hooks.rb @@ -5,5 +5,37 @@ module WebHooks GITLAB_EVENT_HEADER = 'X-Gitlab-Event' GITLAB_INSTANCE_HEADER = 'X-Gitlab-Instance' GITLAB_UUID_HEADER = 'X-Gitlab-Webhook-UUID' + + class << self + def prepare_data(data) + data = data.with_indifferent_access + + return data unless data[:object_kind] == 'wiki_page' + + prepare_wiki_data(data) + end + + private + + # Wiki webhook data does not have "content" attribute yet. + # As Wiki content is versioned in git, we can lazily retrieve the content + # from source control and it will be identical to when webhook event was triggered. + # This is an optimization to serializing wiki content data which can + # sometimes be over the Sidekiq payload limit. + def prepare_wiki_data(data) + project_id = data.dig(:project, :id) + slug = data.dig(:object_attributes, :slug) + version_id = data.dig(:object_attributes, :version_id) + return data unless [project_id, slug, version_id].all?(&:present?) + + wiki = ProjectWiki.find_by_id(project_id) + return data unless wiki + + page = wiki.find_page(slug, version_id) + return data unless page + + data.deep_merge(object_attributes: { content: Gitlab::HookData::WikiPageBuilder.new(page).page_content }) + end + end end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 1ee6afaef416f..d40a2622d1325 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -31778,6 +31778,9 @@ msgstr "" msgid "MergeRequest|Search (e.g. *.vue) (%{MODIFIER_KEY}P)" msgstr "" +msgid "MergeRequest|There are no changes yet" +msgstr "" + msgid "MergeRequest|This description was generated for revision %{revision} using AI" msgstr "" diff --git a/package.json b/package.json index 7343c6c83467a..b6c4130e987e8 100644 --- a/package.json +++ b/package.json @@ -52,8 +52,8 @@ "@apollo/client": "^3.5.10", "@babel/core": "^7.23.7", "@babel/preset-env": "^7.23.7", - "@cubejs-client/core": "^0.34.60", - "@cubejs-client/vue": "^0.34.60", + "@cubejs-client/core": "^0.35.0", + "@cubejs-client/vue": "^0.35.0", "@floating-ui/dom": "^1.2.9", "@gitlab/application-sdk-browser": "^0.3.2", "@gitlab/at.js": "1.5.7", diff --git a/rubocop/cop/gitlab/rails/safe_format.rb b/rubocop/cop/gitlab/rails/safe_format.rb new file mode 100644 index 0000000000000..7d3e01cdce91a --- /dev/null +++ b/rubocop/cop/gitlab/rails/safe_format.rb @@ -0,0 +1,133 @@ +# frozen_string_literal: true + +module RuboCop + module Cop + module Gitlab + module Rails + # Enforce `safe_format` for externalized strings with interpolations and `.html_safe`. + # + # @example + # # bad + # _('string %{open}foo%{close}').html_safe % { open: ''.html_safe, close: ''.html_safe } + # format(_('string %{open}foo%{close}').html_safe, open: ''.html_safe, close: ''.html_safe) + # _('foo').html_safe + # + # # good + # safe_format(_('string %{open}foo%{close}'), tag_pair(tag.b, :open, :close)) + # safe_format('foo') + # + # # also good no `html_safe` + # format(_('string %{var} number'), var: var) + # _('string %{var} number') % { var: var } + class SafeFormat < RuboCop::Cop::Base + extend RuboCop::Cop::AutoCorrector + + MSG = 'Use `safe_format` to interpolate externalized strings. ' \ + 'See https://docs.gitlab.com/ee/development/i18n/externalization.html#html' + + RESTRICT_ON_SEND = %i[_ s_ N_ n_].freeze + + def_node_matcher :wrapped_by?, <<~PATTERN + ^(send _ %method ...) + PATTERN + + def on_send(gettext) + return unless wrapped_by?(gettext, method: :html_safe) + return if wrapped_by?(gettext, method: :safe_format) + + call, args = find_call_and_args(gettext) + + node_to_replace = call + node_to_replace = node_to_replace.parent if wrapped_by?(node_to_replace, method: :html_safe) + node_to_replace = node_to_replace.parent if wrapped_by?(node_to_replace, method: :html_escape) + + add_offense(call.loc.selector) do |corrector| + use_safe_format(corrector, node_to_replace, gettext, args) + end + end + + private + + def find_call_and_args(node) + call = node + args = [] + + node.each_ancestor do |ancestor| + break unless ancestor.send_type? + + case ancestor.send_type? && ancestor.method_name + when :format + call = ancestor + args = ancestor.arguments.drop(1) + break + when :% + call = ancestor + args = ancestor.arguments + break + end + end + + [call, args] + end + + def use_safe_format(corrector, node, gettext, args) + receiver = gettext.source + + if args&.any? + args = unwrap_args(args) + .then { |args| use_tag_pair(args) } + .then { |args| sourcify_args(args) } + + corrector.replace(node, "safe_format(#{receiver}, #{args})") + else + corrector.replace(node, "safe_format(#{receiver})") + end + end + + def unwrap_args(args) + return args[0].children if args[0].array_type? || args[0].hash_type? + + args + end + + # Turns `open: ''.html_safe, close: ''.html_safe` into + # `tag_pair(tag.b, :open, :close)`. + def use_tag_pair(args) + return args unless args.all?(&:pair_type?) + + pair_hash = args.to_h { |pair| [pair.key, pair.value] } + seen = Hash.new { |hash, tag| hash[tag] = [] } + tag_pairs = [] + + args.each do |pair| + # We only care about { a: ''.html_safe }. Ignore the rest + next unless pair.value.send_type? && pair.value.method?(:html_safe) && pair.value.receiver.str_type? + + # Extract the tag from `` or ``. + tag = pair.value.receiver.value[%r{^}, 1] + next unless tag + + seen[tag] << pair + next unless seen[tag].size == 2 + + closing_tag, opening_tag = seen[tag].sort_by { |pair| pair.value.receiver.value } + pair_hash.delete(closing_tag.key) + pair_hash.delete(opening_tag.key) + + keys = [opening_tag, closing_tag].map { |pair| pair.key.value.inspect }.join(", ") + tag_pairs << "tag_pair(tag.#{tag}, #{keys})" + + seen[tag].clear + end + + tag_pairs + pair_hash.keys.map(&:parent) + end + + def sourcify_args(args) + args.map { |arg| arg.respond_to?(:source) ? arg.source : arg }.join(', ') + end + end + end + end + end +end diff --git a/spec/components/pajamas/checkbox_component_spec.rb b/spec/components/pajamas/checkbox_component_spec.rb index ea3ebe7781186..f8d0104409813 100644 --- a/spec/components/pajamas/checkbox_component_spec.rb +++ b/spec/components/pajamas/checkbox_component_spec.rb @@ -34,6 +34,7 @@ let_it_be(:unchecked_value) { 'no' } let_it_be(:checkbox_options) { { class: 'checkbox-foo-bar', checked: true } } let_it_be(:label_options) { { class: 'label-foo-bar' } } + let_it_be(:content_wrapper_options) { { class: 'wrapper-foo-bar' } } before do fake_form_for do |form| @@ -46,7 +47,8 @@ checked_value: checked_value, unchecked_value: unchecked_value, checkbox_options: checkbox_options, - label_options: label_options + label_options: label_options, + content_wrapper_options: content_wrapper_options ) ) end @@ -62,6 +64,10 @@ expect(page).to have_selector('label.label-foo-bar') end + it 'adds CSS class to wrapper' do + expect(page).to have_selector('.gl-form-checkbox.wrapper-foo-bar') + end + it 'renders hidden input with value of `no`' do expect(page).to have_field('user[view_diffs_file_by_file]', type: 'hidden', with: unchecked_value) end diff --git a/spec/frontend/__mocks__/@cubejs-client/core.js b/spec/frontend/__mocks__/@cubejs-client/core.js index 549899aa8d8ca..969ab5e8ae23f 100644 --- a/spec/frontend/__mocks__/@cubejs-client/core.js +++ b/spec/frontend/__mocks__/@cubejs-client/core.js @@ -1,7 +1,7 @@ let mockLoad = jest.fn(); let mockMetadata = jest.fn(); -export const CubejsApi = jest.fn().mockImplementation(() => ({ +export const CubeApi = jest.fn().mockImplementation(() => ({ load: mockLoad, meta: mockMetadata, })); diff --git a/spec/frontend/diffs/components/no_changes_spec.js b/spec/frontend/diffs/components/no_changes_spec.js index fd89d52a59e0e..ef86e3a0e9567 100644 --- a/spec/frontend/diffs/components/no_changes_spec.js +++ b/spec/frontend/diffs/components/no_changes_spec.js @@ -1,4 +1,4 @@ -import { GlButton } from '@gitlab/ui'; +import { GlEmptyState } from '@gitlab/ui'; import { shallowMount, mount } from '@vue/test-utils'; import NoChanges from '~/diffs/components/no_changes.vue'; import store from '~/mr_notes/stores'; @@ -32,6 +32,7 @@ describe('Diff no changes empty state', () => { store.getters['diffs/diffCompareDropdownTargetVersions'] = []; }); + const findEmptyState = (wrapper) => wrapper.findComponent(GlEmptyState); const findMessage = (wrapper) => wrapper.find('[data-testid="no-changes-message"]'); it('prevents XSS', () => { @@ -46,10 +47,10 @@ describe('Diff no changes empty state', () => { }); describe('Renders', () => { - it('Show create commit button', () => { + it('Show empty state', () => { const wrapper = createComponent(); - expect(wrapper.findComponent(GlButton).exists()).toBe(true); + expect(findEmptyState(wrapper).exists()).toBe(true); }); it.each` diff --git a/spec/lib/gitlab/form_builders/gitlab_ui_form_builder_spec.rb b/spec/lib/gitlab/form_builders/gitlab_ui_form_builder_spec.rb index b8829cc794c2a..8b48eb2f9d08a 100644 --- a/spec/lib/gitlab/form_builders/gitlab_ui_form_builder_spec.rb +++ b/spec/lib/gitlab/form_builders/gitlab_ui_form_builder_spec.rb @@ -77,6 +77,7 @@ help_text: 'Instead of all the files changed, show only one file at a time.', checkbox_options: { class: 'checkbox-foo-bar' }, label_options: { class: 'label-foo-bar' }, + content_wrapper_options: { class: 'wrapper-foo-bar' }, checked_value: '3', unchecked_value: '1' } @@ -84,7 +85,7 @@ it 'renders help text' do expected_html = <<~EOS -
+