Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
d49e08600d
commit
42d6e66197
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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? &&
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1 @@
|
|||
9d67ff936bcd2fda808ec22526afe619d39d1ad41d7a6421a0050e78f5c74457
|
||||
|
|
@ -0,0 +1 @@
|
|||
1da7143e770d4b4445f30b661b10aa8d3fb843eaa4766976985041fcdf71700a
|
||||
|
|
@ -0,0 +1 @@
|
|||
f81dbd3fb12060afecb19f95ea8cd76f6ec53497e897ad7a39bf3d162aa36f7b
|
||||
|
|
@ -0,0 +1 @@
|
|||
cc9c1e182efb8afc072a82ce49b0e4515789dd384e82dd2d820590b55b5efea4
|
||||
|
|
@ -0,0 +1 @@
|
|||
c0e8007aa6a154bc17d7fe8058d54f5b8a38b9467679f574ac6fb62a55167111
|
||||
|
|
@ -0,0 +1 @@
|
|||
fb163edb227bb09ea1690cd538e421d067cbc45b7e39d5c7651a362d60e2962b
|
||||
|
|
@ -0,0 +1 @@
|
|||
6a7a412a9b0824743768a13a387d10264d9b88315789bbc348bf5e4420399d5b
|
||||
|
|
@ -0,0 +1 @@
|
|||
bacbe37017b21a651fe8efe665823d41670b3bb2b8536f7c57e45b5fbeceacba
|
||||
|
|
@ -0,0 +1 @@
|
|||
7e9ab7b8e146394480d42b6509c1bd195f7845d907e28cd53c1b9019da2d4790
|
||||
|
|
@ -0,0 +1 @@
|
|||
19c07a8e30917f22a6414cb0dc55ba72b40e98742d748a21efbb86af3d1b640b
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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 | |
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 >}}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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 >}}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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). |
|
||||
|
|
|
|||
|
|
@ -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). |
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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`:
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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**.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -5,7 +5,6 @@ module Banzai
|
|||
class BroadcastMessagePipeline < DescriptionPipeline
|
||||
def self.filters
|
||||
@filters ||= FilterArray[
|
||||
Filter::BlockquoteFenceLegacyFilter,
|
||||
Filter::MarkdownFilter,
|
||||
Filter::BroadcastMessageSanitizationFilter,
|
||||
Filter::SanitizeLinkFilter,
|
||||
|
|
|
|||
|
|
@ -29,8 +29,6 @@ module Banzai
|
|||
Filter::AttributesFilter,
|
||||
Filter::VideoLinkFilter,
|
||||
Filter::AudioLinkFilter,
|
||||
Filter::TableOfContentsLegacyFilter,
|
||||
Filter::TableOfContentsTagLegacyFilter,
|
||||
Filter::TableOfContentsTagFilter,
|
||||
Filter::AutolinkFilter,
|
||||
Filter::SuggestionFilter,
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ module Banzai
|
|||
class NotePipeline < FullPipeline
|
||||
def self.transform_context(context)
|
||||
super(context).merge(
|
||||
# TableOfContentsLegacyFilter
|
||||
no_header_anchors: true
|
||||
)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ module Banzai
|
|||
class QuickActionPipeline < BasePipeline
|
||||
def self.filters
|
||||
FilterArray[
|
||||
Filter::BlockquoteFenceLegacyFilter,
|
||||
Filter::MarkdownFilter,
|
||||
Filter::QuickActionFilter
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
|
|
|||
|
|
@ -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: ', ')
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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 ""
|
||||
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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$ <b>test</b>' | '<p><math>1/2</math> <b>test</b></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+\&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 = {})
|
||||
|
|
|
|||
|
|
@ -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) { '<img src="x" onerror="alert(42)">' }
|
||||
let(:results) { result(header(1, content)) }
|
||||
|
||||
it 'outputs escaped content' do
|
||||
expect(doc.inner_html).to include(content)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -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) { '<img src="x" onerror="alert(42)">' }
|
||||
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
|
||||
|
|
@ -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] == '&' ? '&' : 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! | %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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in New Issue