Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2025-05-30 18:14:51 +00:00
parent d49e08600d
commit 42d6e66197
106 changed files with 1068 additions and 2171 deletions

View File

@ -329,7 +329,6 @@ Layout/LineEndStringConcatenationIndentation:
- 'lib/api/validations/validators/bulk_imports.rb'
- 'lib/backup/manager.rb'
- 'lib/backup/remote_storage.rb'
- 'lib/banzai/filter/dollar_math_pre_legacy_filter.rb'
- 'lib/banzai/filter/spaced_link_filter.rb'
- 'lib/bulk_imports/error.rb'
- 'lib/feature/definition.rb'

View File

@ -118,7 +118,6 @@ Style/SuperArguments:
- 'lib/api/api_guard.rb'
- 'lib/api/entities/project_with_access.rb'
- 'lib/backup/targets/database.rb'
- 'lib/banzai/filter/blockquote_fence_legacy_filter.rb'
- 'lib/banzai/filter/jira_import/adf_to_commonmark_filter.rb'
- 'lib/banzai/filter/markdown_filter.rb'
- 'lib/banzai/filter/references/milestone_reference_filter.rb'

View File

@ -18,6 +18,7 @@ class Projects::IssuesController < Projects::ApplicationController
before_action :disable_query_limiting, only: [:create_merge_request, :move, :bulk_update]
before_action :disable_show_query_limit!, only: :show
before_action :disable_create_query_limit!, only: :create
before_action :check_issues_available!
before_action :issue, unless: ->(c) { ISSUES_EXCEPT_ACTIONS.include?(c.action_name.to_sym) }
@ -412,6 +413,10 @@ class Projects::IssuesController < Projects::ApplicationController
Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab/-/issues/544875', new_threshold: 120)
end
def disable_create_query_limit!
Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab/-/issues/546668', new_threshold: 110)
end
def show_work_item?
# Service Desk issues and incidents should not use the work item view
!issue.from_service_desk? &&

View File

@ -32,7 +32,8 @@ module Organizations
has_one :organization_detail, inverse_of: :organization, autosave: true
has_many :organization_users, inverse_of: :organization
has_many :organization_user_aliases, inverse_of: :organization
has_many :organization_user_aliases, inverse_of: :organization # deprecated
has_many :organization_user_details, inverse_of: :organization
# if considering disable_joins on the below see:
# https://gitlab.com/gitlab-org/gitlab/-/merge_requests/140343#note_1705047949
has_many :users, through: :organization_users, inverse_of: :organizations

View File

@ -0,0 +1,11 @@
# frozen_string_literal: true
module Organizations
class OrganizationUserDetail < ApplicationRecord
belongs_to :organization, inverse_of: :organization_user_details, optional: false
belongs_to :user, inverse_of: :organization_user_details, optional: false
validates :username, presence: true, uniqueness: { scope: :organization_id }
validates :display_name, presence: true
end
end

View File

@ -277,7 +277,8 @@ class User < ApplicationRecord
belongs_to :created_by, class_name: 'User', optional: true
has_many :organization_users, class_name: 'Organizations::OrganizationUser', inverse_of: :user
has_many :organization_user_aliases, class_name: 'Organizations::OrganizationUserAlias', inverse_of: :user
has_many :organization_user_aliases, class_name: 'Organizations::OrganizationUserAlias', inverse_of: :user # deprecated
has_many :organization_user_details, class_name: 'Organizations::OrganizationUserDetail', inverse_of: :user
has_many :organizations, through: :organization_users, class_name: 'Organizations::Organization', inverse_of: :users,
disable_joins: true

View File

@ -9,5 +9,7 @@ description: Preferences for issue boards stored on a per user basis, such as wh
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/33892
milestone: '13.1'
gitlab_schema: gitlab_main_cell
sharding_key_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/514601
sharding_key:
group_id: namespaces
project_id: projects
table_size: small

View File

@ -0,0 +1,13 @@
---
table_name: organization_user_details
classes:
- Organizations::OrganizationUserDetail
feature_categories:
- cell
description: |
Store organization-specific usernames and other user details. This allows users in organizations to be referenced by their organization-assigned handles (usernames) as opposed to their global, platform-wide usernames.
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/191575
milestone: '18.1'
gitlab_schema: gitlab_main_cell
sharding_key:
organization_id: organizations

View File

@ -1,47 +1,15 @@
# frozen_string_literal: true
# See https://docs.gitlab.com/ee/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class MigrateVSCodeExtensionMarketplaceFeatureFlagToData < Gitlab::Database::Migration[2.2]
restrict_gitlab_migration gitlab_schema: :gitlab_main
milestone '17.10'
# NOTE: This approach is lovingly borrowed from this migration:
# https://gitlab.com/gitlab-org/gitlab/-/blob/eae8739ac9d5e4c8316fefb03507cdaeac452a0a/db/migrate/20250109055316_migrate_global_search_settings_in_application_settings.rb#L12
class ApplicationSetting < MigrationRecord
self.table_name = 'application_settings'
end
def up
# TODO: This migration should be noop'd when the feature flag is default enabled or removed
# why: This is not the desired default behavior, only the behavior we want to carry over for
# customers that have chosen to opt-in early by explicitly enabling the flag.
return unless extension_marketplace_flag_enabled?
ApplicationSetting.reset_column_information
application_setting = ApplicationSetting.last
return unless application_setting
application_setting.update_columns(
vscode_extension_marketplace: { enabled: true, preset: "open_vsx" },
updated_at: Time.current
)
# web_ide_extensions_marketplace was default enabled in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/184662
# no-op
end
def down
return unless extension_marketplace_flag_enabled?
application_setting = ApplicationSetting.last
return unless application_setting
application_setting.update_column(:vscode_extension_marketplace, {})
end
private
def extension_marketplace_flag_enabled?
feature_flag_enabled?('web_ide_extensions_marketplace')
# no-op
end
end

View File

@ -5,21 +5,14 @@ class MigrateRequireEmailVerificationFeatureFlagToApplicationSetting < Gitlab::D
milestone '18.1'
def up
return unless feature_flag_enabled?(:require_email_verification)
execute <<-SQL
UPDATE application_settings
SET anti_abuse_settings = COALESCE(anti_abuse_settings, '{}'::jsonb) ||
'{"require_email_verification_on_account_locked": true}'::jsonb,
updated_at = NOW()
SQL
up_migrate_to_jsonb_setting(feature_flag_name: :require_email_verification,
setting_name: :require_email_verification_on_account_locked,
jsonb_column_name: :anti_abuse_settings,
default_enabled: false)
end
def down
execute <<-SQL
UPDATE application_settings
SET anti_abuse_settings = COALESCE(anti_abuse_settings, '{}'::jsonb) - 'require_email_verification_on_account_locked',
updated_at = NOW()
SQL
down_migrate_to_jsonb_setting(setting_name: :require_email_verification_on_account_locked,
jsonb_column_name: :anti_abuse_settings)
end
end

View File

@ -0,0 +1,23 @@
# frozen_string_literal: true
class AddOrganizationUserDetails < Gitlab::Database::Migration[2.3]
milestone '18.1'
def change
create_table :organization_user_details do |t|
t.belongs_to :organization, null: false, index: false
t.belongs_to :user, null: false
t.text :username, null: false, limit: 510
t.text :display_name, null: false, limit: 510
t.timestamps_with_timezone null: false
t.index [:organization_id, :user_id], unique: true,
name: :unique_organization_user_details_organization_id_user_id
t.index [:organization_id, :username], unique: true,
name: :unique_organization_user_details_organization_id_username
t.index 'lower(username)'
end
end
end

View File

@ -0,0 +1,16 @@
# frozen_string_literal: true
class AddFkOrganizationUserDetailsOrganizations < Gitlab::Database::Migration[2.3]
disable_ddl_transaction!
milestone '18.1'
def up
add_concurrent_foreign_key :organization_user_details, :organizations, column: :organization_id, on_delete: :cascade
end
def down
with_lock_retries do
remove_foreign_key :organization_user_details, column: :organization_id
end
end
end

View File

@ -0,0 +1,16 @@
# frozen_string_literal: true
class AddFkOrganizationUserDetailsUsers < Gitlab::Database::Migration[2.3]
disable_ddl_transaction!
milestone '18.1'
def up
add_concurrent_foreign_key :organization_user_details, :users, column: :user_id, on_delete: :cascade
end
def down
with_lock_retries do
remove_foreign_key :organization_user_details, column: :user_id
end
end
end

View File

@ -0,0 +1,10 @@
# frozen_string_literal: true
class AddBoardUserPreferenceShardingKey < Gitlab::Database::Migration[2.3]
milestone '18.1'
def change
add_column :board_user_preferences, :group_id, :bigint
add_column :board_user_preferences, :project_id, :bigint
end
end

View File

@ -0,0 +1,16 @@
# frozen_string_literal: true
class AddBoardUserPreferencesProjectIdIndex < Gitlab::Database::Migration[2.3]
INDEX_NAME = 'index_board_user_preferences_on_project_id'
disable_ddl_transaction!
milestone '18.1'
def up
add_concurrent_index :board_user_preferences, :project_id, name: INDEX_NAME
end
def down
remove_concurrent_index :board_user_preferences, :project_id, name: INDEX_NAME
end
end

View File

@ -0,0 +1,16 @@
# frozen_string_literal: true
class AddBoardUserPreferencesGroupIdIndex < Gitlab::Database::Migration[2.3]
INDEX_NAME = 'index_board_user_preferences_on_group_id'
disable_ddl_transaction!
milestone '18.1'
def up
add_concurrent_index :board_user_preferences, :group_id, name: INDEX_NAME
end
def down
remove_concurrent_index :board_user_preferences, :group_id, name: INDEX_NAME
end
end

View File

