diff --git a/app/assets/javascripts/logo.js b/app/assets/javascripts/logo.js index ca3f1caec67..c76e44a196d 100644 --- a/app/assets/javascripts/logo.js +++ b/app/assets/javascripts/logo.js @@ -3,3 +3,20 @@ export default function initLogoAnimation() { document.querySelector('.tanuki-logo')?.classList.add('animate'); }); } + +export function initPortraitLogoDetection() { + const image = document.querySelector('.js-portrait-logo-detection'); + + image?.addEventListener( + 'load', + ({ currentTarget: img }) => { + const isPortrait = img.height > img.width; + if (isPortrait) { + // Limit the width when the logo has portrait format + img.classList.replace('gl-h-9', 'gl-w-10'); + } + img.classList.remove('gl-visibility-hidden'); + }, + { once: true }, + ); +} diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index e4ff5e55f5c..c3914391a49 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -22,7 +22,7 @@ import { getLocationHash, visitUrl, mergeUrlParams } from './lib/utils/url_utili // everything else import LazyLoader from './lazy_loader'; -import initLogoAnimation from './logo'; +import initLogoAnimation, { initPortraitLogoDetection } from './logo'; import initBreadcrumbs from './breadcrumb'; import initPersistentUserCallouts from './persistent_user_callouts'; import { initUserTracking, initDefaultTrackers } from './tracking'; @@ -83,6 +83,7 @@ function deferredInitialisation() { initBreadcrumbs(); initPrefetchLinks('.js-prefetch-document'); initLogoAnimation(); + initPortraitLogoDetection(); initUserPopovers(); initBroadcastNotifications(); initPersistentUserCallouts(); diff --git a/app/helpers/appearances_helper.rb b/app/helpers/appearances_helper.rb index 531ea08791c..07a5e711d1c 100644 --- a/app/helpers/appearances_helper.rb +++ b/app/helpers/appearances_helper.rb @@ -44,7 +44,7 @@ module AppearancesHelper end def brand_image - image_tag(brand_image_path, alt: brand_title, class: 'gl-w-10') + image_tag(brand_image_path, alt: brand_title, class: 'gl-visibility-hidden gl-h-9 js-portrait-logo-detection') end def brand_image_path diff --git a/app/policies/merge_request_policy.rb b/app/policies/merge_request_policy.rb index 090be645b21..49f9225a1d3 100644 --- a/app/policies/merge_request_policy.rb +++ b/app/policies/merge_request_policy.rb @@ -16,10 +16,6 @@ class MergeRequestPolicy < IssuablePolicy prevent :accept_merge_request end - rule { can?(:read_merge_request) }.policy do - enable :generate_diff_summary - end - rule { can_approve }.policy do enable :approve_merge_request end @@ -47,10 +43,6 @@ class MergeRequestPolicy < IssuablePolicy enable :set_merge_request_metadata end - rule { llm_bot }.policy do - enable :generate_diff_summary - end - private def can_approve? diff --git a/app/services/groups/transfer_service.rb b/app/services/groups/transfer_service.rb index 664c26debb8..79557dae14a 100644 --- a/app/services/groups/transfer_service.rb +++ b/app/services/groups/transfer_service.rb @@ -65,7 +65,6 @@ module Groups ) do Group.transaction do update_group_attributes - remove_paid_features_for_projects(old_root_ancestor_id) ensure_ownership update_integrations remove_issue_contacts(old_root_ancestor_id, was_root_group) @@ -74,6 +73,7 @@ module Groups end end + remove_paid_features_for_projects(old_root_ancestor_id) post_update_hooks(@updated_project_ids, old_root_ancestor_id) propagate_integrations update_pending_builds diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb index 30d9e1922cc..49648216808 100644 --- a/app/services/projects/transfer_service.rb +++ b/app/services/projects/transfer_service.rb @@ -131,8 +131,6 @@ module Projects update_integrations - remove_paid_features - project.old_path_with_namespace = @old_path update_repository_configuration(@new_path) @@ -143,6 +141,7 @@ module Projects end end + remove_paid_features update_pending_builds post_update_hooks(project, @old_group) diff --git a/db/docs/audit_events_streaming_http_instance_namespace_filters.yml b/db/docs/audit_events_streaming_http_instance_namespace_filters.yml new file mode 100644 index 00000000000..9dc7d05a315 --- /dev/null +++ b/db/docs/audit_events_streaming_http_instance_namespace_filters.yml @@ -0,0 +1,12 @@ +--- +table_name: audit_events_streaming_http_instance_namespace_filters +classes: + - AuditEvents::Streaming::HTTP::Instance::NamespaceFilter +feature_categories: + - audit_events +description: Represents a group or project filter for instance-level custom http external audit event destinations. +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/136959 +milestone: '16.7' +gitlab_schema: gitlab_main_cell +sharding_key: + namespace_id: namespaces diff --git a/db/docs/batched_background_migrations/backfill_branch_protection_namespace_setting.yml b/db/docs/batched_background_migrations/backfill_branch_protection_namespace_setting.yml new file mode 100644 index 00000000000..9a596cb056e --- /dev/null +++ b/db/docs/batched_background_migrations/backfill_branch_protection_namespace_setting.yml @@ -0,0 +1,9 @@ +--- +migration_job_name: BackfillBranchProtectionNamespaceSetting +description: This migration back fills column default_branch_protection_defaults of namespace settings table +feature_category: source_code_management +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/136181 +milestone: '16.7' +queued_migration_version: 20231107092912 +finalize_after: '2024-01-23' +finalized_by: # version of the migration that ensured this bbm diff --git a/db/migrate/20231115064007_create_audit_events_streaming_http_instance_namespace_filters.rb b/db/migrate/20231115064007_create_audit_events_streaming_http_instance_namespace_filters.rb new file mode 100644 index 00000000000..e5ca560635b --- /dev/null +++ b/db/migrate/20231115064007_create_audit_events_streaming_http_instance_namespace_filters.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +class CreateAuditEventsStreamingHttpInstanceNamespaceFilters < Gitlab::Database::Migration[2.2] + milestone '16.7' + enable_lock_retries! + + UNIQ_DESTINATION_INDEX_NAME = 'unique_audit_events_instance_namespace_filters_destination_id' + NAMESPACE_INDEX_NAME = 'index_audit_events_instance_namespace_filters_on_namespace_id' + + def change + create_table :audit_events_streaming_http_instance_namespace_filters do |t| + t.timestamps_with_timezone null: false + t.bigint :audit_events_instance_external_audit_event_destination_id, + null: false, + index: { unique: true, name: UNIQ_DESTINATION_INDEX_NAME } + t.bigint :namespace_id, + null: false, + index: { name: NAMESPACE_INDEX_NAME } + end + end +end diff --git a/db/migrate/20231116115237_add_destination_fk_to_audit_events_http_instance_namespace_filters.rb b/db/migrate/20231116115237_add_destination_fk_to_audit_events_http_instance_namespace_filters.rb new file mode 100644 index 00000000000..dab72766f0e --- /dev/null +++ b/db/migrate/20231116115237_add_destination_fk_to_audit_events_http_instance_namespace_filters.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +class AddDestinationFkToAuditEventsHttpInstanceNamespaceFilters < Gitlab::Database::Migration[2.2] + milestone '16.7' + + disable_ddl_transaction! + + def up + add_concurrent_foreign_key :audit_events_streaming_http_instance_namespace_filters, + :audit_events_instance_external_audit_event_destinations, + column: :audit_events_instance_external_audit_event_destination_id, + on_delete: :cascade + end + + def down + with_lock_retries do + remove_foreign_key_if_exists :audit_events_streaming_http_instance_namespace_filters, + column: :audit_events_instance_external_audit_event_destination_id + end + end +end diff --git a/db/migrate/20231116115303_add_namespace_fk_to_audit_events_http_instance_namespace_filters.rb b/db/migrate/20231116115303_add_namespace_fk_to_audit_events_http_instance_namespace_filters.rb new file mode 100644 index 00000000000..375a2a3aa05 --- /dev/null +++ b/db/migrate/20231116115303_add_namespace_fk_to_audit_events_http_instance_namespace_filters.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +class AddNamespaceFkToAuditEventsHttpInstanceNamespaceFilters < Gitlab::Database::Migration[2.2] + milestone '16.7' + + disable_ddl_transaction! + + def up + add_concurrent_foreign_key :audit_events_streaming_http_instance_namespace_filters, + :namespaces, + column: :namespace_id, + on_delete: :cascade + end + + def down + with_lock_retries do + remove_foreign_key_if_exists :audit_events_streaming_http_instance_namespace_filters, + column: :namespace_id + end + end +end diff --git a/db/post_migrate/20231107092912_queue_backfill_branch_protection_namespace_setting.rb b/db/post_migrate/20231107092912_queue_backfill_branch_protection_namespace_setting.rb new file mode 100644 index 00000000000..28af287c75b --- /dev/null +++ b/db/post_migrate/20231107092912_queue_backfill_branch_protection_namespace_setting.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +class QueueBackfillBranchProtectionNamespaceSetting < Gitlab::Database::Migration[2.2] + milestone "16.7" + MIGRATION = "BackfillBranchProtectionNamespaceSetting" + DELAY_INTERVAL = 2.minutes + BATCH_SIZE = 10_000 + SUB_BATCH_SIZE = 100 + + disable_ddl_transaction! + restrict_gitlab_migration gitlab_schema: :gitlab_main + + def up + queue_batched_background_migration( + MIGRATION, + :namespace_settings, + :namespace_id, + job_interval: DELAY_INTERVAL, + batch_size: BATCH_SIZE, + sub_batch_size: SUB_BATCH_SIZE + ) + end + + def down + delete_batched_background_migration(MIGRATION, :namespace_settings, :namespace_id, []) + end +end diff --git a/db/schema_migrations/20231107092912 b/db/schema_migrations/20231107092912 new file mode 100644 index 00000000000..baef0aa2147 --- /dev/null +++ b/db/schema_migrations/20231107092912 @@ -0,0 +1 @@ +f5bd273a05caa2cc662dc430cb2661321602e608054d6ef3f45dfc8cfacec152 \ No newline at end of file diff --git a/db/schema_migrations/20231115064007 b/db/schema_migrations/20231115064007 new file mode 100644 index 00000000000..4a4cf7356cf --- /dev/null +++ b/db/schema_migrations/20231115064007 @@ -0,0 +1 @@ +3f8ea307440353cc193662c9c552609d09f7a58dd1d6acccb7208803f394329c \ No newline at end of file diff --git a/db/schema_migrations/20231116115237 b/db/schema_migrations/20231116115237 new file mode 100644 index 00000000000..cfad779dc60 --- /dev/null +++ b/db/schema_migrations/20231116115237 @@ -0,0 +1 @@ +fce9aa43da310dbbcd7d0175f531ab069548d5cc782104ab8c83f20874cc3644 \ No newline at end of file diff --git a/db/schema_migrations/20231116115303 b/db/schema_migrations/20231116115303 new file mode 100644 index 00000000000..72afbdebbf8 --- /dev/null +++ b/db/schema_migrations/20231116115303 @@ -0,0 +1 @@ +d5d599b967346e13f0cbdd909c33669d8a3268efc7744ca5749fab69e60ff606 \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index c008eae969b..72a8bb9ced3 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -12913,6 +12913,23 @@ CREATE SEQUENCE audit_events_streaming_http_group_namespace_filters_id_seq ALTER SEQUENCE audit_events_streaming_http_group_namespace_filters_id_seq OWNED BY audit_events_streaming_http_group_namespace_filters.id; +CREATE TABLE audit_events_streaming_http_instance_namespace_filters ( + id bigint NOT NULL, + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone NOT NULL, + audit_events_instance_external_audit_event_destination_id bigint NOT NULL, + namespace_id bigint NOT NULL +); + +CREATE SEQUENCE audit_events_streaming_http_instance_namespace_filters_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +ALTER SEQUENCE audit_events_streaming_http_instance_namespace_filters_id_seq OWNED BY audit_events_streaming_http_instance_namespace_filters.id; + CREATE TABLE audit_events_streaming_instance_event_type_filters ( id bigint NOT NULL, created_at timestamp with time zone NOT NULL, @@ -26354,6 +26371,8 @@ ALTER TABLE ONLY audit_events_streaming_headers ALTER COLUMN id SET DEFAULT next ALTER TABLE ONLY audit_events_streaming_http_group_namespace_filters ALTER COLUMN id SET DEFAULT nextval('audit_events_streaming_http_group_namespace_filters_id_seq'::regclass); +ALTER TABLE ONLY audit_events_streaming_http_instance_namespace_filters ALTER COLUMN id SET DEFAULT nextval('audit_events_streaming_http_instance_namespace_filters_id_seq'::regclass); + ALTER TABLE ONLY audit_events_streaming_instance_event_type_filters ALTER COLUMN id SET DEFAULT nextval('audit_events_streaming_instance_event_type_filters_id_seq'::regclass); ALTER TABLE ONLY authentication_events ALTER COLUMN id SET DEFAULT nextval('authentication_events_id_seq'::regclass); @@ -28212,6 +28231,9 @@ ALTER TABLE ONLY audit_events_streaming_headers ALTER TABLE ONLY audit_events_streaming_http_group_namespace_filters ADD CONSTRAINT audit_events_streaming_http_group_namespace_filters_pkey PRIMARY KEY (id); +ALTER TABLE ONLY audit_events_streaming_http_instance_namespace_filters + ADD CONSTRAINT audit_events_streaming_http_instance_namespace_filters_pkey PRIMARY KEY (id); + ALTER TABLE ONLY audit_events_streaming_instance_event_type_filters ADD CONSTRAINT audit_events_streaming_instance_event_type_filters_pkey PRIMARY KEY (id); @@ -31867,6 +31889,8 @@ CREATE UNIQUE INDEX index_atlassian_identities_on_extern_uid ON atlassian_identi CREATE UNIQUE INDEX index_audit_events_external_audit_on_verification_token ON audit_events_external_audit_event_destinations USING btree (verification_token); +CREATE INDEX index_audit_events_instance_namespace_filters_on_namespace_id ON audit_events_streaming_http_instance_namespace_filters USING btree (namespace_id); + CREATE INDEX index_audit_events_on_entity_id_and_entity_type_and_created_at ON ONLY audit_events USING btree (entity_id, entity_type, created_at, id); CREATE INDEX index_authentication_events_on_provider ON authentication_events USING btree (provider); @@ -35421,6 +35445,8 @@ CREATE UNIQUE INDEX unique_audit_events_group_namespace_filters_destination_id O CREATE UNIQUE INDEX unique_audit_events_group_namespace_filters_namespace_id ON audit_events_streaming_http_group_namespace_filters USING btree (namespace_id); +CREATE UNIQUE INDEX unique_audit_events_instance_namespace_filters_destination_id ON audit_events_streaming_http_instance_namespace_filters USING btree (audit_events_instance_external_audit_event_destination_id); + CREATE UNIQUE INDEX unique_batched_background_migrations_queued_migration_version ON batched_background_migrations USING btree (queued_migration_version); CREATE UNIQUE INDEX unique_ci_builds_token_encrypted_and_partition_id ON ci_builds USING btree (token_encrypted, partition_id) WHERE (token_encrypted IS NOT NULL); @@ -37321,6 +37347,9 @@ ALTER TABLE ONLY users_star_projects ALTER TABLE ONLY alert_management_alerts ADD CONSTRAINT fk_2358b75436 FOREIGN KEY (issue_id) REFERENCES issues(id) ON DELETE SET NULL; +ALTER TABLE ONLY audit_events_streaming_http_instance_namespace_filters + ADD CONSTRAINT fk_23f3ab7df0 FOREIGN KEY (namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE; + ALTER TABLE ONLY import_failures ADD CONSTRAINT fk_24b824da43 FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE; @@ -37906,6 +37935,9 @@ ALTER TABLE ONLY identities ALTER TABLE ONLY boards ADD CONSTRAINT fk_ab0a250ff6 FOREIGN KEY (iteration_cadence_id) REFERENCES iterations_cadences(id) ON DELETE CASCADE; +ALTER TABLE ONLY audit_events_streaming_http_instance_namespace_filters + ADD CONSTRAINT fk_abe44125bc FOREIGN KEY (audit_events_instance_external_audit_event_destination_id) REFERENCES audit_events_instance_external_audit_event_destinations(id) ON DELETE CASCADE; + ALTER TABLE ONLY merge_requests ADD CONSTRAINT fk_ad525e1f87 FOREIGN KEY (merge_user_id) REFERENCES users(id) ON DELETE SET NULL; diff --git a/gems/csv_builder/lib/csv_builder/builder.rb b/gems/csv_builder/lib/csv_builder/builder.rb index ff7e51cf7ce..51aaf2132cf 100644 --- a/gems/csv_builder/lib/csv_builder/builder.rb +++ b/gems/csv_builder/lib/csv_builder/builder.rb @@ -78,10 +78,10 @@ module CsvBuilder def row(object) attributes.map do |attribute| - if object.is_a?(Hash) - excel_sanitize(object[attribute]) - elsif attribute.respond_to?(:call) + if attribute.respond_to?(:call) excel_sanitize(attribute.call(object)) + elsif object.is_a?(Hash) + excel_sanitize(object[attribute]) else excel_sanitize(object.public_send(attribute)) # rubocop:disable GitlabSecurity/PublicSend end diff --git a/gems/csv_builder/spec/csv_builder_spec.rb b/gems/csv_builder/spec/csv_builder_spec.rb index 9d6283b3985..ccae8365174 100644 --- a/gems/csv_builder/spec/csv_builder_spec.rb +++ b/gems/csv_builder/spec/csv_builder_spec.rb @@ -1,18 +1,19 @@ # frozen_string_literal: true RSpec.describe CsvBuilder do - let(:object) { double(question: :answer) } let(:csv_data) { subject.render } + let(:header_to_value_hash) do + { 'Q & A' => :question, 'Reversed' => ->(o) { o.question.to_s.reverse } } + end let(:subject) do - described_class.new( - enumerable, 'Q & A' => :question, 'Reversed' => ->(o) { o.question.to_s.reverse }) + described_class.new(enumerable, **header_to_value_hash) end shared_examples 'csv builder examples' do let(:items) { [object] } - it "has a version number" do + it 'has a version number' do expect(CsvBuilder::Version::VERSION).not_to be nil end @@ -45,30 +46,6 @@ RSpec.describe CsvBuilder do end end - describe 'truncation' do - let(:big_object) { double(question: 'Long' * 1024) } - let(:row_size) { big_object.question.length * 2 } - let(:items) { [big_object, big_object, big_object] } - - it 'occurs after given number of bytes' do - expect(subject.render(row_size * 2).length).to be_between(row_size * 2, row_size * 3) - expect(subject).to be_truncated - expect(subject.rows_written).to eq 2 - end - - it 'is ignored by default' do - expect(subject.render.length).to be > row_size * 3 - expect(subject.rows_written).to eq 3 - end - - it 'causes rows_expected to fall back to .count' do - subject.render(0) - - expect(enumerable).to receive(:count).and_call_original - expect(subject.rows_expected).to eq 3 - end - end - it 'avoids loading all data in a single query' do expect(enumerable).to receive(:find_each) @@ -83,42 +60,61 @@ RSpec.describe CsvBuilder do expect(csv_data).to include 'answer' end - it 'allows lamdas to look up more complicated data' do + it 'allows lambdas to look up more complicated data' do expect(csv_data).to include 'rewsna' end + end - describe 'excel sanitization' do - let(:dangerous_title) { double(title: "=cmd|' /C calc'!A0 title", description: "*safe_desc") } - let(:dangerous_desc) { double(title: "*safe_title", description: "=cmd|' /C calc'!A0 desc") } - let(:items) { [dangerous_title, dangerous_desc] } - let(:subject) { described_class.new(enumerable, 'Title' => 'title', 'Description' => 'description') } - let(:csv_data) { subject.render } + shared_examples 'csv builder with truncation ability' do + let(:items) { [big_object, big_object, big_object] } + let(:row_size) { question_value.length * 2 } - it 'sanitizes dangerous characters at the beginning of a column' do - expect(csv_data).to include "'=cmd|' /C calc'!A0 title" - expect(csv_data).to include "'=cmd|' /C calc'!A0 desc" - end + it 'occurs after given number of bytes' do + expect(subject.render(row_size * 2).length).to be_between(row_size * 2, row_size * 3) + expect(subject).to be_truncated + expect(subject.rows_written).to eq 2 + end - it 'does not sanitize safe symbols at the beginning of a column' do - expect(csv_data).not_to include "'*safe_desc" - expect(csv_data).not_to include "'*safe_title" - end + it 'is ignored by default' do + expect(subject.render.length).to be > row_size * 3 + expect(subject.rows_written).to eq 3 + end - context 'when dangerous characters are after a line break' do - let(:items) { [double(title: "Safe title", description: "With task list\n-[x] todo 1")] } + it 'causes rows_expected to fall back to .count' do + subject.render(0) - it 'does not append single quote to description' do - builder = described_class.new(enumerable, 'Title' => 'title', 'Description' => 'description') + expect(enumerable).to receive(:count).and_call_original + expect(subject.rows_expected).to eq 3 + end + end - csv_data = builder.render + shared_examples 'excel sanitization' do + let(:dangerous_title) { double(title: "=cmd|' /C calc'!A0 title", description: "*safe_desc") } + let(:dangerous_desc) { double(title: "*safe_title", description: "=cmd|' /C calc'!A0 desc") } + let(:header_to_value_hash) { { 'Title' => 'title', 'Description' => 'description' } } + let(:items) { [dangerous_title, dangerous_desc] } - expect(csv_data).to eq("Title,Description\nSafe title,\"With task list\n-[x] todo 1\"\n") - end + it 'sanitizes dangerous characters at the beginning of a column' do + expect(csv_data).to include "'=cmd|' /C calc'!A0 title" + expect(csv_data).to include "'=cmd|' /C calc'!A0 desc" + end + + it 'does not sanitize safe symbols at the beginning of a column' do + expect(csv_data).not_to include "'*safe_desc" + expect(csv_data).not_to include "'*safe_title" + end + + context 'when dangerous characters are after a line break' do + let(:items) { [double(title: "Safe title", description: "With task list\n-[x] todo 1")] } + + it 'does not append single quote to description' do + expect(csv_data).to eq("Title,Description\nSafe title,\"With task list\n-[x] todo 1\"\n") end end end context 'when ActiveRecord::Relation like object is given' do + let(:object) { double(question: :answer) } let(:enumerable) { described_class::FakeRelation.new(items) } before do @@ -132,11 +128,43 @@ RSpec.describe CsvBuilder do end it_behaves_like 'csv builder examples' + it_behaves_like 'excel sanitization' + it_behaves_like 'csv builder with truncation ability' do + let(:big_object) { double(question: 'Long' * 1024) } + let(:question_value) { big_object.question } + end end context 'when Enumerable like object is given' do + let(:object) { double(question: :answer) } let(:enumerable) { items } it_behaves_like 'csv builder examples' + it_behaves_like 'excel sanitization' + it_behaves_like 'csv builder with truncation ability' do + let(:big_object) { double(question: 'Long' * 1024) } + let(:question_value) { big_object.question } + end + end + + context 'when Hash object is given' do + let(:object) { { question: :answer } } + let(:enumerable) { items } + let(:header_to_value_hash) do + { 'Q & A' => :question, 'Reversed' => ->(o) { o[:question].to_s.reverse } } + end + + it_behaves_like 'csv builder examples' + + it_behaves_like 'excel sanitization' do + let(:dangerous_title) { { title: "=cmd|' /C calc'!A0 title", description: "*safe_desc" } } + let(:dangerous_desc) { { title: "*safe_title", description: "=cmd|' /C calc'!A0 desc" } } + let(:header_to_value_hash) { { 'Title' => :title, 'Description' => :description } } + end + + it_behaves_like 'csv builder with truncation ability' do + let(:big_object) { { question: 'Long' * 1024 } } + let(:question_value) { big_object[:question] } + end end end diff --git a/lib/gitlab/background_migration/backfill_branch_protection_namespace_setting.rb b/lib/gitlab/background_migration/backfill_branch_protection_namespace_setting.rb new file mode 100644 index 00000000000..c063a990188 --- /dev/null +++ b/lib/gitlab/background_migration/backfill_branch_protection_namespace_setting.rb @@ -0,0 +1,135 @@ +# frozen_string_literal: true + +module Gitlab + module BackgroundMigration + # This class is used to update the default_branch_protection_defaults column + # for user namespaces of the namespace_settings table. + class BackfillBranchProtectionNamespaceSetting < BatchedMigrationJob + operation_name :set_default_branch_protection_defaults + feature_category :source_code_management + + # Migration only version of `namespaces` table + class Namespace < ::ApplicationRecord + self.table_name = 'namespaces' + self.inheritance_column = :_type_disabled + + has_one :namespace_setting, + class_name: '::Gitlab::BackgroundMigration::BackfillDefaultBranchProtectionNamespaceSetting::NamespaceSetting' + end + + # Migration only version of `namespace_settings` table + class NamespaceSetting < ::ApplicationRecord + self.table_name = 'namespace_settings' + belongs_to :namespace, + class_name: '::Gitlab::BackgroundMigration::BackfillDefaultBranchProtectionNamespaceSetting::Namespace' + end + + # Migration only version of Gitlab::Access:BranchProtection application code. + class BranchProtection + attr_reader :level + + def initialize(level) + @level = level + end + + PROTECTION_NONE = 0 + PROTECTION_DEV_CAN_PUSH = 1 + PROTECTION_FULL = 2 + PROTECTION_DEV_CAN_MERGE = 3 + PROTECTION_DEV_CAN_INITIAL_PUSH = 4 + + DEVELOPER = 30 + MAINTAINER = 40 + + def to_hash + case level + when PROTECTION_NONE + self.class.protection_none + when PROTECTION_DEV_CAN_PUSH + self.class.protection_partial + when PROTECTION_FULL + self.class.protected_fully + when PROTECTION_DEV_CAN_MERGE + self.class.protected_against_developer_pushes + when PROTECTION_DEV_CAN_INITIAL_PUSH + self.class.protected_after_initial_push + end + end + + class << self + def protection_none + { + allowed_to_push: [{ 'access_level' => Gitlab::Access::DEVELOPER }], + allowed_to_merge: [{ 'access_level' => Gitlab::Access::DEVELOPER }], + allow_force_push: true + } + end + + def protection_partial + { + allowed_to_push: [{ 'access_level' => Gitlab::Access::DEVELOPER }], + allowed_to_merge: [{ 'access_level' => Gitlab::Access::MAINTAINER }], + allow_force_push: false + } + end + + def protected_fully + { + allowed_to_push: [{ 'access_level' => Gitlab::Access::MAINTAINER }], + allowed_to_merge: [{ 'access_level' => Gitlab::Access::MAINTAINER }], + allow_force_push: false + } + end + + def protected_against_developer_pushes + { + allowed_to_push: [{ 'access_level' => Gitlab::Access::MAINTAINER }], + allowed_to_merge: [{ 'access_level' => Gitlab::Access::DEVELOPER }], + allow_force_push: false + } + end + + def protected_after_initial_push + { + allowed_to_push: [{ 'access_level' => Gitlab::Access::MAINTAINER }], + allowed_to_merge: [{ 'access_level' => Gitlab::Access::MAINTAINER }], + allow_force_push: false, + developer_can_initial_push: true + } + end + end + end + + def perform + each_sub_batch do |sub_batch| + update_default_protection_branch_defaults(sub_batch) + end + end + + private + + def update_default_protection_branch_defaults(batch) + namespace_settings = NamespaceSetting.where(namespace_id: batch.pluck(:namespace_id)).includes(:namespace) + + values_list = namespace_settings.map do |namespace_setting| + level = namespace_setting.namespace.default_branch_protection.to_i + value = BranchProtection.new(level).to_hash.to_json + "(#{namespace_setting.namespace_id}, '#{value}'::jsonb)" + end.join(", ") + + sql = <<~SQL + WITH new_values (namespace_id, default_branch_protection_defaults) AS ( + VALUES + #{values_list} + ) + UPDATE namespace_settings + SET default_branch_protection_defaults = new_values.default_branch_protection_defaults + FROM new_values + WHERE namespace_settings.namespace_id = new_values.namespace_id; + SQL + + connection.execute(sql) + end + end + end +end diff --git a/qa/qa/specs/features/api/1_manage/import/import_large_github_repo_spec.rb b/qa/qa/specs/features/api/1_manage/import/import_large_github_repo_spec.rb index 2ce613d991d..e5c617738f0 100644 --- a/qa/qa/specs/features/api/1_manage/import/import_large_github_repo_spec.rb +++ b/qa/qa/specs/features/api/1_manage/import/import_large_github_repo_spec.rb @@ -646,7 +646,8 @@ module QA def events(comments, label_events, state_events, milestone_events) mapped_label_events = label_events.map { |event| event_mapping["label_#{event[:action]}"] } mapped_milestone_events = milestone_events.map { |event| event_mapping["milestone_#{event[:action]}"] } - mapped_state_event = state_events.map { |event| event[:state] } + # merged events are fetched through comments so duplicates need to be removed + mapped_state_event = state_events.map { |event| event[:state] }.reject { |state| state == "merged" } mapped_comment_events = comments.map do |c| event_mapping[c[:body].match(event_pattern)&.named_captures&.fetch("event", nil)] end diff --git a/spec/features/users/login_spec.rb b/spec/features/users/login_spec.rb index 0f086af227c..63b4d6f7155 100644 --- a/spec/features/users/login_spec.rb +++ b/spec/features/users/login_spec.rb @@ -785,6 +785,13 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions, feature_category: :system_ ensure_no_tabs end + it 'renders logo', :js do + visit new_user_session_path + + image = find('img.js-portrait-logo-detection') + expect(image['class']).to include('gl-h-9') + end + it 'renders link to sign up path' do visit new_user_session_path diff --git a/spec/frontend/logo_spec.js b/spec/frontend/logo_spec.js new file mode 100644 index 00000000000..8e39e75bd3b --- /dev/null +++ b/spec/frontend/logo_spec.js @@ -0,0 +1,55 @@ +import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; +import { initPortraitLogoDetection } from '~/logo'; + +describe('initPortraitLogoDetection', () => { + let img; + + const loadImage = () => { + const loadEvent = new Event('load'); + img.dispatchEvent(loadEvent); + }; + + beforeEach(() => { + setHTMLFixture(''); + initPortraitLogoDetection(); + img = document.querySelector('img'); + }); + + afterEach(() => { + resetHTMLFixture(); + }); + + describe('when logo does not have portrait format', () => { + beforeEach(() => { + img.height = 10; + img.width = 10; + }); + + it('removes gl-visibility-hidden', () => { + expect(img.classList).toContain('gl-visibility-hidden'); + expect(img.classList).toContain('gl-h-9'); + + loadImage(); + + expect(img.classList).not.toContain('gl-visibility-hidden'); + expect(img.classList).toContain('gl-h-9'); + }); + }); + + describe('when logo has portrait format', () => { + beforeEach(() => { + img.height = 11; + img.width = 10; + }); + + it('removes gl-visibility-hidden', () => { + expect(img.classList).toContain('gl-visibility-hidden'); + expect(img.classList).toContain('gl-h-9'); + + loadImage(); + + expect(img.classList).not.toContain('gl-visibility-hidden'); + expect(img.classList).toContain('gl-w-10'); + }); + }); +}); diff --git a/spec/lib/gitlab/background_migration/backfill_branch_protection_namespace_setting_spec.rb b/spec/lib/gitlab/background_migration/backfill_branch_protection_namespace_setting_spec.rb new file mode 100644 index 00000000000..d985e7fae61 --- /dev/null +++ b/spec/lib/gitlab/background_migration/backfill_branch_protection_namespace_setting_spec.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BackgroundMigration::BackfillBranchProtectionNamespaceSetting, + feature_category: :source_code_management do + let(:namespaces_table) { table(:namespaces) } + let(:namespace_settings_table) { table(:namespace_settings) } + let(:group_namespace) do + namespaces_table.create!(name: 'group_namespace', path: 'path-1', type: 'Group', default_branch_protection: 0) + end + + let(:user_namespace) do + namespaces_table.create!(name: 'user_namespace', path: 'path-2', type: 'User', default_branch_protection: 1) + end + + let(:user_three_namespace) do + namespaces_table.create!(name: 'user_three_namespace', path: 'path-3', type: 'User', default_branch_protection: 2) + end + + let(:group_four_namespace) do + namespaces_table.create!(name: 'group_four_namespace', path: 'path-4', type: 'Group', default_branch_protection: 3) + end + + let(:group_five_namespace) do + namespaces_table.create!(name: 'group_five_namespace', path: 'path-5', type: 'Group', default_branch_protection: 4) + end + + let(:start_id) { group_namespace.id } + let(:end_id) { group_five_namespace.id } + + subject(:perform_migration) do + described_class.new( + start_id: start_id, + end_id: end_id, + batch_table: :namespace_settings, + batch_column: :namespace_id, + sub_batch_size: 2, + pause_ms: 0, + connection: ActiveRecord::Base.connection + ).perform + end + + before do + namespace_settings_table.create!(namespace_id: group_namespace.id, default_branch_protection_defaults: {}) + namespace_settings_table.create!(namespace_id: user_namespace.id, default_branch_protection_defaults: {}) + namespace_settings_table.create!(namespace_id: user_three_namespace.id, default_branch_protection_defaults: {}) + namespace_settings_table.create!(namespace_id: group_four_namespace.id, default_branch_protection_defaults: {}) + namespace_settings_table.create!(namespace_id: group_five_namespace.id, default_branch_protection_defaults: {}) + end + + it 'updates default_branch_protection_defaults to a correct value', :aggregate_failures do + expect(ActiveRecord::QueryRecorder.new { perform_migration }.count).to eq(16) + + expect(migrated_attribute(group_namespace.id)).to eq({ "allow_force_push" => true, + "allowed_to_merge" => [{ "access_level" => 30 }], + "allowed_to_push" => [{ "access_level" => 30 }] }) + expect(migrated_attribute(user_namespace.id)).to eq({ "allow_force_push" => false, + "allowed_to_merge" => [{ "access_level" => 40 }], + "allowed_to_push" => [{ "access_level" => 30 }] }) + expect(migrated_attribute(user_three_namespace.id)).to eq({ "allow_force_push" => false, + "allowed_to_merge" => [{ "access_level" => 40 }], + "allowed_to_push" => [{ "access_level" => 40 }] }) + expect(migrated_attribute(group_four_namespace.id)).to eq({ "allow_force_push" => false, + "allowed_to_merge" => [{ "access_level" => 30 }], + "allowed_to_push" => [{ "access_level" => 40 }] }) + expect(migrated_attribute(group_five_namespace.id)).to eq({ "allow_force_push" => false, + "allowed_to_merge" => [{ "access_level" => 40 }], + "allowed_to_push" => [{ "access_level" => 40 }], + "developer_can_initial_push" => true }) + end + + def migrated_attribute(namespace_id) + namespace_settings_table.find(namespace_id).default_branch_protection_defaults + end +end diff --git a/spec/migrations/20231107092912_queue_backfill_branch_protection_namespace_setting_spec.rb b/spec/migrations/20231107092912_queue_backfill_branch_protection_namespace_setting_spec.rb new file mode 100644 index 00000000000..ddf4fb1e1c4 --- /dev/null +++ b/spec/migrations/20231107092912_queue_backfill_branch_protection_namespace_setting_spec.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_migration! + +RSpec.describe QueueBackfillBranchProtectionNamespaceSetting, feature_category: :database do + let!(:batched_migration) { described_class::MIGRATION } + + it 'schedules a new batched migration' do + reversible_migration do |migration| + migration.before -> { + expect(batched_migration).not_to have_scheduled_batched_migration + } + + migration.after -> { + expect(batched_migration).to have_scheduled_batched_migration( + table_name: :namespace_settings, + column_name: :namespace_id, + interval: described_class::DELAY_INTERVAL, + batch_size: described_class::BATCH_SIZE, + sub_batch_size: described_class::SUB_BATCH_SIZE + ) + } + end + end +end diff --git a/spec/policies/merge_request_policy_spec.rb b/spec/policies/merge_request_policy_spec.rb index 285f52956eb..c21e1244402 100644 --- a/spec/policies/merge_request_policy_spec.rb +++ b/spec/policies/merge_request_policy_spec.rb @@ -462,37 +462,6 @@ RSpec.describe MergeRequestPolicy do end end - context 'when enabling generate diff summary permission' do - let_it_be(:project) { create(:project) } - let_it_be(:mr) { create(:merge_request, target_project: project, source_project: project) } - let_it_be(:user) { create(:user) } - let(:policy) { permissions(user, mr) } - - context 'when can read_merge_request' do - before do - project.add_developer(user) - end - - it 'allows to generate_diff_summary' do - expect(policy).to be_allowed(:generate_diff_summary) - end - end - - context 'when can not read_merge_request' do - it 'does not allow to generate_diff_summary' do - expect(policy).not_to be_allowed(:generate_diff_summary) - end - - context 'and when is the LLM bot' do - let(:user) { create(:user, :llm_bot) } - - it 'allows to generate_diff_summary' do - expect(policy).to be_allowed(:generate_diff_summary) - end - end - end - end - context 'when the author of the merge request is banned', feature_category: :insider_threat do let_it_be(:user) { create(:user) } let_it_be(:admin) { create(:user, :admin) }