diff --git a/.gitlab/ci/qa-common/variables.gitlab-ci.yml b/.gitlab/ci/qa-common/variables.gitlab-ci.yml
index b5a74e30fe2..dd93eb4f180 100644
--- a/.gitlab/ci/qa-common/variables.gitlab-ci.yml
+++ b/.gitlab/ci/qa-common/variables.gitlab-ci.yml
@@ -16,4 +16,4 @@ variables:
QA_OMNIBUS_MR_TESTS: "only-smoke"
# Retry failed specs in separate process
QA_RETRY_FAILED_SPECS: "true"
- GITLAB_HELM_CHART_REF: "ec4eb83b98572fd2721516df00799858512b0538" # helm chart ref used by test-on-cng pipeline
+ GITLAB_HELM_CHART_REF: "8590da829037a326808e425488919d374ed36cd2" # helm chart ref used by test-on-cng pipeline
diff --git a/app/assets/javascripts/admin/abuse_reports/components/abuse_report_row.vue b/app/assets/javascripts/admin/abuse_reports/components/abuse_report_row.vue
index 74f4fb53a09..42800eb3e1a 100644
--- a/app/assets/javascripts/admin/abuse_reports/components/abuse_report_row.vue
+++ b/app/assets/javascripts/admin/abuse_reports/components/abuse_report_row.vue
@@ -67,7 +67,7 @@ export default {
{{ title }}
@@ -86,7 +86,7 @@ export default {
- {{ displayDate }}
+ {{ displayDate }}
diff --git a/app/assets/javascripts/content_editor/services/table_of_contents_utils.js b/app/assets/javascripts/content_editor/services/table_of_contents_utils.js
index 49737610635..455f053f528 100644
--- a/app/assets/javascripts/content_editor/services/table_of_contents_utils.js
+++ b/app/assets/javascripts/content_editor/services/table_of_contents_utils.js
@@ -2,16 +2,17 @@ class TOCHeading {
parent = null;
subHeadings = [];
- constructor(text) {
+ constructor(text, href) {
this.text = text;
+ this.href = href;
}
get level() {
return this.parent ? this.parent.level + 1 : 0;
}
- addSubHeading(text) {
- const heading = new TOCHeading(text);
+ addSubHeading(text, href) {
+ const heading = new TOCHeading(text, href);
heading.parent = this;
this.subHeadings.push(heading);
return heading;
@@ -46,6 +47,7 @@ class TOCHeading {
toJSON() {
return {
text: this.text,
+ href: this.href,
level: this.level,
subHeadings: this.subHeadings.map((subHeading) => subHeading.toJSON()),
};
@@ -76,7 +78,7 @@ export function toTree(headings) {
if (heading.level <= currentHeading.level) {
currentHeading = currentHeading.parentAt(heading.level - 1);
}
- currentHeading = (currentHeading || tree).addSubHeading(heading.text);
+ currentHeading = (currentHeading || tree).addSubHeading(heading.text, heading.href);
}
return tree.flattenIfEmpty().toJSON();
@@ -98,3 +100,15 @@ export function getHeadings(editor) {
return toTree(headings).subHeadings;
}
+
+export function getHeadingsFromDOM(containerElement) {
+ const headingSelectors = 'h1, h2, h3, h4, h5, h6';
+
+ return toTree(
+ [...containerElement.querySelectorAll(headingSelectors)].map((heading) => ({
+ level: parseInt(heading.tagName[1], 10),
+ text: heading.textContent.trim(),
+ href: heading.querySelector('a').getAttribute('href'),
+ })),
+ ).subHeadings;
+}
diff --git a/app/assets/javascripts/pages/shared/wikis/components/table_of_contents.vue b/app/assets/javascripts/pages/shared/wikis/components/table_of_contents.vue
new file mode 100644
index 00000000000..7b3d7608b7a
--- /dev/null
+++ b/app/assets/javascripts/pages/shared/wikis/components/table_of_contents.vue
@@ -0,0 +1,30 @@
+
+
+
+
{{ __('On this page') }}
+
+
+
diff --git a/app/assets/javascripts/pages/shared/wikis/components/table_of_contents_heading.vue b/app/assets/javascripts/pages/shared/wikis/components/table_of_contents_heading.vue
new file mode 100644
index 00000000000..16f7d974a9c
--- /dev/null
+++ b/app/assets/javascripts/pages/shared/wikis/components/table_of_contents_heading.vue
@@ -0,0 +1,26 @@
+
+
+
+
+ {{ heading.text }}
+
+
+
+
+
diff --git a/app/assets/javascripts/pages/shared/wikis/components/wiki_content.vue b/app/assets/javascripts/pages/shared/wikis/components/wiki_content.vue
index 99a64409960..fb5e3daa534 100644
--- a/app/assets/javascripts/pages/shared/wikis/components/wiki_content.vue
+++ b/app/assets/javascripts/pages/shared/wikis/components/wiki_content.vue
@@ -1,4 +1,5 @@
@@ -42,7 +52,7 @@ export default {
:work-item-type="workItemType"
:show-labels="showLabels"
@removeChild="$emit('removeChild', $event)"
- @click="$emit('click', Object.assign($event, { childItem: child }))"
+ @click="onClick($event, child)"
/>
diff --git a/app/assets/stylesheets/components/upload_dropzone/upload_dropzone.scss b/app/assets/stylesheets/components/upload_dropzone/upload_dropzone.scss
index d857d1f4f1c..ea16c38803b 100644
--- a/app/assets/stylesheets/components/upload_dropzone/upload_dropzone.scss
+++ b/app/assets/stylesheets/components/upload_dropzone/upload_dropzone.scss
@@ -18,8 +18,8 @@
}
.upload-dropzone-card {
- transition: background $gl-transition-duration-medium $general-hover-transition-curve,
- border $gl-transition-duration-medium $general-hover-transition-curve;
+ @apply gl-transition-[background,border];
+
color: $gl-text-color;
&:hover,
@@ -53,7 +53,9 @@
.upload-dropzone-fade-enter-active,
.upload-dropzone-fade-leave-active {
- transition: opacity $general-hover-transition-duration $general-hover-transition-curve;
+ @apply gl-transition-opacity;
+ @apply gl-duration-fast;
+ @apply gl-ease-linear;
}
.upload-dropzone-fade-enter,
diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss
index 0ee43eaffca..57b450b83dc 100644
--- a/app/assets/stylesheets/framework/markdown_area.scss
+++ b/app/assets/stylesheets/framework/markdown_area.scss
@@ -105,7 +105,7 @@
box-shadow: none;
width: 100%;
resize: none !important;
- transition: box-shadow $gl-transition-duration-medium ease;
+ @apply gl-transition-box-shadow;
}
.md-suggestion-diff {
diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss
index 0e55a98ed9c..002c5d3e077 100644
--- a/app/assets/stylesheets/framework/mixins.scss
+++ b/app/assets/stylesheets/framework/mixins.scss
@@ -435,7 +435,7 @@
}
@mixin side-panel-toggle {
- transition: width $gl-transition-duration-medium;
+ @apply gl-transition-width;
height: $toggle-sidebar-height;
padding: 0 $gl-padding;
background-color: $gray-10;
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index 3eaf5fc5f9f..db3c26f2691 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -17,7 +17,7 @@
}
.page-initialised .content-wrapper {
- transition: padding $gl-transition-duration-medium;
+ @apply gl-transition-padding;
}
.right-sidebar-collapsed {
@@ -134,7 +134,7 @@
@include maintain-sidebar-dimensions;
width: 0;
padding: 0;
- transition: width $gl-transition-duration-medium;
+ @apply gl-transition-width;
&.right-sidebar-expanded {
@include maintain-sidebar-dimensions;
@@ -321,10 +321,10 @@
position: fixed;
bottom: calc(#{$calc-application-footer-height} + var(--mr-review-bar-height));
right: 0;
- transition: width $gl-transition-duration-medium;
background-color: $white;
z-index: 200;
overflow: hidden;
+ @apply gl-transition-width;
}
.right-sidebar {
diff --git a/app/assets/stylesheets/framework/super_sidebar.scss b/app/assets/stylesheets/framework/super_sidebar.scss
index 5fbc060a00e..3554aa6e613 100644
--- a/app/assets/stylesheets/framework/super_sidebar.scss
+++ b/app/assets/stylesheets/framework/super_sidebar.scss
@@ -1,5 +1,5 @@
$super-sidebar-transition-delay: 0.4s;
-$super-sidebar-transition-duration: $gl-transition-duration-medium;
+$super-sidebar-transition-duration: 0.2s;
$super-sidebar-transition-hint-duration: $super-sidebar-transition-duration / 4;
$command-palette-spacing: px-to-rem(14px);
@@ -335,7 +335,7 @@ $command-palette-spacing: px-to-rem(14px);
}
@media (prefers-reduced-motion: no-preference) {
- transition: transform $super-sidebar-transition-duration;
+ @apply gl-transition-transform;
}
.user-bar {
@@ -523,7 +523,7 @@ $command-palette-spacing: px-to-rem(14px);
padding-left: 0;
@media (prefers-reduced-motion: no-preference) {
- transition: padding-left $super-sidebar-transition-duration;
+ @apply gl-transition-padding;
}
&:not(.page-with-super-sidebar-collapsed) {
@@ -661,7 +661,9 @@ $command-palette-spacing: px-to-rem(14px);
.transition-opacity-on-hover--context {
.transition-opacity-on-hover--target {
- transition: opacity $gl-transition-duration-fast linear;
+ @apply gl-transition-opacity;
+ @apply gl-duration-fast;
+ @apply gl-ease-linear;
&:hover {
transition-delay: $super-sidebar-transition-delay;
diff --git a/app/assets/stylesheets/framework/top_bar.scss b/app/assets/stylesheets/framework/top_bar.scss
index c6f2f434887..b8423613399 100644
--- a/app/assets/stylesheets/framework/top_bar.scss
+++ b/app/assets/stylesheets/framework/top_bar.scss
@@ -17,8 +17,6 @@
}
@media (prefers-reduced-motion: no-preference) {
- transition: left $gl-transition-duration-medium,
- right $gl-transition-duration-medium,
- width $gl-transition-duration-medium;
+ @apply gl-transition-[width,left,right];
}
}
diff --git a/app/assets/stylesheets/framework/vue_transitions.scss b/app/assets/stylesheets/framework/vue_transitions.scss
index e3ac615234c..a9acb823cf3 100644
--- a/app/assets/stylesheets/framework/vue_transitions.scss
+++ b/app/assets/stylesheets/framework/vue_transitions.scss
@@ -2,7 +2,7 @@
.fade-leave-active,
.fade-in-enter-active,
.fade-out-leave-active {
- transition: opacity $gl-transition-duration-medium $general-hover-transition-curve;
+ @apply gl-transition-opacity;
}
.fade-enter,
diff --git a/app/assets/stylesheets/page_bundles/boards.scss b/app/assets/stylesheets/page_bundles/boards.scss
index e240ba7cf47..242e96a2fa2 100644
--- a/app/assets/stylesheets/page_bundles/boards.scss
+++ b/app/assets/stylesheets/page_bundles/boards.scss
@@ -11,7 +11,7 @@
flex-direction: column;
@include media-breakpoint-up(sm) {
- transition: width $gl-transition-duration-medium;
+ @apply gl-transition-width;
width: 100%;
&.is-compact {
diff --git a/app/assets/stylesheets/page_bundles/design_management.scss b/app/assets/stylesheets/page_bundles/design_management.scss
index 9645a7ab5ea..ac2a06f1408 100644
--- a/app/assets/stylesheets/page_bundles/design_management.scss
+++ b/app/assets/stylesheets/page_bundles/design_management.scss
@@ -182,9 +182,9 @@ $t-gray-a-16-design-pin: rgba($black, 0.16);
.design-note {
padding: $gl-padding-8;
list-style: none;
- transition: background $gl-transition-duration-medium $general-hover-transition-curve;
border-top-left-radius: $gl-border-radius-base; // same border radius used by .bordered-box
border-top-right-radius: $gl-border-radius-base;
+ @apply gl-transition-background;
video {
width: 100%;
diff --git a/app/assets/stylesheets/page_bundles/merge_requests.scss b/app/assets/stylesheets/page_bundles/merge_requests.scss
index a4a5e5d865a..c9f315a1113 100644
--- a/app/assets/stylesheets/page_bundles/merge_requests.scss
+++ b/app/assets/stylesheets/page_bundles/merge_requests.scss
@@ -999,7 +999,7 @@
padding-right: $right-sidebar-collapsed-width;
background: var(--white, $white);
border-top: 1px solid var(--border-color, $border-color);
- transition: padding $gl-transition-duration-medium;
+ @apply gl-transition-padding;
@media (max-width: map-get($grid-breakpoints, sm)-1) {
padding-left: 0;
diff --git a/app/assets/stylesheets/page_bundles/wiki.scss b/app/assets/stylesheets/page_bundles/wiki.scss
index f1c30ba45f1..309ee921e63 100644
--- a/app/assets/stylesheets/page_bundles/wiki.scss
+++ b/app/assets/stylesheets/page_bundles/wiki.scss
@@ -19,7 +19,6 @@
}
.sidebar-container {
- padding: 20px 0;
padding-right: 100px;
height: 100%;
overflow-y: scroll;
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 6ae7b46d978..65ff426abc0 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -14,17 +14,6 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord
ignore_columns %i[instance_administration_project_id instance_administrators_group_id], remove_with: '16.2', remove_after: '2023-06-22'
ignore_columns %i[repository_storages], remove_with: '16.8', remove_after: '2023-12-21'
ignore_column :required_instance_ci_template, remove_with: '17.1', remove_after: '2024-05-10'
- ignore_columns %i[
- container_registry_import_max_tags_count
- container_registry_import_max_retries
- container_registry_import_start_max_retries
- container_registry_import_max_step_duration
- container_registry_pre_import_tags_rate
- container_registry_pre_import_timeout
- container_registry_import_timeout
- container_registry_import_target_plan
- container_registry_import_created_before
- ], remove_with: '17.2', remove_after: '2024-06-24'
ignore_column %i[sign_in_text help_text], remove_with: '17.3', remove_after: '2024-08-15'
ignore_columns %i[toggle_security_policies_policy_scope lock_toggle_security_policies_policy_scope], remove_with: '17.2', remove_after: '2024-07-12'
ignore_columns %i[arkose_labs_verify_api_url], remove_with: '17.4', remove_after: '2024-08-09'
diff --git a/app/models/import/source_user_placeholder_reference.rb b/app/models/import/source_user_placeholder_reference.rb
new file mode 100644
index 00000000000..66c6647e3d1
--- /dev/null
+++ b/app/models/import/source_user_placeholder_reference.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module Import
+ class SourceUserPlaceholderReference < ApplicationRecord
+ self.table_name = 'import_source_user_placeholder_references'
+
+ belongs_to :source_user, class_name: 'Import::SourceUser'
+ belongs_to :namespace
+
+ validates :model, :namespace_id, :source_user_id, :user_reference_column, presence: true
+ validates :numeric_key, numericality: { only_integer: true, greater_than: 0 }, allow_nil: true
+ validates :composite_key,
+ json_schema: { filename: 'import_source_user_placeholder_reference_composite_key' },
+ allow_nil: true
+ validate :validate_numeric_or_composite_key_present
+
+ attribute :composite_key, :ind_jsonb
+
+ private
+
+ def validate_numeric_or_composite_key_present
+ return if numeric_key.present? ^ composite_key.present?
+
+ errors.add(:base, :blank, message: 'numeric_key or composite_key must be present')
+ end
+ end
+end
diff --git a/app/models/project.rb b/app/models/project.rb
index def7d6151c0..ecb59cb7e7b 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -342,6 +342,8 @@ class Project < ApplicationRecord
has_many :hooks, class_name: 'ProjectHook'
has_many :protected_branches
has_many :exported_protected_branches
+ has_many :all_protected_branches, ->(project) { ProtectedBranch.unscope(:where).from_union(project.protected_branches, project.group_protected_branches) }, class_name: 'ProtectedBranch'
+
has_many :protected_tags
has_many :repository_languages, -> { order "share DESC" }
has_many :designs, inverse_of: :project, class_name: 'DesignManagement::Design'
@@ -1278,8 +1280,8 @@ class Project < ApplicationRecord
def preload_protected_branches
ActiveRecord::Associations::Preloader.new(
- records: [self],
- associations: { protected_branches: [:push_access_levels, :merge_access_levels] }
+ records: [all_protected_branches, protected_branches].flatten,
+ associations: [:push_access_levels, :merge_access_levels]
).call
end
@@ -2948,15 +2950,9 @@ class Project < ApplicationRecord
end
def group_protected_branches
- root_namespace.is_a?(Group) ? root_namespace.protected_branches : ProtectedBranch.none
- end
+ return root_namespace.protected_branches if allow_protected_branches_for_group? && root_namespace.is_a?(Group)
- def all_protected_branches
- if allow_protected_branches_for_group?
- @all_protected_branches ||= ProtectedBranch.from_union([protected_branches, group_protected_branches])
- else
- protected_branches
- end
+ ProtectedBranch.none
end
def allow_protected_branches_for_group?
diff --git a/app/validators/json_schemas/import_source_user_placeholder_reference_composite_key.json b/app/validators/json_schemas/import_source_user_placeholder_reference_composite_key.json
new file mode 100644
index 00000000000..ccfbe0ec527
--- /dev/null
+++ b/app/validators/json_schemas/import_source_user_placeholder_reference_composite_key.json
@@ -0,0 +1,19 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "description": "Stores composite_key data for imported records that are mapped to placeholder users",
+ "type": "object",
+ "minProperties": 1,
+ "patternProperties": {
+ ".*": {
+ "oneOf": [
+ {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ {
+ "type": "integer"
+ }
+ ]
+ }
+ }
+}
diff --git a/app/views/admin/abuse_reports/index.html.haml b/app/views/admin/abuse_reports/index.html.haml
index 485c9044366..45c4ae80f71 100644
--- a/app/views/admin/abuse_reports/index.html.haml
+++ b/app/views/admin/abuse_reports/index.html.haml
@@ -1,7 +1,6 @@
-- page_title _('Abuse Reports')
+- page_title _('Abuse reports')
-%h1.page-title.gl-font-size-h-display{ data: { event_tracking_load: 'true', event_tracking: 'view_admin_abuse_reports_pageload' } }
- = _('Abuse Reports')
+= render ::Layouts::PageHeadingComponent.new(_('Abuse reports'), options: { data: { event_tracking_load: 'true', event_tracking: 'view_admin_abuse_reports_pageload' } })
#js-abuse-reports-list-app{ data: abuse_reports_list_data(@abuse_reports) }
= gl_loading_icon(css_class: 'gl-my-5', size: 'md')
diff --git a/app/views/admin/abuse_reports/show.html.haml b/app/views/admin/abuse_reports/show.html.haml
index ff9ac6a052c..d44aae04937 100644
--- a/app/views/admin/abuse_reports/show.html.haml
+++ b/app/views/admin/abuse_reports/show.html.haml
@@ -1,7 +1,7 @@
-- add_to_breadcrumbs _('Abuse Reports'), admin_abuse_reports_path
+- add_to_breadcrumbs _('Abuse reports'), admin_abuse_reports_path
- breadcrumb_title @abuse_report.user&.name
- @content_class = "limit-container-width" unless fluid_layout
-- page_title @abuse_report.user&.name, _('Abuse Reports')
+- page_title @abuse_report.user&.name, _('Abuse reports')
#js-abuse-reports-detail-view{ data: abuse_report_data(@abuse_report) }
= gl_loading_icon(css_class: 'gl-my-5', size: 'md')
diff --git a/app/views/shared/wikis/_sidebar.html.haml b/app/views/shared/wikis/_sidebar.html.haml
index a757c196973..24defe5d30a 100644
--- a/app/views/shared/wikis/_sidebar.html.haml
+++ b/app/views/shared/wikis/_sidebar.html.haml
@@ -4,14 +4,17 @@
%aside.right-sidebar.right-sidebar-expanded.wiki-sidebar.js-wiki-sidebar.js-right-sidebar{ data: { "offset-top" => "50", "spy" => "affix" }, 'aria-label': _('Wiki') }
.sidebar-container
- .block.gl-mb-3.gl-mx-5.gl-block.sm:gl-hidden{ class: '!gl-pt-0' }
+ .block.gl-mb-3.gl-mx-5.gl-mt-5.gl-block.sm:gl-hidden{ class: '!gl-pt-0' }
%a.gutter-toggle.gl-float-right.gl-block.md:gl-hidden.js-sidebar-wiki-toggle{ href: "#" }
= sprite_icon('chevron-double-lg-right', css_class: 'gl-icon')
- if @sidebar_error.present?
= render 'shared/alert_info', body: s_('Wiki|The sidebar failed to load. You can reload the page to try again.')
- .blocks-container{ class: '!gl-px-3' }
+ - if !editing
+ .js-wiki-toc
+
+ .blocks-container{ class: '!gl-px-3 !gl-my-5' }
.gl-flex.gl-place-content-between.gl-items-center.gl-pb-3.gl-pr-1{ class: (@sidebar_page ? 'js-wiki-expand-pages-list wiki-list collapsed gl-pl-0' : 'gl-pl-3') }
.gl-flex.gl-items-center
- if @sidebar_page
diff --git a/config/tailwind.config.js b/config/tailwind.config.js
index 094d6e35980..6e270a111b6 100644
--- a/config/tailwind.config.js
+++ b/config/tailwind.config.js
@@ -208,6 +208,7 @@ module.exports = {
},
transitionTimingFunction: {
ease: 'ease',
+ linear: 'linear',
},
// TODO: Backport to GitLab UI.
borderRadius: {
@@ -267,8 +268,16 @@ module.exports = {
'1/2': '50%',
},
transitionProperty: {
+ transform: 'transform',
+ background: 'background',
+ opacity: 'opacity',
+ left: 'left',
+ right: 'right',
+ width: 'width',
stroke: 'stroke',
+ padding: 'padding',
'stroke-opacity': 'stroke-opacity',
+ 'box-shadow': 'box-shadow',
},
transitionTimingFunction: {
DEFAULT: 'ease',
diff --git a/db/docs/batched_background_migrations/backfill_epic_issues_into_work_item_parent_links.yml b/db/docs/batched_background_migrations/backfill_epic_issues_into_work_item_parent_links.yml
index b5bb1eabf2f..598d7f6a235 100644
--- a/db/docs/batched_background_migrations/backfill_epic_issues_into_work_item_parent_links.yml
+++ b/db/docs/batched_background_migrations/backfill_epic_issues_into_work_item_parent_links.yml
@@ -6,4 +6,4 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/147509
milestone: '17.1'
queued_migration_version: 20240522183910
finalize_after: '2024-06-20'
-finalized_by:
+finalized_by: 20240701182755
diff --git a/db/docs/import_source_user_placeholder_references.yml b/db/docs/import_source_user_placeholder_references.yml
new file mode 100644
index 00000000000..87e1f72fc44
--- /dev/null
+++ b/db/docs/import_source_user_placeholder_references.yml
@@ -0,0 +1,14 @@
+---
+table_name: import_source_user_placeholder_references
+classes:
+- Import::SourceUserPlaceholderReference
+feature_categories:
+- importers
+description: Used to map placeholder user references from imported data to real users
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/156241
+milestone: '17.2'
+gitlab_schema: gitlab_main_cell
+allow_cross_foreign_keys:
+- gitlab_main_clusterwide
+sharding_key:
+ namespace_id: namespaces
diff --git a/db/migrate/20240612034702_create_import_source_user_placeholder_reference.rb b/db/migrate/20240612034702_create_import_source_user_placeholder_reference.rb
new file mode 100644
index 00000000000..0364d632200
--- /dev/null
+++ b/db/migrate/20240612034702_create_import_source_user_placeholder_reference.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+class CreateImportSourceUserPlaceholderReference < Gitlab::Database::Migration[2.2]
+ milestone '17.2'
+
+ enable_lock_retries!
+
+ INDEX_NAME = 'index_import_source_user_placeholder_references_on_source_user_'
+
+ def change
+ create_table :import_source_user_placeholder_references do |t|
+ t.references :source_user,
+ index: { name: INDEX_NAME },
+ null: false,
+ foreign_key: { to_table: :import_source_users, on_delete: :cascade }
+ t.references :namespace, null: false, index: true, foreign_key: { on_delete: :cascade }
+ t.bigint :numeric_key, null: true
+ t.datetime_with_timezone :created_at, null: false
+ t.text :model, limit: 150, null: false
+ t.text :user_reference_column, limit: 50, null: false
+ t.jsonb :composite_key, null: true
+ end
+ end
+end
diff --git a/db/post_migrate/20240701182755_finalize_backfill_epic_issues_into_work_item_parent_links.rb b/db/post_migrate/20240701182755_finalize_backfill_epic_issues_into_work_item_parent_links.rb
new file mode 100644
index 00000000000..8a3cf6a4a07
--- /dev/null
+++ b/db/post_migrate/20240701182755_finalize_backfill_epic_issues_into_work_item_parent_links.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+class FinalizeBackfillEpicIssuesIntoWorkItemParentLinks < Gitlab::Database::Migration[2.2]
+ milestone '17.2'
+
+ disable_ddl_transaction!
+ restrict_gitlab_migration gitlab_schema: :gitlab_main
+
+ def up
+ ensure_batched_background_migration_is_finished(
+ job_class_name: 'BackfillEpicIssuesIntoWorkItemParentLinks',
+ table_name: :epic_issues,
+ column_name: 'id',
+ job_arguments: [nil],
+ finalize: true
+ )
+ end
+
+ def down
+ # No op
+ end
+end
diff --git a/db/schema_migrations/20240612034702 b/db/schema_migrations/20240612034702
new file mode 100644
index 00000000000..4c11c17c15d
--- /dev/null
+++ b/db/schema_migrations/20240612034702
@@ -0,0 +1 @@
+91e467973c28e98ed562c70ae108f7b5cb1ed0353e3ce0c8b13f052077c75d5a
\ No newline at end of file
diff --git a/db/schema_migrations/20240701182755 b/db/schema_migrations/20240701182755
new file mode 100644
index 00000000000..963f2b92609
--- /dev/null
+++ b/db/schema_migrations/20240701182755
@@ -0,0 +1 @@
+154e67c707fec8122c216af36d26919cc4317089506719ead7a2f2d203a16160
\ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 6688cf76c18..60ad4eb31d5 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -11167,6 +11167,28 @@ CREATE SEQUENCE import_failures_id_seq
ALTER SEQUENCE import_failures_id_seq OWNED BY import_failures.id;
+CREATE TABLE import_source_user_placeholder_references (
+ id bigint NOT NULL,
+ source_user_id bigint NOT NULL,
+ namespace_id bigint NOT NULL,
+ numeric_key bigint,
+ created_at timestamp with time zone NOT NULL,
+ model text NOT NULL,
+ user_reference_column text NOT NULL,
+ composite_key jsonb,
+ CONSTRAINT check_782140eb9d CHECK ((char_length(user_reference_column) <= 50)),
+ CONSTRAINT check_d17bd9dd4d CHECK ((char_length(model) <= 150))
+);
+
+CREATE SEQUENCE import_source_user_placeholder_references_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+ALTER SEQUENCE import_source_user_placeholder_references_id_seq OWNED BY import_source_user_placeholder_references.id;
+
CREATE TABLE import_source_users (
id bigint NOT NULL,
placeholder_user_id bigint,
@@ -20801,6 +20823,8 @@ ALTER TABLE ONLY import_export_uploads ALTER COLUMN id SET DEFAULT nextval('impo
ALTER TABLE ONLY import_failures ALTER COLUMN id SET DEFAULT nextval('import_failures_id_seq'::regclass);
+ALTER TABLE ONLY import_source_user_placeholder_references ALTER COLUMN id SET DEFAULT nextval('import_source_user_placeholder_references_id_seq'::regclass);
+
ALTER TABLE ONLY import_source_users ALTER COLUMN id SET DEFAULT nextval('import_source_users_id_seq'::regclass);
ALTER TABLE ONLY incident_management_escalation_policies ALTER COLUMN id SET DEFAULT nextval('incident_management_escalation_policies_id_seq'::regclass);
@@ -22987,6 +23011,9 @@ ALTER TABLE ONLY import_export_uploads
ALTER TABLE ONLY import_failures
ADD CONSTRAINT import_failures_pkey PRIMARY KEY (id);
+ALTER TABLE ONLY import_source_user_placeholder_references
+ ADD CONSTRAINT import_source_user_placeholder_references_pkey PRIMARY KEY (id);
+
ALTER TABLE ONLY import_source_users
ADD CONSTRAINT import_source_users_pkey PRIMARY KEY (id);
@@ -27322,6 +27349,10 @@ CREATE INDEX index_import_failures_on_project_id_not_null ON import_failures USI
CREATE INDEX index_import_failures_on_user_id_not_null ON import_failures USING btree (user_id) WHERE (user_id IS NOT NULL);
+CREATE INDEX index_import_source_user_placeholder_references_on_namespace_id ON import_source_user_placeholder_references USING btree (namespace_id);
+
+CREATE INDEX index_import_source_user_placeholder_references_on_source_user_ ON import_source_user_placeholder_references USING btree (source_user_id);
+
CREATE INDEX index_import_source_users_on_namespace_id ON import_source_users USING btree (namespace_id);
CREATE INDEX index_import_source_users_on_placeholder_user_id ON import_source_users USING btree (placeholder_user_id);
@@ -33352,6 +33383,9 @@ ALTER TABLE ONLY namespaces_storage_limit_exclusions
ALTER TABLE ONLY users_security_dashboard_projects
ADD CONSTRAINT fk_rails_150cd5682c FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
+ALTER TABLE ONLY import_source_user_placeholder_references
+ ADD CONSTRAINT fk_rails_158995b934 FOREIGN KEY (namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE;
+
ALTER TABLE ONLY import_source_users
ADD CONSTRAINT fk_rails_167f82fd95 FOREIGN KEY (reassign_to_user_id) REFERENCES users(id) ON DELETE SET NULL;
@@ -34720,6 +34754,9 @@ ALTER TABLE ONLY upload_states
ALTER TABLE ONLY epic_metrics
ADD CONSTRAINT fk_rails_d071904753 FOREIGN KEY (epic_id) REFERENCES epics(id) ON DELETE CASCADE;
+ALTER TABLE ONLY import_source_user_placeholder_references
+ ADD CONSTRAINT fk_rails_d0b75c434e FOREIGN KEY (source_user_id) REFERENCES import_source_users(id) ON DELETE CASCADE;
+
ALTER TABLE ONLY subscriptions
ADD CONSTRAINT fk_rails_d0c8bda804 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
diff --git a/doc/administration/review_abuse_reports.md b/doc/administration/review_abuse_reports.md
index badbf327094..8d33a2a60b4 100644
--- a/doc/administration/review_abuse_reports.md
+++ b/doc/administration/review_abuse_reports.md
@@ -39,7 +39,7 @@ To find out more about reporting abuse, see
To access abuse reports:
1. On the left sidebar, at the bottom, select **Admin Area**.
-1. Select **Abuse Reports**.
+1. Select **Abuse reports**.
There are four ways to resolve an abuse report, with a button for each method:
@@ -55,7 +55,7 @@ There are four ways to resolve an abuse report, with a button for each method:
- Allows the user to create issues, notes, snippets, and merge requests without being blocked for spam.
- Prevents abuse reports from being created for this user.
-The following is an example of the **Abuse Reports** page:
+The following is an example of the **Abuse reports** page:

@@ -80,7 +80,7 @@ After blocking, you can still either:
- Remove the user and report if necessary.
- Remove the report.
-The following is an example of a blocked user listed on the **Abuse Reports**
+The following is an example of a blocked user listed on the **Abuse reports**
page:

diff --git a/doc/development/ai_features/duo_chat.md b/doc/development/ai_features/duo_chat.md
index 3dc090f9366..f0320938a20 100644
--- a/doc/development/ai_features/duo_chat.md
+++ b/doc/development/ai_features/duo_chat.md
@@ -246,7 +246,7 @@ To run the QA Evaluation test locally, the following environment variables
must be exported:
```ruby
-REAL_AI_REQUEST=1 bundle exec rspec ee/spec/lib/gitlab/llm/completions/chat_real_requests_spec.rb
+ANTHROPIC_API_KEY='your-key' VERTEX_AI_PROJECT='your-project-id' REAL_AI_REQUEST=1 bundle exec rspec ee/spec/lib/gitlab/llm/completions/chat_real_requests_spec.rb
```
## Testing with CI
diff --git a/doc/development/ai_features/embeddings.md b/doc/development/ai_features/embeddings.md
index 9f22b511e45..35fba125ba8 100644
--- a/doc/development/ai_features/embeddings.md
+++ b/doc/development/ai_features/embeddings.md
@@ -110,14 +110,10 @@ It respects a rate limit of 450 embeddings per minute. Reach out to `@maddievn`
If the following returns 0, all issues for the project have embeddings:
-Expand
-
```shell
curl "http://localhost:9200/gitlab-development-issues/_count" \
--header "Content-Type: application/json" \
--data '{"query": {"bool": {"filter": [{"term": {"project_id": PROJECT_ID}}], "must_not": [{"exists": {"field": "embedding"}}]}}}' | jq '.count'
```
-
-
Replacing `PROJECT_ID` with your project ID.
diff --git a/doc/user/application_security/continuous_vulnerability_scanning/index.md b/doc/user/application_security/continuous_vulnerability_scanning/index.md
index 065e0f308ec..e6fd86ea825 100644
--- a/doc/user/application_security/continuous_vulnerability_scanning/index.md
+++ b/doc/user/application_security/continuous_vulnerability_scanning/index.md
@@ -46,12 +46,15 @@ Continuous Vulnerability Scanning supports components with the following [PURL t
- `npm`
- `nuget`
- `pypi`
+- `rpm`
-Work to support `apk` and `rpm` package URL types is tracked in [issue 428703](https://gitlab.com/gitlab-org/gitlab/-/issues/428703).
+Work to support the `apk` package URL type is tracked in [issue 428703](https://gitlab.com/gitlab-org/gitlab/-/issues/428703).
Go pseudo versions are not supported. A project dependency that references a Go pseudo version is
never considered as affected because this might result in false negatives.
+RPM versions containing `^` are not supported. Work to support these versions is tracked in [issue 459969](https://gitlab.com/gitlab-org/gitlab/-/issues/459969).
+
## How to generate a CycloneDX SBOM report
GitLab offers security analyzers that can generate a [CycloneDX SBOM report](../../../ci/yaml/artifacts_reports.md#artifactsreportscyclonedx) compatible with GitLab:
diff --git a/doc/user/application_security/policies/index.md b/doc/user/application_security/policies/index.md
index 64ee844cca1..015c9828a16 100644
--- a/doc/user/application_security/policies/index.md
+++ b/doc/user/application_security/policies/index.md
@@ -31,6 +31,7 @@ The following policy types are available:
pipeline or on a specified schedule.
- [Merge request approval policy](scan-result-policies.md). Enforce project-level settings and
approval rules based on scan results.
+- [Pipeline execution policy](pipeline_execution_policies.md). Enforce CI/CD jobs as part of project pipelines.
## Security policy project
diff --git a/doc/user/application_security/policies/pipeline_execution_policies.md b/doc/user/application_security/policies/pipeline_execution_policies.md
new file mode 100644
index 00000000000..6b6a1c53811
--- /dev/null
+++ b/doc/user/application_security/policies/pipeline_execution_policies.md
@@ -0,0 +1,91 @@
+---
+stage: Govern
+group: Security Policies
+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
+---
+
+# Pipeline execution policies
+
+DETAILS:
+**Tier:** Ultimate
+**Offering:** GitLab.com, Self-managed, GitLab Dedicated
+
+> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/13266) in GitLab 17.2 [with a flag](../../../administration/feature_flags.md) named `pipeline_execution_policy_type`. Disabled by default.
+
+Use Pipeline execution policies to enforce CI/CD jobs for all applicable projects.
+
+- For a video walkthrough, see [Security Policies: Pipeline Execution Policy Type](https://www.youtube.com/watch?v=QQAOpkZ__pA).
+
+## Pipeline execution policies schema
+
+The YAML file with pipeline execution policies consists of an array of objects matching pipeline execution
+policy schema nested under the `pipeline_execution_policy` key. You can configure a maximum of five
+policies under the `pipeline_execution_policy` key. Any other policies configured after
+the first five are not applied.
+
+When you save a new policy, GitLab validates its contents against [this JSON schema](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/app/validators/json_schemas/security_orchestration_policy.json).
+If you're not familiar with how to read [JSON schemas](https://json-schema.org/),
+the following sections and tables provide an alternative.
+
+| Field | Type | Required | Description |
+|-------|------|----------|-------------|
+| `pipeline_execution_policy` | `array` of pipeline execution policy | true | List of pipeline execution policies (maximum five) |
+
+## Pipeline execution policy schema
+
+| Field | Type | Required | Description |
+|-------|------|----------|-------------|
+| `name` | `string` | true | Name of the policy. Maximum of 255 characters.|
+| `description` (optional) | `string` | true | Description of the policy. |
+| `enabled` | `boolean` | true | Flag to enable (`true`) or disable (`false`) the policy. |
+| `content` | `object` of [`content`](#content-type) | true | Reference to the CI/CD configuration to inject into project pipelines. |
+| `pipeline_config_strategy` | `string` | false | Can either be `inject_ci` or `override_project_ci`. Defines the method for merging the policy configuration with the project pipeline. `inject_ci` preserves the project CI configuration and injects additional jobs from the policy. Having multiple policies enabled injects all jobs additively. `override_project_ci` replaces the project CI configuration and keeps only the policy jobs in the pipeline. |
+| `policy_scope` | `object` of [`policy_scope`](#policy_scope-scope-type) | false | Scopes the policy based on compliance framework labels or projects you define. |
+
+Note the following:
+
+- Jobs variables from pipeline execution policies take precedence over the project's CI/CD configuration.
+- Users triggering a pipeline must have at least read access to CI file specified in `content`.
+- Pipeline execution policy jobs can be assigned to one of the two reserved stages:
+ - `.pipeline-policy-pre` at the beginning of the pipeline, before the `.pre` stage.
+ - `.pipeline-policy-post` at the very end of the pipeline, after the .post stage.
+- Injecting jobs in any of the reserved stages is guaranteed to always work. Execution policy jobs can also be assigned to any standard (build, test, deploy) or user-declared stages. However, in this case, the jobs may be ignored depending on the project pipeline configuration.
+- It is not possible to assign jobs to reserved stages outside of a pipeline execution policy.
+
+### `content` type
+
+| Field | Type | Required | Description |
+|-------|------|----------|-------------|
+| `project` | `string` | true | The full GitLab project path to a project on the same GitLab instance. |
+| `file` | `string` | true | A full file path relative to the root directory (/). The YAML files must have the `.yml` or `.yaml` extension. |
+| `ref` | `string` | false | The ref to retrieve the file from. Defaults to the HEAD of the project when not specified. |
+
+### `policy_scope` scope type
+
+| Field | Type | Possible values | Description |
+|-------|------|-----------------|-------------|
+| `compliance_frameworks` | `array` | | List of IDs of the compliance frameworks in scope of enforcement, in an array of objects with key `id`. |
+| `projects` | `object` | `including`, `excluding` | Use `excluding:` or `including:` then list the IDs of the projects you wish to include or exclude, in an array of objects with key `id`. |
+
+### Example security policies project
+
+You can use the following example in a `.gitlab/security-policies/policy.yml` file stored in a
+[security policy project](index.md#security-policy-project):
+
+```yaml
+---
+pipeline_execution_policy:
+- name: My pipeline execution policy
+ description: Enforces CI/CD jobs
+ enabled: true
+ pipeline_config_strategy: override_project_ci
+ content:
+ include:
+ - project: verify-issue-469027/policy-ci
+ file: policy-ci.yml
+ ref: main # optional
+ policy_scope:
+ projects:
+ including:
+ - id: 361
+```
diff --git a/lib/api/branches.rb b/lib/api/branches.rb
index c3a3058b95b..8d0dff20dba 100644
--- a/lib/api/branches.rb
+++ b/lib/api/branches.rb
@@ -48,8 +48,6 @@ module API
end
get ':id/repository/branches', urgency: :low do
cache_action([user_project, :branches, current_user, declared_params], expires_in: 30.seconds) do
- user_project.preload_protected_branches
-
repository = user_project.repository
branches_finder = BranchesFinder.new(repository, declared_params(include_missing: false))
@@ -57,6 +55,7 @@ module API
merged_branch_names = repository.merged_branch_names(branches.map(&:name))
+ user_project.preload_protected_branches if branches.present?
present_cached(
branches,
with: Entities::Branch,
diff --git a/lib/api/entities/branch.rb b/lib/api/entities/branch.rb
index 7a6a3da6e69..327aa0ffc22 100644
--- a/lib/api/entities/branch.rb
+++ b/lib/api/entities/branch.rb
@@ -36,7 +36,7 @@ module API
type: 'boolean',
example: true
} do |repo_branch, options|
- ::ProtectedBranch.developers_can?(:push, repo_branch.name, protected_refs: options[:project].protected_branches)
+ ::ProtectedBranch.developers_can?(:push, repo_branch.name, protected_refs: options[:project].all_protected_branches)
end
expose :developers_can_merge,
@@ -44,7 +44,7 @@ module API
type: 'boolean',
example: true
} do |repo_branch, options|
- ::ProtectedBranch.developers_can?(:merge, repo_branch.name, protected_refs: options[:project].protected_branches)
+ ::ProtectedBranch.developers_can?(:merge, repo_branch.name, protected_refs: options[:project].all_protected_branches)
end
expose :can_push,
diff --git a/lib/gitlab/pagination/gitaly_keyset_pager.rb b/lib/gitlab/pagination/gitaly_keyset_pager.rb
index bec38c44c4c..f1f3e1a053f 100644
--- a/lib/gitlab/pagination/gitaly_keyset_pager.rb
+++ b/lib/gitlab/pagination/gitaly_keyset_pager.rb
@@ -79,7 +79,7 @@ module Gitlab
end
def apply_headers(records, next_cursor)
- if records.count == params[:per_page]
+ if records.count == params[:per_page] && next_cursor.present?
Gitlab::Pagination::Keyset::HeaderBuilder
.new(request_context)
.add_next_page_header(
diff --git a/lib/sidebars/admin/menus/abuse_reports_menu.rb b/lib/sidebars/admin/menus/abuse_reports_menu.rb
index 72f4d6e6590..b03d37b5b2f 100644
--- a/lib/sidebars/admin/menus/abuse_reports_menu.rb
+++ b/lib/sidebars/admin/menus/abuse_reports_menu.rb
@@ -11,7 +11,7 @@ module Sidebars
override :title
def title
- s_('Admin|Abuse Reports')
+ s_('Admin|Abuse reports')
end
override :sprite_icon
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index f7c8b3513b9..08bd73ae78c 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -2421,9 +2421,6 @@ msgstr ""
msgid "About your company"
msgstr ""
-msgid "Abuse Reports"
-msgstr ""
-
msgid "Abuse reports"
msgstr ""
@@ -4650,7 +4647,7 @@ msgstr ""
msgid "Admin|AI-Powered Features"
msgstr ""
-msgid "Admin|Abuse Reports"
+msgid "Admin|Abuse reports"
msgstr ""
msgid "Admin|Additional users must be reviewed and approved by a system administrator. Learn more about %{help_link_start}usage caps%{help_link_end}."
@@ -36152,6 +36149,9 @@ msgstr[1] ""
msgid "On the left sidebar, select %{compliance_center_link} to view them."
msgstr ""
+msgid "On this page"
+msgstr ""
+
msgid "On track"
msgstr ""
diff --git a/spec/factories/import_source_user_placeholder_references.rb b/spec/factories/import_source_user_placeholder_references.rb
new file mode 100644
index 00000000000..7aa19ad0a5a
--- /dev/null
+++ b/spec/factories/import_source_user_placeholder_references.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :import_source_user_placeholder_reference, class: 'Import::SourceUserPlaceholderReference' do
+ source_user factory: :import_source_user
+ namespace
+ model { 'Note' }
+ user_reference_column { 'author_id' }
+ numeric_key { 1 }
+ end
+end
diff --git a/spec/features/gitlab_experiments_spec.rb b/spec/features/gitlab_experiments_spec.rb
index 0d0afa801c8..434c036d037 100644
--- a/spec/features/gitlab_experiments_spec.rb
+++ b/spec/features/gitlab_experiments_spec.rb
@@ -29,7 +29,7 @@ RSpec.describe "Gitlab::Experiment", :js, feature_category: :activation do
visit admin_abuse_reports_path
- expect(page).to have_content('Abuse Reports')
+ expect(page).to have_content('Abuse reports')
published_experiments = page.evaluate_script('window.gl.experiments')
expect(published_experiments).to include({
diff --git a/spec/frontend/content_editor/services/table_of_contents_utils_spec.js b/spec/frontend/content_editor/services/table_of_contents_utils_spec.js
index f3d4424f48d..bd64e7df4b3 100644
--- a/spec/frontend/content_editor/services/table_of_contents_utils_spec.js
+++ b/spec/frontend/content_editor/services/table_of_contents_utils_spec.js
@@ -1,5 +1,10 @@
import Heading from '~/content_editor/extensions/heading';
-import { toTree, getHeadings, fillEmpty } from '~/content_editor/services/table_of_contents_utils';
+import {
+ toTree,
+ getHeadings,
+ fillEmpty,
+ getHeadingsFromDOM,
+} from '~/content_editor/services/table_of_contents_utils';
import { createTestEditor, createDocBuilder } from '../test_utils';
describe('content_editor/services/table_of_content_utils', () => {
@@ -144,4 +149,57 @@ describe('content_editor/services/table_of_content_utils', () => {
]);
});
});
+
+ describe('getHeadingsFromDOM', () => {
+ it('gets all headings as a tree in a DOM element', () => {
+ const element = document.createElement('div');
+ element.innerHTML = `
+
Heading 1
+ Heading 1.1
+ Heading 1.1.1
+ Heading 1.2
+ Heading 1.2.1
+ Heading 1.3
+ Heading 1.4
+ Heading 1.4.1
+ Heading 2
+ `;
+
+ expect(getHeadingsFromDOM(element)).toEqual([
+ {
+ href: '#heading-1',
+ level: 1,
+ subHeadings: [
+ {
+ href: '#heading-1-1',
+ level: 2,
+ subHeadings: [
+ { href: '#heading-1-1-1', level: 3, subHeadings: [], text: 'Heading 1.1.1' },
+ ],
+ text: 'Heading 1.1',
+ },
+ {
+ href: '#heading-1-2',
+ level: 2,
+ subHeadings: [
+ { href: '#heading-1-2-1', level: 3, subHeadings: [], text: 'Heading 1.2.1' },
+ ],
+ text: 'Heading 1.2',
+ },
+ { href: '#heading-1-3', level: 2, subHeadings: [], text: 'Heading 1.3' },
+ {
+ href: '#heading-1-4',
+ level: 2,
+ subHeadings: [
+ { href: '#heading-1-4-1', level: 3, subHeadings: [], text: 'Heading 1.4.1' },
+ ],
+ text: 'Heading 1.4',
+ },
+ ],
+ text: 'Heading 1',
+ },
+ { href: '#heading-2', level: 1, subHeadings: [], text: 'Heading 2' },
+ ]);
+ });
+ });
});
diff --git a/spec/frontend/notes/stores/mutation_spec.js b/spec/frontend/notes/stores/mutation_spec.js
index a588957dd0e..b3f2804a4a0 100644
--- a/spec/frontend/notes/stores/mutation_spec.js
+++ b/spec/frontend/notes/stores/mutation_spec.js
@@ -1,4 +1,3 @@
-import Vue from 'vue';
import { DISCUSSION_NOTE, ASC, DESC } from '~/notes/constants';
import mutations from '~/notes/stores/mutations';
import {
@@ -598,13 +597,15 @@ describe('Notes Store mutations', () => {
});
it('keeps reactivity of discussion', () => {
- const state = {};
- Vue.set(state, 'discussions', [
- {
- id: 1,
- expanded: false,
- },
- ]);
+ const state = {
+ discussions: [
+ {
+ id: 1,
+ expanded: false,
+ },
+ ],
+ };
+
const discussion = state.discussions[0];
mutations.SET_DISCUSSION_DIFF_LINES(state, {
diff --git a/spec/frontend/pages/shared/wikis/components/wiki_content_spec.js b/spec/frontend/pages/shared/wikis/components/wiki_content_spec.js
index 92fb7a7e00f..629105473cd 100644
--- a/spec/frontend/pages/shared/wikis/components/wiki_content_spec.js
+++ b/spec/frontend/pages/shared/wikis/components/wiki_content_spec.js
@@ -74,7 +74,7 @@ describe('pages/shared/wikis/components/wiki_content', () => {
it('calls renderGFM after nextTick', async () => {
await nextTick();
- expect(renderGFM).toHaveBeenCalledWith(wrapper.element);
+ expect(renderGFM).toHaveBeenCalled();
});
it('handles hash after render', async () => {
diff --git a/spec/frontend/work_items/components/work_item_links/work_item_tree_children_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_tree_children_spec.js
new file mode 100644
index 00000000000..d4b546fd594
--- /dev/null
+++ b/spec/frontend/work_items/components/work_item_links/work_item_tree_children_spec.js
@@ -0,0 +1,49 @@
+import { shallowMount } from '@vue/test-utils';
+import WorkItemTreeChildren from '~/work_items/components/work_item_links/work_item_tree_children.vue';
+import WorkItemLinkChild from '~/work_items/components/work_item_links/work_item_link_child.vue';
+import { childrenWorkItems } from '../../mock_data';
+
+describe('WorkItemTreeChildren', () => {
+ let wrapper;
+
+ const createComponent = ({ children = childrenWorkItems } = {}) => {
+ wrapper = shallowMount(WorkItemTreeChildren, {
+ propsData: {
+ workItemType: 'Objective',
+ workItemId: 'gid:://gitlab/WorkItem/1',
+ children,
+ canUpdate: true,
+ showLabels: true,
+ },
+ });
+ };
+
+ const findWorkItemLinkChildItems = () => wrapper.findAllComponents(WorkItemLinkChild);
+ const findWorkItemLinkChildItem = () => findWorkItemLinkChildItems().at(0);
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders all WorkItemLinkChildItems', () => {
+ expect(findWorkItemLinkChildItems().length).toBe(4);
+ });
+
+ it('emits childItem from WorkItemLinkChildItems on `click` event', () => {
+ const event = {
+ childItem: 'gid://gitlab/WorkItem/2',
+ };
+
+ findWorkItemLinkChildItem().vm.$emit('click', event);
+
+ expect(wrapper.emitted('click')).toEqual([[{ childItem: 'gid://gitlab/WorkItem/2' }]]);
+ });
+
+ it('emits immediate childItem on `click` event', () => {
+ const event = expect.anything();
+
+ findWorkItemLinkChildItem().vm.$emit('click', event);
+
+ expect(wrapper.emitted('click')).toEqual([[{ childItem: 'gid://gitlab/WorkItem/2' }]]);
+ });
+});
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index f9b22e53234..4326de27dc3 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -645,6 +645,7 @@ project:
- notes
- snippets
- hooks
+- all_protected_branches
- protected_branches
- protected_tags
- project_members
diff --git a/spec/lib/gitlab/pagination/gitaly_keyset_pager_spec.rb b/spec/lib/gitlab/pagination/gitaly_keyset_pager_spec.rb
index 914c1e7bb74..5dd340f69fd 100644
--- a/spec/lib/gitlab/pagination/gitaly_keyset_pager_spec.rb
+++ b/spec/lib/gitlab/pagination/gitaly_keyset_pager_spec.rb
@@ -129,6 +129,18 @@ RSpec.describe Gitlab::Pagination::GitalyKeysetPager, feature_category: :source_
pager.paginate(finder)
end
end
+
+ context 'when the current page includes all requested elements and cursor is empty' do
+ let(:base_query) { { per_page: 2 } }
+ let(:branches) { [branch1, branch2] }
+ let(:next_cursor) { '' }
+
+ it 'uses keyset pagination without link headers' do
+ expect(request_context).not_to receive(:header).with('Link', anything)
+
+ pager.paginate(finder)
+ end
+ end
end
end
diff --git a/spec/lib/sidebars/admin/menus/abuse_reports_menu_spec.rb b/spec/lib/sidebars/admin/menus/abuse_reports_menu_spec.rb
index ef5b8055bec..dc75f9e0e1f 100644
--- a/spec/lib/sidebars/admin/menus/abuse_reports_menu_spec.rb
+++ b/spec/lib/sidebars/admin/menus/abuse_reports_menu_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Sidebars::Admin::Menus::AbuseReportsMenu, feature_category: :navigation do
it_behaves_like 'Admin menu',
link: '/admin/abuse_reports',
- title: _('Abuse Reports'),
+ title: _('Abuse reports'),
icon: 'slight-frown'
it_behaves_like 'Admin menu without sub menus', active_routes: { controller: :abuse_reports }
diff --git a/spec/migrations/db/post_migrate/20240701182755_finalize_backfill_epic_issues_into_work_item_parent_links_spec.rb b/spec/migrations/db/post_migrate/20240701182755_finalize_backfill_epic_issues_into_work_item_parent_links_spec.rb
new file mode 100644
index 00000000000..fafe50d39f9
--- /dev/null
+++ b/spec/migrations/db/post_migrate/20240701182755_finalize_backfill_epic_issues_into_work_item_parent_links_spec.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe FinalizeBackfillEpicIssuesIntoWorkItemParentLinks, feature_category: :database, migration_version: 20240701182755 do
+ describe '#up' do
+ it 'ensures the migration is completed for self-managed instances' do
+ # enqueue the migration
+ QueueBackfillEpicIssuesIntoWorkItemParentLinks.new.up
+
+ migration = Gitlab::Database::BackgroundMigration::BatchedMigration.where(
+ job_class_name: 'BackfillEpicIssuesIntoWorkItemParentLinks',
+ table_name: 'epic_issues'
+ ).first
+
+ expect(migration.status).not_to eq(6) # finalized
+
+ migrate!
+
+ expect(migration.reload.status).to eq(6)
+ QueueBackfillEpicIssuesIntoWorkItemParentLinks.new.down
+ end
+ end
+end
diff --git a/spec/models/import/source_user_placeholder_reference_spec.rb b/spec/models/import/source_user_placeholder_reference_spec.rb
new file mode 100644
index 00000000000..e441e065f59
--- /dev/null
+++ b/spec/models/import/source_user_placeholder_reference_spec.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Import::SourceUserPlaceholderReference, feature_category: :importers do
+ describe 'associations' do
+ it { is_expected.to belong_to(:source_user).class_name('Import::SourceUser') }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:user_reference_column) }
+ it { is_expected.to validate_presence_of(:model) }
+ it { is_expected.to validate_presence_of(:namespace_id) }
+ it { is_expected.to validate_presence_of(:source_user_id) }
+ it { is_expected.to validate_numericality_of(:numeric_key).only_integer.is_greater_than(0) }
+ it { expect(described_class).to validate_jsonb_schema(['composite_key']) }
+ it { is_expected.to allow_value({ id: 1 }).for(:composite_key) }
+ it { is_expected.to allow_value({ id: '1' }).for(:composite_key) }
+ it { is_expected.to allow_value({ foo: '1', bar: 2 }).for(:composite_key) }
+ it { is_expected.not_to allow_value({}).for(:composite_key) }
+ it { is_expected.not_to allow_value({ id: 'foo' }).for(:composite_key) }
+ it { is_expected.not_to allow_value(1).for(:composite_key) }
+
+ describe '#validate_numeric_or_composite_key_present' do
+ def validation_errors(...)
+ described_class.new(...).tap(&:validate)
+ .errors
+ .where(:base)
+ end
+
+ it 'must have numeric_key or composite_key present', :aggregate_failures do
+ expect(validation_errors).to be_present
+ expect(validation_errors(numeric_key: 1)).to be_blank
+ expect(validation_errors(composite_key: { id: 1 })).to be_blank
+ expect(validation_errors(numeric_key: 1, composite_key: { id: 1 })).to be_present
+ end
+ end
+ end
+
+ it 'is destroyed when source user is destroyed' do
+ reference = create(:import_source_user_placeholder_reference)
+
+ expect { reference.source_user.destroy! }.to change { described_class.count }.by(-1)
+ end
+end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 51e32d473a6..ce0e3ea1c6e 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -196,6 +196,7 @@ RSpec.describe Project, factory_default: :keep, feature_category: :groups_and_pr
it { is_expected.to have_many(:alert_hooks_integrations).class_name('Integration') }
it { is_expected.to have_many(:incident_hooks_integrations).class_name('Integration') }
it { is_expected.to have_many(:relation_import_trackers).class_name('Projects::ImportExport::RelationImportTracker') }
+ it { is_expected.to have_many(:all_protected_branches).class_name('ProtectedBranch') }
# GitLab Pages
it { is_expected.to have_many(:pages_domains) }
diff --git a/spec/models/protected_branch_spec.rb b/spec/models/protected_branch_spec.rb
index b34e4a7bf30..06f77ea9ebb 100644
--- a/spec/models/protected_branch_spec.rb
+++ b/spec/models/protected_branch_spec.rb
@@ -83,34 +83,14 @@ RSpec.describe ProtectedBranch, feature_category: :source_code_management do
end
describe '.protected_refs' do
- let_it_be(:project) { create(:project) }
+ let(:project) { build_stubbed(:project) }
subject { described_class.protected_refs(project) }
- context 'when feature flag enabled' do
- before do
- stub_feature_flags(group_protected_branches: true)
- stub_feature_flags(allow_protected_branches_for_group: true)
- end
+ it 'call `all_protected_branches`' do
+ expect(project).to receive(:all_protected_branches)
- it 'call `all_protected_branches`' do
- expect(project).to receive(:all_protected_branches)
-
- subject
- end
- end
-
- context 'when feature flag disabled' do
- before do
- stub_feature_flags(group_protected_branches: false)
- stub_feature_flags(allow_protected_branches_for_group: false)
- end
-
- it 'call `protected_branches`' do
- expect(project).to receive(:protected_branches)
-
- subject
- end
+ subject
end
end
diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb
index c1fe5c6d0a9..45ab91c74e8 100644
--- a/spec/requests/api/branches_spec.rb
+++ b/spec/requests/api/branches_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe API::Branches, feature_category: :source_code_management do
let_it_be(:user) { create(:user) }
- let(:project) { create(:project, :repository, creator: user, path: 'my.project', create_branch: 'ends-with.txt') }
+ let(:project) { create(:project, :in_group, :repository, creator: user, path: 'my.project', create_branch: 'ends-with.txt') }
let(:guest) { create(:user, guest_of: project) }
let(:branch_name) { 'feature' }
let(:branch_sha) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' }
@@ -295,10 +295,79 @@ RSpec.describe API::Branches, feature_category: :source_code_management do
new_branch_name = 'protected-branch'
::Branches::CreateService.new(project, current_user).execute(new_branch_name, 'master')
create(:protected_branch, name: new_branch_name, project: project)
+ create(:protected_branch, name: new_branch_name, project: nil, group: project.group)
expect do
get api(route, current_user), params: { per_page: 100 }
- end.not_to exceed_query_limit(control).with_threshold(1)
+ end.not_to exceed_query_limit(control)
+ end
+ end
+
+ context 'with group protected branches' do
+ subject(:request) { get api(route, user) }
+
+ let!(:group_protected_branch) do
+ create(:protected_branch,
+ *access_levels,
+ project: nil,
+ group: project.group,
+ name: '*'
+ )
+ end
+
+ context 'maintainers allowed to push and merge' do
+ let(:access_levels) { [] }
+
+ it 'responds with correct attributes related to push and merge' do
+ request
+
+ expect(json_response.dig(0, 'developers_can_merge')).to be_falsey
+ expect(json_response.dig(0, 'developers_can_push')).to be_falsey
+ expect(json_response.dig(0, 'can_push')).to be_truthy
+ end
+
+ context 'and there is a more permissive project level protected branch' do
+ let!(:project_level_protected_branch) do
+ create(:protected_branch,
+ :developers_can_merge,
+ :developers_can_push,
+ project: project,
+ name: '*'
+ )
+ end
+
+ it 'responds with correct attributes related to push and merge' do
+ request
+
+ expect(json_response.dig(0, 'developers_can_merge')).to be_truthy
+ expect(json_response.dig(0, 'developers_can_push')).to be_truthy
+ expect(json_response.dig(0, 'can_push')).to be_truthy
+ end
+ end
+ end
+
+ context 'when developers can push and merge' do
+ let(:access_levels) { %i[developers_can_merge developers_can_push] }
+
+ it 'responds with correct attributes related to push and merge' do
+ request
+
+ expect(json_response.dig(0, 'developers_can_merge')).to be_truthy
+ expect(json_response.dig(0, 'developers_can_push')).to be_truthy
+ expect(json_response.dig(0, 'can_push')).to be_truthy
+ end
+ end
+
+ context 'when no one can push and merge' do
+ let(:access_levels) { %i[no_one_can_merge no_one_can_push] }
+
+ it 'responds with correct attributes related to push and merge' do
+ request
+
+ expect(json_response.dig(0, 'developers_can_merge')).to be_falsey
+ expect(json_response.dig(0, 'developers_can_push')).to be_falsey
+ expect(json_response.dig(0, 'can_push')).to be_falsey
+ end
end
end
diff --git a/spec/support/shared_examples/features/wiki/user_views_wiki_page_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_views_wiki_page_shared_examples.rb
index f9a34f04237..c4540c8ca2f 100644
--- a/spec/support/shared_examples/features/wiki/user_views_wiki_page_shared_examples.rb
+++ b/spec/support/shared_examples/features/wiki/user_views_wiki_page_shared_examples.rb
@@ -247,6 +247,25 @@ RSpec.shared_examples 'User views a wiki page' do
end
end
+ context 'when a page has headings' do
+ before do
+ wiki_page.update(content: "# Heading 1\n\n## Heading 1.1\n\n### Heading 1.1.1\n\n# Heading 2") # rubocop:disable Rails/SaveBang -- not an ActiveRecord
+ end
+
+ it 'displays the table of contents for the page' do
+ visit(wiki_page_path(wiki, wiki_page))
+
+ within '.js-wiki-toc' do
+ expect(page).to have_content('On this page')
+
+ expect(page).to have_content('Heading 1')
+ expect(page).to have_content('Heading 1.1')
+ expect(page).to have_content('Heading 1.1.1')
+ expect(page).to have_content('Heading 2')
+ end
+ end
+ end
+
context 'when page has invalid content encoding' do
let(:content) { (+'whatever').force_encoding('ISO-8859-1') }
diff --git a/workhorse/cmd/gitlab-workhorse/channel_test.go b/workhorse/cmd/gitlab-workhorse/channel_test.go
index 6e8ad945fd2..11b414cc526 100644
--- a/workhorse/cmd/gitlab-workhorse/channel_test.go
+++ b/workhorse/cmd/gitlab-workhorse/channel_test.go
@@ -174,7 +174,7 @@ func webSocketHandler(upgrader *websocket.Upgrader, connCh chan connWithReq) htt
func channelOkBody(remote *httptest.Server, header http.Header, subprotocols ...string) *api.Response {
out := &api.Response{
Channel: &api.ChannelSettings{
- WsURL: websocketURL(remote.URL),
+ Url: websocketURL(remote.URL),
Header: header,
Subprotocols: subprotocols,
MaxSessionTime: 0,
diff --git a/workhorse/internal/api/channel_settings.go b/workhorse/internal/api/channel_settings.go
index 6e8d2f13b07..779d2ad7e69 100644
--- a/workhorse/internal/api/channel_settings.go
+++ b/workhorse/internal/api/channel_settings.go
@@ -19,7 +19,7 @@ type ChannelSettings struct {
Subprotocols []string
// The websocket URL to connect to.
- WsURL string
+ Url string //nolint:revive,stylecheck // when JSON decoding, the value is provided via 'url'
// Any headers (e.g., Authorization) to send with the websocket request
Header http.Header
@@ -35,7 +35,7 @@ type ChannelSettings struct {
// URL parses the websocket URL in the ChannelSettings and returns a *url.URL.
func (t *ChannelSettings) URL() (*url.URL, error) {
- return url.Parse(t.WsURL)
+ return url.Parse(t.Url)
}
// Dialer returns a websocket Dialer configured with the settings from ChannelSettings.
@@ -72,7 +72,7 @@ func (t *ChannelSettings) Clone() *ChannelSettings {
// Dial establishes a websocket connection using the settings from ChannelSettings.
// It returns a websocket connection, an HTTP response, and an error if any.
func (t *ChannelSettings) Dial() (*websocket.Conn, *http.Response, error) {
- return t.Dialer().Dial(t.WsURL, t.Header)
+ return t.Dialer().Dial(t.Url, t.Header)
}
// Validate checks if the ChannelSettings instance is valid.
@@ -133,7 +133,7 @@ func (t *ChannelSettings) IsEqual(other *ChannelSettings) bool {
}
}
- return t.WsURL == other.WsURL &&
+ return t.Url == other.Url &&
t.CAPem == other.CAPem &&
t.MaxSessionTime == other.MaxSessionTime
}
diff --git a/workhorse/internal/api/channel_settings_test.go b/workhorse/internal/api/channel_settings_test.go
index d33d78222d2..1735a8b6e3d 100644
--- a/workhorse/internal/api/channel_settings_test.go
+++ b/workhorse/internal/api/channel_settings_test.go
@@ -9,7 +9,7 @@ import (
func channel(url string, subprotocols ...string) *ChannelSettings {
return &ChannelSettings{
- WsURL: url,
+ Url: url,
Subprotocols: subprotocols,
MaxSessionTime: 0,
}
@@ -146,7 +146,7 @@ func TestIsEqual(t *testing.T) {
{chann, chann, true},
{chann.Clone(), chann.Clone(), true},
{chann, channel("foo:"), false},
- {chann, channel(chann.WsURL), false},
+ {chann, channel(chann.Url), false},
{header(chann), header(chann), true},
{channHeader2, channHeader2, true},
{channHeader3, channHeader3, true},
diff --git a/workhorse/internal/channel/auth_checker_test.go b/workhorse/internal/channel/auth_checker_test.go
index 36a45b07c1e..b522cf5da1f 100644
--- a/workhorse/internal/channel/auth_checker_test.go
+++ b/workhorse/internal/channel/auth_checker_test.go
@@ -19,7 +19,7 @@ func checkerSeries(values ...*api.ChannelSettings) AuthCheckerFunc {
}
func TestAuthCheckerStopsWhenAuthFails(t *testing.T) {
- template := &api.ChannelSettings{WsURL: "ws://example.com"}
+ template := &api.ChannelSettings{Url: "ws://example.com"}
stopCh := make(chan error)
series := checkerSeries(template, template, template)
ac := NewAuthChecker(series, template, stopCh)
@@ -35,9 +35,9 @@ func TestAuthCheckerStopsWhenAuthFails(t *testing.T) {
}
func TestAuthCheckerStopsWhenAuthChanges(t *testing.T) {
- template := &api.ChannelSettings{WsURL: "ws://example.com"}
+ template := &api.ChannelSettings{Url: "ws://example.com"}
changed := template.Clone()
- changed.WsURL = "wss://example.com"
+ changed.Url = "wss://example.com"
stopCh := make(chan error)
series := checkerSeries(template, changed, template)
ac := NewAuthChecker(series, template, stopCh)