@ -0,0 +1,33 @@
# frozen_string_literal: true
class BackfillBoardUserPreferences < Gitlab::Database::Migration[2.3]
BATCH_SIZE = 100
disable_ddl_transaction!
restrict_gitlab_migration gitlab_schema: :gitlab_main
milestone '18.1'
def up
each_batch(:board_user_preferences, of: BATCH_SIZE) do |batch|
connection.execute(
<<~SQL
UPDATE
"board_user_preferences"
SET
"group_id" = "boards"."group_id",
"project_id" = "boards"."project_id"
FROM
"boards"
WHERE
"board_user_preferences"."board_id" = "boards"."id"
AND "board_user_preferences"."id" IN (#{batch.select(:id).limit(BATCH_SIZE).to_sql})
SQL
)
end
end
def down
# no-op
end
end

View File

@ -0,0 +1,14 @@
# frozen_string_literal: true
class AddBoardUserPreferencesShardingKeyNotNullConstraint < Gitlab::Database::Migration[2.3]
disable_ddl_transaction!
milestone '18.1'
def up
add_multi_column_not_null_constraint(:board_user_preferences, :group_id, :project_id)
end
def down
remove_multi_column_not_null_constraint(:board_user_preferences, :group_id, :project_id)
end
end

View File

@ -0,0 +1,20 @@
# frozen_string_literal: true
class AddBoardUserPreferencesGroupIdFk < Gitlab::Database::Migration[2.3]
disable_ddl_transaction!
milestone '18.1'
def up
add_concurrent_foreign_key :board_user_preferences,
:namespaces,
column: :group_id,
target_column: :id,
reverse_lock_order: true
end
def down
with_lock_retries do
remove_foreign_key_if_exists :board_user_preferences, column: :group_id
end
end
end

View File

@ -0,0 +1,20 @@
# frozen_string_literal: true
class AddBoardUserPreferencesProjectIdFk < Gitlab::Database::Migration[2.3]
disable_ddl_transaction!
milestone '18.1'
def up
add_concurrent_foreign_key :board_user_preferences,
:projects,
column: :project_id,
target_column: :id,
reverse_lock_order: true
end
def down
with_lock_retries do
remove_foreign_key_if_exists :board_user_preferences, column: :project_id
end
end
end

View File

@ -0,0 +1 @@
9d67ff936bcd2fda808ec22526afe619d39d1ad41d7a6421a0050e78f5c74457

View File

@ -0,0 +1 @@
1da7143e770d4b4445f30b661b10aa8d3fb843eaa4766976985041fcdf71700a

View File

@ -0,0 +1 @@
f81dbd3fb12060afecb19f95ea8cd76f6ec53497e897ad7a39bf3d162aa36f7b

View File

@ -0,0 +1 @@
cc9c1e182efb8afc072a82ce49b0e4515789dd384e82dd2d820590b55b5efea4

View File

@ -0,0 +1 @@
c0e8007aa6a154bc17d7fe8058d54f5b8a38b9467679f574ac6fb62a55167111

View File

@ -0,0 +1 @@
fb163edb227bb09ea1690cd538e421d067cbc45b7e39d5c7651a362d60e2962b

View File

@ -0,0 +1 @@
6a7a412a9b0824743768a13a387d10264d9b88315789bbc348bf5e4420399d5b

View File

@ -0,0 +1 @@
bacbe37017b21a651fe8efe665823d41670b3bb2b8536f7c57e45b5fbeceacba

View File

@ -0,0 +1 @@
7e9ab7b8e146394480d42b6509c1bd195f7845d907e28cd53c1b9019da2d4790

View File

@ -0,0 +1 @@
19c07a8e30917f22a6414cb0dc55ba72b40e98742d748a21efbb86af3d1b640b

View File

@ -10301,7 +10301,10 @@ CREATE TABLE board_user_preferences (
board_id bigint NOT NULL,
hide_labels boolean,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL
updated_at timestamp with time zone NOT NULL,
group_id bigint,
project_id bigint,
CONSTRAINT check_86d6706b52 CHECK ((num_nonnulls(group_id, project_id) = 1))
);
CREATE SEQUENCE board_user_preferences_id_seq
@ -18830,6 +18833,27 @@ CREATE SEQUENCE organization_user_aliases_id_seq
ALTER SEQUENCE organization_user_aliases_id_seq OWNED BY organization_user_aliases.id;
CREATE TABLE organization_user_details (
id bigint NOT NULL,
organization_id bigint NOT NULL,
user_id bigint NOT NULL,
username text NOT NULL,
display_name text NOT NULL,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL,
CONSTRAINT check_470dbccf9b CHECK ((char_length(display_name) <= 510)),
CONSTRAINT check_dc5e9cf6f2 CHECK ((char_length(username) <= 510))
);
CREATE SEQUENCE organization_user_details_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE organization_user_details_id_seq OWNED BY organization_user_details.id;
CREATE TABLE organization_users (
id bigint NOT NULL,
organization_id bigint NOT NULL,
@ -27762,6 +27786,8 @@ ALTER TABLE ONLY organization_push_rules ALTER COLUMN id SET DEFAULT nextval('or
ALTER TABLE ONLY organization_user_aliases ALTER COLUMN id SET DEFAULT nextval('organization_user_aliases_id_seq'::regclass);
ALTER TABLE ONLY organization_user_details ALTER COLUMN id SET DEFAULT nextval('organization_user_details_id_seq'::regclass);
ALTER TABLE ONLY organization_users ALTER COLUMN id SET DEFAULT nextval('organization_users_id_seq'::regclass);
ALTER TABLE ONLY organizations ALTER COLUMN id SET DEFAULT nextval('organizations_id_seq'::regclass);
@ -30488,6 +30514,9 @@ ALTER TABLE ONLY organization_settings
ALTER TABLE ONLY organization_user_aliases
ADD CONSTRAINT organization_user_aliases_pkey PRIMARY KEY (id);
ALTER TABLE ONLY organization_user_details
ADD CONSTRAINT organization_user_details_pkey PRIMARY KEY (id);
ALTER TABLE ONLY organization_users
ADD CONSTRAINT organization_users_pkey PRIMARY KEY (id);
@ -34232,6 +34261,10 @@ CREATE UNIQUE INDEX index_board_project_recent_visits_on_user_project_and_board
CREATE INDEX index_board_user_preferences_on_board_id ON board_user_preferences USING btree (board_id);
CREATE INDEX index_board_user_preferences_on_group_id ON board_user_preferences USING btree (group_id);
CREATE INDEX index_board_user_preferences_on_project_id ON board_user_preferences USING btree (project_id);
CREATE UNIQUE INDEX index_board_user_preferences_on_user_id_and_board_id ON board_user_preferences USING btree (user_id, board_id);
CREATE INDEX index_boards_epic_board_labels_on_epic_board_id ON boards_epic_board_labels USING btree (epic_board_id);
@ -36546,6 +36579,10 @@ CREATE UNIQUE INDEX index_organization_push_rules_on_organization_id ON organiza
CREATE INDEX index_organization_user_aliases_on_user_id ON organization_user_aliases USING btree (user_id);
CREATE INDEX index_organization_user_details_on_lower_username ON organization_user_details USING btree (lower(username));
CREATE INDEX index_organization_user_details_on_user_id ON organization_user_details USING btree (user_id);
CREATE INDEX index_organization_users_on_org_id_access_level_user_id ON organization_users USING btree (organization_id, access_level, user_id);
CREATE INDEX index_organization_users_on_organization_id_and_id ON organization_users USING btree (organization_id, id);
@ -38950,6 +38987,10 @@ CREATE UNIQUE INDEX unique_organization_user_alias_organization_id_user_id ON or
CREATE UNIQUE INDEX unique_organization_user_alias_organization_id_username ON organization_user_aliases USING btree (organization_id, username);
CREATE UNIQUE INDEX unique_organization_user_details_organization_id_user_id ON organization_user_details USING btree (organization_id, user_id);
CREATE UNIQUE INDEX unique_organization_user_details_organization_id_username ON organization_user_details USING btree (organization_id, username);
CREATE UNIQUE INDEX unique_organizations_on_path_case_insensitive ON organizations USING btree (lower(path));
CREATE UNIQUE INDEX unique_packages_project_id_and_name_and_version_when_debian ON packages_packages USING btree (project_id, name, version) WHERE ((package_type = 9) AND (status <> 4));
@ -42275,6 +42316,9 @@ ALTER TABLE ONLY project_statistics
ALTER TABLE ONLY work_item_current_statuses
ADD CONSTRAINT fk_1bb76463e0 FOREIGN KEY (custom_status_id) REFERENCES work_item_custom_statuses(id) ON DELETE SET NULL;
ALTER TABLE ONLY board_user_preferences
ADD CONSTRAINT fk_1c0b27016f FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE;
ALTER TABLE ONLY approval_policy_rule_project_links
ADD CONSTRAINT fk_1c78796d52 FOREIGN KEY (approval_policy_rule_id) REFERENCES approval_policy_rules(id) ON DELETE CASCADE;
@ -42653,6 +42697,9 @@ ALTER TABLE ONLY todos
ALTER TABLE ONLY merge_requests_approval_rules_projects
ADD CONSTRAINT fk_451a9dfe93 FOREIGN KEY (approval_rule_id) REFERENCES merge_requests_approval_rules(id) ON DELETE CASCADE;
ALTER TABLE ONLY organization_user_details
ADD CONSTRAINT fk_4533918f8e FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
ALTER TABLE ONLY ai_settings
ADD CONSTRAINT fk_4571bb0ccc FOREIGN KEY (duo_workflow_oauth_application_id) REFERENCES oauth_applications(id) ON DELETE SET NULL;
@ -42902,6 +42949,9 @@ ALTER TABLE ONLY ci_pipeline_chat_data
ALTER TABLE ONLY cluster_agent_tokens
ADD CONSTRAINT fk_64f741f626 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
ALTER TABLE ONLY organization_user_details
ADD CONSTRAINT fk_657140ae14 FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE;
ALTER TABLE ONLY container_repository_states
ADD CONSTRAINT fk_6591698505 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
@ -44144,6 +44194,9 @@ ALTER TABLE ONLY boards
ALTER TABLE ONLY epic_user_mentions
ADD CONSTRAINT fk_f1ab52883e FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE;
ALTER TABLE ONLY board_user_preferences
ADD CONSTRAINT fk_f1c3e9b710 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
ALTER TABLE ONLY observability_metrics_issues_connections
ADD CONSTRAINT fk_f218d84a14 FOREIGN KEY (namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE;

View File

@ -41,7 +41,7 @@ We provide [example diagrams and statements](#examples) to demonstrate correct u
| Promoting | Changing the role of a site from secondary to primary. | Geo-specific | |
| Demoting | Changing the role of a site from primary to secondary. | Geo-specific | |
| Failover | The entire process that shifts users from a primary Site to a secondary site. This includes promoting a secondary, but contains other parts as well. For example, scheduling maintenance. | Geo-specific | |
| Replication | Also called "synchronization". The uni-directional process that updates a resource on a secondary site to match the resource on the primary site. | Geo-specific | |
| Replication | Also called "synchronization." The uni-directional process that updates a resource on a secondary site to match the resource on the primary site. | Geo-specific | |
| Replication slot | The PostgreSQL replication feature that ensures a persistent connection point with the database, and tracks which WAL segments are still needed by standby servers. It can be helpful to name replication slots to match the `geo_node_name` of a site, but this is not required. | PostgreSQL | |
| Verification | The process of comparing the data that exist on a primary site to the data replicated to a secondary site. Used to ensure integrity of replicated data. | Geo-specific | |
| Unified URL | A single external URL used for all Geo sites. Allows requests to be routed to either the primary Geo site or any secondary Geo sites. | Geo-specific | |

View File

@ -315,7 +315,7 @@ end; nil
If the **primary** site's checksums are in question, then you need to make the **primary** site recalculate checksums. A "full re-verification" is then achieved, because after each checksum is recalculated on a **primary** site, events are generated which propagate to all **secondary** sites, causing them to recalculate their checksums and compare values. Any mismatch marks the registry as `sync failed`, which causes sync retries to be scheduled.
The UI does not provide a button to do a "full re-verification". You can simulate this by setting your **primary** site's `Re-verification interval` to 1 (day) in **Admin > Geo > Nodes > Edit**. The **primary** site will then recalculate the checksum of any resource that has been checksummed more than 1 day ago.
The UI does not provide a button to do a full re-verification. You can simulate this by setting your **primary** site's `Re-verification interval` to 1 (day) in **Admin > Geo > Nodes > Edit**. The **primary** site will then recalculate the checksum of any resource that has been checksummed more than 1 day ago.
Optionally, you can do this manually:
@ -613,7 +613,7 @@ To validate if you are experiencing this issue:
```
1. If `last_sync_failure` no longer includes the error `fatal: could not read Username`, then you are
affected by this issue. The state should now be `2`, meaning "synced". If so, then you should upgrade to
affected by this issue. The state should now be `2`, which means that it's synced. If so, then you should upgrade to
a GitLab version with the fix. You may also wish to upvote or comment on
[issue 466681](https://gitlab.com/gitlab-org/gitlab/-/issues/466681) which would have reduced the severity of this
issue.

View File

@ -28,7 +28,7 @@ All Geo sites have the following settings:
| Setting | Description |
| --------| ----------- |
| Primary | This marks a Geo site as **primary** site. There can be only one **primary** site. |
| Name | The unique identifier for the Geo site. It's highly recommended to use a physical location as a name. Good examples are "London Office" or "us-east-1". Avoid words like "primary", "secondary", "Geo", or "DR". This makes the failover process easier because the physical location does not change, but the Geo site role can. All nodes in a single Geo site use the same site name. Nodes use the `gitlab_rails['geo_node_name']` setting in `/etc/gitlab/gitlab.rb` to lookup their Geo site record in the PostgreSQL database. If `gitlab_rails['geo_node_name']` is not set, the node's `external_url` with trailing slash is used as fallback. The value of `Name` is case-sensitive, and most characters are allowed. |
| Name | The unique identifier for the Geo site. It's highly recommended to use a physical location as a name. Good examples are `London Office` or `us-east-1`. Avoid words like `primary`, `secondary`, `Geo`, or `DR`. This makes the failover process easier because the physical location does not change, but the Geo site role can. All nodes in a single Geo site use the same site name. Nodes use the `gitlab_rails['geo_node_name']` setting in `/etc/gitlab/gitlab.rb` to lookup their Geo site record in the PostgreSQL database. If `gitlab_rails['geo_node_name']` is not set, the node's `external_url` with trailing slash is used as fallback. The value of `Name` is case-sensitive, and most characters are allowed. |
| URL | The instance's user-facing URL. |
The site you're browsing is indicated with a blue `Current` label, and

View File

@ -513,7 +513,7 @@ We can also add the bang (Ruby speak for `!`) to `.update`:
user.update!(password: 'password', password_confirmation: 'hunter2')
```
In Ruby, method names ending with `!` are commonly known as "bang methods". By
In Ruby, method names ending with `!` are commonly known as "bang methods." By
convention, the bang indicates that the method directly modifies the object it
is acting on, as opposed to returning the transformed result and leaving the
underlying object untouched. For Active Record methods that write to the

View File

@ -151,7 +151,7 @@ Under the **Mappings** section, first provision the groups:
{{< alert type="note" >}}
Even when **Provision Microsoft Entra ID Groups** is disabled, the mappings section may display "Enabled: Yes". This behavior is a display bug that you can safely ignore.
Even when **Provision Microsoft Entra ID Groups** is disabled, the mappings section might display **Enabled: Yes**. This behavior is a display bug that you can safely ignore.
{{< /alert >}}

View File

@ -2,7 +2,7 @@
stage: Deploy
group: Environments
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
title: Agents API
title: Kubernetes agent API
---
{{< details >}}
@ -18,7 +18,7 @@ title: Agents API
{{< /history >}}
Use the Agents API to work with the GitLab agent for Kubernetes.
Use this API to interact with the [GitLab agent for Kubernetes](../user/clusters/agent/_index.md).
## List the agents for a project

View File

@ -12,10 +12,24 @@ title: Deploy keys API
{{< /details >}}
The deploy keys API can return in responses fingerprints of the public key in the following fields:
Use this API to interact with [deploy keys](../user/project/deploy_keys/_index.md).
- `fingerprint` (MD5 hash). Not available on FIPS-enabled systems.
- `fingerprint_sha256` (SHA256 hash). [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/91302) in GitLab 15.2.
## Deploy key fingerprints
{{< history >}}
- [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/91302) `fingerprint_sha256` attribute in GitLab 15.2.
{{< /history >}}
Some endpoints return public key fingerprints as part of the response. You can use these fingerprints
to identify the user that created the deploy key. For more information,
see [get user by deploy key fingerprint](keys.md#get-user-by-deploy-key-fingerprint).
The following attributes contain the deploy key fingerprint:
- `fingerprint`: Uses an MD5 hash. Not available on FIPS-enabled systems.
- `fingerprint_sha256`: Uses a SHA256 hash.
## List all deploy keys
@ -90,7 +104,7 @@ Example response:
"title": "Another Public key",
"key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDIJFwIL6YNcCgVBLTHgM6hzmoL5vf0ThDKQMWT3HrwCjUCGPwR63vBwn6+/Gx+kx+VTo9FuojzR0O4XfwD3LrYA+oT3ETbn9U4e/VS4AH/G4SDMzgSLwu0YuPe517FfGWhWGQhjiXphkaQ+6bXPmcASWb0RCO5+pYlGIfxv4eFGQ==",
"fingerprint": "0b:cf:58:40:b9:23:96:c7:ba:44:df:0e:9e:87:5e:75",
"fingerprint_sha256": "SHA256:lGI/Ys/Wx7PfMhUO1iuBH92JQKYN+3mhJZvWO4Q5ims",
"": "SHA256:lGI/Ys/Wx7PfMhUO1iuBH92JQKYN+3mhJZvWO4Q5ims",
"created_at": "2013-10-02T11:12:29Z",
"expires_at": null,
"projects_with_write_access": [],
@ -198,7 +212,7 @@ Example response:
"title": "Another Public key",
"key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDIJFwIL6YNcCgVBLTHgM6hzmoL5vf0ThDKQMWT3HrwCjUCGPwR63vBwn6+/Gx+kx+VTo9FuojzR0O4XfwD3LrYA+oT3ETbn9U4e/VS4AH/G4SDMzgSLwu0YuPe517FfGWhWGQhjiXphkaQ+6bXPmcASWb0RCO5+pYlGIfxv4eFGQ==",
"fingerprint": "0b:cf:58:40:b9:23:96:c7:ba:44:df:0e:9e:87:5e:75",
"fingerprint_sha256": "SHA256:lGI/Ys/Wx7PfMhUO1iuBH92JQKYN+3mhJZvWO4Q5ims",
"": "SHA256:lGI/Ys/Wx7PfMhUO1iuBH92JQKYN+3mhJZvWO4Q5ims",
"created_at": "2013-10-02T11:12:29Z",
"expires_at": null,
"can_push": false
@ -244,7 +258,7 @@ Parameters:
"expires_at": null,
"key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDNJAkI3Wdf0r13c8a5pEExB2YowPWCSVzfZV22pNBc1CuEbyYLHpUyaD0GwpGvFdx2aP7lMEk35k6Rz3ccBF6jRaVJyhsn5VNnW92PMpBJ/P1UebhXwsFHdQf5rTt082cSxWuk61kGWRQtk4ozt/J2DF/dIUVaLvc+z4HomT41fQ==",
"fingerprint": "4a:9d:64:15:ed:3a:e6:07:6e:89:36:b3:3b:03:05:d9",
"fingerprint_sha256": "SHA256:Jrs3LD1Ji30xNLtTVf9NDCj7kkBgPBb2pjvTZ3HfIgU"
"": "SHA256:Jrs3LD1Ji30xNLtTVf9NDCj7kkBgPBb2pjvTZ3HfIgU"
}
]
```

View File

@ -2,7 +2,7 @@
stage: Deploy
group: Environments
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
title: Deploy Tokens API
title: Deploy tokens API
---
{{< details >}}
@ -12,6 +12,8 @@ title: Deploy Tokens API
{{< /details >}}
Use this API to interact with [deploy tokens](../user/project/deploy_tokens/_index.md).
## List all deploy tokens
{{< details >}}

View File

@ -18,6 +18,8 @@ title: Deployments API
{{< /history >}}
Use this API to interact with [code deployments](../ci/environments/deployments.md) to GitLab enviroments.
## List project deployments
Get a list of deployments in a project.

View File

@ -19,6 +19,8 @@ title: Environments API
{{< /history >}}
Use this API to interact with [GitLab environments](../ci/environments/_index.md).
## List environments
Get all environments for a given project.

View File

@ -19,14 +19,15 @@ title: Feature flag user lists API
{{< /history >}}
API for accessing GitLab feature flag user lists.
Use this API to interact with GitLab feature flags for [user lists](../operations/feature_flags.md#user-list).
Users with at least the Developer [role](../user/permissions.md) can access the feature flag user lists API.
Prerequisites:
- You must have at least the Developer role.
{{< alert type="note" >}}
`GET` requests return twenty results at a time because the API results
are [paginated](rest/_index.md#pagination). You can change this value.
To interact with feature flags for all users, see the [Feature flag API](feature_flags.md).
{{< /alert >}}
@ -38,6 +39,9 @@ Gets all feature flag user lists for the requested project.
GET /projects/:id/feature_flags_user_lists
```
Use the `page` and `per_page` [pagination](rest/_index.md#offset-based-pagination) parameters to
control the pagination of results.
| Attribute | Type | Required | Description |
| --------- | -------------- | -------- | -------------------------------------------------------------------------------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](rest/_index.md#namespaced-paths). |
@ -122,6 +126,9 @@ Gets a feature flag user list.
GET /projects/:id/feature_flags_user_lists/:iid
```
Use the `page` and `per_page` [pagination](rest/_index.md#offset-based-pagination) parameters to
control the pagination of results.
| Attribute | Type | Required | Description |
| ------------------- | ---------------- | ---------- | ---------------------------------------------------------------------------------------|
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](rest/_index.md#namespaced-paths). |

View File

@ -2,7 +2,7 @@
stage: Deploy
group: Environments
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
title: Feature flags API
title: Feature flag API
---
{{< details >}}
@ -19,14 +19,11 @@ title: Feature flags API
{{< /history >}}
API for accessing resources of [GitLab feature flags](../operations/feature_flags.md).
Use this API to interact with GitLab [feature flags](../operations/feature_flags.md).
Users with at least the Developer [role](../user/permissions.md) can access the feature flag API.
Prerequisites:
## Feature flags pagination
By default, `GET` requests return 20 results at a time because the API results
are [paginated](rest/_index.md#pagination).
- You must have at least the Developer role.
## List feature flags for a project
@ -36,6 +33,9 @@ Gets all feature flags of the requested project.
GET /projects/:id/feature_flags
```
Use the `page` and `per_page` [pagination](rest/_index.md#offset-based-pagination) parameters to
control the pagination of results.
| Attribute | Type | Required | Description |
| ------------------- | ---------------- | ---------- | --------------------------------------------------------------------------------------------------------------------------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](rest/_index.md#namespaced-paths). |
@ -136,6 +136,9 @@ Gets a single feature flag.
GET /projects/:id/feature_flags/:feature_flag_name
```
Use the `page` and `per_page` [pagination](rest/_index.md#offset-based-pagination) parameters to
control the pagination of results.
| Attribute | Type | Required | Description |
| ------------------- | ---------------- | ---------- | ---------------------------------------------------------------------------------------|
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](rest/_index.md#namespaced-paths). |

View File

@ -12,7 +12,7 @@ title: Freeze Periods API
{{< /details >}}
You can use the Freeze Periods API to manipulate GitLab [Freeze Period](../user/project/releases/_index.md#prevent-unintentional-releases-by-setting-a-deploy-freeze) entries.
Use this API to interact with deployment [freeze periods](../user/project/releases/_index.md#prevent-unintentional-releases-by-setting-a-deploy-freeze).
## Permissions and security
@ -53,9 +53,9 @@ Example response:
]
```
## Get a freeze period by a `freeze_period_id`
## Get a freeze period
Get a freeze period for the given `freeze_period_id`.
Get a freeze period for a specified `freeze_period_id`.
```plaintext
GET /projects/:id/freeze_periods/:freeze_period_id

View File

@ -20,7 +20,13 @@ title: Group-level protected environments API
{{< /history >}}
Read more about [group-level protected environments](../ci/environments/protected_environments.md#group-level-protected-environments).
Use this API to interact with [group-level protected environments](../ci/environments/protected_environments.md#group-level-protected-environments).
{{< alert type="note" >}}
For protected environments, see [protected environments API](protected_environments.md)
{{< /alert >}}
## Valid access levels

View File

@ -2,7 +2,7 @@
stage: Deploy
group: Environments
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
title: Group releases API
title: Group release API
---
{{< details >}}
@ -19,17 +19,17 @@ title: Group releases API
{{< /history >}}
Review your groups' [releases](../user/project/releases/_index.md) with the REST API.
Use this API to interact with [projects releases](../user/project/releases/_index.md) in groups.
{{< alert type="note" >}}
For more information about the project releases API, see [Releases API](releases/_index.md).
To interact with project releases directly, see the [project release API](releases/_index.md).
{{< /alert >}}
## List group releases
## List all releases in a group
Returns a list of group releases.
Lists all releases for projects in a specified group.
```plaintext
GET /groups/:id/releases
@ -38,14 +38,15 @@ GET /groups/:id/releases?simple=true
Parameters:
| Attribute | Type | Required | Description |
|---------------------|----------------|----------|---------------------------------------------------------------------------------------------------------------|
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](rest/_index.md#namespaced-paths). |
| `sort` | string | no | The direction of the order. Either `desc` (default) for descending order or `asc` for ascending order. |
| `simple` | boolean | no | Return only limited fields for each release. |
| Attribute | Type | Required | Description |
| --------- | -------------- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path](rest/_index.md#namespaced-paths) of the group. |
| `sort` | string | no | The direction of the order. Possible values: `desc` or `asc`. |
| `simple` | boolean | no | If `true`, only returns limited fields for each release. |
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/5/releases"
curl --header "PRIVATE-TOKEN: <your_access_token>"
--url "https://gitlab.example.com/api/v4/groups/5/releases"
```
Example response:

View File

@ -12,6 +12,14 @@ title: Protected environments API
{{< /details >}}
Use this API to interact with [protected environments](../ci/environments/protected_environments.md).
{{< alert type="note" >}}
For group-level protected environments, see [group-level protected environments API](group_protected_environments.md)
{{< /alert >}}
## Valid access levels
The access levels are defined in the `ProtectedEnvironments::DeployAccessLevel::ALLOWED_ACCESS_LEVELS` method.

View File

@ -2,7 +2,7 @@
stage: Deploy
group: Environments
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
title: Releases API
title: Project release API
---
{{< details >}}
@ -12,9 +12,15 @@ title: Releases API
{{< /details >}}
Use this API to manipulate [release entries](../../user/project/releases/_index.md).
Use this API to interact with [releases](../../user/project/releases/_index.md) for projects.
To manipulate links as a release asset, see [Release Links API](links.md).
{{< alert type="note" >}}
To interact with releases for a group, see the [group release API](../group_releases.md).
To interact with links as a release asset, see [release links API](links.md).
{{< /alert >}}
## Authentication

View File

@ -18,10 +18,19 @@ title: Release links API
{{< /history >}}
Use this API to manipulate GitLab [Release](../../user/project/releases/_index.md)
links. For manipulating other Release assets, see [Release API](_index.md).
Use this API to interact with links to [releases](../../user/project/releases/_index.md).
GitLab supports links to `http`, `https`, and `ftp` assets.
GitLab supports asset links with the following protocols:
- `http`
- `https`
- `ftp`
{{< alert type="note" >}}
To interact with project releases directly, see the [project release API](_index.md).
{{< /alert >}}
## List links of a release

View File

@ -12,7 +12,7 @@ title: Resource group API
{{< /details >}}
You can read more about [controlling the job concurrency with resource groups](../ci/resource_groups/_index.md).
Use this API to interact with [resource groups](../ci/resource_groups/_index.md).
## Get all resource groups for a project

View File

@ -74,7 +74,7 @@ To use GitLab CI/CD with a Bitbucket Cloud repository:
is created in Bitbucket, but the mirroring process copies it to the GitLab mirror. The GitLab
CI/CD pipeline runs the script, and pushes the status back to Bitbucket.
Create a file `build_status` and insert the script below and run
Create a file `build_status`, insert the following script and run
`chmod +x build_status` in your terminal to make the script executable.
```shell

View File

@ -30,7 +30,7 @@ Prerequisites:
- Access to an existing Azure Subscription with `Owner` access level.
- Access to the corresponding Azure Active Directory Tenant with at least the `Application Developer` access level.
- A local installation of the [Azure CLI](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli).
Alternatively, you can follow all the steps below with the [Azure Cloud Shell](https://portal.azure.com/#cloudshell/).
Alternatively, you can use all the following steps with the [Azure Cloud Shell](https://portal.azure.com/#cloudshell/).
- Your GitLab instance must be publicly accessible over the internet as Azure must to connect to the GitLab OIDC endpoint.
- A GitLab project.

View File

@ -265,7 +265,7 @@ You can access a private registry using two approaches. Both require setting the
private registry, add `DOCKER_AUTH_CONFIG` as an environment variable in the
runner's configuration.
See below for examples of each.
See the following sections for examples of each.
#### Determine your `DOCKER_AUTH_CONFIG` data
@ -554,7 +554,7 @@ and manual credential management.
COPY --from=aws-tools /root/.docker/config.json /root/.docker/config.json
```
1. To build the custom GitLab Runner Docker image in a `.gitlab-ci.yml`, include the following example below:
1. To build the custom GitLab Runner Docker image in a `.gitlab-ci.yml`, include the following example:
```yaml
variables:

View File

@ -59,7 +59,7 @@ and supports multiple secrets engines.
{{< /alert >}}
You must replace the `vault.example.com` URL below with the URL of your Vault server, and `gitlab.example.com` with the URL of your GitLab instance.
You must replace the `vault.example.com` URL in the following examples with the URL of your Vault server, and `gitlab.example.com` with the URL of your GitLab instance.
## Vault Secrets Engines

View File

@ -48,7 +48,7 @@ To follow along, you must have:
- A Vault server that you are already using.
- CI/CD jobs retrieving secrets from Vault with `CI_JOB_JWT`.
In the examples below, replace:
In the following examples, replace:
- `vault.example.com` with the URL of your Vault server.
- `gitlab.example.com` with the URL of your GitLab instance.

View File

@ -41,7 +41,7 @@ To follow along, you must have:
{{< alert type="note" >}}
You must replace the `vault.example.com` URL below with the URL of your Vault server,
You must replace the `vault.example.com` URL in the following example with the URL of your Vault server,
and `gitlab.example.com` with the URL of your GitLab instance.
{{< /alert >}}
@ -269,7 +269,7 @@ The claim fields listed in [the previous table](#hashicorp-vault-secrets-integra
[Vault's policy path templating](https://developer.hashicorp.com/vault/tutorials/policies/policy-templating?in=vault%2Fpolicies)
purposes by using the accessor name of the JWT auth in Vault.
The [mount accessor name](https://developer.hashicorp.com/vault/tutorials/auth-methods/identity#step-1-create-an-entity-with-alias)
(`ACCESSOR_NAME` in the example below) can be retrieved by running `vault auth list`.
(`ACCESSOR_NAME` in the following example) can be retrieved by running `vault auth list`.
Policy template example making use of a named metadata field named `project_path`:

View File

@ -162,7 +162,7 @@ run the following query on the instance's PostgreSQL terminal:
SELECT encrypted_ci_jwt_signing_key FROM application_settings;
```
If the returned value is empty, use the Rails snippet below to generate a new key
If the returned value is empty, use the following Rails snippet to generate a new key
and replace it internally:
```ruby

View File

@ -6,8 +6,12 @@ description: Writing styles, markup, formatting, and other standards for GitLab
title: Product availability details
---
Product availability details provide information about a feature. If the details apply to the whole page, place them at the top
of the page, but after the front matter. If they apply to a specific section, place the details under the applicable section titles.
Product availability details provide information about a feature.
- If the details apply to the whole page, place them at the top
of the page, but after the front matter.
- If they apply to a specific section, place the details under the applicable
section titles.
Availability details include the tier, offering, status, and history.
@ -96,10 +100,22 @@ Generally available features should not have a status.
### History
The documentation site uses [shortcodes](../hugo_migration.md#shortcodes) to render the version history.
The documentation site uses [shortcodes](../hugo_migration.md#shortcodes) to render the version history,
for example:
```markdown
{{</* history */>}}
- [Introduced](https://issue-link) in GitLab 16.3.
- [Changed](https://issue-link) in GitLab 16.4.
{{</* /history */>}}
```
In addition:
- Ensure that history notes are listed after the details (if any), and immediately
after the heading.
- Ensure that the output generates properly.
- Ensure the version history begins with `-`.
- If possible, include a link to the related issue. If there is no related issue, link to a merge request, or epic.

View File

@ -449,24 +449,61 @@ deleting feature flags.
## Migrate an `ops` feature flag to an application setting
To migrate an `ops` feature flag to an application setting:
1. In application settings, create or identify an existing `JSONB` column to store the setting.
1. Write a migration to backfill the column. Avoid using `Feature.enabled?` in the migration. Use the `feature_flag_enabled?` migration helper method.
1. Optional. In application settings, update the documentation for the setting.
1. In the **Admin** area, create a setting to enable or disable the feature.
1. Replace the feature flag everywhere with the application setting.
1. Update all the relevant documentation pages.
1. To remove a feature flag from an existing migration, replace `Feature.enabled?` with migration helper method `feature_flag_enabled?`.
{{< alert type="warning" >}}
The changes to backfill application settings and use the settings in the code must be merged in the same milestone.
{{< /alert >}}
If frontend changes are merged in a later milestone, you should add documentation about how to update the settings
by using the [application settings API](../../api/settings.md) or the Rails console.
To migrate an `ops` feature flag to an application setting:
1. In application settings, create or identify an existing `JSONB` column to store the setting.
1. The application setting default should match `default_enabled:` in the feature flag YAML definition
1. Write a migration to backfill the column. This allows instances which have
opted out of the default behavior to remain in the same state. Avoid using `Feature.enabled?` or `Feature.disabled?`
in the migration. Use the `Gitlab::Database::MigrationHelpers::FeatureFlagMigratorHelpers` migration helpers. These
helpers will only migrate feature flags that are explicitly set to `true` or `false`. If a feature flag is set for a
percentage or specific actor, the default value will be used.
1. In the **Admin** area, create a setting to enable or disable the feature.
1. Replace the feature flag everywhere with the application setting.
1. Update all the relevant documentation pages. If frontend changes are merged in a later milestone, you should add
documentation about how to update the settings by using the [application settings API](../../api/settings.md) or
the Rails console.
An example migration for a `JSONB` column:
```ruby
# default_enabled copied from feature flag definition YAML before it is removed
DEFAULT_ENABLED = true
def up
up_migrate_to_jsonb_setting(feature_flag_name: :my_flag_name,
setting_name: :my_setting,
jsonb_column_name: :settings,
default_enabled: DEFAULT_ENABLED)
end
def down
down_migrate_to_jsonb_setting(setting_name: :my_setting, jsonb_column_name: :settings)
end
```
An example migration for a boolean column:
```ruby
# default_enabled copied from feature flag definition YAML before it is removed
DEFAULT_ENABLED = true
def up
up_migrate_to_setting(feature_flag_name: :my_flag_name,
setting_name: :my_setting,
default_enabled: DEFAULT_ENABLED)
end
def down
down_migrate_to_setting(setting_name: :my_setting, default_enabled: DEFAULT_ENABLED)
end
```
## Develop with a feature flag

View File

@ -112,9 +112,6 @@ D, [2024-12-20T13:35:24.246463 #34584] DEBUG -- : 0.000012_s (0.000012_s): Norma
D, [2024-12-20T13:35:24.246543 #34584] DEBUG -- : 0.000007_s (0.000019_s): TruncateSourceFilter [PreProcessPipeline]
D, [2024-12-20T13:35:24.246589 #34584] DEBUG -- : 0.000028_s (0.000047_s): FrontMatterFilter [PreProcessPipeline]
D, [2024-12-20T13:35:24.246662 #34584] DEBUG -- : 0.000005_s (0.000005_s): IncludeFilter [FullPipeline]
D, [2024-12-20T13:35:24.246684 #34584] DEBUG -- : 0.000003_s (0.000008_s): MarkdownPreEscapeLegacyFilter [FullPipeline]
D, [2024-12-20T13:35:24.246699 #34584] DEBUG -- : 0.000002_s (0.000010_s): DollarMathPreLegacyFilter [FullPipeline]
D, [2024-12-20T13:35:24.246715 #34584] DEBUG -- : 0.000003_s (0.000013_s): BlockquoteFenceLegacyFilter [FullPipeline]
D, [2024-12-20T13:35:24.246816 #34584] DEBUG -- : 0.000088_s (0.000101_s): MarkdownFilter [FullPipeline]
...
D, [2024-12-20T13:35:24.252338 #34584] DEBUG -- : 0.000013_s (0.004394_s): CustomEmojiFilter [FullPipeline]

View File

@ -7,7 +7,7 @@ title: Hardening - CI/CD Recommendations
General hardening guidelines and philosophies are outlined in the [main hardening documentation](hardening.md).
The hardening recommendations and concepts for CI/CD are discussed below.
The hardening recommendations and concepts for CI/CD are discussed in the following section.
## Basic Recommendations

View File

@ -119,7 +119,7 @@ To use the script:
{{< tab title="Rails console session" >}}
1. In your terminal window, start a Rails console session with `sudo gitlab-rails console`.
1. Paste in the entire `extend_expiring_tokens.rb` script below.
1. Paste in the entire `extend_expiring_tokens.rb` script from the following section.
If desired, change the `expiring_date` to a different date.
1. Press <kbd>Enter</kbd>.
@ -128,7 +128,7 @@ To use the script:
{{< tab title="Rails Runner" >}}
1. In your terminal window, connect to your instance.
1. Copy this entire `extend_expiring_tokens.rb` script below, and save it as a file on your instance:
1. Copy the entire `extend_expiring_tokens.rb` script from the following section, and save it as a file on your instance:
- Name it `extend_expiring_tokens.rb`.
- If desired, change the `expiring_date` to a different date.
- The file must be accessible to `git:git`.
@ -230,8 +230,8 @@ To use it:
1. In your terminal window, connect to your instance.
1. Start a Rails console session with `sudo gitlab-rails console`.
1. Depending on your needs, copy either the entire `expired_tokens.rb`
or `expired_tokens_date_range.rb` script below, and paste it into the console.
1. Depending on your needs, copy either the entire `expired_tokens.rb` from the following section
or `expired_tokens_date_range.rb` script from the section after that, and paste it into the console.
Change the `expires_at_date` to the date one year after your instance was upgraded to GitLab 16.0.
1. Press <kbd>Enter</kbd>.
@ -240,8 +240,8 @@ To use it:
{{< tab title="Rails Runner" >}}
1. In your terminal window, connect to your instance.
1. Depending on your needs, copy either the entire `expired_tokens.rb`
or `expired_tokens_date_range.rb` script below, and save it
1. Depending on your needs, copy either the entire `expired_tokens.rb` from the following section
or `expired_tokens_date_range.rb` script from the section after that, and save it
as a file on your instance:
- Name it `expired_tokens.rb`.
- Change the `expires_at_date` to the date one year after your instance was upgraded to GitLab 16.0.
@ -290,7 +290,7 @@ end
{{< alert type="note" >}}
To not only hide, but also remove, tokens belonging to blocked users, add `token.destroy!` directly below
To hide and also remove tokens belonging to blocked users, add `token.destroy!` directly below
`if token.user.blocked?`. However, this action does not leave an audit event,
unlike the [API method](../../api/personal_access_tokens.md#revoke-a-personal-access-token).
@ -306,7 +306,7 @@ the exact date your instance was upgraded to GitLab 16.0. To use it:
{{< tab title="Rails console session" >}}
1. In your terminal window, start a Rails console session with `sudo gitlab-rails console`.
1. Paste in the entire `tokens_with_no_expiry.rb` script below.
1. Paste in the entire `expired_tokens_date_range.rb` script from the next section.
If desired, change the `date_range` to a different range.
1. Press <kbd>Enter</kbd>.
@ -315,7 +315,7 @@ the exact date your instance was upgraded to GitLab 16.0. To use it:
{{< tab title="Rails Runner" >}}
1. In your terminal window, connect to your instance.
1. Copy this entire `tokens_with_no_expiry.rb` script below, and save it as a file on your instance:
1. Copy the entire `expired_tokens_date_range.rb` script from the next section, and save it as a file on your instance:
- Name it `expired_tokens_date_range.rb`.
- If desired, change the `date_range` to a different range.
- The file must be accessible to `git:git`.
@ -429,7 +429,7 @@ or the [Rails Runner](../../administration/operations/rails_console.md#using-the
1. In your terminal window, connect to your instance.
1. Start a Rails console session with `sudo gitlab-rails console`.
1. Paste in the entire `tokens_with_no_expiry.rb` script below.
1. Paste in the entire `tokens_with_no_expiry.rb` script from the following section.
1. Press <kbd>Enter</kbd>.
{{< /tab >}}
@ -437,7 +437,7 @@ or the [Rails Runner](../../administration/operations/rails_console.md#using-the
{{< tab title="Rails Runner" >}}
1. In your terminal window, connect to your instance.
1. Copy this entire `tokens_with_no_expiry.rb` script below, and save it as a file on your instance:
1. Copy this entire `tokens_with_no_expiry.rb` script from the following section, and save it as a file on your instance:
- Name it `tokens_with_no_expiry.rb`.
- The file must be accessible to `git:git`.
1. Run this command, changing the path to the _full_ path to your `tokens_with_no_expiry.rb` file:

View File

@ -109,7 +109,8 @@ You can enforce 2FA for all users in a group or subgroup.
{{< alert type="note" >}}
2FA enforcement applies to both [direct and inherited members](../user/project/members/_index.md#membership-types) group members. If 2FA is enforced on a subgroup, members of the parent group must also enroll an authentication factor.
2FA enforcement applies to both [direct and inherited members](../user/project/members/_index.md#membership-types) group members.
If 2FA is enforced on a subgroup, inherited members (members of the ancestor groups) must also enroll an authentication factor.
{{< /alert >}}
@ -138,6 +139,13 @@ The GitLab [incoming email](../administration/incoming_email.md) feature does no
By default, each subgroup can configure 2FA requirements that might differ from the parent group.
{{< alert type="note" >}}
Inherited members might also have different 2FA requirements applied at higher levels in the hierarchy.
In such cases, the most restrictive requirement takes precedence.
{{< /alert >}}
To prevent subgroups from setting individual 2FA requirements:
1. Go to the top-level group's **Settings > General**.

View File

@ -193,7 +193,7 @@ GitLab comes in two flavors: [Community Edition](https://about.gitlab.com/featur
and [Enterprise Edition](https://about.gitlab.com/features/#enterprise) which builds on top of the Community Edition and
includes extra features mainly aimed at organizations with more than 100 users.
Below you can find some guides to help you change GitLab editions.
In the following section you can find some guides to help you change GitLab editions.
### Community to Enterprise Edition
@ -204,7 +204,7 @@ The following guides are for subscribers of the Enterprise Edition only.
{{< /alert >}}
If you wish to upgrade your GitLab installation from Community to Enterprise
Edition, follow the guides below based on the installation method:
Edition, follow the guides in the following list based on the installation method:
- [Source CE to EE upgrade guides](upgrading_from_ce_to_ee.md) - The steps are very similar
to a version upgrade: stop the server, get the code, update configuration files for

View File

@ -245,13 +245,17 @@ To star a project:
{{< history >}}
- Default behavior [changed](https://gitlab.com/gitlab-org/gitlab/-/issues/389557) to delayed project deletion for Premium and Ultimate tiers on [GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/issues/393622) and [GitLab Self-Managed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/119606) in 16.0.
- Option to delete projects immediately as a group setting removed [on GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/issues/393622) and [on GitLab Self-Managed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/119606) in GitLab 16.0.
- Default behavior changed to delayed project deletion for [GitLab Free](https://gitlab.com/groups/gitlab-org/-/epics/17208) and [personal projects](https://gitlab.com/gitlab-org/gitlab/-/issues/536244) in 18.0.
- Option to delete projects immediately [moved](https://gitlab.com/groups/gitlab-org/-/epics/17208) from GitLab Premium to GitLab Free in 18.0.
{{< /history >}}
You can mark a project to be deleted.
After you delete a project for the first time, it enters a pending deletion state.
If you delete a project a second time, it is removed permanently.
You can schedule a project for deletion.
By default, when you delete a project for the first time, it enters a pending deletion state.
Delete a project again to remove it immediately.
On GitLab.com, after a project is deleted, its data is retained for seven days.
Prerequisites:
@ -265,71 +269,14 @@ To delete a project:
1. Expand **Advanced**.
1. In the **Delete project** section, select **Delete project**.
1. On the confirmation dialog, enter the project name and select **Yes, delete project**.
This action marks the project for deletion and places it in a pending deletion state.
1. Optional. To delete the project immediately, repeat these steps.
You can also [delete projects using the Rails console](troubleshooting.md#delete-a-project-using-console).
### Delayed project deletion
{{< history >}}
- [Enabled for projects in personal namespaces](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/89466) in GitLab 15.1.
- [Disabled for projects in personal namespaces](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/95495) in GitLab 15.3.
- Enabled delayed deletion by default and removed the option to delete immediately [on GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/issues/393622) and [on GitLab Self-Managed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/119606) in GitLab 16.0.
- [Moved](https://gitlab.com/groups/gitlab-org/-/epics/17208) from GitLab Premium to GitLab Free in 18.0.
- [Enabled for projects in personal namespaces](https://gitlab.com/gitlab-org/gitlab/-/issues/536244) in GitLab 18.0.
{{< /history >}}
Prerequisites:
- You must have the Owner role for the project.
Projects enter a pending deletion state when deleted for the first time.
On GitLab Self-Managed instances, group administrators can define a deletion delay period of between 1 and 90 days.
On SaaS, there is a non-adjustable default retention period of seven days.
You can [view projects that are pending deletion](#view-projects-pending-deletion),
and use the Rails console to
[find projects that are pending deletion](troubleshooting.md#find-projects-that-are-pending-deletion).
If the user who scheduled the project deletion loses access to the project (for example, by leaving the project, having their role downgraded, or being banned from the project) before the deletion occurs,
the deletion job will instead restore and unarchive the project, so the project will no longer be scheduled for deletion.
{{< alert type="warning" >}}
If the user who scheduled the project deletion regains Owner role or administrator access before the job runs, then the job removes the project permanently.
{{< /alert >}}
### Delete a project immediately
{{< history >}}
- Option to delete projects immediately from the **Admin** area and as a group setting removed [on GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/issues/393622) and [on GitLab Self-Managed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/119606) in GitLab 16.0.
- [Moved](https://gitlab.com/groups/gitlab-org/-/epics/17208) from GitLab Premium to GitLab Free in 18.0.
{{< /history >}}
If you don't want to wait for delayed deletion, you can delete a project immediately. To do this, perform the steps for [deleting a projects](#delete-a-project) again.
In the first cycle of deleting a project, the project is moved to the delayed deletion queue and automatically deleted after the retention period has passed.
If during this delayed deletion time you run a second deletion cycle, the project is deleted immediately.
Prerequisites:
- You must have the Owner role for the project.
- The project must be [pending deletion](#delete-a-project).
To immediately delete a project pending deletion:
1. On the left sidebar, select **Search or go to** and find your project.
1. Select **Settings > General**.
1. Expand **Advanced**.
1. In the **Delete project** section, select **Delete project**.
1. On the confirmation dialog, enter the project name and select **Yes, delete project**.
If the user who scheduled the project deletion loses access to the project before the deletion occurs
(for example, by leaving the project, having their role downgraded, or being banned from the project),
the deletion job restores the project. However, if the user regains access before the deletion job runs,
the job removes the project permanently.
### View projects pending deletion
@ -355,7 +302,7 @@ Each project in the list shows:
- The time the project is scheduled for final deletion.
- A **Restore** action to stop the project being eventually deleted.
## Restore a project
### Restore a project
{{< history >}}
@ -367,7 +314,6 @@ Each project in the list shows:
Prerequisites:
- You must have the Owner role for the project.
- The project must be [pending deletion](#delete-a-project).
To restore a project pending deletion:

View File

@ -1,93 +0,0 @@
# frozen_string_literal: true
# TODO: This is now a legacy filter, and is only used with the Ruby parser.
# The current markdown parser now properly handles multiline block quotes.
# The Ruby parser is now only for benchmarking purposes.
# issue: https://gitlab.com/gitlab-org/gitlab/-/issues/454601
module Banzai
module Filter
class BlockquoteFenceLegacyFilter < HTML::Pipeline::TextFilter
MARKDOWN_CODE_BLOCK_REGEX = %r{
(?<code>
# Code blocks:
# ```
# Anything, including `>>>` blocks which are ignored by this filter
# ```
^```
.+?
\n```\ *$
)
}mx
MARKDOWN_HTML_BLOCK_REGEX = %r{
(?<html>
# HTML block:
# <tag>
# Anything, including `>>>` blocks which are ignored by this filter
# </tag>
^<[^>]+?>\ *\n
.+?
\n</[^>]+?>\ *$
)
}mx
MARKDOWN_CODE_OR_HTML_BLOCKS = %r{
#{MARKDOWN_CODE_BLOCK_REGEX}
|
#{MARKDOWN_HTML_BLOCK_REGEX}
}mx
REGEX = %r{
#{MARKDOWN_CODE_OR_HTML_BLOCKS}
|
(?=(?<=^\n|\A)\ *>>>\ *\n.*\n\ *>>>\ *(?=\n$|\z))(?:
# Blockquote:
# >>>
# Anything, including code and HTML blocks
# >>>
(?<=^\n|\A)(?<indent>\ *)>>>\ *\n
(?<blockquote>
(?:
# Any character that doesn't introduce a code or HTML block
(?!
^```
|
^<[^>]+?>\ *\n
)
.
|
# A code block
\g<code>
|
# An HTML block
\g<html>
)+?
)
\n\ *>>>\ *(?=\n$|\z)
)
}mx
def initialize(text, context = nil, result = nil)
super text, context, result
end
def call
return @text if MarkdownFilter.glfm_markdown?(context)
@text.gsub(REGEX) do
if $~[:blockquote]
# keep the same number of source lines/positions by replacing the
# fence lines with newlines
indent = $~[:indent]
"\n#{$~[:blockquote].gsub(/^#{Regexp.quote(indent)}/, "#{indent}> ").gsub(/^> $/, '>')}\n"
else
$~[0]
end
end
end
end
end
end

View File

@ -1,83 +0,0 @@
# frozen_string_literal: true
# TODO: This is now a legacy filter, and is only used with the Ruby parser.
# The current markdown parser now properly handles math.
# The Ruby parser is now only for benchmarking purposes.
# issue: https://gitlab.com/gitlab-org/gitlab/-/issues/454601
module Banzai
module Filter
# HTML filter that implements our dollar math syntax, one of three filters:
# DollarMathPreLegacyFilter, DollarMathPostLegacyFilter, and MathFilter
#
class DollarMathPostLegacyFilter < HTML::Pipeline::Filter
# Based on the Pandoc heuristics,
# https://pandoc.org/MANUAL.html#extension-tex_math_dollars
#
# Handle the $...$ and $$...$$ inline syntax in this filter, after markdown processing
# but before post-handling of escaped characters. Any escaped $ will have been specially
# encoded and will therefore not interfere with the detection of the dollar syntax.
# Corresponds to the "$...$" syntax
DOLLAR_INLINE_UNTRUSTED =
'(?P<matched>\$(?P<math>(?:\S[^$\n]*?\S|[^$\s]))\$)(?:[^\d]|$)'
DOLLAR_INLINE_UNTRUSTED_REGEX =
Gitlab::UntrustedRegexp.new(DOLLAR_INLINE_UNTRUSTED, multiline: false).freeze
# Corresponds to the "$$...$$" syntax
DOLLAR_DISPLAY_INLINE_UNTRUSTED =
'(?P<matched>\$\$\ *(?P<math>[^$\n]+?)\ *\$\$)'
DOLLAR_DISPLAY_INLINE_UNTRUSTED_REGEX =
Gitlab::UntrustedRegexp.new(DOLLAR_DISPLAY_INLINE_UNTRUSTED, multiline: false).freeze
# Order dependent. Handle the `$$` syntax before the `$` syntax
DOLLAR_MATH_PIPELINE = [
{ pattern: DOLLAR_DISPLAY_INLINE_UNTRUSTED_REGEX, style: :display },
{ pattern: DOLLAR_INLINE_UNTRUSTED_REGEX, style: :inline }
].freeze
# Do not recognize math inside these tags
IGNORED_ANCESTOR_TAGS = %w[pre code tt].to_set
def call
return doc if MarkdownFilter.glfm_markdown?(context)
process_dollar_pipeline
doc
end
def process_dollar_pipeline
doc.xpath('descendant-or-self::text()').each do |node|
next if has_ancestor?(node, IGNORED_ANCESTOR_TAGS)
node_html = node.to_html
next unless DOLLAR_INLINE_UNTRUSTED_REGEX.match?(node_html) ||
DOLLAR_DISPLAY_INLINE_UNTRUSTED_REGEX.match?(node_html)
temp_doc = Nokogiri::HTML.fragment(node_html)
DOLLAR_MATH_PIPELINE.each do |pipeline|
temp_doc.xpath('child::text()').each do |temp_node|
html = temp_node.to_html
pipeline[:pattern].scan(temp_node.content).each do |match|
math = pipeline[:pattern].extract_named_group(:math, match)
html.sub!(match.first, math_html(math: math, style: pipeline[:style]))
end
temp_node.replace(html)
end
end
node.replace(temp_doc)
end
end
private
def math_html(math:, style:)
"<code data-math-style=\"#{style}\">#{math}</code>"
end
end
end
end

View File

@ -1,51 +0,0 @@
# frozen_string_literal: true
# TODO: This is now a legacy filter, and is only used with the Ruby parser.
# The current markdown parser now properly handles math.
# The Ruby parser is now only for benchmarking purposes.
# issue: https://gitlab.com/gitlab-org/gitlab/-/issues/454601
module Banzai
module Filter
# HTML filter that implements our dollar math syntax, one of three filters:
# DollarMathPreLegacyFilter, DollarMathPostLegacyFilter, and MathFilter
#
class DollarMathPreLegacyFilter < HTML::Pipeline::TextFilter
# Based on the Pandoc heuristics,
# https://pandoc.org/MANUAL.html#extension-tex_math_dollars
#
# Handle the $$\n...\n$$ syntax in this filter, before markdown processing,
# by converting it into the ```math syntax. In this way, we can ensure
# that it's considered a code block and will not have any markdown processed inside it.
# Display math block:
# $$
# latex math
# $$
REGEX =
"#{::Gitlab::Regex.markdown_code_or_html_blocks_or_html_comments_untrusted}" \
'|' \
'^\$\$\ *\n' \
'(?P<display_math>' \
'(?:\n|.)*?' \
')' \
'\n\$\$\ *$' \
.freeze
def call
return @text if MarkdownFilter.glfm_markdown?(context)
regex = Gitlab::UntrustedRegexp.new(REGEX, multiline: true)
return @text unless regex.match?(@text)
regex.replace_gsub(@text) do |match|
# change from $$ to ```math
if match[:display_math]
"```math\n#{match[:display_math]}\n```"
else
match.to_s
end
end
end
end
end
end

View File

@ -1,148 +0,0 @@
# frozen_string_literal: true
module Banzai
module Filter
# TODO: This is now a legacy filter, and is only used with the Ruby parser.
# The current markdown parser now properly handles escaping characters.
# The Ruby parser is now only for benchmarking purposes.
# issue: https://gitlab.com/gitlab-org/gitlab/-/issues/454601
#
# See comments in MarkdownPreEscapeFilter for details on strategy
class MarkdownPostEscapeLegacyFilter < HTML::Pipeline::Filter
LITERAL_KEYWORD = MarkdownPreEscapeLegacyFilter::LITERAL_KEYWORD
LITERAL_REGEX = %r{#{LITERAL_KEYWORD}-(\+[a-k])-#{LITERAL_KEYWORD}}
NOT_LITERAL_REGEX = %r{#{LITERAL_KEYWORD}-((%5C|\\).{1,2})-#{LITERAL_KEYWORD}}
SPAN_REGEX = %r{<span data-escaped-char>(.)</span>}
XPATH_A = Gitlab::Utils::Nokogiri.css_to_xpath('a').freeze
XPATH_LANG_TAG = Gitlab::Utils::Nokogiri.css_to_xpath('pre').freeze
XPATH_ESCAPED_CHAR = Gitlab::Utils::Nokogiri.css_to_xpath('span[data-escaped-char]').freeze
def call
return doc if MarkdownFilter.glfm_markdown?(context)
return doc unless result[:escaped_literals]
new_html = unescaped_literals(doc.to_html)
new_html = add_spans(new_html)
@doc = parse_html(new_html)
remove_spans_in_certain_attributes
remove_unnecessary_escapes
doc
end
private
# For any literals that actually didn't get escape processed
# (for example in code blocks), remove the special sequence.
def unescaped_literals(html)
html.gsub!(NOT_LITERAL_REGEX) do |_match|
last_match = ::Regexp.last_match(1)
last_match_token = last_match.sub('%5C', '\\')
escaped_item = Banzai::Filter::MarkdownPreEscapeLegacyFilter::ESCAPABLE_CHARS.find do |item|
item[:token] == last_match_token
end
escaped_char = escaped_item ? escaped_item[:escaped] : last_match
escaped_char = escaped_char.sub('\\', '%5C') if last_match.start_with?('%5C')
escaped_char
end
html
end
# Replace any left over literal sequences with `span` so that our
# reference processing is short-circuited
def add_spans(html)
html.gsub!(LITERAL_REGEX) do |_match|
last_match = ::Regexp.last_match(1)
last_match_token = "\\#{last_match}"
escaped_item = Banzai::Filter::MarkdownPreEscapeLegacyFilter::ESCAPABLE_CHARS.find do |item|
item[:token] == last_match_token
end
escaped_char = escaped_item ? escaped_item[:char] : ::Regexp.last_match(1)
"<span data-escaped-char>#{escaped_char}</span>"
end
html
end
# Since literals are converted in links, we need to remove any surrounding `span`.
def remove_spans_in_certain_attributes
doc.xpath(XPATH_A).each do |node|
if node.attributes['href']
node.attributes['href'].value = node.attributes['href'].value.gsub(SPAN_REGEX, '\1')
end
if node.attributes['title']
node.attributes['title'].value = node.attributes['title'].value.gsub(SPAN_REGEX, '\1')
end
end
doc.xpath(XPATH_LANG_TAG).each do |node|
if node.attributes['lang']
node.attributes['lang'].value = node.attributes['lang'].value.gsub(SPAN_REGEX, '\1')
end
end
end
def remove_unnecessary_escapes
doc.xpath(XPATH_ESCAPED_CHAR).each do |node|
escaped_item =
Banzai::Filter::MarkdownPreEscapeLegacyFilter::ESCAPABLE_CHARS.find { |item| item[:char] == node.content }
next unless escaped_item
if node.parent.name == 'code'
# For any `data-escaped-char` that makes it into a `<code>` element,
# convert back to the escaped character, such as `\$`. Usually this would
# only happen for dollar math
content = +escaped_item[:escaped]
elsif escaped_item[:latex] && !escaped_item[:reference]
# Character only used in latex, since it's outside of a code block we can
# transform into the regular character
content = +escaped_item[:char]
else
# Escaped reference character, so leave as is. This is so that our normal
# reference processing can be short-circuited by escaping the reference,
# like \@username
next
end
merge_adjacent_text_nodes(node, content)
end
end
def text_node?(node)
node.is_a?(Nokogiri::XML::Text)
end
# Merge directly adjacent text nodes and replace existing node with
# the merged content. For example, the document could be
# #(Text "~c_bug"), #(Element:0x57724 { name = "span" }, children = [ #(Text "_")] })]
# Our reference processing requires a single string of text to match against. So even if it was
# #(Text "~c_bug"), #(Text "_")
# it wouldn't match. Merging together will give
# #(Text "~c_bug_")
def merge_adjacent_text_nodes(node, content)
if text_node?(node.previous)
content.prepend(node.previous.content)
node.previous.remove
end
if text_node?(node.next)
content.concat(node.next.content)
node.next.remove
end
node.replace(content)
end
end
end
end

View File

@ -1,84 +0,0 @@
# frozen_string_literal: true
module Banzai
module Filter
# TODO: This is now a legacy filter, and is only used with the Ruby parser.
# The current markdown parser now properly handles escaping characters.
# The Ruby parser is now only for benchmarking purposes.
# issue: https://gitlab.com/gitlab-org/gitlab/-/issues/454601
#
# In order to allow a user to short-circuit our reference shortcuts
# (such as # or !), the user should be able to escape them, like \#.
# CommonMark supports this, however it removes all information about
# what was actually a literal. In order to short-circuit the reference,
# we must surround backslash escaped ASCII punctuation with a custom sequence.
# This way CommonMark will properly handle the backslash escaped chars
# but we will maintain knowledge (the sequence) that it was a literal.
#
# This processing is also important for the handling of escaped characters
# in LaTeX math. These will need to be converted back into their escaped
# versions if they are detected in math blocks.
#
# We need to surround the character, not just prefix it. It could
# get converted into an entity by CommonMark and we wouldn't know how many
# characters there are. The entire literal needs to be surrounded with
# a `span` tag, which short-circuits our reference processing.
#
# We can't use a custom HTML tag since we could be initially surrounding
# text in an href, and then CommonMark will not be able to parse links
# properly. So we use `cmliteral-` and `-cmliteral`
#
# https://spec.commonmark.org/0.29/#backslash-escapes
#
# This filter does the initial surrounding, and MarkdownPostEscapeLegacyFilter
# does the conversion into span tags.
class MarkdownPreEscapeLegacyFilter < HTML::Pipeline::TextFilter
# Table of characters that need this special handling. It consists of the
# GitLab special reference characters and special LaTeX characters.
#
# The `token` is used when we do the initial replacement - for example converting
# `\$` into `cmliteral-\+a-cmliteral`. We don't simply replace `\$` with `$`,
# because this can cause difficulties in parsing math blocks that use `$` as a
# delimiter. We also include a character that _can_ be escaped, `\+`. By examining
# the text once it's been passed to markdown, we can determine that `cmliteral-\+a-cmliteral`
# was in a block that markdown did _not_ escape the character, for example an inline
# code block or some other element. In this case, we must convert back to the
# original escaped version, `\$`. However if we detect `cmliteral-+a-cmliteral`,
# then we know markdown considered it an escaped character, and we should replace it
# with the non-escaped version, `$`.
# See the MarkdownPostEscapeLegacyFilter for how this is done.
ESCAPABLE_CHARS = [
{ char: '$', escaped: '\$', token: '\+a', reference: true, latex: true },
{ char: '%', escaped: '\%', token: '\+b', reference: true, latex: true },
{ char: '#', escaped: '\#', token: '\+c', reference: true, latex: true },
{ char: '&', escaped: '\&', token: '\+d', reference: true, latex: true },
{ char: '{', escaped: '\{', token: '\+e', reference: false, latex: true },
{ char: '}', escaped: '\}', token: '\+f', reference: false, latex: true },
{ char: '_', escaped: '\_', token: '\+g', reference: false, latex: true },
{ char: '@', escaped: '\@', token: '\+h', reference: true, latex: false },
{ char: '!', escaped: '\!', token: '\+i', reference: true, latex: false },
{ char: '~', escaped: '\~', token: '\+j', reference: true, latex: false },
{ char: '^', escaped: '\^', token: '\+k', reference: true, latex: false }
].freeze
TARGET_CHARS = ESCAPABLE_CHARS.pluck(:char).join.freeze
ASCII_PUNCTUATION = %r{(\\[#{TARGET_CHARS}])}
LITERAL_KEYWORD = 'cmliteral'
def call
return @text if MarkdownFilter.glfm_markdown?(context)
@text.gsub(ASCII_PUNCTUATION) do |match|
# The majority of markdown does not have literals. If none
# are found, we can bypass the post filter
result[:escaped_literals] = true
escaped_item = ESCAPABLE_CHARS.find { |item| item[:escaped] == match }
token = escaped_item ? escaped_item[:token] : match
"#{LITERAL_KEYWORD}-#{token}-#{LITERAL_KEYWORD}"
end
end
end
end
end

View File

@ -1,10 +1,5 @@
# frozen_string_literal: true
# TODO: Portions of this are legacy code, and is only used with the Ruby parser.
# The current markdown parser now properly handles math.
# The Ruby parser is now only for benchmarking purposes.
# issue: https://gitlab.com/gitlab-org/gitlab/-/issues/454601
# Generated HTML is transformed back to GFM by:
# - app/assets/javascripts/content_editor/extensions/math_inline.js
# - app/assets/javascripts/content_editor/extensions/code_block_highlight.js
@ -29,8 +24,6 @@ module Banzai
@nodes_count = 0
process_existing
process_dollar_backtick_inline unless MarkdownFilter.glfm_markdown?(context)
process_math_codeblock unless MarkdownFilter.glfm_markdown?(context)
doc
end
@ -62,56 +55,6 @@ module Banzai
def group
context[:project]&.parent || context[:group]
end
#-----------------------------------------------------------------
# TODO: Legacy code
# issue: https://gitlab.com/gitlab-org/gitlab/-/issues/454601
CSS_MATH = 'pre[data-canonical-lang="math"] > code'
XPATH_MATH = Gitlab::Utils::Nokogiri.css_to_xpath(CSS_MATH).freeze
CSS_CODE = 'code'
XPATH_CODE = Gitlab::Utils::Nokogiri.css_to_xpath(CSS_CODE).freeze
CSS_INLINE_CODE = 'code[data-math-style]'
XPATH_INLINE_CODE = Gitlab::Utils::Nokogiri.css_to_xpath(CSS_INLINE_CODE).freeze
# Attribute indicating inline or display math.
STYLE_ATTRIBUTE = 'data-math-style'
# Class used for tagging elements that should be rendered
MATH_CLASSES = "code math #{TAG_CLASS}"
DOLLAR_SIGN = '$'
# Corresponds to the "$`...`$" syntax
def process_dollar_backtick_inline
doc.xpath(XPATH_CODE).each do |code|
break if render_nodes_limit_reached?(@nodes_count)
closing = code.next
opening = code.previous
# We need a sibling before and after.
# They should end and start with $ respectively.
next unless closing && opening &&
closing.text? && opening.text? &&
closing.content.first == DOLLAR_SIGN &&
opening.content.last == DOLLAR_SIGN
code[STYLE_ATTRIBUTE] = 'inline'
code[:class] = MATH_CLASSES
closing.content = closing.content[1..]
opening.content = opening.content[0..-2]
@nodes_count += 1
end
end
# Corresponds to the "```math...```" syntax
def process_math_codeblock
doc.xpath(XPATH_MATH).each do |node|
pre_node = node.parent
pre_node[STYLE_ATTRIBUTE] = 'display'
pre_node[:class] = TAG_CLASS
end
end
end
end
end

View File

@ -1,131 +0,0 @@
# frozen_string_literal: true
require 'cgi/util'
# TODO: This is now a legacy filter, and is only used with the Ruby parser.
# The current markdown parser now properly handles adding anchors to headers.
# The Ruby parser is now only for benchmarking purposes.
# issue: https://gitlab.com/gitlab-org/gitlab/-/issues/454601
# Generated HTML is transformed back to GFM by app/assets/javascripts/behaviors/markdown/nodes/table_of_contents.js
module Banzai
module Filter
# HTML filter that adds an anchor child element to all Headers in a
# document, so that they can be linked to.
#
# Generates the Table of Contents with links to each header. See Results.
#
# Based on HTML::Pipeline::TableOfContentsFilter.
#
# Context options:
# :no_header_anchors - Skips all processing done by this filter.
#
# Results:
# :toc - String containing Table of Contents data as a `ul` element with
# `li` child elements.
class TableOfContentsLegacyFilter < HTML::Pipeline::Filter
include Gitlab::Utils::Markdown
CSS = 'h1, h2, h3, h4, h5, h6'
XPATH = Gitlab::Utils::Nokogiri.css_to_xpath(CSS).freeze
def call
return doc if MarkdownFilter.glfm_markdown?(context)
return doc if context[:no_header_anchors]
result[:toc] = +""
headers = Hash.new(0)
header_root = current_header = HeaderNode.new
doc.xpath(XPATH).each do |node|
header_content = node.children.first
next unless header_content
id = string_to_anchor(node.text[0...255])
uniq = headers[id] > 0 ? "-#{headers[id]}" : ''
headers[id] += 1
href = "#{id}#{uniq}"
current_header = HeaderNode.new(node: node, href: href, previous_header: current_header)
header_content.add_previous_sibling(anchor_tag(href))
end
push_toc(header_root.children, root: true)
doc
end
private
def anchor_tag(href)
escaped_href = CGI.escape(href) # account for non-ASCII characters
<<~TAG.squish
<a id="#{Banzai::Renderer::USER_CONTENT_ID_PREFIX}#{href}"
class="anchor" href="##{escaped_href}" aria-hidden="true"></a>
TAG
end
def push_toc(children, root: false)
return if children.empty?
klass = ' class="section-nav"' if root
result[:toc] << "<ul#{klass}>"
children.each { |child| push_anchor(child) }
result[:toc] << '</ul>'
end
def push_anchor(header_node)
result[:toc] << %(<li><a href="##{header_node.href}">#{header_node.text}</a>)
push_toc(header_node.children)
result[:toc] << '</li>'
end
class HeaderNode
attr_reader :node, :href, :parent, :children
def initialize(node: nil, href: nil, previous_header: nil)
@node = node
@href = CGI.escape(href) if href
@children = []
@parent = find_parent(previous_header)
@parent.children.push(self) if @parent
end
def level
return 0 unless node
@level ||= node.name[1].to_i
end
def text
return '' unless node
@text ||= CGI.escapeHTML(node.text)
end
private
def find_parent(previous_header)
return unless previous_header
if level == previous_header.level
parent = previous_header.parent
elsif level > previous_header.level
parent = previous_header
else
parent = previous_header
parent = parent.parent while parent.level >= level
end
parent
end
end
end
end
end

View File

@ -1,150 +0,0 @@
# frozen_string_literal: true
# TODO: This is now a legacy filter, and is only used with the Ruby parser.
# The Ruby parser is now only for benchmarking purposes.
# issue: https://gitlab.com/gitlab-org/gitlab/-/issues/454601
module Banzai
module Filter
# Using `[[_TOC_]]` or `[TOC]` (both case insensitive), inserts a Table of Contents list.
#
# `[[_TOC_]]` is based on the Gollum syntax. This way we have
# some consistency between with wiki and normal markdown.
# The support for this has been removed from GollumTagsFilter
#
# `[toc]` is a generally accepted form, used by Typora for example.
#
# Based on Banzai::Filter::GollumTagsFilter
#
# rubocop:disable Gitlab/NoCodeCoverageComment -- no coverage needed for a legacy filter
# :nocov: undercoverage
class TableOfContentsTagLegacyFilter < HTML::Pipeline::Filter
TEXT_QUERY = %q(descendant-or-self::text()[ancestor::p and contains(translate(., 'TOC', 'toc'), 'toc')])
HEADER_CSS = 'h1, h2, h3, h4, h5, h6'
HEADER_XPATH = Gitlab::Utils::Nokogiri.css_to_xpath(HEADER_CSS).freeze
def call
return doc if MarkdownFilter.glfm_markdown?(context)
return doc if context[:no_header_anchors]
doc.xpath(TEXT_QUERY).each do |node|
if toc_tag?(node)
# Support [TOC] / [toc] tags, which don't have a wrapping <em>-tag
process_toc_tag(node)
elsif toc_tag_em?(node)
# Support Gollum like ToC tag (`[[_TOC_]]` / `[[_toc_]]`), which will be converted
# into `[[<em>TOC</em>]]` by the markdown filter, so it
# needs special-case handling
process_toc_tag_em(node)
end
end
doc
end
private
# Replace an entire `[[<em>TOC</em>]]` node
def process_toc_tag_em(node)
process_toc_tag(node.parent)
end
# Replace an entire `[TOC]` node
def process_toc_tag(node)
build_toc
# we still need to go one step up to also replace the surrounding <p></p>
node.parent.replace(result[:toc].presence || '')
end
def toc_tag_em?(node)
node.content.casecmp?('toc') &&
node.parent.name == 'em' &&
node.parent.parent.text.casecmp?('[[toc]]')
end
def toc_tag?(node)
node.parent.text.casecmp?('[toc]')
end
def build_toc
return if result[:toc]
result[:toc] = +""
header_root = current_header = HeaderNode.new
doc.xpath(HEADER_XPATH).each do |node|
header_anchor = node.css('a.anchor').first
next unless header_anchor
# remove leading anchor `#` so we can add it back later
href = header_anchor[:href].slice(1..)
current_header = HeaderNode.new(node: node, href: href, previous_header: current_header)
end
push_toc(header_root.children, root: true)
end
def push_toc(children, root: false)
return if children.empty?
klass = ' class="section-nav"' if root
result[:toc] << "<ul#{klass}>"
children.each { |child| push_anchor(child) }
result[:toc] << '</ul>'
end
def push_anchor(header_node)
result[:toc] << %(<li><a href="##{header_node.href}">#{header_node.text}</a>)
push_toc(header_node.children)
result[:toc] << '</li>'
end
class HeaderNode
attr_reader :node, :href, :parent, :children
def initialize(node: nil, href: nil, previous_header: nil)
@node = node
@href = CGI.escape(href) if href
@children = []
@parent = find_parent(previous_header)
@parent.children.push(self) if @parent
end
def level
return 0 unless node
@level ||= node.name[1].to_i
end
def text
return '' unless node
@text ||= CGI.escapeHTML(node.text)
end
private
def find_parent(previous_header)
return unless previous_header
if level == previous_header.level
parent = previous_header.parent
elsif level > previous_header.level
parent = previous_header
else
parent = previous_header
parent = parent.parent while parent.level >= level
end
parent
end
end
end
# :nocov:
# rubocop:enable Gitlab/NoCodeCoverageComment
end
end

View File

@ -5,7 +5,6 @@ module Banzai
class BroadcastMessagePipeline < DescriptionPipeline
def self.filters
@filters ||= FilterArray[
Filter::BlockquoteFenceLegacyFilter,
Filter::MarkdownFilter,
Filter::BroadcastMessageSanitizationFilter,
Filter::SanitizeLinkFilter,

View File

@ -29,8 +29,6 @@ module Banzai
Filter::AttributesFilter,
Filter::VideoLinkFilter,
Filter::AudioLinkFilter,
Filter::TableOfContentsLegacyFilter,
Filter::TableOfContentsTagLegacyFilter,
Filter::TableOfContentsTagFilter,
Filter::AutolinkFilter,
Filter::SuggestionFilter,

View File

@ -5,7 +5,6 @@ module Banzai
class NotePipeline < FullPipeline
def self.transform_context(context)
super(context).merge(
# TableOfContentsLegacyFilter
no_header_anchors: true
)
end

View File

@ -6,13 +6,8 @@ module Banzai
def self.filters
FilterArray[
Filter::IncludeFilter,
Filter::MarkdownPreEscapeLegacyFilter,
Filter::DollarMathPreLegacyFilter,
Filter::BlockquoteFenceLegacyFilter,
Filter::MarkdownFilter,
Filter::ConvertTextToDocFilter,
Filter::DollarMathPostLegacyFilter,
Filter::MarkdownPostEscapeLegacyFilter
]
end
end

View File

@ -7,7 +7,6 @@ module Banzai
class QuickActionPipeline < BasePipeline
def self.filters
FilterArray[
Filter::BlockquoteFenceLegacyFilter,
Filter::MarkdownFilter,
Filter::QuickActionFilter
]

View File

@ -17,6 +17,7 @@ module Gitlab
klass.connection = connection
klass
end
module_function :define_batchable_model
def each_batch(table_name, connection:, scope: ->(table) { table.all }, of: BATCH_SIZE, **opts)
if transaction_open?

View File

@ -13,6 +13,7 @@ module Gitlab
include Migrations::SidekiqHelpers
include Migrations::RedisHelpers
include DynamicModelHelpers
include FeatureFlagMigratorHelpers
include RenameTableHelpers
include AsyncIndexes::MigrationHelpers
include AsyncConstraints::MigrationHelpers
@ -1139,20 +1140,6 @@ into similar problems in the future (e.g. when new tables are created).
YAML.safe_load_file(File.join(INTEGER_IDS_YET_TO_INITIALIZED_TO_BIGINT_FILE_PATH))
end
def feature_flag_enabled?(feature_flag_name)
quoted_name = connection.quote(feature_flag_name)
result = execute <<~SQL.squish
SELECT 1
FROM feature_gates
WHERE feature_key = #{quoted_name}
AND value = 'true'
LIMIT 1;
SQL
result.ntuples > 0
end
private
def multiple_columns(columns, separator: ', ')

View File

@ -0,0 +1,147 @@
# frozen_string_literal: true
# This module provides helper methods for migrating `ops` type feature flags to application settings.
# It handles regular application settings and JSONB column application settings.
# The module includes methods for:
# - Migrating feature flag state to application settings during migrations (up)
# - Reverting application settings to default values during rollbacks (down)
#
# WARNING: These helpers will only migrate feature flags that are explicitly set to `true` or `false`.
# If a feature flag is set for a percentage or specific actor, the default value will be used.
module Gitlab
module Database
module MigrationHelpers
module FeatureFlagMigratorHelpers
# Migrates a feature flag to an application setting.
#
# @param feature_flag_name [Symbol, String] The name of the feature flag to migrate
# @param setting_name [Symbol, String] The name of the application setting column to update
# @param default_enabled [Boolean] The default value to use if the feature flag is not set
# @return [Integer] The number of affected rows for UPDATE statement
def up_migrate_to_setting(feature_flag_name:, setting_name:, default_enabled:)
if feature_flag_name.blank? || setting_name.blank? || default_enabled.nil?
raise ArgumentError, 'feature_flag_name, setting_name, and default_enabled are required'
end
raise ArgumentError, 'default_enabled must be a boolean' unless [true, false].include?(default_enabled)
feature_flag_state = feature_flag_state(feature_flag_name, default_enabled)
sql = <<~SQL
UPDATE application_settings
SET #{setting_name} = #{feature_flag_state}, updated_at = NOW()
WHERE id = (SELECT MAX(id) FROM application_settings)
SQL
execute(sql)
end
# Migrates a feature flag to a JSONB application setting.
#
# @param feature_flag_name[Symbol, String] The name of the feature flag to migrate
# @param setting_name [Symbol, String] The name of the application setting to update
# @param jsonb_column_name [Symbol, String] The name of the application setting JSONB column to update
# @param default_enabled [Boolean] The default value to use if the feature flag is not set
# @return [Integer] The number of affected rows for UPDATE statement
def up_migrate_to_jsonb_setting(feature_flag_name:, setting_name:, jsonb_column_name:, default_enabled:)
if feature_flag_name.blank? || setting_name.blank? || jsonb_column_name.blank? || default_enabled.nil?
raise ArgumentError, 'feature_flag_name, jsonb_column_name, setting_name, and default_enabled are required'
end
raise ArgumentError, 'default_enabled must be a boolean' unless [true, false].include?(default_enabled)
feature_flag_state = feature_flag_state(feature_flag_name, default_enabled)
sql = <<~SQL
UPDATE application_settings
SET #{jsonb_column_name} = jsonb_set(
COALESCE(#{jsonb_column_name}, '{}'::jsonb),
'{#{setting_name}}',
to_jsonb(#{feature_flag_state})
),
updated_at = NOW()
WHERE id = (SELECT MAX(id) FROM application_settings)
SQL
execute(sql)
end
# Reverts an application setting to its default value during a migration rollback.
#
# @param setting_name [Symbol, String] The name of the application setting column to revert
# @param default_enabled [Boolean] The default value to set for the application setting
# @return [Integer] The number of affected rows for UPDATE statement
def down_migrate_to_setting(setting_name:, default_enabled:)
if setting_name.blank? || default_enabled.nil?
raise ArgumentError, 'setting_name and default_enabled are required'
end
raise ArgumentError, 'default_enabled must be a boolean' unless [true, false].include?(default_enabled)
sql = <<~SQL
UPDATE application_settings
SET #{setting_name} = #{default_enabled}, updated_at = NOW()
WHERE id = (SELECT MAX(id) FROM application_settings)
SQL
execute(sql)
end
# Reverts a JSONB application setting to its default state during a migration rollback.
# This method removes the specified setting from the JSONB column.
#
# @param setting_name [Symbol, String] The name of the application setting to remove from the JSONB column
# @param jsonb_column_name [Symbol, String] The name of the application setting JSONB column to update
# @return [Integer] The number of affected rows for UPDATE statement
def down_migrate_to_jsonb_setting(setting_name:, jsonb_column_name:)
if setting_name.blank? || jsonb_column_name.nil?
raise ArgumentError, 'setting_name and jsonb_column_name are required'
end
sql = <<~SQL
UPDATE application_settings
SET #{jsonb_column_name} = #{jsonb_column_name} - '#{setting_name}',
updated_at = NOW()
WHERE id = (SELECT MAX(id) FROM application_settings)
SQL
execute(sql)
end
private
def validate_all_arguments_present!(*args, error_msg)
return if args.all? { |arg| !arg.nil? }
raise ArgumentError, error_msg
end
def feature_flag_state(feature_flag_name, default_enabled)
# no record of feature flag being set, return default_enabled
return default_enabled unless exists_in_features?(feature_flag_name)
set_to_true_in_feature_gates?(feature_flag_name)
end
def feature_gates
@feature_gates ||= DynamicModelHelpers.define_batchable_model('feature_gates',
connection: ActiveRecord::Base.connection) # rubocop:disable Database/MultipleDatabases -- Flipper models do not inherit from ApplicationRecord
end
def features
@features ||= DynamicModelHelpers.define_batchable_model('features',
connection: ActiveRecord::Base.connection) # rubocop:disable Database/MultipleDatabases -- Flipper models do not inherit from ApplicationRecord
end
def exists_in_features?(feature_flag_name)
features.where(key: feature_flag_name).exists?
end
def set_to_true_in_feature_gates?(feature_flag_name)
feature_gates.where(feature_key: feature_flag_name, key: 'boolean', value: 'true').exists?
end
end
end
end
end

View File

@ -67494,6 +67494,9 @@ msgid_plural "VirtualRegistry|%{hours} hours cache"
msgstr[0] ""
msgstr[1] ""
msgid "VirtualRegistry|%{size} KB"
msgstr ""
msgid "VirtualRegistry|%{size} storage used"
msgstr ""
@ -67530,6 +67533,9 @@ msgstr ""
msgid "VirtualRegistry|Delete Maven upstream"
msgstr ""
msgid "VirtualRegistry|Delete Maven upstream cache entry?"
msgstr ""
msgid "VirtualRegistry|Delete registry"
msgstr ""
@ -67548,6 +67554,12 @@ msgstr ""
msgid "VirtualRegistry|Enter password"
msgstr ""
msgid "VirtualRegistry|Failed to delete cache entry."
msgstr ""
msgid "VirtualRegistry|Failed to fetch cache entries."
msgstr ""
msgid "VirtualRegistry|Failed to fetch list of maven virtual registries."
msgstr ""
@ -67632,6 +67644,9 @@ msgstr ""
msgid "VirtualRegistry|Virtual registry"
msgstr ""
msgid "VirtualRegistry|last checked %{date}"
msgstr ""
msgid "Visibility"
msgstr ""

View File

@ -9,7 +9,7 @@ gem 'activesupport', '~> 7.1.5.1' # This should stay in sync with the root's Gem
gem 'allure-rspec', '~> 2.27.0'
gem 'capybara', '~> 3.40.0'
gem 'capybara-screenshot', '~> 1.0.26'
gem 'rake', '~> 13', '>= 13.2.1'
gem 'rake', '~> 13', '>= 13.3.0'
gem 'rspec', '~> 3.13', '>= 3.13.1'
gem 'selenium-webdriver', '= 4.33.0'
gem 'rest-client', '~> 2.1.0'

View File

@ -268,7 +268,7 @@ GEM
loofah (~> 2.21)
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
rainbow (3.1.1)
rake (13.2.1)
rake (13.3.0)
regexp_parser (2.1.1)
representable (3.2.0)
declarative (< 0.1.0)
@ -393,7 +393,7 @@ DEPENDENCIES
parallel_tests (~> 5.1)
pry-byebug (~> 3.11.0)
rainbow (~> 3.1.1)
rake (~> 13, >= 13.2.1)
rake (~> 13, >= 13.3.0)
rest-client (~> 2.1.0)
rotp (~> 6.3.0)
rspec (~> 3.13, >= 3.13.1)

View File

@ -7,13 +7,13 @@ module RuboCop
module Migration
# This cop prevents the use of Feature.enabled? and Feature.disabled? in migrations.
# Using feature flags in migrations is forbidden to avoid breaking the migration in the future.
# Instead, use the feature_flag_enabled?(feature_name) migration helper method.
# Instead, use the Gitlab::Database::MigrationHelpers::FeatureFlagMigratorHelpers migration helpers.
# https://docs.gitlab.com/development/migration_style_guide/#using-application-code-in-migrations-discouraged
class PreventFeatureFlagsUsage < RuboCop::Cop::Base
include MigrationHelpers
MSG = "Do not use Feature.enabled? or Feature.disabled? in migrations. " \
"Use the feature_flag_enabled?(feature_name) migration helper method."
"Use the Gitlab::Database::MigrationHelpers::FeatureFlagMigratorHelpers migration helpers."
# @!method feature_enabled?(node)
def_node_matcher :feature_enabled?, <<~PATTERN

View File

@ -150,7 +150,6 @@ spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_contents_labe
spec/frontend/sidebar/components/milestone/milestone_dropdown_spec.js
spec/frontend/sidebar/components/subscriptions/subscriptions_dropdown_spec.js
spec/frontend/snippets/components/snippet_description_edit_spec.js
spec/frontend/super_sidebar/components/organization_switcher_spec.js
spec/frontend/super_sidebar/components/sidebar_portal_spec.js
spec/frontend/super_sidebar/components/user_menu_spec.js
spec/frontend/tooltips/index_spec.js

View File

@ -235,7 +235,7 @@ RSpec.describe HelpController do
context 'when requested file exists' do
before do
stub_doc_file_read(file_name: 'user/ssh.md', content: fixture_file('blockquote_fence_legacy_after.md'))
stub_doc_file_read(file_name: 'user/ssh.md', content: fixture_file('sample_doc.md'))
stub_application_setting(help_page_documentation_base_url: '')
subject

View File

@ -272,6 +272,21 @@ RSpec.describe Projects::TreeController, feature_category: :source_code_manageme
let(:create_merge_request) { 1 }
end
end
context 'and the merge request already exists' do
let(:create_merge_request) { true }
let(:merge_request) { create(:merge_request, source_project: project) }
it 'redirects to the merge_request details page without flash notice' do
allow(controller).to receive(:merge_request_exists?) do
controller.instance_variable_set(:@merge_request, merge_request)
merge_request
end
expect(create_dir).to redirect_to(project_merge_request_path(project, merge_request))
expect(flash[:notice]).to be_nil
end
end
end
end
@ -302,6 +317,19 @@ RSpec.describe Projects::TreeController, feature_category: :source_code_manageme
end
end
end
context 'when is not authorized to edit the tree' do
before do
allow(controller).to receive(:can_collaborate_with_project?).and_return(false)
end
it 'renders a not_found error and template' do
create_dir
expect(response).to have_gitlab_http_status(:not_found)
expect(response).to render_template('errors/not_found')
end
end
end
end
end

View File

@ -0,0 +1,11 @@
# frozen_string_literal: true
FactoryBot.define do
factory :organization_user_detail, class: 'Organizations::OrganizationUserDetail' do
user
organization
sequence(:username) { |n| "user_alias_#{n}" }
display_name { username.humanize.titleize }
end
end

View File

@ -267,10 +267,6 @@ RSpec.describe 'GitLab Markdown', :aggregate_failures, feature_category: :markdo
expect(doc).to parse_emoji
end
aggregate_failures 'TableOfContentsLegacyFilter' do
expect(doc).to create_header_links
end
aggregate_failures 'TableOfContentsTagFilter' do
expect(doc).to create_toc
end
@ -390,10 +386,6 @@ RSpec.describe 'GitLab Markdown', :aggregate_failures, feature_category: :markdo
expect(doc).to parse_emoji
end
aggregate_failures 'TableOfContentsLegacyFilter' do
expect(doc).to create_header_links
end
aggregate_failures 'TableOfContentsTagFilter' do
expect(doc).to create_toc
end

View File

@ -1,189 +0,0 @@
TODO: This is now a legacy filter, and is only used with the Ruby parser.
The current markdown parser now properly handles multiline block quotes.
The Ruby parser is now only for benchmarking purposes.
issue: https://gitlab.com/gitlab-org/gitlab/-/issues/454601
Single `>>>` inside code block:
```
# Code
>>>
# Code
```
Double `>>>` inside code block:
```txt
# Code
>>>
# Code
>>>
# Code
```
Blockquote outside code block:
> Quote
Code block inside blockquote:
> Quote
>
> ```
> # Code
> ```
>
> Quote
Single `>>>` inside code block inside blockquote:
> Quote
>
> ```
> # Code
> >>>
> # Code
> ```
>
> Quote
Double `>>>` inside code block inside blockquote:
> Quote
>
> ```
> # Code
> >>>
> # Code
> >>>
> # Code
> ```
>
> Quote
Single `>>>` inside HTML:
<pre>
# Code
>>>
# Code
</pre>
Double `>>>` inside HTML:
<pre>
# Code
>>>
# Code
>>>
# Code
</pre>
Blockquote outside HTML:
> Quote
HTML inside blockquote:
> Quote
>
> <pre>
> # Code
> </pre>
>
> Quote
Single `>>>` inside HTML inside blockquote:
> Quote
>
> <pre>
> # Code
> >>>
> # Code
> </pre>
>
> Quote
Double `>>>` inside HTML inside blockquote:
> Quote
>
> <pre>
> # Code
> >>>
> # Code
> >>>
> # Code
> </pre>
>
> Quote
Blockquote inside an unordered list
- Item one
> Foo and
> bar
- Sub item
> Foo
Blockquote inside an ordered list
1. Item one
> Bar
1. Sub item
> Foo
Requires a leading blank line
>>>
Not a quote
>>>
Requires a trailing blank line
>>>
Not a quote
>>>
Lorem
Triple quoting is not our blockquote
>>> foo
>>> bar
>>>
> baz
> boo
>>> far
>>>
>>> faz

View File

@ -1,189 +0,0 @@
TODO: This is now a legacy filter, and is only used with the Ruby parser.
The current markdown parser now properly handles multiline block quotes.
The Ruby parser is now only for benchmarking purposes.
issue: https://gitlab.com/gitlab-org/gitlab/-/issues/454601
Single `>>>` inside code block:
```
# Code
>>>
# Code
```
Double `>>>` inside code block:
```txt
# Code
>>>
# Code
>>>
# Code
```
Blockquote outside code block:
>>>
Quote
>>>
Code block inside blockquote:
>>>
Quote
```
# Code
```
Quote
>>>
Single `>>>` inside code block inside blockquote:
>>>
Quote
```
# Code
>>>
# Code
```
Quote
>>>
Double `>>>` inside code block inside blockquote:
>>>
Quote
```
# Code
>>>
# Code
>>>
# Code
```
Quote
>>>
Single `>>>` inside HTML:
<pre>
# Code
>>>
# Code
</pre>
Double `>>>` inside HTML:
<pre>
# Code
>>>
# Code
>>>
# Code
</pre>
Blockquote outside HTML:
>>>
Quote
>>>
HTML inside blockquote:
>>>
Quote
<pre>
# Code
</pre>
Quote
>>>
Single `>>>` inside HTML inside blockquote:
>>>
Quote
<pre>
# Code
>>>
# Code
</pre>
Quote
>>>
Double `>>>` inside HTML inside blockquote:
>>>
Quote
<pre>
# Code
>>>
# Code
>>>
# Code
</pre>
Quote
>>>
Blockquote inside an unordered list
- Item one
>>>
Foo and
bar
>>>
- Sub item
>>>
Foo
>>>
Blockquote inside an ordered list
1. Item one
>>>
Bar
>>>
1. Sub item
>>>
Foo
>>>
Requires a leading blank line
>>>
Not a quote
>>>
Requires a trailing blank line
>>>
Not a quote
>>>
Lorem
Triple quoting is not our blockquote
>>> foo
>>> bar
>>>
> baz
> boo
>>> far
>>>
>>> faz

View File

@ -109,7 +109,7 @@ describe('OrganizationSwitcher', () => {
await waitForPromises();
expect(findDropdownItemByIndex(1).text()).toContain(firstOrganization.name);
expect(findDropdownItemByIndex(1).element.firstChild.getAttribute('href')).toBe(
expect(findDropdownItemByIndex(1).find('a').attributes('href')).toBe(
firstOrganization.webUrl,
);
expect(findDropdownItemByIndex(1).findComponent(GlAvatar).props()).toMatchObject({
@ -119,7 +119,7 @@ describe('OrganizationSwitcher', () => {
});
expect(findDropdownItemByIndex(2).text()).toContain(secondOrganization.name);
expect(findDropdownItemByIndex(2).element.firstChild.getAttribute('href')).toBe(
expect(findDropdownItemByIndex(2).find('a').attributes('href')).toBe(
secondOrganization.webUrl,
);
expect(findDropdownItemByIndex(2).findComponent(GlAvatar).props()).toMatchObject({

View File

@ -1,40 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
# TODO: This is now a legacy filter, and is only used with the Ruby parser.
# The current markdown parser now properly handles multiline block quotes.
# The Ruby parser is now only for benchmarking purposes.
# issue: https://gitlab.com/gitlab-org/gitlab/-/issues/454601
RSpec.describe Banzai::Filter::BlockquoteFenceLegacyFilter, feature_category: :markdown do
include FilterSpecHelper
let_it_be(:context) { { markdown_engine: Banzai::Filter::MarkdownFilter::CMARK_ENGINE } }
it 'converts blockquote fences to blockquote lines', :unlimited_max_formatted_output_length do
content = File.read(Rails.root.join('spec/fixtures/blockquote_fence_legacy_before.md'))
expected = File.read(Rails.root.join('spec/fixtures/blockquote_fence_legacy_after.md'))
output = filter(content, context)
expect(output).to eq(expected)
end
it 'does not require newlines at start or end of string' do
expect(filter(">>>\ntest\n>>>", context)).to eq("\n> test\n")
end
it 'allows trailing whitespace on blockquote fence lines' do
expect(filter(">>> \ntest\n>>> ", context)).to eq("\n> test\n")
end
context 'when incomplete blockquote fences with multiple blocks are present' do
it 'does not raise timeout error' do
test_string = ">>>#{"\n```\nfoo\n```" * 20}"
expect do
Timeout.timeout(BANZAI_FILTER_TIMEOUT_MAX) { filter(test_string, context) }
end.not_to raise_error
end
end
end

View File

@ -2,6 +2,8 @@
require 'spec_helper'
# note that extensive syntax test are performed in the parser,
# https://gitlab.com/gitlab-org/ruby/gems/gitlab-glfm-markdown/blob/main/spec/math_spec.rb
RSpec.describe Banzai::Filter::MathFilter, feature_category: :markdown do
include FilterSpecHelper
@ -94,232 +96,6 @@ RSpec.describe Banzai::Filter::MathFilter, feature_category: :markdown do
end
end
# TODO: This portion is legacy code, and is only used with the Ruby parser.
# The current markdown parser now properly handles math.
# The Ruby parser is now only for benchmarking purposes.
# issue: https://gitlab.com/gitlab-org/gitlab/-/issues/454601
#
# Note that the "extensive" syntax testing for the new math filter
# is now handled in https://gitlab.com/gitlab-org/ruby/gems/gitlab-glfm-markdown/-/blob/main/spec/math_spec.rb
describe 'legacy math filter' do
using RSpec::Parameterized::TableSyntax
let_it_be(:context) { { markdown_engine: Banzai::Filter::MarkdownFilter::CMARK_ENGINE } }
shared_examples 'inline math' do
it 'removes surrounding dollar signs and adds class code, math and js-render-math' do
doc = legacy_pipeline_filter(text, context)
expected = result_template.gsub('<math>', '<code data-math-style="inline" class="code math js-render-math">')
expected.gsub!('</math>', '</code>')
expect(doc.to_s).to eq expected
end
end
shared_examples 'display math' do
let_it_be(:template_prefix_with_pre) { '<pre data-canonical-lang="math" data-math-style="display" class="js-render-math"><code>' }
let_it_be(:template_prefix_with_code) { '<code data-math-style="display" class="code math js-render-math">' }
let(:use_pre_tags) { false }
it 'removes surrounding dollar signs and adds class code, math and js-render-math' do
doc = legacy_pipeline_filter(text, context)
template_prefix = use_pre_tags ? template_prefix_with_pre : template_prefix_with_code
template_suffix = "</code>#{'</pre>' if use_pre_tags}"
expected = result_template.gsub('<math>', template_prefix)
expected.gsub!('</math>', template_suffix)
expect(doc.to_s).to eq expected
end
end
describe 'inline math using $...$ syntax' do
context 'with valid syntax' do
where(:text, :result_template) do
'$2+2$' | '<p><math>2+2</math></p>'
'$22+1$ and $22 + a^2$' | '<p><math>22+1</math> and <math>22 + a^2</math></p>'
'$22 and $2+2$' | '<p>$22 and <math>2+2</math></p>'
'$2+2$ $22 and flightjs/Flight$22 $2+2$' | '<p><math>2+2</math> $22 and flightjs/Flight$22 <math>2+2</math></p>'
'$1/2$ &lt;b&gt;test&lt;/b&gt;' | '<p><math>1/2</math> &lt;b&gt;test&lt;/b&gt;</p>'
'$a!$' | '<p><math>a!</math></p>'
'$x$' | '<p><math>x</math></p>'
'$1+2\$$' | '<p><math>1+2\$</math></p>'
'$1+\$2$' | '<p><math>1+\$2</math></p>'
'$1+\%2$' | '<p><math>1+\%2</math></p>'
'$1+\#2$' | '<p><math>1+\#2</math></p>'
'$1+\&2$' | '<p><math>1+\&amp;2</math></p>'
'$1+\{2$' | '<p><math>1+\{2</math></p>'
'$1+\}2$' | '<p><math>1+\}2</math></p>'
'$1+\_2$' | '<p><math>1+\_2</math></p>'
end
with_them do
it_behaves_like 'inline math'
end
end
end
describe 'inline math using $`...`$ syntax' do
context 'with valid syntax' do
where(:text, :result_template) do
'$`2+2`$' | '<p><math>2+2</math></p>'
'$`22+1`$ and $`22 + a^2`$' | '<p><math>22+1</math> and <math>22 + a^2</math></p>'
'$22 and $`2+2`$' | '<p>$22 and <math>2+2</math></p>'
'$`2+2`$ $22 and flightjs/Flight$22 $`2+2`$' | '<p><math>2+2</math> $22 and flightjs/Flight$22 <math>2+2</math></p>'
'test $$`2+2`$$ test' | '<p>test $<math>2+2</math>$ test</p>'
'$`1+\$2`$' | '<p><math>1+\$2</math></p>'
end
with_them do
it_behaves_like 'inline math'
end
end
end
describe 'inline display math using $$...$$ syntax' do
context 'with valid syntax' do
where(:text, :result_template) do
'$$2+2$$' | '<p><math>2+2</math></p>'
'$$ 2+2 $$' | '<p><math>2+2</math></p>'
'$$22+1$$ and $$22 + a^2$$' | '<p><math>22+1</math> and <math>22 + a^2</math></p>'
'$22 and $$2+2$$' | '<p>$22 and <math>2+2</math></p>'
'$$2+2$$ $22 and flightjs/Flight$22 $$2+2$$' | '<p><math>2+2</math> $22 and flightjs/Flight$22 <math>2+2</math></p>'
'flightjs/Flight$22 and $$a^2 + b^2 = c^2$$' | '<p>flightjs/Flight$22 and <math>a^2 + b^2 = c^2</math></p>'
'$$a!$$' | '<p><math>a!</math></p>'
'$$x$$' | '<p><math>x</math></p>'
'$$20,000 and $$30,000' | '<p><math>20,000 and</math>30,000</p>'
end
with_them do
it_behaves_like 'display math'
end
end
end
describe 'block display math using $$\n...\n$$ syntax' do
context 'with valid syntax' do
where(:text, :result_template) do
"$$\n2+2\n$$" | "<math>2+2\n</math>"
"$$ \n2+2\n$$" | "<math>2+2\n</math>"
"$$\n2+2\n3+4\n$$" | "<math>2+2\n3+4\n</math>"
end
with_them do
it_behaves_like 'display math' do
let(:use_pre_tags) { true }
end
end
end
context 'when it spans multiple lines' do
let(:math) do
<<~MATH
\\begin{align*}
\\Delta t \\frac{d(b_i, a_i)}{c} + \\Delta t_{b_i}
\\end{align*}
MATH
end
let(:text) { "$$\n#{math}$$" }
let(:result_template) { "<math>#{math}</math>" }
it_behaves_like 'display math' do
let(:use_pre_tags) { true }
end
end
context 'when it contains \\' do
let(:math) do
<<~MATH
E = mc^2 \\\\
E = \\$mc^2
MATH
end
let(:text) { "$$\n#{math}$$" }
let(:result_template) { "<math>#{math}</math>" }
it_behaves_like 'display math' do
let(:use_pre_tags) { true }
end
end
end
describe 'display math using ```math...``` syntax' do
it 'adds data-math-style display attribute to display math' do
doc = legacy_pipeline_filter("```math\n2+2\n```", context)
pre = doc.xpath('descendant-or-self::pre').first
expect(pre['data-math-style']).to eq 'display'
end
it 'adds js-render-math class to display math' do
doc = legacy_pipeline_filter("```math\n2+2\n```", context)
pre = doc.xpath('descendant-or-self::pre').first
expect(pre[:class]).to include("js-render-math")
end
it 'ignores code blocks that are not math' do
input = "```plaintext\n2+2\n```"
doc = legacy_pipeline_filter(input, context)
expect(doc.to_s).to eq "<pre data-canonical-lang=\"plaintext\"><code>2+2\n</code></pre>"
end
it 'requires the pre to contain both code and math' do
input = '<pre data-canonical-lang="math">something</pre>'
doc = legacy_pipeline_filter(input, context)
expect(doc.to_s).to eq input
end
end
describe 'unrecognized syntax' do
where(:text, :result) do
'`2+2`' | '<p><code>2+2</code></p>'
'test $`2+2` test' | '<p>test $<code>2+2</code> test</p>'
'test `2+2`$ test' | '<p>test <code>2+2</code>$ test</p>'
'$20,000 and $30,000' | '<p>$20,000 and $30,000</p>'
'$20,000 in $USD' | '<p>$20,000 in $USD</p>'
'$ a^2 $' | '<p>$ a^2 $</p>'
"test $$\n2+2\n$$" | "<p>test $$\n2+2\n$$</p>"
"$\n$" | "<p>$\n$</p>"
'$$$' | '<p>$$$</p>'
'`$1+2$`' | '<p><code>$1+2$</code></p>'
'`$$1+2$$`' | '<p><code>$$1+2$$</code></p>'
'`$\$1+2$$`' | '<p><code>$\$1+2$$</code></p>'
end
with_them do
it 'is ignored' do
expect(legacy_pipeline_filter(text, context).to_s).to eq result
end
end
end
it 'handles multiple styles in one text block' do
doc = legacy_pipeline_filter('$`2+2`$ + $3+3$ + $$4+4$$', context)
expect(doc.search('.js-render-math').count).to eq(3)
expect(doc.search('[data-math-style="inline"]').count).to eq(2)
expect(doc.search('[data-math-style="display"]').count).to eq(1)
end
def legacy_pipeline_filter(text, context = {})
context = { project: nil, no_sourcepos: true }.merge(context)
doc = Banzai::Pipeline::PreProcessPipeline.call(text, {})
doc = Banzai::Pipeline::PlainMarkdownPipeline.call(doc[:output], context)
doc = Banzai::Filter::CodeLanguageFilter.call(doc[:output], context, nil)
doc = Banzai::Filter::SanitizationFilter.call(doc, context, nil)
doc = Banzai::Filter::SanitizeLinkFilter.call(doc, context, nil)
filter(doc, context)
end
end
it_behaves_like 'pipeline timing check'
def pipeline_filter(text, context = {})

View File

@ -1,173 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
# TODO: This is now a legacy filter, and is only used with the Ruby parser.
# The current markdown parser now properly handles adding anchors to headers.
# The Ruby parser is now only for benchmarking purposes.
# issue: https://gitlab.com/gitlab-org/gitlab/-/issues/454601
RSpec.describe Banzai::Filter::TableOfContentsLegacyFilter, feature_category: :markdown do
include FilterSpecHelper
def header(level, text)
"<h#{level}>#{text}</h#{level}>\n"
end
let_it_be(:context) { { markdown_engine: Banzai::Filter::MarkdownFilter::CMARK_ENGINE } }
it 'does nothing when :no_header_anchors is truthy' do
exp = act = header(1, 'Header')
expect(filter(act, context.merge({ no_header_anchors: 1 })).to_html).to eq exp
end
it 'does nothing with empty headers' do
exp = act = header(1, nil)
expect(filter(act, context).to_html).to eq exp
end
1.upto(6) do |i|
it "processes h#{i} elements" do
html = header(i, "Header #{i}")
doc = filter(html, context)
expect(doc.css("h#{i} a").first.attr('id')).to eq "user-content-header-#{i}"
end
end
describe 'anchor tag' do
it 'has an `anchor` class' do
doc = filter(header(1, 'Header'), context)
expect(doc.css('h1 a').first.attr('class')).to eq 'anchor'
end
it 'has a namespaced id' do
doc = filter(header(1, 'Header'), context)
expect(doc.css('h1 a').first.attr('id')).to eq 'user-content-header'
end
it 'links to the non-namespaced id' do
doc = filter(header(1, 'Header'), context)
expect(doc.css('h1 a').first.attr('href')).to eq '#header'
end
describe 'generated IDs' do
it 'translates spaces to dashes' do
doc = filter(header(1, 'This header has spaces in it'), context)
expect(doc.css('h1 a').first.attr('href')).to eq '#this-header-has-spaces-in-it'
end
it 'squeezes multiple spaces and dashes' do
doc = filter(header(1, 'This---header is poorly-formatted'), context)
expect(doc.css('h1 a').first.attr('href')).to eq '#this-header-is-poorly-formatted'
end
it 'removes punctuation' do
doc = filter(header(1, "This, header! is, filled. with @ punctuation?"), context)
expect(doc.css('h1 a').first.attr('href')).to eq '#this-header-is-filled-with-punctuation'
end
it 'removes any leading or trailing spaces' do
doc = filter(header(1, " \r\n\tTitle with spaces\r\n\t "), context)
expect(doc.css('h1 a').first.attr('href')).to eq '#title-with-spaces'
end
it 'appends a unique number to duplicates' do
doc = filter(header(1, 'One') + header(2, 'One'), context)
expect(doc.css('h1 a').first.attr('href')).to eq '#one'
expect(doc.css('h2 a').first.attr('href')).to eq '#one-1'
end
it 'prepends a prefix to digits-only ids' do
doc = filter(header(1, "123") + header(2, "1.0"), context)
expect(doc.css('h1 a').first.attr('href')).to eq '#anchor-123'
expect(doc.css('h2 a').first.attr('href')).to eq '#anchor-10'
end
it 'supports Unicode' do
doc = filter(header(1, '한글'), context)
expect(doc.css('h1 a').first.attr('id')).to eq 'user-content-한글'
# check that we encode the href to avoid issues with the
# ExternalLinkFilter (see https://gitlab.com/gitlab-org/gitlab/issues/26210)
expect(doc.css('h1 a').first.attr('href')).to eq "##{CGI.escape('한글')}"
end
it 'limits header href length with 255 characters' do
doc = filter(header(1, 'a' * 500), context)
expect(doc.css('h1 a').first.attr('href')).to eq "##{'a' * 255}"
end
end
end
describe 'result' do
def result(html)
HTML::Pipeline.new([described_class], context).call(html)
end
let(:results) { result(header(1, 'Header 1') + header(2, 'Header 2')) }
let(:doc) { Nokogiri::XML::DocumentFragment.parse(results[:toc]) }
it 'is contained within a `ul` element' do
expect(doc.children.first.name).to eq 'ul'
expect(doc.children.first.attr('class')).to eq 'section-nav'
end
it 'contains an `li` element for each header' do
expect(doc.css('li').length).to eq 2
links = doc.css('li a')
expect(links.first.attr('href')).to eq '#header-1'
expect(links.first.text).to eq 'Header 1'
expect(links.last.attr('href')).to eq '#header-2'
expect(links.last.text).to eq 'Header 2'
end
context 'when table of contents nesting' do
let(:results) do
result(
header(1, 'Header 1') +
header(2, 'Header 1-1') +
header(3, 'Header 1-1-1') +
header(2, 'Header 1-2') +
header(1, 'Header 2') +
header(2, 'Header 2-1')
)
end
it 'keeps list levels regarding header levels' do
items = doc.css('li')
# Header 1
expect(items[0].ancestors).to satisfy_none { |node| node.name == 'li' }
# Header 1-1
expect(items[1].ancestors).to include(items[0])
# Header 1-1-1
expect(items[2].ancestors).to include(items[0], items[1])
# Header 1-2
expect(items[3].ancestors).to include(items[0])
expect(items[3].ancestors).not_to include(items[1])
# Header 2
expect(items[4].ancestors).to satisfy_none { |node| node.name == 'li' }
# Header 2-1
expect(items[5].ancestors).to include(items[4])
end
end
context 'when header text contains escaped content' do
let(:content) { '&lt;img src="x" onerror="alert(42)"&gt;' }
let(:results) { result(header(1, content)) }
it 'outputs escaped content' do
expect(doc.inner_html).to include(content)
end
end
end
end

View File

@ -1,138 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
# TODO: This is now a legacy filter, and is only used with the Ruby parser.
# The Ruby parser is now only for benchmarking purposes.
# issue: https://gitlab.com/gitlab-org/gitlab/-/issues/454601
# rubocop:disable RSpec/ContextWording -- legacy code
# rubocop:disable Layout/LineLength -- legacy code
RSpec.describe Banzai::Filter::TableOfContentsTagLegacyFilter, feature_category: :markdown do
include FilterSpecHelper
let_it_be(:context) { { markdown_engine: Banzai::Filter::MarkdownFilter::CMARK_ENGINE } }
context 'table of contents' do
shared_examples 'table of contents tag' do
it 'replaces toc tag with ToC result' do
doc = filter(html, context, { toc: "FOO" })
expect(doc.to_html).to eq("FOO")
end
it 'handles an empty ToC result' do
doc = filter(html, context)
expect(doc.to_html).to eq ''
end
end
context '[[_TOC_]] as tag' do
it_behaves_like 'table of contents tag' do
let(:html) { '<p>[[<em>TOC</em>]]</p>' }
end
end
context '[[_toc_]] as tag' do
it_behaves_like 'table of contents tag' do
let(:html) { '<p>[[<em>toc</em>]]</p>' }
end
end
context '[TOC] as tag' do
it_behaves_like 'table of contents tag' do
let(:html) { '<p>[TOC]</p>' }
end
end
context '[toc] as tag' do
it_behaves_like 'table of contents tag' do
let(:html) { '<p>[toc]</p>' }
end
end
end
describe 'structure of a toc' do
def header(level, text)
"#{'#' * level} #{text}\n"
end
def result(html)
HTML::Pipeline.new([Banzai::Filter::MarkdownFilter, Banzai::Filter::TableOfContentsLegacyFilter, described_class]).call(html, context)
end
let(:results) { result("[toc]\n\n#{header(1, 'Header 1')}#{header(2, 'Header 2')}") }
let(:doc) { results[:output] }
it 'is contained within a `ul` element' do
expect(doc.children.first.name).to eq 'ul'
expect(doc.children.first.attr('class')).to eq 'section-nav'
end
it 'contains an `li` element for each header' do
expect(doc.css('li').length).to eq 2
links = doc.css('li a')
expect(links.first.attr('href')).to eq '#header-1'
expect(links.first.text).to eq 'Header 1'
expect(links.last.attr('href')).to eq '#header-2'
expect(links.last.text).to eq 'Header 2'
end
context 'table of contents nesting' do
let(:results) do
result(
<<~MARKDOWN
[toc]
#{header(1, 'Header 1')}
#{header(2, 'Header 1-1')}
#{header(3, 'Header 1-1-1')}
#{header(2, 'Header 1-2')}
#{header(1, 'Header 2')}
#{header(2, 'Header 2-1')}
#{header(2, 'Header 2-1b')}
MARKDOWN
)
end
it 'keeps list levels regarding header levels' do
items = doc.css('li')
# Header 1
expect(items[0].ancestors).to satisfy_none { |node| node.name == 'li' }
# Header 1-1
expect(items[1].ancestors).to include(items[0])
# Header 1-1-1
expect(items[2].ancestors).to include(items[0], items[1])
# Header 1-2
expect(items[3].ancestors).to include(items[0])
expect(items[3].ancestors).not_to include(items[1])
# Header 2
expect(items[4].ancestors).to satisfy_none { |node| node.name == 'li' }
# Header 2-1
expect(items[5].ancestors).to include(items[4])
# Header 2-1b
expect(items[6].ancestors).to include(items[4])
end
end
context 'header text contains escaped content' do
let(:content) { '&lt;img src="x" onerror="alert(42)"&gt;' }
let(:results) { result(header(1, content)) }
it 'outputs escaped content' do
expect(doc.inner_html).to include(content)
end
end
end
end
# rubocop:enable Layout/LineLength
# rubocop:enable RSpec/ContextWording

View File

@ -240,114 +240,6 @@ RSpec.describe Banzai::Pipeline::PlainMarkdownPipeline, feature_category: :markd
end
end
# TODO: This is legacy code, and is only used with the Ruby parser.
# The current markdown parser now handles adding data-escaped-char.
# The Ruby parser is now only for benchmarking purposes.
# issue: https://gitlab.com/gitlab-org/gitlab/-/issues/454601
describe 'legacy backslash handling', :aggregate_failures do
let_it_be(:project) { create(:project, :public) }
let_it_be(:issue) { create(:issue, project: project) }
let_it_be(:context) do
{
project: project,
no_sourcepos: true,
markdown_engine: Banzai::Filter::MarkdownFilter::CMARK_ENGINE
}
end
it 'converts all escapable punctuation to literals' do
markdown = Banzai::Filter::MarkdownPreEscapeLegacyFilter::ESCAPABLE_CHARS.pluck(:escaped).join
result = described_class.call(markdown, context)
output = result[:output].to_html
Banzai::Filter::MarkdownPreEscapeLegacyFilter::ESCAPABLE_CHARS.each do |item|
char = item[:char] == '&' ? '&amp;' : item[:char]
if item[:reference]
expect(output).to include("<span data-escaped-char>#{char}</span>")
else
expect(output).not_to include("<span data-escaped-char>#{char}</span>")
expect(output).to include(char)
end
end
expect(result[:escaped_literals]).to be_truthy
end
it 'ensure we handle all the GitLab reference characters', :eager_load do
reference_chars = ObjectSpace.each_object(Class).map do |klass|
next unless klass.included_modules.include?(Referable)
next unless klass.respond_to?(:reference_prefix)
next unless klass.reference_prefix.length == 1
klass.reference_prefix
end.compact
reference_chars.all? do |char|
Banzai::Filter::MarkdownPreEscapeLegacyFilter::TARGET_CHARS.include?(char)
end
end
it 'does not convert non-reference/latex punctuation to spans' do
markdown = %q(\"\'\*\+\,\-\.\/\:\;\<\=\>\?\[\]\`\|) + %q[\(\)\\\\]
result = described_class.call(markdown, context)
output = result[:output].to_html
expect(output).not_to include('<span')
expect(result[:escaped_literals]).to be_falsey
end
it 'does not convert other characters to literals' do
markdown = %q(\\A\a\ \3\φ\«)
expected = '\→\A\a\ \3\φ\«'
result = correct_html_included(markdown, expected, context)
expect(result[:escaped_literals]).to be_falsey
end
describe 'backslash escapes are untouched in code blocks, code spans, autolinks, or raw HTML' do
where(:markdown, :expected) do
%q(`` \@\! ``) | %q(<code>\@\!</code>)
%q( \@\!) | %(<code>\\@\\!\n</code>)
%(~~~\n\\@\\!\n~~~) | %(<code>\\@\\!\n</code>)
%q($1+\$2$) | %q(<code data-math-style="inline">1+\\$2</code>)
%q(<http://example.com?find=\@>) | %q(<a href="http://example.com?find=%5C@">http://example.com?find=\@</a>)
%q[<a href="/bar\@)">] | %q[<a href="/bar\@)">]
end
with_them do
it { correct_html_included(markdown, expected, context) }
end
end
describe 'work in all other contexts, including URLs and link titles, link references, and info strings in fenced code blocks' do
let(:markdown) { %(``` foo\\@bar\nfoo\n```) }
it 'renders correct html' do
correct_html_included(markdown, %(<pre lang="foo@bar"><code>foo\n</code></pre>), context)
end
where(:markdown, :expected) do
%q![foo](/bar\@ "\@title")! | %q(<a href="/bar@" title="@title">foo</a>)
%([foo]\n\n[foo]: /bar\\@ "\\@title") | %q(<a href="/bar@" title="@title">foo</a>)
end
with_them do
it { correct_html_included(markdown, expected, context) }
end
end
it 'does not have a polynomial regex' do
markdown = "x \\#\n\n#{'mliteralcmliteral-' * 450000}mliteral"
expect do
Timeout.timeout(BANZAI_FILTER_TIMEOUT_MAX) { described_class.to_html(markdown, project: project) }
end.not_to raise_error
end
end
def correct_html_included(markdown, expected, context = {})
result = described_class.call(markdown, context)

View File

@ -7,8 +7,8 @@ RSpec.describe Gitlab::Database::DynamicModelHelpers, feature_category: :databas
let(:table_name) { Project.table_name }
let(:connection) { Project.connection }
describe '#define_batchable_model' do
subject(:model) { including_class.new.define_batchable_model(table_name, connection: connection) }
describe '.define_batchable_model' do
subject(:model) { described_class.define_batchable_model(table_name, connection: connection) }
it 'is an ActiveRecord model' do
expect(model.ancestors).to include(ActiveRecord::Base)
@ -28,7 +28,7 @@ RSpec.describe Gitlab::Database::DynamicModelHelpers, feature_category: :databas
context 'for primary key' do
subject(:model) do
including_class.new.define_batchable_model(table_name, connection: connection, primary_key: primary_key)
described_class.define_batchable_model(table_name, connection: connection, primary_key: primary_key)
end
context 'when table primary key is a single column' do
@ -171,7 +171,7 @@ RSpec.describe Gitlab::Database::DynamicModelHelpers, feature_category: :databas
let(:namespace_settings2) { create(:namespace_settings) }
let(:table_name) { NamespaceSetting.table_name }
let(:connection) { NamespaceSetting.connection }
let(:primary_key) { subject.define_batchable_model(table_name, connection: connection).primary_key }
let(:primary_key) { described_class.define_batchable_model(table_name, connection: connection).primary_key }
it 'iterates table in batch ranges using the correct primary key' do
expect(primary_key).to eq("namespace_id") # Sanity check the primary key is not id

View File

@ -0,0 +1,259 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Database::MigrationHelpers::FeatureFlagMigratorHelpers, feature_category: :database do
include Database::TableSchemaHelpers
include Database::TriggerHelpers
include MigrationsHelpers
let(:feature_flag_name) { 'test_feature_flag' }
let_it_be(:application_settings) { table(:application_settings) }
let_it_be(:features) { table(:features) }
let_it_be(:feature_gates) { table(:feature_gates) }
let(:model) do
ActiveRecord::Migration.new.extend(described_class)
end
before do
allow(model).to receive(:puts)
end
shared_examples 'raises an error' do |argument_error_message|
it 'raises an ArgumentError' do
expect { subject }.to raise_error(ArgumentError, argument_error_message)
end
end
describe '#up_migrate_to_setting' do
using RSpec::Parameterized::TableSyntax
let(:setting_name) { :invisible_captcha_enabled }
let(:default_enabled) { false }
subject(:up_migrate_to_setting) do
model.up_migrate_to_setting(feature_flag_name:, setting_name:, default_enabled:)
end
where(:default_enabled, :feature_flag_setting, :expected_application_setting) do
true | nil | true
true | true | true
true | false | false
false | nil | false
false | true | true
false | false | false
end
with_them do
before do
application_settings.create!
features.create!(key: feature_flag_name) unless feature_flag_setting.nil?
feature_gates.create!(feature_key: feature_flag_name, key: 'boolean', value: 'true') if feature_flag_setting
end
it 'sets the expected value in the application settings column' do
up_migrate_to_setting
expect(application_settings.last.send(setting_name)).to eq(expected_application_setting)
end
end
context 'when feature_flag_name is nil' do
let(:feature_flag_name) { nil }
it_behaves_like 'raises an error', 'feature_flag_name, setting_name, and default_enabled are required'
end
context 'when setting_name is nil' do
let(:setting_name) { nil }
it_behaves_like 'raises an error', 'feature_flag_name, setting_name, and default_enabled are required'
end
context 'when default_enabled is nil' do
let(:default_enabled) { nil }
it_behaves_like 'raises an error', 'feature_flag_name, setting_name, and default_enabled are required'
end
context 'when default_enabled is not a boolean value' do
let(:default_enabled) { 1 }
it_behaves_like 'raises an error', 'default_enabled must be a boolean'
end
end
describe '#down_migrate_to_setting' do
using RSpec::Parameterized::TableSyntax
let(:default_enabled) { false }
let(:setting_name) { :invisible_captcha_enabled }
subject(:down_migrate_to_setting) do
model.down_migrate_to_setting(setting_name:, default_enabled:)
end
where(:default_enabled, :application_setting, :expected_application_setting) do
true | true | true
true | false | true
false | true | false
false | false | false
end
with_them do
before do
application_settings.create!(setting_name => application_setting)
end
it 'sets the expected value in the application settings column' do
down_migrate_to_setting
expect(application_settings.last.send(setting_name)).to eq(expected_application_setting)
end
end
context 'when setting_name is nil' do
let(:setting_name) { nil }
it_behaves_like 'raises an error', 'setting_name and default_enabled are required'
end
context 'when default_enabled is nil' do
let(:default_enabled) { nil }
it_behaves_like 'raises an error', 'setting_name and default_enabled are required'
end
context 'when default_enabled is not a boolean value' do
let(:default_enabled) { 1 }
it_behaves_like 'raises an error', 'default_enabled must be a boolean'
end
end
describe '#up_migrate_to_jsonb_setting' do
using RSpec::Parameterized::TableSyntax
let(:default_enabled) { true }
let(:jsonb_column_name) { :clickhouse }
let(:setting_name) { :use_clickhouse_for_analytics }
subject(:up_migrate_to_jsonb_setting) do
model.up_migrate_to_jsonb_setting(feature_flag_name:, setting_name:,
jsonb_column_name:, default_enabled:)
end
where(:default_enabled, :feature_flag_setting, :expected_application_setting) do
true | nil | true
true | true | true
true | false | false
false | nil | false
false | true | true
false | false | false
end
with_them do
before do
application_settings.create!
features.create!(key: feature_flag_name) unless feature_flag_setting.nil?
feature_gates.create!(feature_key: feature_flag_name, key: 'boolean', value: 'true') if feature_flag_setting
end
it 'sets the expected value in the application settings jsonb column' do
up_migrate_to_jsonb_setting
jsonb_column = application_settings.last.send(jsonb_column_name).with_indifferent_access
expect(jsonb_column[setting_name]).to eq(expected_application_setting)
end
end
context 'when feature_flag_name is nil' do
let(:feature_flag_name) { nil }
it_behaves_like 'raises an error',
'feature_flag_name, jsonb_column_name, setting_name, and default_enabled are required'
end
context 'when setting_name is nil' do
let(:setting_name) { nil }
it_behaves_like 'raises an error',
'feature_flag_name, jsonb_column_name, setting_name, and default_enabled are required'
end
context 'when jsonb_column_name is nil' do
let(:jsonb_column_name) { nil }
it_behaves_like 'raises an error',
'feature_flag_name, jsonb_column_name, setting_name, and default_enabled are required'
end
context 'when default_enabled is nil' do
let(:default_enabled) { nil }
it_behaves_like 'raises an error',
'feature_flag_name, jsonb_column_name, setting_name, and default_enabled are required'
end
context 'when default_enabled is not a boolean value' do
let(:default_enabled) { 1 }
it_behaves_like 'raises an error', 'default_enabled must be a boolean'
end
end
describe '#down_migrate_to_jsonb_setting' do
using RSpec::Parameterized::TableSyntax
let(:jsonb_column_name) { :clickhouse }
let(:feature_flag_setting) { true }
let(:setting_name) { :use_clickhouse_for_analytics }
subject(:down_migrate_to_jsonb_setting) do
model.down_migrate_to_jsonb_setting(setting_name:, jsonb_column_name:)
end
where(:default_enabled, :application_setting) do
true | true
true | false
false | true
false | false
end
with_them do
before do
application_settings.create!
features.create!(key: feature_flag_name)
feature_gates.create!(feature_key: feature_flag_name, key: 'boolean', value: 'true')
end
it 'sets the expected value in the application settings jsonb column' do
model.up_migrate_to_jsonb_setting(feature_flag_name:, setting_name:,
jsonb_column_name:, default_enabled:)
jsonb_column = application_settings.last.send(jsonb_column_name).with_indifferent_access
expect(jsonb_column[setting_name]).to eq(feature_flag_setting)
down_migrate_to_jsonb_setting
jsonb_column = application_settings.last.send(jsonb_column_name).with_indifferent_access
expect(jsonb_column[setting_name]).to be_nil
end
end
context 'when setting_name is nil' do
let(:setting_name) { nil }
it_behaves_like 'raises an error', 'setting_name and jsonb_column_name are required'
end
context 'when jsonb_column_name is nil' do
let(:jsonb_column_name) { nil }
it_behaves_like 'raises an error', 'setting_name and jsonb_column_name are required'
end
end
end

Some files were not shown because too many files have changed in this diff Show More