Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-12-06 06:14:51 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-12-06 06:14:51 +0300
commit313ce461cafef54a87c1943b941dc1327246e1e2 (patch)
tree1abfbf775593a2fbcff01a78a9a5389dfd12f2cc
parent1e2aa980a7214f025d22e1d8936147391b670a89 (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.rubocop_todo/rspec/before_all_role_assignment.yml1
-rw-r--r--.rubocop_todo/rspec/named_subject.yml1
-rw-r--r--Gemfile.lock2
-rw-r--r--app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_add_note.vue4
-rw-r--r--app/assets/javascripts/ci/job_details/components/sidebar/sidebar.vue2
-rw-r--r--app/assets/javascripts/emoji/queries/custom_emoji.query.graphql2
-rw-r--r--app/assets/javascripts/work_items/components/item_title.vue21
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_add_note.vue4
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_notes_activity_header.vue14
-rw-r--r--app/assets/javascripts/work_items/components/work_item_detail.vue2
-rw-r--r--app/assets/javascripts/work_items/components/work_item_notes.vue6
-rw-r--r--app/assets/javascripts/work_items/components/work_item_title.vue12
-rw-r--r--app/assets/stylesheets/framework/contextual_sidebar.scss4
-rw-r--r--app/assets/stylesheets/framework/variables.scss3
-rw-r--r--app/assets/stylesheets/page_bundles/build.scss5
-rw-r--r--app/assets/stylesheets/pages/note_form.scss1
-rw-r--r--app/assets/stylesheets/themes/_dark.scss15
-rw-r--r--app/graphql/mutations/container_registry/protection/rule/delete.rb41
-rw-r--r--app/graphql/types/mutation_type.rb1
-rw-r--r--app/helpers/notes_helper.rb2
-rw-r--r--app/services/container_registry/protection/delete_rule_service.rb45
-rw-r--r--app/views/protected_branches/shared/_create_protected_branch.html.haml2
-rw-r--r--config/application.rb1
-rw-r--r--config/feature_flags/development/bitbucket_server_importer_exponential_backoff.yml8
-rw-r--r--config/initializers/7_redis.rb2
-rw-r--r--db/post_migrate/20231124213241_add_index_to_bulk_imports_on_updated_at_and_status.rb17
-rw-r--r--db/schema_migrations/202311242132411
-rw-r--r--db/structure.sql2
-rw-r--r--doc/.vale/gitlab/SubstitutionWarning.yml1
-rw-r--r--doc/administration/monitoring/prometheus/gitlab_metrics.md2
-rw-r--r--doc/administration/server_hooks.md4
-rw-r--r--doc/api/graphql/reference/index.md25
-rw-r--r--doc/ci/environments/deployment_approvals.md15
-rw-r--r--doc/development/ai_features/index.md1
-rw-r--r--doc/development/documentation/styleguide/word_list.md5
-rw-r--r--doc/gitlab-basics/start-using-git.md2
-rw-r--r--doc/topics/git/git_rebase.md328
-rw-r--r--doc/user/project/git_attributes.md70
-rw-r--r--doc/user/project/merge_requests/conflicts.md4
-rw-r--r--doc/user/project/merge_requests/methods/index.md2
-rw-r--r--doc/user/project/protected_branches.md2
-rw-r--r--doc/user/project/settings/import_export_troubleshooting.md2
-rw-r--r--gems/gitlab-secret_detection/Gemfile.lock30
-rw-r--r--gems/gitlab-secret_detection/gitlab-secret_detection.gemspec7
-rw-r--r--gems/gitlab-secret_detection/lib/gitlab/secret_detection.rb6
-rw-r--r--gems/gitlab-secret_detection/lib/gitlab/secret_detection/finding.rb28
-rw-r--r--gems/gitlab-secret_detection/lib/gitlab/secret_detection/response.rb28
-rw-r--r--gems/gitlab-secret_detection/lib/gitlab/secret_detection/scan.rb193
-rw-r--r--gems/gitlab-secret_detection/lib/gitlab/secret_detection/status.rb16
-rw-r--r--gems/gitlab-secret_detection/lib/gitleaks.toml49
-rw-r--r--gems/gitlab-secret_detection/spec/gitlab/secret_detection_spec.rb7
-rw-r--r--gems/gitlab-secret_detection/spec/lib/gitlab/secret_detection/scan_spec.rb199
-rw-r--r--lib/bitbucket_server/connection.rb38
-rw-r--r--lib/bitbucket_server/retry_with_delay.rb32
-rw-r--r--lib/gitlab/instrumentation/connection_pool.rb40
-rw-r--r--lib/gitlab/redis/wrapper.rb2
-rw-r--r--locale/gitlab.pot6
-rw-r--r--package.json2
-rw-r--r--spec/frontend/admin/abuse_report/components/notes/abuse_report_add_note_spec.js2
-rw-r--r--spec/frontend/work_items/components/item_title_spec.js9
-rw-r--r--spec/frontend/work_items/components/notes/work_item_notes_activity_header_spec.js18
-rw-r--r--spec/lib/bitbucket_server/client_spec.rb2
-rw-r--r--spec/lib/bitbucket_server/connection_spec.rb147
-rw-r--r--spec/lib/bitbucket_server/retry_with_delay_spec.rb60
-rw-r--r--spec/lib/gitlab/instrumentation/connection_pool_spec.rb69
-rw-r--r--spec/requests/api/graphql/mutations/container_registry/protection/rule/delete_spec.rb102
-rw-r--r--spec/services/container_registry/protection/delete_rule_service_spec.rb106
-rw-r--r--spec/support/shared_examples/redis/redis_shared_examples.rb6
-rw-r--r--yarn.lock8
69 files changed, 1607 insertions, 289 deletions
diff --git a/.rubocop_todo/rspec/before_all_role_assignment.yml b/.rubocop_todo/rspec/before_all_role_assignment.yml
index 5ab492b4a29..7f4eeeb3d00 100644
--- a/.rubocop_todo/rspec/before_all_role_assignment.yml
+++ b/.rubocop_todo/rspec/before_all_role_assignment.yml
@@ -1098,6 +1098,7 @@ RSpec/BeforeAllRoleAssignment:
- 'spec/lib/bulk_imports/projects/pipelines/ci_pipelines_pipeline_spec.rb'
- 'spec/lib/bulk_imports/projects/pipelines/commit_notes_pipeline_spec.rb'
- 'spec/lib/bulk_imports/projects/pipelines/issues_pipeline_spec.rb'
+ - 'spec/lib/bulk_imports/projects/pipelines/legacy_references_pipeline_spec.rb'
- 'spec/lib/bulk_imports/projects/pipelines/merge_requests_pipeline_spec.rb'
- 'spec/lib/bulk_imports/projects/pipelines/pipeline_schedules_pipeline_spec.rb'
- 'spec/lib/bulk_imports/projects/pipelines/project_pipeline_spec.rb'
diff --git a/.rubocop_todo/rspec/named_subject.yml b/.rubocop_todo/rspec/named_subject.yml
index dd4928d9c8a..bcb59ffbcb7 100644
--- a/.rubocop_todo/rspec/named_subject.yml
+++ b/.rubocop_todo/rspec/named_subject.yml
@@ -1770,6 +1770,7 @@ RSpec/NamedSubject:
- 'spec/lib/bulk_imports/projects/graphql/get_project_query_spec.rb'
- 'spec/lib/bulk_imports/projects/graphql/get_repository_query_spec.rb'
- 'spec/lib/bulk_imports/projects/graphql/get_snippet_repository_query_spec.rb'
+ - 'spec/lib/bulk_imports/projects/pipelines/legacy_references_pipeline_spec.rb'
- 'spec/lib/bulk_imports/projects/pipelines/references_pipeline_spec.rb'
- 'spec/lib/bulk_imports/projects/stage_spec.rb'
- 'spec/lib/bulk_imports/source_url_builder_spec.rb'
diff --git a/Gemfile.lock b/Gemfile.lock
index 6ef11ee6a13..bce114b23e8 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -65,6 +65,8 @@ PATH
remote: gems/gitlab-secret_detection
specs:
gitlab-secret_detection (0.1.0)
+ re2 (~> 2.4)
+ toml-rb (~> 2.2)
PATH
remote: gems/gitlab-utils
diff --git a/app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_add_note.vue b/app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_add_note.vue
index d13d7611143..a1e2c3f3c7a 100644
--- a/app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_add_note.vue
+++ b/app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_add_note.vue
@@ -8,7 +8,7 @@ import AbuseReportCommentForm from './abuse_report_comment_form.vue';
export default {
name: 'AbuseReportAddNote',
i18n: {
- reply: __('Reply'),
+ reply: __('Reply…'),
replyToComment: __('Reply to comment'),
commentError: __('Your comment could not be submitted because %{reason}.'),
genericError: __(
@@ -139,7 +139,7 @@ export default {
v-else
ref="textarea"
rows="1"
- class="reply-placeholder-text-field gl-font-regular!"
+ class="reply-placeholder-text-field"
data-testid="abuse-report-note-reply-textarea"
:placeholder="$options.i18n.reply"
:aria-label="$options.i18n.replyToComment"
diff --git a/app/assets/javascripts/ci/job_details/components/sidebar/sidebar.vue b/app/assets/javascripts/ci/job_details/components/sidebar/sidebar.vue
index 231f45d7ae6..08eaa7c8ecd 100644
--- a/app/assets/javascripts/ci/job_details/components/sidebar/sidebar.vue
+++ b/app/assets/javascripts/ci/job_details/components/sidebar/sidebar.vue
@@ -85,7 +85,7 @@ export default {
};
</script>
<template>
- <aside class="right-sidebar build-sidebar" data-offset-top="101" data-spy="affix">
+ <aside class="right-sidebar build-sidebar">
<div class="sidebar-container">
<div class="blocks-container gl-p-4 gl-pt-0">
<sidebar-header
diff --git a/app/assets/javascripts/emoji/queries/custom_emoji.query.graphql b/app/assets/javascripts/emoji/queries/custom_emoji.query.graphql
index 951027ec274..ba3911ab091 100644
--- a/app/assets/javascripts/emoji/queries/custom_emoji.query.graphql
+++ b/app/assets/javascripts/emoji/queries/custom_emoji.query.graphql
@@ -1,7 +1,7 @@
query getCustomEmoji($groupPath: ID!) {
group(fullPath: $groupPath) {
id
- customEmoji {
+ customEmoji(includeAncestorGroups: true) {
nodes {
id
name
diff --git a/app/assets/javascripts/work_items/components/item_title.vue b/app/assets/javascripts/work_items/components/item_title.vue
index 74bcc2717bd..23a9671c914 100644
--- a/app/assets/javascripts/work_items/components/item_title.vue
+++ b/app/assets/javascripts/work_items/components/item_title.vue
@@ -18,6 +18,18 @@ export default {
required: false,
default: false,
},
+ useH1: {
+ type: Boolean,
+ default: false,
+ required: false,
+ },
+ },
+ computed: {
+ headerClasses() {
+ return this.useH1
+ ? 'gl-w-full gl-font-size-h-display gl-m-0!'
+ : 'gl-font-weight-normal gl-sm-font-weight-bold gl-mb-1 gl-mt-0 gl-w-full';
+ },
},
methods: {
handleBlur({ target }) {
@@ -39,9 +51,10 @@ export default {
</script>
<template>
- <h2
- class="gl-font-weight-normal gl-sm-font-weight-bold gl-mb-1 gl-mt-0 gl-w-full"
- :class="{ 'gl-cursor-text': disabled }"
+ <component
+ :is="useH1 ? 'h1' : 'h2'"
+ class="gl-w-full"
+ :class="[{ 'gl-cursor-text': disabled }, headerClasses]"
aria-labelledby="item-title"
>
<span
@@ -64,5 +77,5 @@ export default {
@keydown.meta.b.prevent
>{{ title }}</span
>
- </h2>
+ </component>
</template>
diff --git a/app/assets/javascripts/work_items/components/notes/work_item_add_note.vue b/app/assets/javascripts/work_items/components/notes/work_item_add_note.vue
index c3b7b7a2953..3636f222c2d 100644
--- a/app/assets/javascripts/work_items/components/notes/work_item_add_note.vue
+++ b/app/assets/javascripts/work_items/components/notes/work_item_add_note.vue
@@ -287,9 +287,9 @@ export default {
v-else
ref="textarea"
rows="1"
- class="reply-placeholder-text-field gl-font-regular!"
+ class="reply-placeholder-text-field"
data-testid="note-reply-textarea"
- :placeholder="__('Reply')"
+ :placeholder="__('Reply…')"
:aria-label="__('Reply to comment')"
@focus="showReplyForm"
@click="showReplyForm"
diff --git a/app/assets/javascripts/work_items/components/notes/work_item_notes_activity_header.vue b/app/assets/javascripts/work_items/components/notes/work_item_notes_activity_header.vue
index 1578c78ac4f..722ba898f80 100644
--- a/app/assets/javascripts/work_items/components/notes/work_item_notes_activity_header.vue
+++ b/app/assets/javascripts/work_items/components/notes/work_item_notes_activity_header.vue
@@ -36,6 +36,16 @@ export default {
default: WORK_ITEM_NOTES_FILTER_ALL_NOTES,
required: false,
},
+ useH2: {
+ type: Boolean,
+ default: false,
+ required: false,
+ },
+ },
+ computed: {
+ headerClasses() {
+ return this.useH2 ? 'gl-font-size-h1 gl-m-0' : 'gl-font-base gl-m-0';
+ },
},
methods: {
changeNotesSortOrder(direction) {
@@ -58,7 +68,9 @@ export default {
<div
class="gl-display-flex gl-justify-content-space-between gl-flex-wrap gl-pb-3 gl-align-items-center"
>
- <h3 class="gl-font-base gl-m-0">{{ $options.i18n.activityLabel }}</h3>
+ <component :is="useH2 ? 'h2' : 'h3'" :class="headerClasses">{{
+ $options.i18n.activityLabel
+ }}</component>
<div class="gl-display-flex gl-gap-3">
<work-item-activity-sort-filter
:work-item-type="workItemType"
diff --git a/app/assets/javascripts/work_items/components/work_item_detail.vue b/app/assets/javascripts/work_items/components/work_item_detail.vue
index ec17b635d22..ad9eb936d85 100644
--- a/app/assets/javascripts/work_items/components/work_item_detail.vue
+++ b/app/assets/javascripts/work_items/components/work_item_detail.vue
@@ -492,6 +492,7 @@ export default {
:work-item-type="workItemType"
:work-item-parent-id="workItemParentId"
:can-update="canUpdate"
+ :use-h1="!isModal"
@error="updateError = $event"
/>
<work-item-created-updated
@@ -582,6 +583,7 @@ export default {
:report-abuse-path="reportAbusePath"
:is-work-item-confidential="workItem.confidential"
class="gl-pt-5"
+ :use-h2="!isModal"
@error="updateError = $event"
@has-notes="updateHasNotes"
@openReportAbuse="openReportAbuseDrawer"
diff --git a/app/assets/javascripts/work_items/components/work_item_notes.vue b/app/assets/javascripts/work_items/components/work_item_notes.vue
index 83958ee4ef3..faf43c3d5dd 100644
--- a/app/assets/javascripts/work_items/components/work_item_notes.vue
+++ b/app/assets/javascripts/work_items/components/work_item_notes.vue
@@ -89,6 +89,11 @@ export default {
required: false,
default: false,
},
+ useH2: {
+ type: Boolean,
+ default: false,
+ required: false,
+ },
},
data() {
return {
@@ -330,6 +335,7 @@ export default {
:disable-activity-filter-sort="disableActivityFilterSort"
:work-item-type="workItemType"
:discussion-filter="discussionFilter"
+ :use-h2="useH2"
@changeSort="changeNotesSortOrder"
@changeFilter="filterDiscussions"
/>
diff --git a/app/assets/javascripts/work_items/components/work_item_title.vue b/app/assets/javascripts/work_items/components/work_item_title.vue
index 9b5803421dd..8bdf616cf47 100644
--- a/app/assets/javascripts/work_items/components/work_item_title.vue
+++ b/app/assets/javascripts/work_items/components/work_item_title.vue
@@ -42,6 +42,11 @@ export default {
required: false,
default: false,
},
+ useH1: {
+ type: Boolean,
+ default: false,
+ required: false,
+ },
},
computed: {
tracking() {
@@ -101,5 +106,10 @@ export default {
</script>
<template>
- <item-title :title="workItemTitle" :disabled="!canUpdate" @title-changed="updateTitle" />
+ <item-title
+ :title="workItemTitle"
+ :disabled="!canUpdate"
+ :use-h1="useH1"
+ @title-changed="updateTitle"
+ />
</template>
diff --git a/app/assets/stylesheets/framework/contextual_sidebar.scss b/app/assets/stylesheets/framework/contextual_sidebar.scss
index defe67b68ad..792357acc2c 100644
--- a/app/assets/stylesheets/framework/contextual_sidebar.scss
+++ b/app/assets/stylesheets/framework/contextual_sidebar.scss
@@ -65,10 +65,6 @@
.avatar-container {
margin: 0 auto;
}
-
- li.active:not(.fly-out-top-item) > a {
- background-color: $indigo-900-alpha-008;
- }
}
@mixin sub-level-items-flyout {
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index bcb48d69b6b..2ba720f8083 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -110,9 +110,6 @@ $t-gray-a-24: rgba($gray-950, 0.24) !default;
$white-dark: darken($gray-50, 2) !default;
-// To do this variant right for darkmode, we need to create a variable for it.
-$indigo-900-alpha-008: rgba($theme-indigo-900, 0.08);
-
$border-white-light: darken($white, $darken-border-factor) !default;
$border-white-normal: darken($gray-50, $darken-border-factor) !default;
diff --git a/app/assets/stylesheets/page_bundles/build.scss b/app/assets/stylesheets/page_bundles/build.scss
index b25ab5564ac..37014292925 100644
--- a/app/assets/stylesheets/page_bundles/build.scss
+++ b/app/assets/stylesheets/page_bundles/build.scss
@@ -82,6 +82,7 @@
.right-sidebar.build-sidebar {
padding: 0;
+ top: $calc-application-header-height;
@include media-breakpoint-up(lg) {
@include gl-border-l-0;
@@ -92,9 +93,7 @@
}
.sidebar-container {
- @include gl-sticky;
- top: #{$top-bar-height - 1px};
- max-height: calc(100vh - #{$top-bar-height - 1px} - var(--performance-bar-height));
+ max-height: 100%;
overflow-y: scroll;
overflow-x: hidden;
-webkit-overflow-scrolling: touch;
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index e5808c71a6d..5d644d63666 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -326,7 +326,6 @@ table {
.discussion-reply-holder {
.reply-placeholder-text-field {
- @include gl-font-monospace;
border-radius: $gl-border-radius-base;
width: 100%;
resize: none;
diff --git a/app/assets/stylesheets/themes/_dark.scss b/app/assets/stylesheets/themes/_dark.scss
index 0fce87faa29..e61308e3d81 100644
--- a/app/assets/stylesheets/themes/_dark.scss
+++ b/app/assets/stylesheets/themes/_dark.scss
@@ -8,19 +8,6 @@ $gray-dark: darken($gray-100, 2);
$gray-darker: darken($gray-200, 2);
$gray-darkest: $gray-700;
-// $data-viz blue shades required for $calendar-activity-colors
-$data-viz-blue-50: #2a2b59;
-$data-viz-blue-100: #303470;
-$data-viz-blue-200: #374291;
-$data-viz-blue-300: #3f51ae;
-$data-viz-blue-400: #4e65cd;
-$data-viz-blue-500: #617ae2;
-$data-viz-blue-600: #7992f5;
-$data-viz-blue-700: #97acff;
-$data-viz-blue-800: #b7c6ff;
-$data-viz-blue-900: #d2dcff;
-$data-viz-blue-950: #e9ebff;
-
// Some of the other $t-gray-a variables are used
// for borders and some other places, so we cannot override
// them. These are used only for box shadows so we can
@@ -30,8 +17,6 @@ $t-gray-a-24: rgba($gray-10, 0.24);
$black-normal: $gray-900;
$white-dark: $gray-100;
-$theme-indigo-50: #1a1a40;
-
$border-color: #4f4f4f;
$border-white-normal: $border-color;
diff --git a/app/graphql/mutations/container_registry/protection/rule/delete.rb b/app/graphql/mutations/container_registry/protection/rule/delete.rb
new file mode 100644
index 00000000000..b1673b7c43e
--- /dev/null
+++ b/app/graphql/mutations/container_registry/protection/rule/delete.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+module Mutations
+ module ContainerRegistry
+ module Protection
+ module Rule
+ class Delete < ::Mutations::BaseMutation
+ graphql_name 'DeleteContainerRegistryProtectionRule'
+ description 'Deletes a container registry protection rule. ' \
+ 'Available only when feature flag `container_registry_protected_containers` is enabled.'
+
+ authorize :admin_container_image
+
+ argument :id,
+ ::Types::GlobalIDType[::ContainerRegistry::Protection::Rule],
+ required: true,
+ description: 'Global ID of the container registry protection rule to delete.'
+
+ field :container_registry_protection_rule,
+ Types::ContainerRegistry::Protection::RuleType,
+ null: true,
+ description: 'Container registry protection rule that was deleted successfully.'
+
+ def resolve(id:, **_kwargs)
+ if Feature.disabled?(:container_registry_protected_containers)
+ raise_resource_not_available_error!("'container_registry_protected_containers' feature flag is disabled")
+ end
+
+ container_registry_protection_rule = authorized_find!(id: id)
+
+ response = ::ContainerRegistry::Protection::DeleteRuleService.new(container_registry_protection_rule,
+ current_user: current_user).execute
+
+ { container_registry_protection_rule: response.payload[:container_registry_protection_rule],
+ errors: response.errors }
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb
index ec5a3ffa177..9b3c443200d 100644
--- a/app/graphql/types/mutation_type.rb
+++ b/app/graphql/types/mutation_type.rb
@@ -138,6 +138,7 @@ module Types
mount_mutation Mutations::DesignManagement::Update
mount_mutation Mutations::ContainerExpirationPolicies::Update
mount_mutation Mutations::ContainerRegistry::Protection::Rule::Create, alpha: { milestone: '16.6' }
+ mount_mutation Mutations::ContainerRegistry::Protection::Rule::Delete, alpha: { milestone: '16.7' }
mount_mutation Mutations::ContainerRepositories::Destroy
mount_mutation Mutations::ContainerRepositories::DestroyTags
mount_mutation Mutations::Ci::Catalog::Resources::Create, alpha: { milestone: '15.11' }
diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb
index aad3e07ea28..e67e6c22e1a 100644
--- a/app/helpers/notes_helper.rb
+++ b/app/helpers/notes_helper.rb
@@ -65,7 +65,7 @@ module NotesHelper
content_tag(
:textarea,
rows: 1,
- placeholder: _('Reply...'),
+ placeholder: _('Reply…'),
'aria-label': _('Reply to comment'),
class: 'reply-placeholder-text-field js-discussion-reply-button',
data: {
diff --git a/app/services/container_registry/protection/delete_rule_service.rb b/app/services/container_registry/protection/delete_rule_service.rb
new file mode 100644
index 00000000000..bfd91c75b8b
--- /dev/null
+++ b/app/services/container_registry/protection/delete_rule_service.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+module ContainerRegistry
+ module Protection
+ class DeleteRuleService
+ include Gitlab::Allowable
+
+ def initialize(container_registry_protection_rule, current_user:)
+ if container_registry_protection_rule.blank? || current_user.blank?
+ raise ArgumentError,
+ 'container_registry_protection_rule and current_user must be set'
+ end
+
+ @container_registry_protection_rule = container_registry_protection_rule
+ @current_user = current_user
+ end
+
+ def execute
+ unless can?(current_user, :admin_container_image, container_registry_protection_rule.project)
+ error_message = _('Unauthorized to delete a container registry protection rule')
+ return service_response_error(message: error_message)
+ end
+
+ deleted_container_registry_protection_rule = container_registry_protection_rule.destroy!
+
+ ServiceResponse.success(
+ payload: { container_registry_protection_rule: deleted_container_registry_protection_rule }
+ )
+ rescue StandardError => e
+ service_response_error(message: e.message)
+ end
+
+ private
+
+ attr_reader :container_registry_protection_rule, :current_user
+
+ def service_response_error(message:)
+ ServiceResponse.error(
+ message: message,
+ payload: { container_registry_protection_rule: nil }
+ )
+ end
+ end
+ end
+end
diff --git a/app/views/protected_branches/shared/_create_protected_branch.html.haml b/app/views/protected_branches/shared/_create_protected_branch.html.haml
index bb1d56dcc61..48c3752e826 100644
--- a/app/views/protected_branches/shared/_create_protected_branch.html.haml
+++ b/app/views/protected_branches/shared/_create_protected_branch.html.haml
@@ -39,7 +39,7 @@
= render Pajamas::ToggleComponent.new(classes: 'js-force-push-toggle',
label: s_("ProtectedBranch|Allowed to force push"),
label_position: :hidden) do
- - force_push_docs_url = help_page_url('topics/git/git_rebase', anchor: 'force-push')
+ - force_push_docs_url = help_page_url('topics/git/git_rebase', anchor: 'force-pushing')
- force_push_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: force_push_docs_url }
= (s_("ProtectedBranch|Allow all users with push access to %{tag_start}force push%{tag_end}.") % { tag_start: force_push_link_start, tag_end: '</a>' }).html_safe
= render_if_exists 'protected_branches/ee/code_owner_approval_form', f: f, protected_branch_entity: protected_branch_entity
diff --git a/config/application.rb b/config/application.rb
index 7fd209b1191..d9a237eba50 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -315,7 +315,6 @@ module Gitlab
config.assets.precompile << "page_bundles/jira_connect.css"
config.assets.precompile << "page_bundles/learn_gitlab.css"
config.assets.precompile << "page_bundles/login.css"
- config.assets.precompile << "page_bundles/marketing_popover.css"
config.assets.precompile << "page_bundles/members.css"
config.assets.precompile << "page_bundles/merge_conflicts.css"
config.assets.precompile << "page_bundles/merge_request_analytics.css"
diff --git a/config/feature_flags/development/bitbucket_server_importer_exponential_backoff.yml b/config/feature_flags/development/bitbucket_server_importer_exponential_backoff.yml
new file mode 100644
index 00000000000..c167efddf49
--- /dev/null
+++ b/config/feature_flags/development/bitbucket_server_importer_exponential_backoff.yml
@@ -0,0 +1,8 @@
+---
+name: bitbucket_server_importer_exponential_backoff
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/137974
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/432974
+milestone: '16.7'
+type: development
+group: group::import and integrate
+default_enabled: false
diff --git a/config/initializers/7_redis.rb b/config/initializers/7_redis.rb
index 25c2c6aa11f..64aee6f760d 100644
--- a/config/initializers/7_redis.rb
+++ b/config/initializers/7_redis.rb
@@ -27,6 +27,8 @@ Redis::Cluster::SlotLoader.prepend(Gitlab::Patch::SlotLoader)
Redis::Cluster::CommandLoader.prepend(Gitlab::Patch::CommandLoader)
Redis::Cluster.prepend(Gitlab::Patch::RedisCluster)
+ConnectionPool.prepend(Gitlab::Instrumentation::ConnectionPool)
+
if Gitlab::Redis::Workhorse.params[:cluster].present?
raise "Do not configure workhorse with a Redis Cluster as pub/sub commands are not cluster-compatible."
end
diff --git a/db/post_migrate/20231124213241_add_index_to_bulk_imports_on_updated_at_and_status.rb b/db/post_migrate/20231124213241_add_index_to_bulk_imports_on_updated_at_and_status.rb
new file mode 100644
index 00000000000..c3e657a4873
--- /dev/null
+++ b/db/post_migrate/20231124213241_add_index_to_bulk_imports_on_updated_at_and_status.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class AddIndexToBulkImportsOnUpdatedAtAndStatus < Gitlab::Database::Migration[2.2]
+ milestone '16.7'
+ disable_ddl_transaction!
+
+ INDEX_NAME = 'index_bulk_imports_on_updated_at_and_id_for_stale_status'
+
+ def up
+ add_concurrent_index :bulk_imports, [:updated_at, :id],
+ where: 'STATUS in (0, 1)', name: INDEX_NAME
+ end
+
+ def down
+ remove_concurrent_index_by_name :bulk_imports, name: INDEX_NAME
+ end
+end
diff --git a/db/schema_migrations/20231124213241 b/db/schema_migrations/20231124213241
new file mode 100644
index 00000000000..d2dab8b5c41
--- /dev/null
+++ b/db/schema_migrations/20231124213241
@@ -0,0 +1 @@
+15853bc68a9e5bbf2e45ed646f3630fcfbeed9a8a21b1edbd02f92946b410b88 \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 4084e589f0c..c64a5a234c4 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -31920,6 +31920,8 @@ CREATE INDEX index_bulk_import_failures_on_bulk_import_entity_id ON bulk_import_
CREATE INDEX index_bulk_import_failures_on_correlation_id_value ON bulk_import_failures USING btree (correlation_id_value);
+CREATE INDEX index_bulk_imports_on_updated_at_and_id_for_stale_status ON bulk_imports USING btree (updated_at, id) WHERE (status = ANY (ARRAY[0, 1]));
+
CREATE INDEX index_bulk_imports_on_user_id ON bulk_imports USING btree (user_id);
CREATE INDEX index_catalog_resource_components_on_catalog_resource_id ON catalog_resource_components USING btree (catalog_resource_id);
diff --git a/doc/.vale/gitlab/SubstitutionWarning.yml b/doc/.vale/gitlab/SubstitutionWarning.yml
index d4bbe9fd83b..fe15b8fc42c 100644
--- a/doc/.vale/gitlab/SubstitutionWarning.yml
+++ b/doc/.vale/gitlab/SubstitutionWarning.yml
@@ -30,6 +30,7 @@ swap:
ex: "for example"
filename: "file name"
filesystem: "file system"
+ fullscreen: "full screen"
info: "information"
installation from source: self-compiled installation
installations from source: self-compiled installations
diff --git a/doc/administration/monitoring/prometheus/gitlab_metrics.md b/doc/administration/monitoring/prometheus/gitlab_metrics.md
index 1ec7a9c6bc5..7c2a6c6c06d 100644
--- a/doc/administration/monitoring/prometheus/gitlab_metrics.md
+++ b/doc/administration/monitoring/prometheus/gitlab_metrics.md
@@ -177,6 +177,8 @@ The following metrics are available:
| `gitlab_ci_queue_iteration_duration_seconds` | Histogram | 16.3 | Time it takes to find a build in CI/CD queue |
| `gitlab_ci_queue_retrieval_duration_seconds` | Histogram | 16.3 | Time it takes to execute a SQL query to retrieve builds queue |
| `gitlab_ci_queue_active_runners_total` | Histogram | 16.3 | The amount of active runners that can process queue in a project |
+| `gitlab_connection_pool_size` | Gauge | 16.7 | Size of connection pool |
+| `gitlab_connection_pool_available_count` | Gauge | 16.7 | Number of available connections in the pool |
## Metrics controlled by a feature flag
diff --git a/doc/administration/server_hooks.md b/doc/administration/server_hooks.md
index 61ed5d751ec..3abd18bec9a 100644
--- a/doc/administration/server_hooks.md
+++ b/doc/administration/server_hooks.md
@@ -18,8 +18,8 @@ on the GitLab server. You can use them to run Git-related tasks such as:
Git server hooks use `pre-receive`, `post-receive`, and `update`
[Git server-side hooks](https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks#_server_side_hooks).
-GitLab administrators configure server hooks on the file system of the GitLab server. If you don't have file system access,
-alternatives to server hooks include:
+GitLab administrators configure server hooks through the Gitaly CLI, which connects to the Gitaly gRPC API.
+If you don't have access to the Gitaly CLI, alternatives to server hooks include:
- [Webhooks](../user/project/integrations/webhooks.md).
- [GitLab CI/CD](../ci/index.md).
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index d6d901f9532..f8a97632623 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -3241,6 +3241,31 @@ Input type: `DeleteAnnotationInput`
| <a id="mutationdeleteannotationclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationdeleteannotationerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
+### `Mutation.deleteContainerRegistryProtectionRule`
+
+Deletes a container registry protection rule. Available only when feature flag `container_registry_protected_containers` is enabled.
+
+WARNING:
+**Introduced** in 16.7.
+This feature is an Experiment. It can be changed or removed at any time.
+
+Input type: `DeleteContainerRegistryProtectionRuleInput`
+
+#### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="mutationdeletecontainerregistryprotectionruleclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| <a id="mutationdeletecontainerregistryprotectionruleid"></a>`id` | [`ContainerRegistryProtectionRuleID!`](#containerregistryprotectionruleid) | Global ID of the container registry protection rule to delete. |
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="mutationdeletecontainerregistryprotectionruleclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| <a id="mutationdeletecontainerregistryprotectionrulecontainerregistryprotectionrule"></a>`containerRegistryProtectionRule` | [`ContainerRegistryProtectionRule`](#containerregistryprotectionrule) | Container registry protection rule that was deleted successfully. |
+| <a id="mutationdeletecontainerregistryprotectionruleerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
+
### `Mutation.deletePackagesProtectionRule`
Deletes a protection rule for packages. Available only when feature flag `packages_protected_packages` is enabled.
diff --git a/doc/ci/environments/deployment_approvals.md b/doc/ci/environments/deployment_approvals.md
index 3b19569ad4c..8545f329138 100644
--- a/doc/ci/environments/deployment_approvals.md
+++ b/doc/ci/environments/deployment_approvals.md
@@ -215,11 +215,7 @@ The approval status details are shown:
## View blocked deployments
-Use the UI or API to review the status of your deployments, including whether a deployment is blocked.
-
-::Tabs
-
-:::TabTitle With the UI
+Review the status of your deployments, including whether a deployment is blocked.
To view your deployments:
@@ -229,16 +225,9 @@ To view your deployments:
A deployment with the **blocked** label is blocked.
-:::TabTitle With the API
-
-To view your deployments:
-
-- Using the [deployments API](../../api/deployments.md#get-a-specific-deployment), get a specific deployment, or a list of all deployments in a project.
-
+To view your deployments, you can also [use the API](../../api/deployments.md#get-a-specific-deployment).
The `status` field indicates whether a deployment is blocked.
-::EndTabs
-
## Related topics
- [Deployment approvals feature epic](https://gitlab.com/groups/gitlab-org/-/epics/6832)
diff --git a/doc/development/ai_features/index.md b/doc/development/ai_features/index.md
index f550ad0c715..35c329ff1e6 100644
--- a/doc/development/ai_features/index.md
+++ b/doc/development/ai_features/index.md
@@ -72,6 +72,7 @@ RAILS_ENV=development bundle exec rake gitlab:duo:setup['<test-group-name>']
1. **Group Settings** > **General** -> **Permissions and group features**
1. Enable **Experiment & Beta features**
1. Enable the specific feature flag for the feature you want to test
+1. You can use Rake task `rake gitlab:duo:enable_feature_flags` to enable all feature flags that are assigned to group AI Framework
1. Set the required access token. To receive an access token:
1. For Vertex, follow the [instructions below](#configure-gcp-vertex-access).
1. For all other providers, like Anthropic, create an access request where `@m_gill`, `@wayne`, and `@timzallmann` are the tech stack owners.
diff --git a/doc/development/documentation/styleguide/word_list.md b/doc/development/documentation/styleguide/word_list.md
index affe2bc0991..3cbafc8f2c4 100644
--- a/doc/development/documentation/styleguide/word_list.md
+++ b/doc/development/documentation/styleguide/word_list.md
@@ -700,6 +700,11 @@ The **upstream project** (also known as the **source project**) and the **fork**
If the **fork relationship** is removed, the
**fork** is **unlinked** from the **upstream project**.
+## full screen
+
+Use two words for **full screen**.
+([Vale](../testing.md#vale) rule: [`SubstitutionWarning.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/.vale/gitlab/SubstitutionWarning.yml))
+
## future tense
When possible, use present tense instead of future tense. For example, use **after you execute this command, GitLab displays the result** instead of **after you execute this command, GitLab will display the result**. ([Vale](../testing.md#vale) rule: [`FutureTense.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/.vale/gitlab/FutureTense.yml))
diff --git a/doc/gitlab-basics/start-using-git.md b/doc/gitlab-basics/start-using-git.md
index 473986bc4da..de7b727fd18 100644
--- a/doc/gitlab-basics/start-using-git.md
+++ b/doc/gitlab-basics/start-using-git.md
@@ -369,7 +369,7 @@ git push origin main
```
Sometimes Git does not allow you to push to a repository. Instead,
-you must [force an update](../topics/git/git_rebase.md#force-push).
+you must [force an update](../topics/git/git_rebase.md#force-pushing).
### Delete all changes in the branch
diff --git a/doc/topics/git/git_rebase.md b/doc/topics/git/git_rebase.md
index 05773ec7e92..e4dce8bbf57 100644
--- a/doc/topics/git/git_rebase.md
+++ b/doc/topics/git/git_rebase.md
@@ -7,95 +7,107 @@ description: "Introduction to Git rebase and force push, methods to resolve merg
# Git rebase and force push **(FREE ALL)**
-This guide helps you to get started with rebases, force pushes, and fixing
-[merge conflicts](../../user/project/merge_requests/conflicts.md) locally.
-Before you attempt a force push or a rebase, make sure you are familiar with
-[Git through the command line](../../gitlab-basics/start-using-git.md).
+In Git, a rebase updates your branch with the contents of another branch.
+A rebase confirms that changes in your branch don't conflict with
+changes in the target branch.
-WARNING:
-`git rebase` rewrites the commit history. It **can be harmful** to do it in
-shared branches. It can cause complex and hard to resolve
-[merge conflicts](../../user/project/merge_requests/conflicts.md). In
-these cases, instead of rebasing your branch against the default branch,
-consider pulling it instead (`git pull origin master`). Pulling has similar
-effects with less risk compromising the work of your contributors.
+If you have a [merge conflict](../../user/project/merge_requests/conflicts.md),
+you can rebase to fix it.
-In Git, a rebase updates your feature branch with the contents of another branch.
-This step is important for Git-based development strategies. Use a rebase to confirm
-that your branch's changes don't conflict with any changes added to your target branch
-_after_ you created your feature branch.
+## What happens during rebase
When you rebase:
-1. Git imports all the commits submitted to your target branch _after_ you initially created
- your feature branch from it.
-1. Git stacks the commits you have in your feature branch on top of all
+1. Git imports all the commits submitted to your target branch after you initially created
+ your branch from it.
+1. Git stacks the commits you have in your branch on top of all
the commits it imported from that branch:
-![Git rebase illustration](img/git_rebase_v13_5.png)
+ ![Git rebase illustration](img/git_rebase_v13_5.png)
While most rebases are performed against `main`, you can rebase against any other
branch, such as `release-15-3`. You can also specify a different remote repository
(such as `upstream`) instead of `origin`.
-## Back up a branch before rebase
+WARNING:
+`git rebase` rewrites the commit history. It **can be harmful** to do it in
+shared branches. It can cause complex and hard to resolve
+merge conflicts. Instead of rebasing your branch against the default branch,
+consider pulling it instead (`git pull origin master`). Pulling has similar
+effects with less risk of compromising others' work.
-To back up a branch before taking any destructive action, like a rebase or force push:
+## Rebase by using Git
-1. Open your feature branch in the terminal: `git checkout my-feature`
-1. Create a backup branch: `git branch my-feature-backup`
- Any changes added to `my-feature` after this point are lost
- if you restore from the backup branch.
+When you use Git to rebase, each commit is applied to your branch.
+When merge conflicts occur, you are prompted to address them.
-Your branch is backed up, and you can try a rebase or a force push.
-If anything goes wrong, restore your branch from its backup:
+If you want more advanced options for your commits,
+do [an interactive rebase](#rebase-interactively-by-using-git).
-1. Make sure you're in the correct branch (`my-feature`): `git checkout my-feature`
-1. Reset it against `my-feature-backup`: `git reset --hard my-feature-backup`
+Prerequisites:
-## Rebase a branch
+- You must have permission to force push to branches.
-[Rebases](https://git-scm.com/docs/git-rebase) are very common operations in
-Git, and have these options:
+To use Git to rebase your branch against the target branch:
-- **Regular rebases.** This type of rebase can be done through the
- [command line](#regular-rebase) and [the GitLab UI](#from-the-gitlab-ui).
-- [**Interactive rebases**](#interactive-rebase) give more flexibility by
- enabling you to specify how to handle each commit. Interactive rebases
- must be done on the command line.
+1. Open a terminal and change to your project.
+1. Ensure you have the latest contents of the target branch.
+ In this example, the target branch is `main`:
-Any user who rebases a branch is treated as having added commits to that branch.
-If a project is configured to
-[**prevent approvals by users who add commits**](../../user/project/merge_requests/approvals/settings.md#prevent-approvals-by-users-who-add-commits),
-a user who rebases a branch cannot also approve its merge request.
+ ```shell
+ git fetch origin main
+ ```
-### Regular rebase
+1. Check out your branch:
-Standard rebases replay the previous commits on a branch without changes, stopping
-only if merge conflicts occur.
+ ```shell
+ git checkout my-branch
+ ```
-Prerequisites:
+1. Optional. Create a backup of your branch:
-- You must have permission to force push branches.
+ ```shell
+ git branch my-branch-backup
+ ```
-To update your branch `my-feature` with recent changes from your
-[default branch](../../user/project/repository/branches/default.md) (here, using `main`):
+ Changes added to `my-branch` after this point are lost
+ if you restore from the backup branch.
-1. Fetch the latest changes from `main`: `git fetch origin main`
-1. Check out your feature branch: `git checkout my-feature`
-1. Rebase it against `main`: `git rebase origin/main`
-1. [Force push](#force-push) to your branch.
+1. Rebase against the main branch:
-If there are merge conflicts, Git prompts you to fix them before continuing the rebase.
+ ```shell
+ git rebase origin/main
+ ```
-### From the GitLab UI
+1. If merge conflicts exist:
+ 1. Fix the conflicts in your editor.
-The `/rebase` [quick action](../../user/project/quick_actions.md#issues-merge-requests-and-epics)
-rebases your feature branch directly from its merge request if all of these
-conditions are met:
+ 1. Add the files:
-- No merge conflicts exist for your feature branch.
-- You have the **Developer** role for the source project. This role grants you
+ ```shell
+ git add .
+ ```
+
+ 1. Continue the rebase:
+
+ ```shell
+ git rebase --continue
+ ```
+
+1. Force push your changes to the target branch, while protecting others' commits:
+
+ ```shell
+ git push origin my-branch --force-with-lease
+ ```
+
+## Rebase from the UI
+
+You can rebase a merge request from the GitLab UI.
+
+Prerequisites:
+
+- No merge conflicts must exist.
+- You must have at least the **Developer** role for the source project. This role grants you
permission to push to the source branch for the source project.
- If the merge request is in a fork, the fork must allow commits
[from members of the upstream project](../../user/project/merge_requests/allow_collaboration.md).
@@ -106,91 +118,112 @@ To rebase from the UI:
1. Type `/rebase` in a comment.
1. Select **Comment**.
-GitLab schedules a rebase of the feature branch against the default branch and
+GitLab schedules a rebase of the branch against the default branch and
executes it as soon as possible.
-### Interactive rebase
-
-Use an interactive rebase (the `--interactive` flag, or `-i`) to simultaneously
-update a branch while you modify how its commits are handled.
-For example, to edit the last five commits in your branch (`HEAD~5`), run:
-
-```shell
-git rebase -i HEAD~5
-```
-
-Git opens the last five commits in your terminal text editor, oldest commit first.
-Each commit shows the action to take on it, the SHA, and the commit title:
-
-```shell
-pick 111111111111 Second round of structural revisions
-pick 222222222222 Update inbound link to this changed page
-pick 333333333333 Shifts from H4 to H3
-pick 444444444444 Adds revisions from editorial
-pick 555555555555 Revisions continue to build the concept part out
-
-# Rebase 111111111111..222222222222 onto zzzzzzzzzzzz (5 commands)
-#
-# Commands:
-# p, pick <commit> = use commit
-# r, reword <commit> = use commit, but edit the commit message
-# e, edit <commit> = use commit, but stop for amending
-# s, squash <commit> = use commit, but meld into previous commit
-# f, fixup [-C | -c] <commit> = like "squash" but keep only the previous
-```
-
-After the list of commits, a commented-out section shows some common actions you
-can take on a commit:
-
-- **Pick** a commit to use it with no changes. The default option.
-- **Reword** a commit message.
-- **Edit** a commit to use it, but pause the rebase to amend (add changes to) it.
-- **Squash** multiple commits together to simplify the commit history
- of your feature branch.
-
-Replace the keyword `pick` according to
-the operation you want to perform in each commit. To do so, edit
-the commits in your terminal's text editor.
-
-For example, with [Vim](https://www.vim.org/) as the text editor in
-a macOS Zsh shell, you can `squash` or `fixup` (combine) all of the commits together:
-
-NOTE:
-The steps for editing through the command line can be slightly
-different depending on your operating system and the shell you use.
-
-1. Press <kbd>i</kbd> on your keyboard to switch to Vim's editing mode.
-1. Use your keyboard arrows to edit the **second** commit keyword
- from `pick` to `squash` or `fixup` (or `s` or `f`). Do the same to the remaining commits.
- Leave the first commit **unchanged** (`pick`) as we want to squash
- all other commits into it.
-1. Press <kbd>Escape</kbd> to leave the editing mode.
-1. Type `:wq` to "write" (save) and "quit".
+## Rebase interactively by using Git
+
+Use an interactive rebase when you want to specify how to handle each commit.
+You must do an interactive rebase from the command line.
+
+Prerequisites:
+
+- [Vim](https://www.vim.org/) must be your text editor to follow these instructions.
+
+To rebase interactively:
+
+1. Open a terminal and change to your project.
+1. Ensure you have the latest contents of the target branch.
+ In this example, the target branch is `main`:
+
+ ```shell
+ git fetch origin main
+ ```
+
+1. Check out your branch:
+
+ ```shell
+ git checkout my-branch
+ ```
+
+1. Optional. Create a backup of your branch:
+
+ ```shell
+ git branch my-branch-backup
+ ```
+
+ Changes added to `my-branch` after this point are lost
+ if you restore from the backup branch.
+
+1. In the GitLab UI, in your merge request, confirm how many commits
+ you want to rebase by viewing the **Commits** tab.
+
+1. Open these commits. For example, to edit the last five commits in your branch (`HEAD~5`), type:
+
+ ```shell
+ git rebase -i HEAD~5
+ ```
+
+ Git opens the last five commits in your terminal text editor, oldest commit first.
+ Each commit shows the action to take on it, the SHA, and the commit title:
+
+ ```shell
+ pick 111111111111 Second round of structural revisions
+ pick 222222222222 Update inbound link to this changed page
+ pick 333333333333 Shifts from H4 to H3
+ pick 444444444444 Adds revisions from editorial
+ pick 555555555555 Revisions continue to build the concept part out
+
+ # Rebase 111111111111..222222222222 onto zzzzzzzzzzzz (5 commands)
+ #
+ # Commands:
+ # p, pick <commit> = use commit
+ # r, reword <commit> = use commit, but edit the commit message
+ # e, edit <commit> = use commit, but stop for amending
+ # s, squash <commit> = use commit, but meld into previous commit
+ # f, fixup [-C | -c] <commit> = like "squash" but keep only the previous
+ ```
+
+1. Switch to Vim's edit mode by pressing <kbd>i</kbd>.
+1. Move to the second commit in the list by using your keyboard arrows.
+1. Change the word `pick` to `squash` or `fixup` (or `s` or `f`).
+1. Do the same for the remaining commits. Leave the first commit as `pick`.
+1. End edit mode, save, and quit:
+
+ - Press <kbd>ESC</kbd>.
+ - Type `:wq`.
+
1. When squashing, Git outputs the commit message so you have a chance to edit it:
+
- All lines starting with `#` are ignored and not included in the commit
- message. Everything else is included.
- - To leave it as it is, type `:wq`. To edit the commit message: switch to the
- editing mode, edit the commit message, and save it as you just did.
-1. If you haven't pushed your commits to the remote branch before rebasing,
- push your changes without a force push. If you had pushed these commits already,
- [force push](#force-push) instead.
+ message. Everything else is included.
+ - To leave it as-is, type `:wq`. To edit the commit message, switch to
+ edit mode, edit the commit message, and save.
+
+1. Commit to the target branch.
-#### Configure squash options for a project
+ - If you didn't push your commits to the target branch before rebasing,
+ push your changes without a force push:
-Keeping the default branch commit history clean doesn't require you to
-manually squash all your commits on each merge request. GitLab provides
-[squash and merge](../../user/project/merge_requests/squash_and_merge.md#configure-squash-options-for-a-project),
-options at a project level.
+ ```shell
+ git push origin my-branch
+ ```
-## Force push
+ - If you pushed these commits already, use a force push:
+
+ ```shell
+ git push origin my-branch --force-with-lease
+ ```
+
+## Force pushing
Complex operations in Git require you to force an update to the remote branch.
Operations like squashing commits, resetting a branch, or rebasing a branch rewrite
the history of your branch. Git requires a forced update to help safeguard against
these more destructive changes from happening accidentally.
-Force pushing is not recommended on shared branches, as you risk destroying the
-changes of others.
+Force pushing is not recommended on shared branches, because you risk destroying
+others' changes.
If the branch you want to force push is [protected](../../user/project/protected_branches.md),
you can't force push to it unless you either:
@@ -201,27 +234,32 @@ you can't force push to it unless you either:
Then you can force push and protect it again.
-### `--force-with-lease` flag
+## Restore your backed up branch
+
+Your branch is backed up, and you can try a rebase or a force push.
+If anything goes wrong, restore your branch from its backup:
+
+1. Make sure you're in the correct branch:
-The [`--force-with-lease`](https://git-scm.com/docs/git-push#Documentation/git-push.txt---force-with-leaseltrefnamegt)
-flag force pushes. Because it preserves any new commits added to the remote
-branch by other people, it is safer than `--force`:
+ ```shell
+ git checkout my-branch
+ ```
-```shell
-git push --force-with-lease origin my-feature
-```
+1. Reset your branch against the backup:
-### `--force` flag
+ ```shell
+ git reset --hard my-branch-backup
+ ```
-The `--force` flag forces pushes, but does not preserve any new commits added to
-the remote branch by other people. To use this method, pass the flag `--force` or `-f`
-to the `push` command:
+## Approving after rebase
-```shell
-git push --force origin my-feature
-```
+If you rebase a branch, you've added commits.
+If your project is configured to
+[prevent approvals by users who add commits](../../user/project/merge_requests/approvals/settings.md#prevent-approvals-by-users-who-add-commits),
+you can't approve a merge request if you have rebased it.
## Related topics
- [Numerous undo possibilities in Git](numerous_undo_possibilities_in_git/index.md#undo-staged-local-changes-without-modifying-history)
- [Git documentation for branches and rebases](https://git-scm.com/book/en/v2/Git-Branching-Rebasing)
+- [Project squash and merge settings](../../user/project/merge_requests/squash_and_merge.md#configure-squash-options-for-a-project)
diff --git a/doc/user/project/git_attributes.md b/doc/user/project/git_attributes.md
index f2280f68f1a..2a3e98037ce 100644
--- a/doc/user/project/git_attributes.md
+++ b/doc/user/project/git_attributes.md
@@ -51,3 +51,73 @@ For more information, see [working-tree-encoding](https://git-scm.com/docs/gitat
The `.gitattributes` file can be used to define which language to use when
syntax highlighting files and diffs. For more information, see
[Syntax highlighting](highlighting.md).
+
+## Custom merge drivers
+
+> Ability to configure custom merge drivers through GitLab introduced in GitLab 15.10.
+
+You can define [custom merge drivers](https://git-scm.com/docs/gitattributes#_defining_a_custom_merge_driver)
+in a GitLab configuration file, then use the custom merge drivers in a Git
+`.gitattributes` file.
+
+You might configure a custom merge driver, for example, if there are certain
+files that should be ignored during a merge such as build files and configuration files.
+
+### Configure a custom merge driver
+
+The following example illustrates how to define and use a custom merge driver in
+GitLab.
+
+How to configure a custom merge driver depends on the type of installation.
+
+::Tabs
+
+:::TabTitle Linux package (Omnibus)
+
+1. Edit `/etc/gitlab/gitlab.rb`.
+1. Add configuration similar to the following:
+
+ ```ruby
+ gitaly['configuration'] = {
+ # ...
+ git: {
+ # ...
+ config: [
+ # ...
+ { key: "merge.foo.driver", value: "true" },
+ ],
+ },
+ }
+ ```
+
+:::TabTitle Self-compiled (source)
+
+1. Edit `gitaly.toml`.
+1. Add configuration similar to the following:
+
+ ```toml
+ [[git.config]]
+ key = "merge.foo.driver"
+ value = "true"
+ ```
+
+::EndTabs
+
+In this example, during a merge, Git uses the `driver` value as the command to execute. In
+this case, because we are using [`true`](https://man7.org/linux/man-pages/man1/true.1.html)
+with no arguments, it always returns a non-zero return code. This means that for
+the files specified in `.gitattributes`, merges do nothing.
+
+To use your own merge driver, replace the value in `driver` to point to an
+executable. For more details on how this command is invoked, please see the Git
+documentation on [custom merge drivers](https://git-scm.com/docs/gitattributes#_defining_a_custom_merge_driver).
+
+### Use `.gitattributes` to set files custom merge driver applies to
+
+In a `.gitattributes` file, you can set the paths of files you want to use with the custom merge driver. For example:
+
+```plaintext
+config/* merge=foo
+```
+
+In this case, every file under the `config/` folder uses the custom merge driver called `foo` defined in the GitLab configuration.
diff --git a/doc/user/project/merge_requests/conflicts.md b/doc/user/project/merge_requests/conflicts.md
index 0ebf1c8ed9e..e4640b4d635 100644
--- a/doc/user/project/merge_requests/conflicts.md
+++ b/doc/user/project/merge_requests/conflicts.md
@@ -105,7 +105,7 @@ most control over each change:
git switch my-feature-branch
```
-1. [Rebase your branch](../../../topics/git/git_rebase.md#regular-rebase) against the
+1. [Rebase your branch](../../../topics/git/git_rebase.md#rebase-by-using-git) against the
target branch (here, `main`) so Git prompts you with the conflicts:
```shell
@@ -150,7 +150,7 @@ most control over each change:
running `git rebase`.
After you run `git rebase --continue`, you cannot abort the rebase.
-1. [Force-push](../../../topics/git/git_rebase.md#force-push) the changes to your
+1. [Force-push](../../../topics/git/git_rebase.md#force-pushing) the changes to your
remote branch.
## Merge commit strategy
diff --git a/doc/user/project/merge_requests/methods/index.md b/doc/user/project/merge_requests/methods/index.md
index 8acf380c6f6..9ecb3f8ae62 100644
--- a/doc/user/project/merge_requests/methods/index.md
+++ b/doc/user/project/merge_requests/methods/index.md
@@ -198,7 +198,7 @@ In these merge methods, you can merge only when your source branch is up-to-date
If a fast-forward merge is not possible but a conflict-free rebase is possible,
GitLab provides:
-- The [`/rebase` quick action](../../../../topics/git/git_rebase.md#from-the-gitlab-ui).
+- The [`/rebase` quick action](../../../../topics/git/git_rebase.md#rebase-from-the-ui).
- The option to select **Rebase** in the user interface.
You must rebase the source branch locally before a fast-forward merge if both
diff --git a/doc/user/project/protected_branches.md b/doc/user/project/protected_branches.md
index a77babb6cd6..0525729ef58 100644
--- a/doc/user/project/protected_branches.md
+++ b/doc/user/project/protected_branches.md
@@ -267,7 +267,7 @@ Deploy keys are not available in the **Allowed to merge** dropdown list.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/15611) in GitLab 13.10 [with a flag](../../administration/feature_flags.md) named `allow_force_push_to_protected_branches`. Disabled by default.
> - [Enabled on GitLab.com and self-managed](https://gitlab.com/gitlab-org/gitlab/-/issues/323431) in GitLab 14.0. Feature flag `allow_force_push_to_protected_branches` removed.
-You can allow [force pushes](../../topics/git/git_rebase.md#force-push) to
+You can allow [force pushes](../../topics/git/git_rebase.md#force-pushing) to
protected branches.
To protect a new branch and enable force push:
diff --git a/doc/user/project/settings/import_export_troubleshooting.md b/doc/user/project/settings/import_export_troubleshooting.md
index 79401a7734a..aa1317608fb 100644
--- a/doc/user/project/settings/import_export_troubleshooting.md
+++ b/doc/user/project/settings/import_export_troubleshooting.md
@@ -86,7 +86,7 @@ reduce the repository size for another import attempt:
1. To reduce the repository size, work on this `smaller-tmp-main` branch:
[identify and remove large files](../repository/reducing_the_repo_size_using_git.md)
- or [interactively rebase and fixup](../../../topics/git/git_rebase.md#interactive-rebase)
+ or [interactively rebase and fixup](../../../topics/git/git_rebase.md#rebase-interactively-by-using-git)
to reduce the number of commits.
```shell
diff --git a/gems/gitlab-secret_detection/Gemfile.lock b/gems/gitlab-secret_detection/Gemfile.lock
index 2f615b24d86..dd9f621ee4a 100644
--- a/gems/gitlab-secret_detection/Gemfile.lock
+++ b/gems/gitlab-secret_detection/Gemfile.lock
@@ -2,6 +2,8 @@ PATH
remote: .
specs:
gitlab-secret_detection (0.1.0)
+ re2 (~> 2.4)
+ toml-rb (~> 2.2)
GEM
remote: https://rubygems.org/
@@ -24,6 +26,7 @@ GEM
bigdecimal (3.1.4)
binding_of_caller (1.0.0)
debug_inspector (>= 0.0.1)
+ citrus (3.0.2)
coderay (1.1.3)
concurrent-ruby (1.2.2)
connection_pool (2.4.1)
@@ -31,8 +34,8 @@ GEM
diff-lcs (1.5.0)
drb (2.2.0)
ruby2_keywords
- gitlab-styles (10.1.0)
- rubocop (~> 1.50.2)
+ gitlab-styles (11.0.0)
+ rubocop (~> 1.57.1)
rubocop-graphql (~> 0.18)
rubocop-performance (~> 1.15)
rubocop-rails (~> 2.17)
@@ -40,6 +43,8 @@ GEM
i18n (1.14.1)
concurrent-ruby (~> 1.0)
json (2.6.3)
+ language_server-protocol (3.17.0.3)
+ mini_portile2 (2.8.5)
minitest (5.20.0)
mutex_m (0.2.0)
parallel (1.23.0)
@@ -50,9 +55,11 @@ GEM
coderay
parser
unparser
- racc (1.7.1)
+ racc (1.7.3)
rack (3.0.8)
rainbow (3.1.1)
+ re2 (2.4.3)
+ mini_portile2 (~> 2.8.5)
regexp_parser (2.8.2)
rexml (3.2.6)
rspec (3.12.0)
@@ -84,14 +91,15 @@ GEM
binding_of_caller
rspec-parameterized-core (< 2)
rspec-support (3.12.1)
- rubocop (1.50.2)
+ rubocop (1.57.2)
json (~> 2.3)
+ language_server-protocol (>= 3.17.0)
parallel (~> 1.10)
- parser (>= 3.2.0.0)
+ parser (>= 3.2.2.4)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.8, < 3.0)
rexml (>= 3.2.5, < 4.0)
- rubocop-ast (>= 1.28.0, < 2.0)
+ rubocop-ast (>= 1.28.1, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 3.0)
rubocop-ast (1.30.0)
@@ -115,10 +123,12 @@ GEM
rubocop-factory_bot (~> 2.22)
ruby-progressbar (1.13.0)
ruby2_keywords (0.0.5)
+ toml-rb (2.2.0)
+ citrus (~> 3.0, > 3.0)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
unicode-display_width (2.5.0)
- unparser (0.6.9)
+ unparser (0.6.10)
diff-lcs (~> 1.3)
parser (>= 3.2.2.4)
@@ -127,13 +137,13 @@ PLATFORMS
DEPENDENCIES
gitlab-secret_detection!
- gitlab-styles (~> 10.1.0)
+ gitlab-styles (~> 11.0)
rspec (~> 3.0)
rspec-benchmark (~> 0.6.0)
rspec-parameterized (~> 1.0)
- rubocop (~> 1.50)
+ rubocop (~> 1.57)
rubocop-rails (<= 2.20)
rubocop-rspec (~> 2.22)
BUNDLED WITH
- 2.4.14
+ 2.4.22
diff --git a/gems/gitlab-secret_detection/gitlab-secret_detection.gemspec b/gems/gitlab-secret_detection/gitlab-secret_detection.gemspec
index ff5121846f4..be9db3aa389 100644
--- a/gems/gitlab-secret_detection/gitlab-secret_detection.gemspec
+++ b/gems/gitlab-secret_detection/gitlab-secret_detection.gemspec
@@ -24,11 +24,14 @@ Gem::Specification.new do |spec|
spec.files = Dir['lib/**/*.rb']
spec.require_paths = ["lib"]
- spec.add_development_dependency "gitlab-styles", "~> 10.1.0"
+ spec.add_runtime_dependency "re2", "~> 2.4"
+ spec.add_runtime_dependency "toml-rb", "~> 2.2"
+
+ spec.add_development_dependency "gitlab-styles", "~> 11.0"
spec.add_development_dependency "rspec", "~> 3.0"
spec.add_development_dependency "rspec-benchmark", "~> 0.6.0"
spec.add_development_dependency "rspec-parameterized", "~> 1.0"
- spec.add_development_dependency "rubocop", "~> 1.50"
+ spec.add_development_dependency "rubocop", "~> 1.57"
spec.add_development_dependency "rubocop-rails", "<= 2.20" # https://github.com/rubocop/rubocop-rails/issues/1173
spec.add_development_dependency "rubocop-rspec", "~> 2.22"
end
diff --git a/gems/gitlab-secret_detection/lib/gitlab/secret_detection.rb b/gems/gitlab-secret_detection/lib/gitlab/secret_detection.rb
index 54e0eb794a3..95da376b7c1 100644
--- a/gems/gitlab-secret_detection/lib/gitlab/secret_detection.rb
+++ b/gems/gitlab-secret_detection/lib/gitlab/secret_detection.rb
@@ -1,6 +1,10 @@
# frozen_string_literal: true
-require_relative "secret_detection/version"
+require_relative 'secret_detection/version'
+require_relative 'secret_detection/status'
+require_relative 'secret_detection/finding'
+require_relative 'secret_detection/response'
+require_relative 'secret_detection/scan'
module Gitlab
module SecretDetection
diff --git a/gems/gitlab-secret_detection/lib/gitlab/secret_detection/finding.rb b/gems/gitlab-secret_detection/lib/gitlab/secret_detection/finding.rb
new file mode 100644
index 00000000000..9bded2dbf97
--- /dev/null
+++ b/gems/gitlab-secret_detection/lib/gitlab/secret_detection/finding.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module SecretDetection
+ # Finding is a data object representing a secret finding identified within a blob
+ class Finding
+ attr_reader :blob_id, :status, :line_number, :type, :description
+
+ def initialize(blob_id, status, line_number = nil, type = nil, description = nil)
+ @blob_id = blob_id
+ @status = status
+ @line_number = line_number
+ @type = type
+ @description = description
+ end
+
+ def ==(other)
+ self.class == other.class && other.state == state
+ end
+
+ protected
+
+ def state
+ [blob_id, status, line_number, type, description]
+ end
+ end
+ end
+end
diff --git a/gems/gitlab-secret_detection/lib/gitlab/secret_detection/response.rb b/gems/gitlab-secret_detection/lib/gitlab/secret_detection/response.rb
new file mode 100644
index 00000000000..a34fba7c0b6
--- /dev/null
+++ b/gems/gitlab-secret_detection/lib/gitlab/secret_detection/response.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module SecretDetection
+ # Response is the data object returned by the scan operation with the following structure
+ #
+ # +status+:: One of values from SecretDetection::Status indicating the scan operation's status
+ # +results+:: Array of SecretDetection::Finding values. Default value is nil.
+ class Response
+ attr_reader :status, :results
+
+ def initialize(status, results = nil)
+ @status = status
+ @results = results
+ end
+
+ def ==(other)
+ self.class == other.class && other.state == state
+ end
+
+ protected
+
+ def state
+ [status, results]
+ end
+ end
+ end
+end
diff --git a/gems/gitlab-secret_detection/lib/gitlab/secret_detection/scan.rb b/gems/gitlab-secret_detection/lib/gitlab/secret_detection/scan.rb
new file mode 100644
index 00000000000..83fc65a9b33
--- /dev/null
+++ b/gems/gitlab-secret_detection/lib/gitlab/secret_detection/scan.rb
@@ -0,0 +1,193 @@
+# frozen_string_literal: true
+
+require 'toml-rb'
+require 're2'
+require 'logger'
+require 'timeout'
+
+module Gitlab
+ module SecretDetection
+ # Scan is responsible for running Secret Detection scan operation
+ class Scan
+ # RulesetParseError is thrown when the code fails to parse the
+ # ruleset file from the given path
+ RulesetParseError = Class.new(StandardError)
+
+ # RulesetCompilationError is thrown when the code fails to compile
+ # the predefined rulesets
+ RulesetCompilationError = Class.new(StandardError)
+
+ # default time limit(in seconds) for running the scan operation per invocation
+ DEFAULT_SCAN_TIMEOUT_SECS = 60
+ # default time limit(in seconds) for running the scan operation on a single blob
+ DEFAULT_BLOB_TIMEOUT_SECS = 5
+ # file path where the secrets ruleset file is located
+ RULESET_FILE_PATH = File.expand_path('../../gitleaks.toml', __dir__)
+ # ignore the scanning of a line which ends with the following keyword
+ GITLEAKS_KEYWORD_IGNORE = 'gitleaks:allow'
+
+ # Initializes the instance with logger along with following operations:
+ # 1. Parse ruleset for the given +ruleset_path+(default: +RULESET_FILE_PATH+). Raises +RulesetParseError+
+ # incase the operation fails.
+ # 2. Extract keywords from the parsed ruleset to use it for matching keywords before regex operation.
+ # 3. Build and Compile rule regex patterns obtained from the ruleset. Raises +RulesetCompilationError+
+ # in case the compilation fails.
+ def initialize(logger: Logger.new($stdout), ruleset_path: RULESET_FILE_PATH)
+ @logger = logger
+ @rules = parse_ruleset ruleset_path
+ @keywords = create_keywords @rules
+ @matcher = build_pattern_matcher @rules
+ end
+
+ # Runs Secret Detection scan on the list of given blobs. Both the total scan duration and
+ # the duration for each blob is time bound via +timeout+ and +blob_timeout+ respectively.
+ #
+ # +blobs+:: Array of blobs with each blob to have `id` and `data` properties.
+ # +timeout+:: No of seconds(accepts floating point for smaller time values) to limit the total scan duration
+ # +blob_timeout+:: No of seconds(accepts floating point for smaller time values) to limit
+ # the scan duration on each blob
+ #
+ # Returns an instance of SecretDetection::Response by following below structure:
+ # {
+ # status: One of the SecretDetection::Status values
+ # results: [SecretDetection::Finding]
+ # }
+ #
+ #
+ def secrets_scan(blobs, timeout: DEFAULT_SCAN_TIMEOUT_SECS, blob_timeout: DEFAULT_BLOB_TIMEOUT_SECS)
+ return SecretDetection::Response.new(SecretDetection::Status::INPUT_ERROR) unless validate_scan_input(blobs)
+
+ Timeout.timeout timeout do
+ matched_blobs = filter_by_keywords(blobs)
+
+ next SecretDetection::Response.new(SecretDetection::Status::NOT_FOUND) if matched_blobs.empty?
+
+ secrets = find_secrets_bulk(matched_blobs, blob_timeout)
+
+ scan_status = overall_scan_status secrets
+
+ SecretDetection::Response.new(scan_status, secrets)
+ end
+ rescue Timeout::Error => e
+ @logger.error "Secret Detection operation timed out: #{e}"
+ SecretDetection::Response.new(SecretDetection::Status::SCAN_TIMEOUT)
+ end
+
+ private
+
+ attr_reader :logger, :rules, :keywords, :matcher
+
+ # parses given ruleset file and returns the parsed rules
+ def parse_ruleset(ruleset_file_path)
+ rules_data = TomlRB.load_file(ruleset_file_path)
+ rules_data['rules']
+ rescue StandardError => e
+ logger.error "Failed to parse Secret Detection ruleset from '#{ruleset_file_path}' path: #{e}"
+ raise RulesetParseError
+ end
+
+ # builds RE2::Set pattern matcher for the given rules
+ def build_pattern_matcher(rules)
+ matcher = RE2::Set.new
+ rules.each do |rule|
+ matcher.add(rule['regex'])
+ end
+
+ unless matcher.compile
+ logger.error "Failed to compile Secret Detection rulesets in RE::Set"
+ raise RulesetCompilationError
+ end
+
+ matcher
+ end
+
+ # creates and returns the unique set of rule matching keywords
+ def create_keywords(rules)
+ secrets_keywords = []
+ rules.each do |rule|
+ secrets_keywords << rule['keywords']
+ end
+
+ secrets_keywords.flatten.compact.to_set
+ end
+
+ # returns only those blobs that contain atleast one of the keywords
+ # from the keywords list
+ def filter_by_keywords(blobs)
+ matched_blobs = []
+
+ blobs.each do |blob|
+ matched_blobs << blob if keywords.any? { |keyword| blob.data.include?(keyword) }
+ end
+
+ matched_blobs.freeze
+ end
+
+ # finds secrets in the given list of blobs
+ def find_secrets_bulk(blobs, blob_timeout)
+ found_secrets = []
+ blobs.each do |blob|
+ found_secrets << Timeout.timeout(blob_timeout) do
+ find_secrets(blob)
+ end
+ rescue Timeout::Error => e
+ logger.error "Secret Detection scan timed out on the blob(id:#{blob.id}): #{e}"
+ found_secrets << SecretDetection::Finding.new(blob.id,
+ SecretDetection::Status::BLOB_TIMEOUT)
+ end
+
+ found_secrets.flatten.freeze
+ end
+
+ # finds secrets in the given blob with a timeout circuit breaker
+ def find_secrets(blob)
+ secrets = []
+
+ blob.data.each_line.with_index do |line, index|
+ # ignore the line scan if it is suffixed with '#gitleaks:allow'
+ next if line.end_with?(GITLEAKS_KEYWORD_IGNORE)
+
+ patterns = matcher.match(line, :exception => false)
+ next unless patterns.any?
+
+ line_no = index + 1
+ patterns.each do |pattern|
+ type = rules[pattern]['id']
+ description = rules[pattern]['description']
+ secrets << SecretDetection::Finding.new(blob.id, SecretDetection::Status::FOUND, line_no, type,
+ description)
+ end
+ end
+ secrets
+ rescue StandardError => e
+ logger.error "Secret Detection scan failed on the blob(id:#{blob.id}): #{e}"
+ SecretDetection::Finding.new(blob.id, SecretDetection::Status::SCAN_ERROR)
+ end
+
+ def validate_scan_input(blobs)
+ return false if blobs.nil? || !blobs.instance_of?(Array)
+
+ blobs.all? do |blob|
+ next false unless blob.respond_to?(:id) || blob.respond_to?(:data)
+
+ blob.data.freeze # freeze blobs to avoid additional object allocations on strings
+ end
+ end
+
+ def overall_scan_status(found_secrets)
+ return SecretDetection::Status::NOT_FOUND if found_secrets.empty?
+
+ timed_out_blobs = found_secrets.count { |el| el.status == SecretDetection::Status::BLOB_TIMEOUT }
+
+ case timed_out_blobs
+ when 0
+ SecretDetection::Status::FOUND
+ when found_secrets.length
+ SecretDetection::Status::SCAN_TIMEOUT
+ else
+ SecretDetection::Status::FOUND_WITH_ERRORS
+ end
+ end
+ end
+ end
+end
diff --git a/gems/gitlab-secret_detection/lib/gitlab/secret_detection/status.rb b/gems/gitlab-secret_detection/lib/gitlab/secret_detection/status.rb
new file mode 100644
index 00000000000..45ac04a81b7
--- /dev/null
+++ b/gems/gitlab-secret_detection/lib/gitlab/secret_detection/status.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module SecretDetection
+ # All the possible statuses emitted by the Scan operation
+ class Status
+ NOT_FOUND = 0 # When scan operation completes with zero findings
+ FOUND = 1 # When scan operation completes with one or more findings
+ FOUND_WITH_ERRORS = 2 # When scan operation completes with one or more findings along with some errors
+ SCAN_TIMEOUT = 3 # When the scan operation runs beyond given time out
+ BLOB_TIMEOUT = 4 # When the scan operation on a blob runs beyond given time out
+ SCAN_ERROR = 5 # When the scan operation fails due to regex error
+ INPUT_ERROR = 6 # When the scan operation fails due to invalid input
+ end
+ end
+end
diff --git a/gems/gitlab-secret_detection/lib/gitleaks.toml b/gems/gitlab-secret_detection/lib/gitleaks.toml
new file mode 100644
index 00000000000..de679a41ea2
--- /dev/null
+++ b/gems/gitlab-secret_detection/lib/gitleaks.toml
@@ -0,0 +1,49 @@
+# This file contains a subset of rules pulled from the original source file.
+# Original Source: https://gitlab.com/gitlab-org/security-products/analyzers/secrets/-/blob/master/gitleaks.toml
+# Reference: https://gitlab.com/gitlab-org/gitlab/-/issues/427011
+title = "gitleaks config"
+
+[[rules]]
+id = "gitlab_personal_access_token"
+description = "GitLab Personal Access Token"
+regex = '''glpat-[0-9a-zA-Z_\-]{20}'''
+tags = ["gitlab", "revocation_type"]
+keywords = [
+ "glpat",
+]
+
+[[rules]]
+id = "gitlab_pipeline_trigger_token"
+description = "GitLab Pipeline Trigger Token"
+regex = '''glptt-[0-9a-zA-Z_\-]{20}'''
+tags = ["gitlab"]
+keywords = [
+ "glptt",
+]
+
+[[rules]]
+id = "gitlab_runner_registration_token"
+description = "GitLab Runner Registration Token"
+regex = '''GR1348941[0-9a-zA-Z_\-]{20}'''
+tags = ["gitlab"]
+keywords = [
+ "GR1348941",
+]
+
+[[rules]]
+id = "gitlab_runner_auth_token"
+description = "GitLab Runner Authentication Token"
+regex = '''glrt-[0-9a-zA-Z_\-]{20}'''
+tags = ["gitlab"]
+keywords = [
+ "glrt",
+]
+
+[[rules]]
+id = "gitlab_feed_token"
+description = "GitLab Feed Token"
+regex = '''glft-[0-9a-zA-Z_\-]{20}'''
+tags = ["gitlab"]
+keywords = [
+ "glft",
+]
diff --git a/gems/gitlab-secret_detection/spec/gitlab/secret_detection_spec.rb b/gems/gitlab-secret_detection/spec/gitlab/secret_detection_spec.rb
deleted file mode 100644
index 112ab8c7468..00000000000
--- a/gems/gitlab-secret_detection/spec/gitlab/secret_detection_spec.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-# frozen_string_literal: true
-
-RSpec.describe Gitlab::SecretDetection do
- it "has a version number" do
- expect(Gitlab::SecretDetection::VERSION).not_to be_nil
- end
-end
diff --git a/gems/gitlab-secret_detection/spec/lib/gitlab/secret_detection/scan_spec.rb b/gems/gitlab-secret_detection/spec/lib/gitlab/secret_detection/scan_spec.rb
new file mode 100644
index 00000000000..dfe3fdf4bb9
--- /dev/null
+++ b/gems/gitlab-secret_detection/spec/lib/gitlab/secret_detection/scan_spec.rb
@@ -0,0 +1,199 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::SecretDetection::Scan, feature_category: :secret_detection do
+ subject(:scan) { described_class.new }
+
+ def new_blob(id:, data:)
+ Struct.new(:id, :data).new(id, data)
+ end
+
+ let(:ruleset) do
+ {
+ "title" => "gitleaks config",
+ "rules" => [
+ { "id" => "gitlab_personal_access_token",
+ "description" => "GitLab Personal Access Token",
+ "regex" => "glpat-[0-9a-zA-Z_\\-]{20}",
+ "tags" => %w[gitlab revocation_type],
+ "keywords" => ["glpat"] },
+ { "id" => "gitlab_pipeline_trigger_token",
+ "description" => "GitLab Pipeline Trigger Token",
+ "regex" => "glptt-[0-9a-zA-Z_\\-]{20}",
+ "tags" => ["gitlab"],
+ "keywords" => ["glptt"] },
+ { "id" => "gitlab_runner_registration_token",
+ "description" => "GitLab Runner Registration Token",
+ "regex" => "GR1348941[0-9a-zA-Z_-]{20}",
+ "tags" => ["gitlab"],
+ "keywords" => ["GR1348941"] },
+ { "id" => "gitlab_feed_token",
+ "description" => "GitLab Feed Token",
+ "regex" => "glft-[0-9a-zA-Z_-]{20}",
+ "tags" => ["gitlab"],
+ "keywords" => ["glft"] }
+ ]
+ }
+ end
+
+ it "does not raise an error parsing the toml file" do
+ expect { scan }.not_to raise_error
+ end
+
+ context "when it creates RE2 patterns from file data" do
+ before do
+ allow(scan).to receive(:parse_ruleset).and_return(ruleset)
+ end
+
+ it "does not raise an error when building patterns" do
+ expect { scan }.not_to raise_error
+ end
+ end
+
+ context "when matching patterns" do
+ before do
+ allow(scan).to receive(:parse_ruleset).and_return(ruleset)
+ end
+
+ context 'when the blob does not contain a secret' do
+ let(:blobs) do
+ [
+ new_blob(id: 1234, data: "no secrets")
+ ]
+ end
+
+ it "does not match" do
+ expected_response = Gitlab::SecretDetection::Response.new(Gitlab::SecretDetection::Status::NOT_FOUND)
+ expect(scan.secrets_scan(blobs)).to eq(expected_response)
+ end
+
+ it "attempts to keyword match returning no blobs for further scan" do
+ expect(scan).to receive(:filter_by_keywords).with(blobs).and_return([])
+ scan.secrets_scan(blobs)
+ end
+
+ it "does not attempt to regex match" do
+ expect(scan).not_to receive(:match_rules_bulk)
+ scan.secrets_scan(blobs)
+ end
+ end
+
+ context "when multiple blobs contains secrets" do
+ let(:blobs) do
+ [
+ new_blob(id: 111, data: "glpat-12312312312312312312"), # gitleaks:allow
+ new_blob(id: 222, data: "\n\nglptt-12312312312312312312"), # gitleaks:allow
+ new_blob(id: 333, data: "data with no secret"),
+ new_blob(id: 444, data: "GR134894112312312312312312312\nglft-12312312312312312312") # gitleaks:allow
+ ]
+ end
+
+ it "matches glpat" do
+ expected_response = Gitlab::SecretDetection::Response.new(
+ Gitlab::SecretDetection::Status::FOUND,
+ [
+ Gitlab::SecretDetection::Finding.new(
+ blobs[0].id,
+ Gitlab::SecretDetection::Status::FOUND,
+ 1,
+ ruleset['rules'][0]['id'],
+ ruleset['rules'][0]['description']
+ ),
+ Gitlab::SecretDetection::Finding.new(
+ blobs[1].id,
+ Gitlab::SecretDetection::Status::FOUND,
+ 3,
+ ruleset['rules'][1]['id'],
+ ruleset['rules'][1]['description']
+ ),
+ Gitlab::SecretDetection::Finding.new(
+ blobs[3].id,
+ Gitlab::SecretDetection::Status::FOUND,
+ 1,
+ ruleset['rules'][2]['id'],
+ ruleset['rules'][2]['description']
+ ),
+ Gitlab::SecretDetection::Finding.new(
+ blobs[3].id,
+ Gitlab::SecretDetection::Status::FOUND,
+ 2,
+ ruleset['rules'][3]['id'],
+ ruleset['rules'][3]['description']
+ )
+ ]
+ )
+
+ expect(scan.secrets_scan(blobs)).to eq(expected_response)
+ end
+ end
+
+ context "when configured with time out" do
+ let(:large_data) do
+ ("large data with a secret glpat-12312312312312312312\n" * 10_000_000).freeze # gitleaks:allow
+ end
+
+ let(:blobs) do
+ [
+ new_blob(id: 111, data: "GR134894112312312312312312312"), # gitleaks:allow
+ new_blob(id: 333, data: "data with no secret"),
+ new_blob(id: 333, data: large_data)
+ ]
+ end
+
+ it "whole secret detection scan operation times out" do
+ scan_timeout_secs = 0.000_001 # 1 micro-sec to intentionally timeout large blob
+ response = Gitlab::SecretDetection::Response.new(Gitlab::SecretDetection::Status::SCAN_TIMEOUT)
+ expect(scan.secrets_scan(blobs, timeout: scan_timeout_secs)).to eq(response)
+ end
+
+ it "one of the blobs times out while others continue to get scanned" do
+ each_blob_timeout_secs = 0.000_001 # 1 micro-sec to intentionally timeout large blob
+
+ expected_response = Gitlab::SecretDetection::Response.new(
+ Gitlab::SecretDetection::Status::FOUND_WITH_ERRORS,
+ [
+ Gitlab::SecretDetection::Finding.new(
+ blobs[0].id, Gitlab::SecretDetection::Status::FOUND, 1,
+ ruleset['rules'][2]['id'],
+ ruleset['rules'][2]['description']
+ ),
+ Gitlab::SecretDetection::Finding.new(
+ blobs[2].id, Gitlab::SecretDetection::Status::BLOB_TIMEOUT
+ )
+ ])
+
+ expect(scan.secrets_scan(blobs, blob_timeout: each_blob_timeout_secs)).to eq(expected_response)
+ end
+
+ it "all the blobs time out" do
+ each_blob_timeout_secs = 0.000_001 # 1 micro-sec to intentionally timeout large blob
+
+ all_large_blobs = [
+ new_blob(id: 111, data: large_data),
+ new_blob(id: 222, data: large_data),
+ new_blob(id: 333, data: large_data)
+ ]
+
+ # scan status changes to SCAN_TIMEOUT when *all* the blobs time out
+ expected_scan_status = Gitlab::SecretDetection::Status::SCAN_TIMEOUT
+
+ expected_response = Gitlab::SecretDetection::Response.new(
+ expected_scan_status,
+ [
+ Gitlab::SecretDetection::Finding.new(
+ all_large_blobs[0].id, Gitlab::SecretDetection::Status::BLOB_TIMEOUT
+ ),
+ Gitlab::SecretDetection::Finding.new(
+ all_large_blobs[1].id, Gitlab::SecretDetection::Status::BLOB_TIMEOUT
+ ),
+ Gitlab::SecretDetection::Finding.new(
+ all_large_blobs[2].id, Gitlab::SecretDetection::Status::BLOB_TIMEOUT
+ )
+ ])
+
+ expect(scan.secrets_scan(all_large_blobs, blob_timeout: each_blob_timeout_secs)).to eq(expected_response)
+ end
+ end
+ end
+end
diff --git a/lib/bitbucket_server/connection.rb b/lib/bitbucket_server/connection.rb
index 845acf034a5..668a4e79da0 100644
--- a/lib/bitbucket_server/connection.rb
+++ b/lib/bitbucket_server/connection.rb
@@ -3,6 +3,7 @@
module BitbucketServer
class Connection
include ActionView::Helpers::SanitizeHelper
+ include BitbucketServer::RetryWithDelay
DEFAULT_API_VERSION = '1.0'
SEPARATOR = '/'
@@ -31,10 +32,13 @@ module BitbucketServer
end
def get(path, extra_query = {})
- response = Gitlab::HTTP.get(build_url(path),
- basic_auth: auth,
- headers: accept_headers,
- query: extra_query)
+ response = if Feature.enabled?(:bitbucket_server_importer_exponential_backoff)
+ retry_with_delay do
+ Gitlab::HTTP.get(build_url(path), basic_auth: auth, headers: accept_headers, query: extra_query)
+ end
+ else
+ Gitlab::HTTP.get(build_url(path), basic_auth: auth, headers: accept_headers, query: extra_query)
+ end
check_errors!(response)
@@ -44,10 +48,13 @@ module BitbucketServer
end
def post(path, body)
- response = Gitlab::HTTP.post(build_url(path),
- basic_auth: auth,
- headers: post_headers,
- body: body)
+ response = if Feature.enabled?(:bitbucket_server_importer_exponential_backoff)
+ retry_with_delay do
+ Gitlab::HTTP.post(build_url(path), basic_auth: auth, headers: post_headers, body: body)
+ end
+ else
+ Gitlab::HTTP.post(build_url(path), basic_auth: auth, headers: post_headers, body: body)
+ end
check_errors!(response)
@@ -63,10 +70,13 @@ module BitbucketServer
def delete(resource, path, body)
url = delete_url(resource, path)
- response = Gitlab::HTTP.delete(url,
- basic_auth: auth,
- headers: post_headers,
- body: body)
+ response = if Feature.enabled?(:bitbucket_server_importer_exponential_backoff)
+ retry_with_delay do
+ Gitlab::HTTP.delete(url, basic_auth: auth, headers: post_headers, body: body)
+ end
+ else
+ Gitlab::HTTP.delete(url, basic_auth: auth, headers: post_headers, body: body)
+ end
check_errors!(response)
@@ -121,5 +131,9 @@ module BitbucketServer
build_url(path)
end
end
+
+ def logger
+ Gitlab::BitbucketServerImport::Logger
+ end
end
end
diff --git a/lib/bitbucket_server/retry_with_delay.rb b/lib/bitbucket_server/retry_with_delay.rb
new file mode 100644
index 00000000000..8a8c0e2dc14
--- /dev/null
+++ b/lib/bitbucket_server/retry_with_delay.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module BitbucketServer
+ module RetryWithDelay
+ extend ActiveSupport::Concern
+
+ MAXIMUM_DELAY = 20
+
+ def retry_with_delay(&block)
+ run_retry_with_delay(&block)
+ end
+
+ private
+
+ def run_retry_with_delay
+ response = yield
+
+ if response.code == 429 && response.headers.has_key?('retry-after')
+ retry_after = response.headers['retry-after'].to_i
+
+ if retry_after <= MAXIMUM_DELAY
+ logger.info(message: "Retrying in #{retry_after} seconds due to 429 Too Many Requests")
+ sleep retry_after
+
+ response = yield
+ end
+ end
+
+ response
+ end
+ end
+end
diff --git a/lib/gitlab/instrumentation/connection_pool.rb b/lib/gitlab/instrumentation/connection_pool.rb
new file mode 100644
index 00000000000..76e6af34054
--- /dev/null
+++ b/lib/gitlab/instrumentation/connection_pool.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Instrumentation
+ # rubocop:disable Gitlab/ModuleWithInstanceVariables -- this module patches ConnectionPool to instrument it
+ module ConnectionPool
+ def initialize(options = {}, &block)
+ @name = options.fetch(:name, 'unknown')
+
+ super
+ end
+
+ def checkout(options = {})
+ conn = super
+
+ connection_class = conn.class.to_s
+ track_available_connections(connection_class)
+ track_pool_size(connection_class)
+
+ conn
+ end
+
+ def track_pool_size(connection_class)
+ # this means that the size metric for this pool key has been sent
+ return if @size_gauge
+
+ @size_gauge ||= ::Gitlab::Metrics.gauge(:gitlab_connection_pool_size, 'Size of connection pool', {}, :all)
+ @size_gauge.set({ pool_name: @name, pool_key: @key, connection_class: connection_class }, @size)
+ end
+
+ def track_available_connections(connection_class)
+ @available_gauge ||= ::Gitlab::Metrics.gauge(:gitlab_connection_pool_available_count,
+ 'Number of available connections in the pool', {}, :all)
+
+ @available_gauge.set({ pool_name: @name, pool_key: @key, connection_class: connection_class }, available)
+ end
+ end
+ # rubocop:enable Gitlab/ModuleWithInstanceVariables
+ end
+end
diff --git a/lib/gitlab/redis/wrapper.rb b/lib/gitlab/redis/wrapper.rb
index 401ac50509d..bb231eec226 100644
--- a/lib/gitlab/redis/wrapper.rb
+++ b/lib/gitlab/redis/wrapper.rb
@@ -30,7 +30,7 @@ module Gitlab
end
def pool
- @pool ||= ConnectionPool.new(size: pool_size) { redis }
+ @pool ||= ConnectionPool.new(size: pool_size, name: store_name.underscore) { redis }
end
def pool_size
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 12f1f35e5ea..6514b302fd5 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -40277,9 +40277,6 @@ msgstr ""
msgid "Reply to this email directly or %{view_it_on_gitlab}."
msgstr ""
-msgid "Reply..."
-msgstr ""
-
msgid "Reply…"
msgstr ""
@@ -51541,6 +51538,9 @@ msgstr ""
msgid "Unauthorized to create an environment"
msgstr ""
+msgid "Unauthorized to delete a container registry protection rule"
+msgstr ""
+
msgid "Unauthorized to delete a package protection rule"
msgstr ""
diff --git a/package.json b/package.json
index 834847e1f96..6bf620b2896 100644
--- a/package.json
+++ b/package.json
@@ -60,7 +60,7 @@
"@gitlab/favicon-overlay": "2.0.0",
"@gitlab/fonts": "^1.3.0",
"@gitlab/svgs": "3.72.0",
- "@gitlab/ui": "^71.1.1",
+ "@gitlab/ui": "^71.3.0",
"@gitlab/visual-review-tools": "1.7.3",
"@gitlab/web-ide": "^0.0.1-dev-20231129035648",
"@mattiasbuelens/web-streams-adapter": "^0.1.0",
diff --git a/spec/frontend/admin/abuse_report/components/notes/abuse_report_add_note_spec.js b/spec/frontend/admin/abuse_report/components/notes/abuse_report_add_note_spec.js
index 09193f8052a..5a3ec3836c5 100644
--- a/spec/frontend/admin/abuse_report/components/notes/abuse_report_add_note_spec.js
+++ b/spec/frontend/admin/abuse_report/components/notes/abuse_report_add_note_spec.js
@@ -118,7 +118,7 @@ describe('Abuse Report Add Note', () => {
expect(findReplyTextarea().exists()).toBe(true);
expect(findReplyTextarea().attributes()).toMatchObject({
rows: '1',
- placeholder: 'Reply',
+ placeholder: 'Reply…',
'aria-label': 'Reply to comment',
});
});
diff --git a/spec/frontend/work_items/components/item_title_spec.js b/spec/frontend/work_items/components/item_title_spec.js
index 3a84ba4bd5e..660ff671a80 100644
--- a/spec/frontend/work_items/components/item_title_spec.js
+++ b/spec/frontend/work_items/components/item_title_spec.js
@@ -2,11 +2,12 @@ import { shallowMount } from '@vue/test-utils';
import { escape } from 'lodash';
import ItemTitle from '~/work_items/components/item_title.vue';
-const createComponent = ({ title = 'Sample title', disabled = false } = {}) =>
+const createComponent = ({ title = 'Sample title', disabled = false, useH1 = false } = {}) =>
shallowMount(ItemTitle, {
propsData: {
title,
disabled,
+ useH1,
},
});
@@ -27,6 +28,12 @@ describe('ItemTitle', () => {
expect(findInputEl().text()).toBe('Sample title');
});
+ it('renders H1 if useH1 is true, otherwise renders H2', () => {
+ expect(wrapper.element.tagName).toBe('H2');
+ wrapper = createComponent({ useH1: true });
+ expect(wrapper.element.tagName).toBe('H1');
+ });
+
it('renders title contents with editing disabled', () => {
wrapper = createComponent({
disabled: true,
diff --git a/spec/frontend/work_items/components/notes/work_item_notes_activity_header_spec.js b/spec/frontend/work_items/components/notes/work_item_notes_activity_header_spec.js
index daf74f7a93b..dff54fef9fe 100644
--- a/spec/frontend/work_items/components/notes/work_item_notes_activity_header_spec.js
+++ b/spec/frontend/work_items/components/notes/work_item_notes_activity_header_spec.js
@@ -9,7 +9,8 @@ import {
describe('Work Item Note Activity Header', () => {
let wrapper;
- const findActivityLabelHeading = () => wrapper.find('h3');
+ const findActivityLabelH2Heading = () => wrapper.find('h2');
+ const findActivityLabelH3Heading = () => wrapper.find('h3');
const findActivityFilterDropdown = () => wrapper.findByTestId('work-item-filter');
const findActivitySortDropdown = () => wrapper.findByTestId('work-item-sort');
@@ -18,6 +19,7 @@ describe('Work Item Note Activity Header', () => {
sortOrder = ASC,
workItemType = 'Task',
discussionFilter = WORK_ITEM_NOTES_FILTER_ALL_NOTES,
+ useH2 = false,
} = {}) => {
wrapper = shallowMountExtended(WorkItemNotesActivityHeader, {
propsData: {
@@ -25,6 +27,7 @@ describe('Work Item Note Activity Header', () => {
sortOrder,
workItemType,
discussionFilter,
+ useH2,
},
});
};
@@ -34,7 +37,18 @@ describe('Work Item Note Activity Header', () => {
});
it('Should have the Activity label', () => {
- expect(findActivityLabelHeading().text()).toBe(WorkItemNotesActivityHeader.i18n.activityLabel);
+ expect(findActivityLabelH3Heading().text()).toBe(
+ WorkItemNotesActivityHeader.i18n.activityLabel,
+ );
+ });
+
+ it('Should render an H2 instead of an H3 if useH2 is true', () => {
+ createComponent();
+ expect(findActivityLabelH3Heading().exists()).toBe(true);
+ expect(findActivityLabelH2Heading().exists()).toBe(false);
+ createComponent({ useH2: true });
+ expect(findActivityLabelH2Heading().exists()).toBe(true);
+ expect(findActivityLabelH3Heading().exists()).toBe(false);
});
it('Should have Activity filtering dropdown', () => {
diff --git a/spec/lib/bitbucket_server/client_spec.rb b/spec/lib/bitbucket_server/client_spec.rb
index cd3179f19d4..1d6e8492760 100644
--- a/spec/lib/bitbucket_server/client_spec.rb
+++ b/spec/lib/bitbucket_server/client_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe BitbucketServer::Client do
+RSpec.describe BitbucketServer::Client, feature_category: :importers do
let(:base_uri) { 'https://test:7990/stash/' }
let(:options) { { base_uri: base_uri, user: 'bitbucket', password: 'mypassword' } }
let(:project) { 'SOME-PROJECT' }
diff --git a/spec/lib/bitbucket_server/connection_spec.rb b/spec/lib/bitbucket_server/connection_spec.rb
index 8341ca10f43..59eda91285f 100644
--- a/spec/lib/bitbucket_server/connection_spec.rb
+++ b/spec/lib/bitbucket_server/connection_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe BitbucketServer::Connection do
+RSpec.describe BitbucketServer::Connection, feature_category: :importers do
let(:options) { { base_uri: 'https://test:7990', user: 'bitbucket', password: 'mypassword' } }
let(:payload) { { 'test' => 1 } }
let(:headers) { { "Content-Type" => "application/json" } }
@@ -11,83 +11,162 @@ RSpec.describe BitbucketServer::Connection do
subject { described_class.new(options) }
describe '#get' do
- it 'returns JSON body' do
- WebMock.stub_request(:get, url).with(headers: { 'Accept' => 'application/json' }).to_return(body: payload.to_json, status: 200, headers: headers)
+ before do
+ WebMock.stub_request(:get, url).with(headers: { 'Accept' => 'application/json' })
+ .to_return(body: payload.to_json, status: 200, headers: headers)
+ end
+
+ it 'runs with retry_with_delay' do
+ expect(subject).to receive(:retry_with_delay).and_call_original.once
- expect(subject.get(url, { something: 1 })).to eq(payload)
+ subject.get(url)
end
- it 'throws an exception if the response is not 200' do
- WebMock.stub_request(:get, url).with(headers: { 'Accept' => 'application/json' }).to_return(body: payload.to_json, status: 500, headers: headers)
+ shared_examples 'handles get requests' do
+ it 'returns JSON body' do
+ expect(subject.get(url, { something: 1 })).to eq(payload)
+ end
+
+ it 'throws an exception if the response is not 200' do
+ WebMock.stub_request(:get, url).with(headers: { 'Accept' => 'application/json' }).to_return(body: payload.to_json, status: 500, headers: headers)
+
+ expect { subject.get(url) }.to raise_error(described_class::ConnectionError)
+ end
+
+ it 'throws an exception if the response is not JSON' do
+ WebMock.stub_request(:get, url).with(headers: { 'Accept' => 'application/json' }).to_return(body: 'bad data', status: 200, headers: headers)
- expect { subject.get(url) }.to raise_error(described_class::ConnectionError)
+ expect { subject.get(url) }.to raise_error(described_class::ConnectionError)
+ end
+
+ it 'throws an exception upon a network error' do
+ WebMock.stub_request(:get, url).with(headers: { 'Accept' => 'application/json' }).to_raise(OpenSSL::SSL::SSLError)
+
+ expect { subject.get(url) }.to raise_error(described_class::ConnectionError)
+ end
end
- it 'throws an exception if the response is not JSON' do
- WebMock.stub_request(:get, url).with(headers: { 'Accept' => 'application/json' }).to_return(body: 'bad data', status: 200, headers: headers)
+ it_behaves_like 'handles get requests'
+
+ context 'when the response is a 429 rate limit reached error' do
+ let(:response) do
+ instance_double(HTTParty::Response, parsed_response: payload, code: 429, headers: headers.merge('retry-after' => '0'))
+ end
+
+ before do
+ allow(Gitlab::HTTP).to receive(:get).and_return(response)
+ end
- expect { subject.get(url) }.to raise_error(described_class::ConnectionError)
+ it 'sleeps, retries and if the error persists it fails' do
+ expect(Gitlab::BitbucketServerImport::Logger).to receive(:info)
+ .with(message: 'Retrying in 0 seconds due to 429 Too Many Requests')
+ .once
+
+ expect { subject.get(url) }.to raise_error(BitbucketServer::Connection::ConnectionError)
+ end
end
- it 'throws an exception upon a network error' do
- WebMock.stub_request(:get, url).with(headers: { 'Accept' => 'application/json' }).to_raise(OpenSSL::SSL::SSLError)
+ context 'when the bitbucket_server_importer_exponential_backoff feature flag is disabled' do
+ before do
+ stub_feature_flags(bitbucket_server_importer_exponential_backoff: false)
+ end
- expect { subject.get(url) }.to raise_error(described_class::ConnectionError)
+ it_behaves_like 'handles get requests'
end
end
describe '#post' do
let(:headers) { { 'Accept' => 'application/json', 'Content-Type' => 'application/json' } }
- it 'returns JSON body' do
+ before do
WebMock.stub_request(:post, url).with(headers: headers).to_return(body: payload.to_json, status: 200, headers: headers)
-
- expect(subject.post(url, payload)).to eq(payload)
end
- it 'throws an exception if the response is not 200' do
- WebMock.stub_request(:post, url).with(headers: headers).to_return(body: payload.to_json, status: 500, headers: headers)
+ it 'runs with retry_with_delay' do
+ expect(subject).to receive(:retry_with_delay).and_call_original.once
- expect { subject.post(url, payload) }.to raise_error(described_class::ConnectionError)
+ subject.post(url, payload)
end
- it 'throws an exception upon a network error' do
- WebMock.stub_request(:post, url).with(headers: { 'Accept' => 'application/json' }).to_raise(OpenSSL::SSL::SSLError)
+ shared_examples 'handles post requests' do
+ it 'returns JSON body' do
+ expect(subject.post(url, payload)).to eq(payload)
+ end
+
+ it 'throws an exception if the response is not 200' do
+ WebMock.stub_request(:post, url).with(headers: headers).to_return(body: payload.to_json, status: 500, headers: headers)
- expect { subject.post(url, payload) }.to raise_error(described_class::ConnectionError)
+ expect { subject.post(url, payload) }.to raise_error(described_class::ConnectionError)
+ end
+
+ it 'throws an exception upon a network error' do
+ WebMock.stub_request(:post, url).with(headers: { 'Accept' => 'application/json' }).to_raise(OpenSSL::SSL::SSLError)
+
+ expect { subject.post(url, payload) }.to raise_error(described_class::ConnectionError)
+ end
+
+ it 'throws an exception if the URI is invalid' do
+ stub_request(:post, url).with(headers: { 'Accept' => 'application/json' }).to_raise(URI::InvalidURIError)
+
+ expect { subject.post(url, payload) }.to raise_error(described_class::ConnectionError)
+ end
end
- it 'throws an exception if the URI is invalid' do
- stub_request(:post, url).with(headers: { 'Accept' => 'application/json' }).to_raise(URI::InvalidURIError)
+ it_behaves_like 'handles post requests'
- expect { subject.post(url, payload) }.to raise_error(described_class::ConnectionError)
+ context 'when the bitbucket_server_importer_exponential_backoff feature flag is disabled' do
+ before do
+ stub_feature_flags(bitbucket_server_importer_exponential_backoff: false)
+ end
+
+ it_behaves_like 'handles post requests'
end
end
describe '#delete' do
let(:headers) { { 'Accept' => 'application/json', 'Content-Type' => 'application/json' } }
+ before do
+ WebMock.stub_request(:delete, branch_url).with(headers: headers).to_return(body: payload.to_json, status: 200, headers: headers)
+ end
+
context 'branch API' do
let(:branch_path) { '/projects/foo/repos/bar/branches' }
let(:branch_url) { 'https://test:7990/rest/branch-utils/1.0/projects/foo/repos/bar/branches' }
let(:path) {}
- it 'returns JSON body' do
- WebMock.stub_request(:delete, branch_url).with(headers: headers).to_return(body: payload.to_json, status: 200, headers: headers)
+ it 'runs with retry_with_delay' do
+ expect(subject).to receive(:retry_with_delay).and_call_original.once
- expect(subject.delete(:branches, branch_path, payload)).to eq(payload)
+ subject.delete(:branches, branch_path, payload)
end
- it 'throws an exception if the response is not 200' do
- WebMock.stub_request(:delete, branch_url).with(headers: headers).to_return(body: payload.to_json, status: 500, headers: headers)
+ shared_examples 'handles delete requests' do
+ it 'returns JSON body' do
+ expect(subject.delete(:branches, branch_path, payload)).to eq(payload)
+ end
- expect { subject.delete(:branches, branch_path, payload) }.to raise_error(described_class::ConnectionError)
+ it 'throws an exception if the response is not 200' do
+ WebMock.stub_request(:delete, branch_url).with(headers: headers).to_return(body: payload.to_json, status: 500, headers: headers)
+
+ expect { subject.delete(:branches, branch_path, payload) }.to raise_error(described_class::ConnectionError)
+ end
+
+ it 'throws an exception upon a network error' do
+ WebMock.stub_request(:delete, branch_url).with(headers: headers).to_raise(OpenSSL::SSL::SSLError)
+
+ expect { subject.delete(:branches, branch_path, payload) }.to raise_error(described_class::ConnectionError)
+ end
end
- it 'throws an exception upon a network error' do
- WebMock.stub_request(:delete, branch_url).with(headers: headers).to_raise(OpenSSL::SSL::SSLError)
+ it_behaves_like 'handles delete requests'
+
+ context 'with the bitbucket_server_importer_exponential_backoff feature flag disabled' do
+ before do
+ stub_feature_flags(bitbucket_server_importer_exponential_backoff: false)
+ end
- expect { subject.delete(:branches, branch_path, payload) }.to raise_error(described_class::ConnectionError)
+ it_behaves_like 'handles delete requests'
end
end
end
diff --git a/spec/lib/bitbucket_server/retry_with_delay_spec.rb b/spec/lib/bitbucket_server/retry_with_delay_spec.rb
new file mode 100644
index 00000000000..99685b08299
--- /dev/null
+++ b/spec/lib/bitbucket_server/retry_with_delay_spec.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe BitbucketServer::RetryWithDelay, feature_category: :importers do
+ let(:service) { dummy_class.new }
+ let(:body) { 'test' }
+ let(:response) { instance_double(HTTParty::Response, body: body, code: 200) }
+ let(:response_caller) { -> { response } }
+
+ let(:dummy_class) do
+ Class.new do
+ def logger
+ @logger ||= Logger.new(File::NULL)
+ end
+
+ def dummy_method(response_caller)
+ retry_with_delay do
+ response_caller.call
+ end
+ end
+
+ include BitbucketServer::RetryWithDelay
+ end
+ end
+
+ subject(:execute) { service.dummy_method(response_caller) }
+
+ describe '.retry_with_delay' do
+ context 'when the function succeeds on the first try' do
+ it 'calls the function once and returns its result' do
+ expect(response_caller).to receive(:call).once.and_call_original
+
+ execute
+ end
+ end
+
+ context 'when the request has a status code of 429' do
+ let(:headers) { { 'retry-after' => '0' } }
+ let(:body) { 'HTTP Status 429 - Too Many Requests' }
+ let(:response) { instance_double(HTTParty::Response, body: body, code: 429, headers: headers) }
+
+ before do
+ stub_const("#{described_class}::MAXIMUM_DELAY", 0)
+ end
+
+ it 'calls the function again after a delay' do
+ expect(response_caller).to receive(:call).twice.and_call_original
+
+ expect_next_instance_of(Logger) do |logger|
+ expect(logger).to receive(:info)
+ .with(message: 'Retrying in 0 seconds due to 429 Too Many Requests')
+ .once
+ end
+
+ execute
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/instrumentation/connection_pool_spec.rb b/spec/lib/gitlab/instrumentation/connection_pool_spec.rb
new file mode 100644
index 00000000000..b7cab2e9900
--- /dev/null
+++ b/spec/lib/gitlab/instrumentation/connection_pool_spec.rb
@@ -0,0 +1,69 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require 'support/helpers/rails_helpers'
+
+RSpec.describe Gitlab::Instrumentation::ConnectionPool, feature_category: :redis do
+ let(:option) { { name: 'test', size: 5 } }
+ let(:pool) { ConnectionPool.new(option) { 'nothing' } }
+
+ let_it_be(:size_gauge_args) { [:gitlab_connection_pool_size, 'Size of connection pool', {}, :all] }
+ let_it_be(:available_gauge_args) do
+ [:gitlab_connection_pool_available_count,
+ 'Number of available connections in the pool', {}, :all]
+ end
+
+ subject(:checkout_pool) { pool.checkout }
+
+ describe '.checkout' do
+ let(:size_gauge_double) { instance_double(::Prometheus::Client::Gauge) }
+
+ context 'when tracking for the first time' do
+ it 'initialises gauges' do
+ expect(::Gitlab::Metrics).to receive(:gauge).with(*size_gauge_args).and_call_original
+ expect(::Gitlab::Metrics).to receive(:gauge).with(*available_gauge_args).and_call_original
+
+ checkout_pool
+ end
+ end
+
+ it 'sets the size gauge only once' do
+ expect(::Gitlab::Metrics.gauge(*size_gauge_args)).to receive(:set).with(
+ { pool_name: 'test', pool_key: anything, connection_class: "String" }, 5).once
+
+ checkout_pool
+ checkout_pool
+ end
+
+ context 'when tracking on subsequent calls' do
+ before do
+ pool.checkout # initialise instance variables
+ end
+
+ it 'uses memoized gauges' do
+ expect(::Gitlab::Metrics).not_to receive(:gauge).with(*size_gauge_args)
+ expect(::Gitlab::Metrics).not_to receive(:gauge).with(*available_gauge_args)
+
+ expect(pool.instance_variable_get(:@size_gauge)).not_to receive(:set)
+ .with({ pool_name: 'test', pool_key: anything, connection_class: "String" }, 5)
+ expect(pool.instance_variable_get(:@available_gauge)).to receive(:set)
+ .with({ pool_name: 'test', pool_key: anything, connection_class: "String" }, 4)
+
+ checkout_pool
+ end
+
+ context 'when pool name is omitted' do
+ let(:option) { {} }
+
+ it 'uses unknown name' do
+ expect(pool.instance_variable_get(:@size_gauge)).not_to receive(:set)
+ .with({ pool_name: 'unknown', pool_key: anything, connection_class: "String" }, 5)
+ expect(pool.instance_variable_get(:@available_gauge)).to receive(:set)
+ .with({ pool_name: 'unknown', pool_key: anything, connection_class: "String" }, 4)
+
+ checkout_pool
+ end
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/container_registry/protection/rule/delete_spec.rb b/spec/requests/api/graphql/mutations/container_registry/protection/rule/delete_spec.rb
new file mode 100644
index 00000000000..126d4bfdd4a
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/container_registry/protection/rule/delete_spec.rb
@@ -0,0 +1,102 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Deleting a container registry protection rule', :aggregate_failures, feature_category: :container_registry do
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be_with_refind(:container_protection_rule) do
+ create(:container_registry_protection_rule, project: project)
+ end
+
+ let_it_be(:current_user) { create(:user, maintainer_projects: [project]) }
+
+ let(:mutation) { graphql_mutation(:delete_container_registry_protection_rule, input) }
+ let(:mutation_response) { graphql_mutation_response(:delete_container_registry_protection_rule) }
+ let(:input) { { id: container_protection_rule.to_global_id } }
+
+ subject(:post_graphql_mutation_delete_container_registry_protection_rule) do
+ post_graphql_mutation(mutation, current_user: current_user)
+ end
+
+ shared_examples 'an erroneous reponse' do
+ it { post_graphql_mutation_delete_container_registry_protection_rule.tap { expect(mutation_response).to be_blank } }
+
+ it do
+ expect { post_graphql_mutation_delete_container_registry_protection_rule }
+ .not_to change { ::ContainerRegistry::Protection::Rule.count }
+ end
+ end
+
+ it_behaves_like 'a working GraphQL mutation'
+
+ it 'responds with deleted container registry protection rule' do
+ expect { post_graphql_mutation_delete_container_registry_protection_rule }
+ .to change { ::ContainerRegistry::Protection::Rule.count }.from(1).to(0)
+
+ expect_graphql_errors_to_be_empty
+
+ expect(mutation_response).to include(
+ 'errors' => be_blank,
+ 'containerRegistryProtectionRule' => {
+ 'id' => container_protection_rule.to_global_id.to_s,
+ 'containerPathPattern' => container_protection_rule.container_path_pattern,
+ 'deleteProtectedUpToAccessLevel' => container_protection_rule.delete_protected_up_to_access_level.upcase,
+ 'pushProtectedUpToAccessLevel' => container_protection_rule.push_protected_up_to_access_level.upcase
+ }
+ )
+ end
+
+ context 'with existing container registry protection rule belonging to other project' do
+ let_it_be(:container_protection_rule) do
+ create(:container_registry_protection_rule, container_path_pattern: 'protection_rule_other_project')
+ end
+
+ it_behaves_like 'an erroneous reponse'
+
+ it { is_expected.tap { expect_graphql_errors_to_include(/you don't have permission to perform this action/) } }
+ end
+
+ context 'with deleted container registry protection rule' do
+ let!(:container_protection_rule) do
+ create(:container_registry_protection_rule, project: project,
+ container_path_pattern: 'protection_rule_deleted').destroy!
+ end
+
+ it_behaves_like 'an erroneous reponse'
+
+ it { is_expected.tap { expect_graphql_errors_to_include(/you don't have permission to perform this action/) } }
+ end
+
+ context 'when current_user does not have permission' do
+ let_it_be(:developer) { create(:user).tap { |u| project.add_developer(u) } }
+ let_it_be(:reporter) { create(:user).tap { |u| project.add_reporter(u) } }
+ let_it_be(:guest) { create(:user).tap { |u| project.add_guest(u) } }
+ let_it_be(:anonymous) { create(:user) }
+
+ where(:current_user) do
+ [ref(:developer), ref(:reporter), ref(:guest), ref(:anonymous)]
+ end
+
+ with_them do
+ it_behaves_like 'an erroneous reponse'
+
+ it { is_expected.tap { expect_graphql_errors_to_include(/you don't have permission to perform this action/) } }
+ end
+ end
+
+ context "when feature flag ':container_registry_protected_containers' disabled" do
+ before do
+ stub_feature_flags(container_registry_protected_containers: false)
+ end
+
+ it_behaves_like 'an erroneous reponse'
+
+ it do
+ post_graphql_mutation_delete_container_registry_protection_rule
+
+ expect_graphql_errors_to_include(/'container_registry_protected_containers' feature flag is disabled/)
+ end
+ end
+end
diff --git a/spec/services/container_registry/protection/delete_rule_service_spec.rb b/spec/services/container_registry/protection/delete_rule_service_spec.rb
new file mode 100644
index 00000000000..bdc2ca727d2
--- /dev/null
+++ b/spec/services/container_registry/protection/delete_rule_service_spec.rb
@@ -0,0 +1,106 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ContainerRegistry::Protection::DeleteRuleService, '#execute', feature_category: :container_registry do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:current_user) { create(:user, maintainer_projects: [project]) }
+ let_it_be_with_refind(:container_registry_protection_rule) do
+ create(:container_registry_protection_rule, project: project)
+ end
+
+ subject(:service_execute) do
+ described_class.new(container_registry_protection_rule, current_user: current_user).execute
+ end
+
+ shared_examples 'a successful service response' do
+ it { is_expected.to be_success }
+
+ it do
+ is_expected.to have_attributes(
+ errors: be_blank,
+ message: be_blank,
+ payload: { container_registry_protection_rule: container_registry_protection_rule }
+ )
+ end
+
+ it do
+ service_execute
+
+ expect { container_registry_protection_rule.reload }.to raise_error ActiveRecord::RecordNotFound
+ end
+ end
+
+ shared_examples 'an erroneous service response' do
+ it { is_expected.to be_error }
+
+ it do
+ is_expected.to have_attributes(message: be_present, payload: { container_registry_protection_rule: be_blank })
+ end
+
+ it do
+ expect { service_execute }.not_to change { ContainerRegistry::Protection::Rule.count }
+
+ expect { container_registry_protection_rule.reload }.not_to raise_error
+ end
+ end
+
+ it_behaves_like 'a successful service response'
+
+ it 'deletes the container registry protection rule in the database' do
+ expect { service_execute }
+ .to change {
+ project.reload.container_registry_protection_rules
+ }.from([container_registry_protection_rule]).to([])
+ .and change { ::ContainerRegistry::Protection::Rule.count }.from(1).to(0)
+ end
+
+ context 'with deleted container registry protection rule' do
+ let!(:container_registry_protection_rule) do
+ create(:container_registry_protection_rule, project: project,
+ container_path_pattern: 'protection_rule_deleted').destroy!
+ end
+
+ it_behaves_like 'a successful service response'
+ end
+
+ context 'when error occurs during delete operation' do
+ before do
+ allow(container_registry_protection_rule).to receive(:destroy!).and_raise(StandardError.new('Some error'))
+ end
+
+ it_behaves_like 'an erroneous service response'
+
+ it { is_expected.to have_attributes message: /Some error/ }
+ end
+
+ context 'when current_user does not have permission' do
+ let_it_be(:developer) { create(:user).tap { |u| project.add_developer(u) } }
+ let_it_be(:reporter) { create(:user).tap { |u| project.add_reporter(u) } }
+ let_it_be(:guest) { create(:user).tap { |u| project.add_guest(u) } }
+ let_it_be(:anonymous) { create(:user) }
+
+ where(:current_user) do
+ [ref(:developer), ref(:reporter), ref(:guest), ref(:anonymous)]
+ end
+
+ with_them do
+ it_behaves_like 'an erroneous service response'
+
+ it { is_expected.to have_attributes message: /Unauthorized to delete a container registry protection rule/ }
+ end
+ end
+
+ context 'without container registry protection rule' do
+ let(:container_registry_protection_rule) { nil }
+
+ it { expect { service_execute }.to raise_error(ArgumentError) }
+ end
+
+ context 'without current_user' do
+ let(:current_user) { nil }
+ let(:container_registry_protection_rule) { build_stubbed(:container_registry_protection_rule, project: project) }
+
+ it { expect { service_execute }.to raise_error(ArgumentError) }
+ end
+end
diff --git a/spec/support/shared_examples/redis/redis_shared_examples.rb b/spec/support/shared_examples/redis/redis_shared_examples.rb
index f184f678283..796b483820b 100644
--- a/spec/support/shared_examples/redis/redis_shared_examples.rb
+++ b/spec/support/shared_examples/redis/redis_shared_examples.rb
@@ -223,7 +223,8 @@ RSpec.shared_examples "redis_shared_examples" do
end
it 'instantiates a connection pool with size 5' do
- expect(ConnectionPool).to receive(:new).with(size: 5).and_call_original
+ expect(ConnectionPool).to receive(:new)
+ .with(size: 5, name: described_class.store_name.underscore).and_call_original
described_class.with { |_redis_shared_example| true }
end
@@ -236,7 +237,8 @@ RSpec.shared_examples "redis_shared_examples" do
end
it 'instantiates a connection pool with a size based on the concurrency of the worker' do
- expect(ConnectionPool).to receive(:new).with(size: 18 + 5).and_call_original
+ expect(ConnectionPool).to receive(:new)
+ .with(size: 18 + 5, name: described_class.store_name.underscore).and_call_original
described_class.with { |_redis_shared_example| true }
end
diff --git a/yarn.lock b/yarn.lock
index eefa4895f3b..dcb28a12a66 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1274,10 +1274,10 @@
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-3.72.0.tgz#5daaa7366913b52ea89439305067e030f967c8a5"
integrity sha512-VbSdwXxu9Y6NAXNFTROjZa83e2b8QeDAO7byqjJ0z+2Y3gGGXdw+HclAzz0Ns8B0+DMV5mV7dtmTlv/1xAXXYQ==
-"@gitlab/ui@^71.1.1":
- version "71.1.1"
- resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-71.1.1.tgz#3853fc98287736992aae2464de8ba0f482a68f27"
- integrity sha512-yhKjn0TJ5kI+If3T5mSQfmWkhXtzFWjr4+Qi6FTN9f3vJTOYXtzsFXtZr66V8202peJWbif4A5KpoNZIJdo8YQ==
+"@gitlab/ui@^71.3.0":
+ version "71.4.0"
+ resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-71.4.0.tgz#481d594f7cdc01aac6529cc7c801221ccde13b86"
+ integrity sha512-6ddhlYo5wVQJ2j0AhlrmxwBpYS7UhM6sR3XeXeMRbDqJaA/17ARwyl8JMxCqVcIcGbTmDd9FJluXzObQsyUzUQ==
dependencies:
"@floating-ui/dom" "1.2.9"
bootstrap-vue "2.23.1"